Merge PR #2748: M10: a booklet decides two-sided itself, as Acrobat does (#2707)

Conflicts: crates/automation/src/printing.rs: kept job_duplex (#2748) next to bad; #4161's paper_named/spool_job unchanged.
This commit is contained in:
aiongg
2026-10-24 06:22:34 +00:00
7 changed files with 105 additions and 22 deletions
+24 -1
View File
@@ -22,6 +22,16 @@ fn duplex(value: &str, cut_stack: bool) -> Result<Option<spool::Duplex>> {
})
}
/// [`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<Option<spool::Duplex>> {
let chosen = duplex(value, matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }))?;
Ok(match layout {
Layout::Booklet { subset, .. } => Some(print::booklet_duplex(*subset)),
_ => chosen,
})
}
/// An invalid-arguments error saying `m`.
fn bad(m: impl Into<String>) -> ToolError {
ToolError::InvalidArgs(m.into())
@@ -161,7 +171,7 @@ impl Automation {
},
l => return Err(bad(format!("unknown layout {l:?}"))),
};
let duplex = duplex(a.opt_str("duplex")?.unwrap_or("off"), matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }))?;
let duplex = job_duplex(a.opt_str("duplex")?.unwrap_or("off"), &layout)?;
if a.opt_str("order")? == Some("cut-stack") && !matches!(layout, Layout::Multiple { .. }) {
return Err(bad("cut-stack order needs layout multiple"));
}
@@ -215,6 +225,19 @@ impl Automation {
mod tests {
use super::*;
/// #2707: a booklet sets two-sided itself whatever `duplex` asks; a bad name is still an error.
#[test]
fn a_booklet_decides_two_sided_itself() {
let booklet = |subset| Layout::Booklet { subset, binding: Binding::Left };
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!(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");
}
/// `printer` leaves two-sided to the printer's own default (#2422); cut and stack still asks
/// for single-sided sheets, and refuses an explicit edge.
#[test]
+1 -1
View File
@@ -543,7 +543,7 @@ pub fn tools() -> Vec<ToolDef> {
t(
"doc_print",
"Print",
"Print with Acrobat's Print dialog options, or save the print-ready PDF. pages: a range such as \"1-3, 6, 9-\" (page labels allowed; default all); subset odd/even (by page number, as Acrobat counts them: pages 2, 7-10 with even prints 2, 8 and 10, not every second page of the range); reverse. layout: fit (default), actual, shrink, custom (scale %), multiple (per_sheet 2/4/6/9/16, order, border, auto_rotate; cut-stack order arranges single-sided sheets for cutting into piles and stacking left to right, top to bottom, keeping sheet order within each pile; duplex must be off), booklet (booklet_subset both/front/back, binding left/right), poster (scale %, overlap pt, cut_marks). orientation auto/portrait/landscape; comments_forms document / document-and-markups (default) / document-and-stamps / form-fields-only; paper Letter/Legal/Tabloid, ARCH A/B/C/D/E1/E, ANSI C/D/E, A0/A1/A2/A3/A4/A5/A6, or US #10 Envelope (short names or full preset labels); use layout actual for 100% in the print-ready PDF; printer drivers may still substitute paper or scale. Then path (save) or printer (a name or \"default\") with copies, collate, duplex off/long-edge/short-edge, grayscale, and options (driver options from printer_options, CUPS).",
"Print with Acrobat's Print dialog options, or save the print-ready PDF. pages: a range such as \"1-3, 6, 9-\" (page labels allowed; default all); subset odd/even (by page number, as Acrobat counts them: pages 2, 7-10 with even prints 2, 8 and 10, not every second page of the range); reverse. layout: fit (default), actual, shrink, custom (scale %), multiple (per_sheet 2/4/6/9/16, order, border, auto_rotate; cut-stack order arranges single-sided sheets for cutting into piles and stacking left to right, top to bottom, keeping sheet order within each pile; duplex must be off), booklet (booklet_subset both/front/back, binding left/right; a booklet sets two-sided itself, as Acrobat does: both flips on the short edge, front or back prints one-sided), poster (scale %, overlap pt, cut_marks). orientation auto/portrait/landscape; comments_forms document / document-and-markups (default) / document-and-stamps / form-fields-only; paper Letter/Legal/Tabloid, ARCH A/B/C/D/E1/E, ANSI C/D/E, A0/A1/A2/A3/A4/A5/A6, or US #10 Envelope (short names or full preset labels); use layout actual for 100% in the print-ready PDF; printer drivers may still substitute paper or scale. Then path (save) or printer (a name or \"default\") with copies, collate, duplex off/long-edge/short-edge, grayscale, and options (driver options from printer_options, CUPS).",
)
.with(schema(
json!({
+13
View File
@@ -149,6 +149,19 @@ pub enum BookletSubset {
BackOnly,
}
/// How a booklet goes through the printer (#2707): it decides two-sided printing itself, as
/// Acrobat does (its Print dialog hides Two-sided while Booklet is chosen; Using Acrobat X Pro,
/// Print dialog box options: "Acrobat automatically enables duplex printing, if available, for
/// booklet printing"). Both sides flip on the short edge, the fold of a 2-up sheet turned
/// sideways; front side only or back side only is one pass of a manual duplex and prints
/// single-sided.
pub fn booklet_duplex(subset: BookletSubset) -> spool::Duplex {
match subset {
BookletSubset::BothSides => spool::Duplex::ShortEdge,
BookletSubset::FrontOnly | BookletSubset::BackOnly => spool::Duplex::Off,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Binding {
#[default]
+8
View File
@@ -1023,3 +1023,11 @@ fn odd_and_even_pages_are_page_numbers_not_places_in_the_range() {
// A range with no page of the kind prints nothing.
assert_eq!(select_pages(12, Some("3, 5"), &[], Subset::Even, false), Err(PrintError::NoPages));
}
/// #2707: a booklet's two-sided setting follows its subset.
#[test]
fn a_booklet_prints_both_sides_on_the_short_edge() {
assert_eq!(booklet_duplex(BookletSubset::BothSides), spool::Duplex::ShortEdge);
assert_eq!(booklet_duplex(BookletSubset::FrontOnly), spool::Duplex::Off);
assert_eq!(booklet_duplex(BookletSubset::BackOnly), spool::Duplex::Off);
}
+29 -18
View File
@@ -230,8 +230,15 @@ impl PrintDraft {
printer: self.printer.clone(),
copies: self.copies.max(1),
collate: self.collate,
// Cut and stack needs single-sided sheets, whatever the printer's default.
duplex: if self.cut_and_stack() { Some(spool::Duplex::Off) } else { self.duplex },
// 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.
duplex: if self.cut_and_stack() {
Some(spool::Duplex::Off)
} else if self.handling == Handling::Booklet {
Some(print::booklet_duplex(self.booklet_subset))
} else {
self.duplex
},
scaling: spool::Scaling::Exact,
grayscale: self.grayscale,
title: title.to_string(),
@@ -1019,22 +1026,26 @@ pub(crate) fn body(
});
});
ui.end_row();
ui.label(tl!("Two-sided:"));
ui.add_enabled_ui(!browser, |ui| {
combo(
ui,
"duplex",
&mut d.duplex,
&[
(None, tl!("Printer default")),
(Some(spool::Duplex::Off), tl!("Off")),
(Some(spool::Duplex::LongEdge), tl!("Flip on long edge")),
(Some(spool::Duplex::ShortEdge), tl!("Flip on short edge")),
],
160.0,
);
});
ui.end_row();
// A booklet decides two-sided itself (#2707), so the choice is hidden meanwhile, as
// in Acrobat's Print dialog; it is kept for the other layouts.
if d.handling != Handling::Booklet {
ui.label(tl!("Two-sided:"));
ui.add_enabled_ui(!browser, |ui| {
combo(
ui,
"duplex",
&mut d.duplex,
&[
(None, tl!("Printer default")),
(Some(spool::Duplex::Off), tl!("Off")),
(Some(spool::Duplex::LongEdge), tl!("Flip on long edge")),
(Some(spool::Duplex::ShortEdge), tl!("Flip on short edge")),
],
160.0,
);
});
ui.end_row();
}
ui.label(tl!("Paper:"));
let paper_name = PAPERS.get(d.paper).map_or("US Letter", |p| p.0);
let before = d.paper;
+28
View File
@@ -873,3 +873,31 @@ fn restored_paper_keeps_preview_and_saved_sheet_dimensions() {
assert!((size.0 - a4.0).abs() < 0.01 && (size.1 - a4.1).abs() < 0.01, "saved sheet: {size:?}");
std::fs::remove_file(out).unwrap();
}
/// #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.
#[test]
fn a_booklet_decides_two_sided_itself() {
use pdfcraft_engine::print::{BookletSubset, spool::Duplex};
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
h.state_mut().print_draft.duplex = Some(Duplex::LongEdge);
h.run_steps(1);
assert!(h.query_by_label("Two-sided:").is_some());
h.get_by_label("Booklet").click();
h.run_steps(2);
assert!(h.query_by_label("Two-sided:").is_none(), "hidden while Booklet is chosen");
let d = &h.state().print_draft;
assert_eq!(d.duplex, Some(Duplex::LongEdge), "the Two-sided choice is kept");
assert_eq!(d.job("x").duplex, Some(Duplex::ShortEdge), "both sides flip on the short edge");
for subset in [BookletSubset::FrontOnly, BookletSubset::BackOnly] {
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");
}
h.get_by_label("Size").click();
h.run_steps(2);
assert!(h.query_by_label("Two-sided:").is_some());
assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::LongEdge), "back to the choice");
}
+2 -2
View File
@@ -8073,8 +8073,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["doc_print"]
evidence = ["crates/print/src/tests.rs::booklets_pair_pages_for_folding"]
notes = "both/front/back, left/right binding"
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"]
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)"
[[feature]]
id = "print.poster"