diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index 19cd48ef4..39cd45760 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -6,6 +6,22 @@ use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed, write_atomic}; +/// The job's two-sided setting for `doc_print`'s `duplex`: `printer` sends none, so the printer's +/// own default applies (as the Print dialog's Printer default, #2422), except that cut and stack +/// needs single-sided sheets. +fn duplex(value: &str, cut_stack: bool) -> Result> { + Ok(match value { + "printer" if cut_stack => Some(spool::Duplex::Off), + "printer" => None, + "off" => Some(spool::Duplex::Off), + "long-edge" if cut_stack => return Err(bad("cut-and-stack printing needs duplex off (single-sided sheets)")), + "short-edge" if cut_stack => return Err(bad("cut-and-stack printing needs duplex off (single-sided sheets)")), + "long-edge" => Some(spool::Duplex::LongEdge), + "short-edge" => Some(spool::Duplex::ShortEdge), + d => return Err(bad(format!("unknown duplex {d:?}"))), + }) +} + fn bad(m: impl Into) -> ToolError { ToolError::InvalidArgs(m.into()) } @@ -92,9 +108,7 @@ impl Automation { }, l => return Err(bad(format!("unknown layout {l:?}"))), }; - if matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }) && a.opt_str("duplex")?.unwrap_or("off") != "off" { - return Err(bad("cut-and-stack printing needs duplex off (single-sided sheets)")); - } + let duplex = duplex(a.opt_str("duplex")?.unwrap_or("off"), matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }))?; if a.opt_str("order")? == Some("cut-stack") && !matches!(layout, Layout::Multiple { .. }) { return Err(bad("cut-stack order needs layout multiple")); } @@ -153,12 +167,7 @@ impl Automation { printer: if printer == "default" { None } else { Some(printer.to_string()) }, copies: a.opt_int("copies")?.unwrap_or(1).clamp(1, 999) as u32, collate: a.opt_bool("collate")?.unwrap_or(true), - duplex: Some(match a.opt_str("duplex")?.unwrap_or("off") { - "off" => spool::Duplex::Off, - "long-edge" => spool::Duplex::LongEdge, - "short-edge" => spool::Duplex::ShortEdge, - d => return Err(bad(format!("unknown duplex {d:?}"))), - }), + duplex, scaling: spool::Scaling::Exact, grayscale: a.opt_bool("grayscale")?.unwrap_or(false), title: name, @@ -181,3 +190,21 @@ impl Automation { Ok(out) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// `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] + fn duplex_printer_leaves_two_sided_to_the_printer() { + assert_eq!(duplex("printer", false).ok(), Some(None)); + assert_eq!(duplex("printer", true).ok(), Some(Some(spool::Duplex::Off))); + assert_eq!(duplex("off", false).ok(), Some(Some(spool::Duplex::Off))); + assert_eq!(duplex("long-edge", false).ok(), Some(Some(spool::Duplex::LongEdge))); + assert_eq!(duplex("short-edge", false).ok(), Some(Some(spool::Duplex::ShortEdge))); + assert!(duplex("long-edge", true).is_err()); + assert!(duplex("sideways", false).is_err()); + } +} diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 5e89aef78..58e4fa508 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -507,7 +507,7 @@ pub fn tools() -> Vec { "printer": { "type": "string" }, "copies": { "type": "integer", "minimum": 1, "maximum": 999 }, "collate": { "type": "boolean" }, - "duplex": { "type": "string", "enum": ["off", "long-edge", "short-edge"] }, + "duplex": { "type": "string", "enum": ["off", "long-edge", "short-edge", "printer"], "description": "Two-sided printing (default off); printer sends no setting, so the printer's own default applies." }, "grayscale": { "type": "boolean" }, "as_image": { "type": "boolean", "description": "Print as image (Acrobat's Advanced Print Setup): every page rendered to a picture at dpi before it is placed on the paper, for printers that skip text, shading or transparency; the print is no longer searchable text, and with grayscale the pictures are grey." }, "dpi": { "type": "number", "minimum": 72, "maximum": 600, "description": "Resolution for as_image (default 300)." }, diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 1e662d8f3..66f968a43 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -5300,6 +5300,9 @@ fn cut_stack_printing_through_tools() { } assert!(matches!(a.call("doc_print", &json!({"doc": doc, "order": "cut-stack", "path": "refused.pdf"})), Err(ToolError::InvalidArgs(_)))); assert!(!dir.join("refused.pdf").exists()); + // The printer's own two-sided default isn't refused: cut and stack sends single-sided (#2422). + ok(&mut a, "doc_print", json!({"doc": doc, "layout": "multiple", "order": "cut-stack", "duplex": "printer", "path": "printer-default.pdf"})); + assert!(dir.join("printer-default.pdf").exists()); assert!(a.call("doc_print", &json!({"doc": doc, "layout": "multiple", "order": "cut-stack", "path": "../escaped.pdf"})).is_err()); let _ = std::fs::remove_dir_all(dir); } diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7ecd6b403..99550a9ad 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8020,8 +8020,8 @@ milestone = "M10" status = "shipped" commands = ["print.dialog"] tools = ["doc_print"] -evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/ui-egui/src/print_ui.rs::two_sided_follows_the_printer_until_chosen"] -notes = "long/short edge via the spooler: lp -o sides=…, and the Windows driver's Duplex setting. Two-sided starts at Printer default, as in Acrobat: nothing is sent, so the printer's own default (its Properties) applies until Off or an edge is chosen. Missing: the document's /ViewerPreferences /Duplex preset isn't read yet" +evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/ui-egui/src/print_ui.rs::two_sided_follows_the_printer_until_chosen", "crates/automation/src/printing.rs::duplex_printer_leaves_two_sided_to_the_printer"] +notes = "long/short edge via the spooler: lp -o sides=…, and the Windows driver's Duplex setting. Two-sided starts at Printer default, as in Acrobat: nothing is sent, so the printer's own default (its Properties) applies until Off or an edge is chosen (doc_print: duplex \"printer\"). Missing: the document's /ViewerPreferences /Duplex preset isn't read yet" [[feature]] id = "print.n-up"