From 6cded6a5a3ed53ecd9f3bb3edc1f98a6434a1f6e Mon Sep 17 00:00:00 2001 From: austin62jake Date: Tue, 10 Nov 2026 09:34:48 -0600 Subject: [PATCH] M10: a booklet's both sides follow a driver that keeps its own two-sided setting (#5772) (#5903) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * M10: a booklet's both sides follow a driver that keeps its own two-sided setting (#5772) Booklet ▸ Both sides always asked for two-sided printing on the short edge (#2707). A driver that doesn't take two-sided settings from other programs (no two-sided printing, manual two-sided only in its Properties, Microsoft Print to PDF) then made craft-print refuse the whole job. The reporter's Epson ET-2870 offers only OneSided, and its manual two-sided setting is what prints their normal two-sided jobs. print::booklet_job_duplex asks such a driver for nothing on both sides, so its own setting applies, as for ordinary printing (#2422) and as Acrobat enables two-sided for a booklet only "if available". Unknown support still asks for the short edge; one side on its own stays one-sided. The Print dialog takes the chosen printer's support from the printer list (PrintDraft::printer_two_sided, also behind its "its own setting applies" note), and doc_print reads it only for a booklet's both sides. The dialog's booklet test used the machine's printers, and on Windows the default, Microsoft Print to PDF, now gives the other answer: it sets its own printers and checks the note too. Co-Authored-By: Claude Opus 5.5 * M10: doc_print's lookup of a booklet printer's two-sided support is tested (#5772) The booklet test fed job_duplex the printer's answer directly, so a lookup that missed the default printer or matched the wrong one would have gone unnoticed. The search over the printer list is now its own function, two_sided_in, tested with a list of named, default and unknown printers. Co-Authored-By: Claude Opus 5.5 * M10: a booklet printed to "default" goes to the printer whose driver was asked (#5772) doc_print decides a booklet's both sides from the printer's two-sided support. For "default", the job named no printer, so the spooler picked whichever printer was the default when the job was submitted, after the document was rendered. The printer found in the list is now the job's printer, by name, and the lookup test covers a one-sided default. Co-Authored-By: Claude Opus 5.5 * #5903: the booklet test lists its printers through a stand-in source It set the dialog's printer list by hand while the real list was still loading. On a machine with no printers (CI's Linux runner) the list that arrived replaced them and reset the printer, so the dialog the test clicked through was not the one it set up. The printers now come from the dialog's own source, drivers unasked as #3554 lists them, with each driver's two-sided answer in the map. --------- Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> --- crates/automation/src/printing.rs | 63 ++++++++++++++++--- crates/print/src/lib.rs | 2 +- crates/print/src/settings.rs | 14 +++++ crates/print/src/tests.rs | 13 ++++ crates/ui-egui/src/workflows/print_ui.rs | 27 ++++++++ crates/ui-egui/src/workflows/print_ui/body.rs | 6 +- .../ui-egui/src/workflows/print_ui/draft.rs | 15 ++++- crates/ui-egui/tests/it/print.rs | 28 ++++++++- parity/acrobat-features.toml | 4 +- 9 files changed, 151 insertions(+), 21 deletions(-) diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index ba15639039..ca423b194b 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -23,15 +23,24 @@ fn duplex(value: &str, cut_stack: bool) -> Result> { } /// [`duplex`] for a job laid out as `layout`: the name is checked either way, and a booklet -/// decides two-sided itself (#2707), as the Print dialog does (`print::booklet_duplex`). -fn job_duplex(value: &str, layout: &Layout) -> Result> { +/// decides two-sided itself (#2707) as far as the printer's driver takes it, as the Print dialog +/// does (`print::booklet_job_duplex`). `two_sided` says whether it does; it is only asked for a +/// booklet's both sides, the one job it changes. +fn job_duplex(value: &str, layout: &Layout, two_sided: impl FnOnce() -> Option) -> Result> { let chosen = duplex(value, matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }))?; Ok(match layout { - Layout::Booklet { subset, .. } => Some(print::booklet_duplex(*subset)), + Layout::Booklet { subset: BookletSubset::BothSides, .. } => print::booklet_job_duplex(BookletSubset::BothSides, two_sided()), + Layout::Booklet { subset, .. } => print::booklet_job_duplex(*subset, None), _ => chosen, }) } +/// `printer` (a name, or "default" for the default printer) in `printers`; `None` when the list +/// has no such printer. +fn printer_in(printers: Vec, printer: &str) -> Option { + printers.into_iter().find(|p| if printer == "default" { p.default } else { p.name == printer }) +} + /// An invalid-arguments error saying `m`. fn bad(m: impl Into) -> ToolError { ToolError::InvalidArgs(m.into()) @@ -242,7 +251,15 @@ impl Automation { }, l => Layout::Size(print::names::parse_size_mode(l, 100.0).ok_or_else(|| bad(format!("unknown layout {l:?}")))?), }; - let duplex = job_duplex(a.opt_str("duplex")?.unwrap_or("off"), &layout)?; + let printer = a.opt_str("printer")?; + // The printer whose driver a booklet's both sides were decided for. The job goes to it by + // name, so "default" can't become another printer while the document is rendered. The + // queues are listed without asking every driver (#2192); only this one is asked. + let mut asked = None; + let duplex = job_duplex(a.opt_str("duplex")?.unwrap_or("off"), &layout, || { + asked = printer.and_then(|p| printer_in(spool::printer_queues(), p)); + spool::two_sided(&asked.as_ref()?.name) + })?; if (a.opt_int("cols")?.is_some() || a.opt_int("rows")?.is_some()) && !matches!(layout, Layout::Multiple { .. }) { return Err(bad("cols and rows need layout multiple")); } @@ -285,13 +302,14 @@ impl Automation { let sheets = laid_out.len(); let bytes = self.session.print_pdf(id, &settings).map_err(failed)?; let mut out = json!({ "sheets": sheets, "pages": settings.pages.len(), "bytes": bytes.len() }); - match (a.opt_str("path")?, a.opt_str("printer")?) { + match (a.opt_str("path")?, printer) { (Some(p), None) => { let path = self.resolve(p, true)?; write_atomic(&path, &bytes)?; out["path"] = json!(path.to_string_lossy()); } (None, Some(printer)) => { + let printer = asked.as_ref().map_or(printer, |p| p.name.as_str()); let job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index, settings.raster, &laid_out)?; let bytes = pdfcraft_engine::spooler::spooler_copy(bytes); out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?); @@ -320,16 +338,41 @@ mod tests { use super::*; /// #2707: a booklet sets two-sided itself whatever `duplex` asks; a bad name is still an error. + /// #5772: its both sides leave two-sided to a driver that doesn't take the setting, and only + /// they look the printer up. #[test] fn a_booklet_decides_two_sided_itself() { let booklet = |subset| Layout::Booklet { subset, binding: Binding::Left, creep: 0.0 }; + let unread = || -> Option { panic!("the printer's two-sided support is read for nothing") }; for asked in ["printer", "off", "long-edge", "short-edge"] { - assert_eq!(job_duplex(asked, &booklet(BookletSubset::BothSides)).ok(), Some(Some(spool::Duplex::ShortEdge)), "{asked}"); - assert_eq!(job_duplex(asked, &booklet(BookletSubset::FrontOnly)).ok(), Some(Some(spool::Duplex::Off)), "{asked}"); - assert_eq!(job_duplex(asked, &booklet(BookletSubset::BackOnly)).ok(), Some(Some(spool::Duplex::Off)), "{asked}"); + assert_eq!(job_duplex(asked, &booklet(BookletSubset::BothSides), || Some(true)).ok(), Some(Some(spool::Duplex::ShortEdge)), "{asked}"); + assert_eq!(job_duplex(asked, &booklet(BookletSubset::BothSides), || None).ok(), Some(Some(spool::Duplex::ShortEdge)), "{asked}"); + assert_eq!(job_duplex(asked, &booklet(BookletSubset::BothSides), || Some(false)).ok(), Some(None), "{asked}"); + assert_eq!(job_duplex(asked, &booklet(BookletSubset::FrontOnly), unread).ok(), Some(Some(spool::Duplex::Off)), "{asked}"); + assert_eq!(job_duplex(asked, &booklet(BookletSubset::BackOnly), unread).ok(), Some(Some(spool::Duplex::Off)), "{asked}"); } - assert!(job_duplex("sideways", &booklet(BookletSubset::BothSides)).is_err()); - assert_eq!(job_duplex("long-edge", &Layout::Size(SizeMode::Fit)).ok(), Some(Some(spool::Duplex::LongEdge)), "other layouts keep it"); + assert!(job_duplex("sideways", &booklet(BookletSubset::BothSides), || None).is_err()); + assert_eq!(job_duplex("long-edge", &Layout::Size(SizeMode::Fit), unread).ok(), Some(Some(spool::Duplex::LongEdge)), "other layouts keep it"); + } + + /// #5772: a booklet's printer is found by name, or as the default for "default", and only + /// what the list says about it counts. "default" is found by its name, which the job is sent + /// to. + #[test] + fn a_booklets_printer_comes_from_the_list() { + let printer = |name: &str, default, two_sided| spool::Printer { name: name.into(), default, two_sided, description: String::new() }; + let list = [printer("Epson", false, Some(false)), printer("Laser", true, Some(true)), printer("Network", false, None)]; + let found = |list: &[spool::Printer], name| printer_in(list.to_vec(), name).map(|p| (p.name, p.two_sided)); + assert_eq!(found(&list, "Epson"), Some(("Epson".into(), Some(false)))); + assert_eq!(found(&list, "Laser"), Some(("Laser".into(), Some(true)))); + assert_eq!(found(&list, "default"), Some(("Laser".into(), Some(true))), "the default printer"); + assert_eq!(found(&list, "Network"), Some(("Network".into(), None)), "the list doesn't say"); + assert_eq!(found(&list, "Gone"), None, "no such printer"); + assert_eq!(found(&list, "epson"), None, "names are exact"); + let default_one_sided = [printer("Epson", true, Some(false)), printer("Laser", false, Some(true))]; + assert_eq!(found(&default_one_sided, "default"), Some(("Epson".into(), Some(false))), "the default printer"); + let without_default = [printer("Epson", false, Some(false))]; + assert_eq!(found(&without_default, "default"), None); } /// `printer` leaves two-sided to the printer's own default (#2422); cut and stack still asks diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index 84cca070da..885a5953b3 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -39,7 +39,7 @@ use imposition::{PAGES_LISTED, appearance, appearance_matrix, decoded_contents, pub use imposition::{impose, keep_printed, sheet_pages}; pub use settings::{ Binding, BookletSubset, Content, Crop, LABEL_PAPER, Layout, MAX_GRID, Orientation, PAPERS, PageOrder, Raster, Settings, SizeMode, booklet_duplex, - cups_media, driver_needs_grayscale, media_option, + booklet_job_duplex, cups_media, driver_needs_grayscale, media_option, }; #[cfg(test)] use sheets::text_direction; diff --git a/crates/print/src/settings.rs b/crates/print/src/settings.rs index bdcb3e3540..619f775926 100644 --- a/crates/print/src/settings.rs +++ b/crates/print/src/settings.rs @@ -50,6 +50,20 @@ pub fn booklet_duplex(subset: BookletSubset) -> spool::Duplex { } } +/// What a booklet's job asks the printer for, given whether its driver takes two-sided settings +/// from other programs (`two_sided`, as the printer list reports it): [`booklet_duplex`], only as +/// Acrobat enables two-sided printing "if available". A driver that doesn't (`Some(false)`) would +/// refuse a two-sided job and print nothing, so both sides ask for nothing and its own setting +/// applies, such as manual two-sided printing chosen in its Properties (#5772). A driver of +/// unknown support is asked for two-sided as before; one side on its own is always one-sided. +pub fn booklet_job_duplex(subset: BookletSubset, two_sided: Option) -> Option { + match booklet_duplex(subset) { + spool::Duplex::Off => Some(spool::Duplex::Off), + _ if two_sided == Some(false) => None, + two_sided_duplex => Some(two_sided_duplex), + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum Binding { #[default] diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index 402b2aaa13..29d22d8eff 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -1538,6 +1538,19 @@ fn a_booklet_prints_both_sides_on_the_short_edge() { assert_eq!(booklet_duplex(BookletSubset::BackOnly), spool::Duplex::Off); } +/// #5772: both sides ask a driver that takes two-sided settings for the short edge, and one that +/// doesn't for nothing, so its own setting (manual two-sided, say) prints the booklet rather than +/// the job being refused. One side on its own asks every driver for one side. +#[test] +fn a_booklet_leaves_two_sided_to_a_driver_that_keeps_its_own() { + use BookletSubset::{BackOnly, BothSides, FrontOnly}; + for (two_sided, both) in [(Some(true), Some(spool::Duplex::ShortEdge)), (None, Some(spool::Duplex::ShortEdge)), (Some(false), None)] { + assert_eq!(booklet_job_duplex(BothSides, two_sided), both, "{two_sided:?}"); + assert_eq!(booklet_job_duplex(FrontOnly, two_sided), Some(spool::Duplex::Off), "{two_sided:?}"); + assert_eq!(booklet_job_duplex(BackOnly, two_sided), Some(spool::Duplex::Off), "{two_sided:?}"); + } +} + /// #3502: `/Contents` array pieces are joined with a space, matching the renderer, so a `%` /// comment at the end of a piece without a line break continues through the first line of the /// next piece in both `impose` and `text_reads_up`. diff --git a/crates/ui-egui/src/workflows/print_ui.rs b/crates/ui-egui/src/workflows/print_ui.rs index 89b51b34e2..c5b90ec12a 100644 --- a/crates/ui-egui/src/workflows/print_ui.rs +++ b/crates/ui-egui/src/workflows/print_ui.rs @@ -832,6 +832,33 @@ mod tests { assert!(cut.settings(4, &[]).is_err()); } + /// #5772: a booklet's both sides ask the printer for the short edge, unless its driver doesn't + /// take two-sided settings: it would refuse the job, so its own setting (manual two-sided, say) + /// prints it. One side on its own stays one-sided. + #[test] + fn a_booklet_leaves_two_sided_to_a_driver_that_keeps_its_own() { + // Listed as the dialog lists them (#3554), drivers unasked; the answers for the printers + // picked arrive in `two_sided`, and the network printer's never came. + let printer = |name: &str| spool::Printer { name: name.into(), default: false, two_sided: None, description: String::new() }; + let mut d = PrintDraft { + printers: vec![printer("Epson"), printer("Laser"), printer("Network")], + two_sided: [("Epson".to_string(), Some(false)), ("Laser".to_string(), Some(true))].into_iter().collect(), + handling: Handling::Booklet, + ..PrintDraft::default() + }; + for (name, both) in [("Epson", None), ("Laser", Some(spool::Duplex::ShortEdge)), ("Network", Some(spool::Duplex::ShortEdge))] { + d.printer = Some(name.into()); + d.booklet_subset = BookletSubset::BothSides; + assert_eq!(d.job("a.pdf").duplex, both, "{name}"); + d.booklet_subset = BookletSubset::FrontOnly; + assert_eq!(d.job("a.pdf").duplex, Some(spool::Duplex::Off), "{name}"); + } + // Save as PDF has no driver: as before. + d.printer = None; + d.booklet_subset = BookletSubset::BothSides; + assert_eq!(d.job("a.pdf").duplex, Some(spool::Duplex::ShortEdge)); + } + /// A job being spooled holds the progress card, counting its sheets as the spooler reports /// them; the card goes when the job ends and its answer arrives as a notice. A card another /// long job is showing is left alone. diff --git a/crates/ui-egui/src/workflows/print_ui/body.rs b/crates/ui-egui/src/workflows/print_ui/body.rs index 43e5323bd3..d4c9b3444d 100644 --- a/crates/ui-egui/src/workflows/print_ui/body.rs +++ b/crates/ui-egui/src/workflows/print_ui/body.rs @@ -288,10 +288,8 @@ pub(crate) fn body( } }); // Windows says which drivers can't print two-sided as they're asked; they keep their - // own setting, even when Two-sided is Off. - if let Some(p) = d.printers.iter().find(|p| d.printer.as_ref() == Some(&p.name)) - && p.two_sided.or_else(|| d.two_sided.get(&p.name).copied().flatten()) == Some(false) - { + // own setting, even when Two-sided is Off, and for a booklet's both sides. + if d.printer_two_sided() == Some(false) { ui.label(egui::RichText::new(tl!("This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties…")).small().color(t.text_muted)); } if d.printer.as_deref() == Some(crate::web_print::BROWSER_PRINTER) { diff --git a/crates/ui-egui/src/workflows/print_ui/draft.rs b/crates/ui-egui/src/workflows/print_ui/draft.rs index e7ad00df79..cf65351141 100644 --- a/crates/ui-egui/src/workflows/print_ui/draft.rs +++ b/crates/ui-egui/src/workflows/print_ui/draft.rs @@ -312,6 +312,16 @@ impl PrintDraft { self.handling == Handling::Multiple && self.order == PageOrder::CutStack } + /// Whether the chosen printer's driver takes two-sided settings from other programs: + /// `Some(false)` for one that keeps its own (no two-sided printing, or only manual two-sided in + /// its Properties); `None` when unknown, and for Save as PDF. The printer list says when it + /// knows; on Windows it lists the queues without asking their drivers (#3554), and the answer + /// for the printer picked arrives in [`PrintDraft::two_sided`]. + pub fn printer_two_sided(&self) -> Option { + let p = self.printers.iter().find(|p| self.printer.as_ref() == Some(&p.name))?; + p.two_sided.or_else(|| self.two_sided.get(&p.name).copied().flatten()) + } + /// The spooler job for this print: the dialog's copies, two-sided and colour settings, the /// driver options changed from the printer's defaults, and the chosen paper. pub fn job(&self, title: &str) -> spool::Job { @@ -320,11 +330,12 @@ impl PrintDraft { copies: self.copies.max(1), collate: self.collate, // Cut and stack needs single-sided sheets, whatever the printer's default; a booklet - // decides two-sided itself (#2707), leaving the Two-sided choice as it was. + // decides two-sided itself (#2707), as far as the driver takes it (#5772), leaving the + // Two-sided choice as it was. duplex: if self.cut_and_stack() { Some(spool::Duplex::Off) } else if self.handling == Handling::Booklet { - Some(print::booklet_duplex(self.booklet_subset)) + print::booklet_job_duplex(self.booklet_subset, self.printer_two_sided()) } else { self.duplex }, diff --git a/crates/ui-egui/tests/it/print.rs b/crates/ui-egui/tests/it/print.rs index 5e57179b13..89f9885477 100644 --- a/crates/ui-egui/tests/it/print.rs +++ b/crates/ui-egui/tests/it/print.rs @@ -1199,12 +1199,24 @@ fn a_custom_paper_prints_at_its_size_and_its_printer_keeps_it() { /// #2707: a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided /// while Booklet is chosen): the job flips on the short edge for both sides and prints /// one-sided for the front or back only, and the Two-sided choice is kept for other layouts. +/// #5772: on a printer whose driver doesn't take two-sided settings, both sides leave it to the +/// driver's own setting, and the dialog says so. #[test] fn a_booklet_decides_two_sided_itself() { - use pdfcraft_engine::print::{BookletSubset, spool::Duplex}; + use pdfcraft_engine::print::{BookletSubset, spool::Duplex, spool::Printer}; let mut h = harness(); + // Printers of its own, listed as the dialog lists them (drivers unasked, #3554), with what each + // driver said about two-sided: the machine's would decide what a booklet asks for, and on a + // machine with none the list that loads would take these away. + h.state_mut().printer_source = Some(std::sync::Arc::new(|| { + let printer = |name: &str| Printer { name: name.into(), default: name == "Laser", two_sided: None, description: String::new() }; + vec![printer("Laser"), printer("Inkjet")] + })); + h.state_mut().two_sided_source = None; + h.state_mut().print_draft.two_sided = [("Laser".to_string(), Some(true)), ("Inkjet".to_string(), Some(false))].into_iter().collect(); assert!(h.state_mut().execute("print.dialog")); - h.run_steps(3); + settle(&mut h); + assert_eq!(h.state().print_draft.printer.as_deref(), Some("Laser")); h.state_mut().print_draft.duplex = Some(Duplex::LongEdge); h.run_steps(1); assert!(h.query_by_role_and_label(Role::ComboBox, "Two-sided:").is_some()); @@ -1222,6 +1234,18 @@ fn a_booklet_decides_two_sided_itself() { h.state_mut().print_draft.booklet_subset = subset; assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::Off), "{subset:?} is one pass of a manual duplex"); } + let keeps_its_own = + "This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties…"; + assert!(h.query_by_label(keeps_its_own).is_none()); + h.state_mut().print_draft.printer = Some("Inkjet".into()); + h.state_mut().print_draft.booklet_subset = BookletSubset::BothSides; + h.run_steps(2); + assert_eq!(h.state().print_draft.job("x").duplex, None, "the inkjet's own two-sided setting prints both sides"); + assert!(h.query_by_label(keeps_its_own).is_some(), "the dialog says so"); + h.state_mut().print_draft.booklet_subset = BookletSubset::FrontOnly; + assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::Off), "one side stays one-sided"); + h.state_mut().print_draft.printer = Some("Laser".into()); + h.run_steps(1); h.get_by_label("Size").click(); h.run_steps(2); assert!(h.query_by_role_and_label(Role::ComboBox, "Two-sided:").is_some()); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index ce9188932e..eced73a64a 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8309,8 +8309,8 @@ milestone = "M10" status = "shipped" commands = ["print.dialog"] tools = ["doc_print"] -evidence = ["crates/print/src/tests.rs::booklets_pair_pages_for_folding", "crates/ui-egui/tests/it/print.rs::a_booklet_decides_two_sided_itself", "crates/automation/src/printing.rs::a_booklet_decides_two_sided_itself", "crates/print/src/tests.rs::a_booklet_prints_both_sides_on_the_short_edge", "crates/print/src/tests.rs::booklet_creep_moves_inner_sheets_toward_the_fold"] -notes = "both/front/back, left/right binding; a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided while Booklet is chosen; Using Acrobat X Pro: Acrobat automatically enables duplex printing for booklet printing): both sides flip on the short edge, front or back only prints one-sided, and the Two-sided choice is kept for the other layouts (#2707); creep compensation (points per sheet, default 0) moves each sheet's pages toward the fold by their sheet's place from the outside, clipped at the fold (#6777); the amount is entered, not worked out from the paper's thickness" +evidence = ["crates/print/src/tests.rs::booklets_pair_pages_for_folding", "crates/ui-egui/tests/it/print.rs::a_booklet_decides_two_sided_itself", "crates/automation/src/printing.rs::a_booklet_decides_two_sided_itself", "crates/print/src/tests.rs::a_booklet_prints_both_sides_on_the_short_edge", "crates/print/src/tests.rs::booklet_creep_moves_inner_sheets_toward_the_fold", "crates/print/src/tests.rs::a_booklet_leaves_two_sided_to_a_driver_that_keeps_its_own", "crates/ui-egui/src/workflows/print_ui.rs::a_booklet_leaves_two_sided_to_a_driver_that_keeps_its_own"] +notes = "both/front/back, left/right binding; a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided while Booklet is chosen; Using Acrobat X Pro: Acrobat automatically enables duplex printing for booklet printing): both sides flip on the short edge (as Acrobat, \"if available\": a printer whose driver doesn't take two-sided settings, such as one with only manual two-sided in its Properties or Microsoft Print to PDF, is asked for nothing, so its own setting applies rather than the job being refused, #5772), front or back only prints one-sided, and the Two-sided choice is kept for the other layouts (#2707). Not yet: a guided manual two-sided pass (front, turn the stack, back) on such a printer; creep compensation (points per sheet, default 0) moves each sheet's pages toward the fold by their sheet's place from the outside, clipped at the fold (#6777); the amount is entered, not worked out from the paper's thickness" [[feature]] id = "print.poster"