mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 20:18:06 -06:00
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:
co-authored by
Claude Opus 5.5
parent
3fbfa4e2af
commit
428cbc083d
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)." },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user