M10: doc_print takes duplex "printer", leaving two-sided to the printer (#2422)

The headless counterpart of the Print dialog's Printer default: duplex
"printer" sends no two-sided setting, so the printer's own default applies.
The default stays "off", so existing scripts behave as before. Cut and stack
with "printer" sends single-sided sheets, as the dialog does, and still
refuses an explicit edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
magdale76
2026-10-17 06:42:10 +02:00
co-authored by Claude Opus 5.5
parent 3fbfa4e2af
commit 428cbc083d
4 changed files with 42 additions and 12 deletions
+36 -9
View File
@@ -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<Option<spool::Duplex>> {
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<String>) -> 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());
}
}
+1 -1
View File
@@ -507,7 +507,7 @@ pub fn tools() -> Vec<ToolDef> {
"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)." },
+3
View File
@@ -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);
}
+2 -2
View File
@@ -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"