Merge PR #2584: M10: Two-sided starts at Printer default, so the printer's own duplex applies (#2422)

ATTRIBUTION.toml: kept main's sha256 lines; hashes recomputed after the track.
This commit is contained in:
aiongg
2026-10-22 01:45:00 +00:00
28 changed files with 96 additions and 23 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
@@ -518,7 +518,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
@@ -5683,6 +5683,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);
}
+1
View File
@@ -1219,6 +1219,7 @@
Off متوقف
Flip on long edge قلب على الحافة الطويلة
Flip on short edge قلب على الحافة القصيرة
Printer default افتراضي الطابعة
Paper: الورق:
Pages to Print الصفحات المراد طباعتها
More options: خيارات أخرى:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -899,6 +899,7 @@
Off Изключено
Flip on long edge Обръщане по дългия ръб
Flip on short edge Обръщане по късия ръб
Printer default По подразбиране за принтера
Paper: Хартия:
Pages to Print Страници за печат
More options: Още опции:
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -1287,6 +1287,7 @@ key Space Mezerník
Off Vypnuto
Flip on long edge Převrátit podle delší strany
Flip on short edge Převrátit podle kratší strany
Printer default Podle tiskárny
Paper: Papír:
Pages to Print Stránky k tisku
More options: Další možnosti:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1205,6 +1205,7 @@
Off Aus
Flip on long edge An langer Kante wenden
Flip on short edge An kurzer Kante wenden
Printer default Druckerstandard
Paper: Papier:
Pages to Print Zu druckende Seiten
More options: Weitere Optionen:
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -764,6 +764,7 @@
Two-sided: Εκτύπωση διπλής όψης:
Flip on long edge Αναστροφή στη μεγάλη πλευρά
Flip on short edge Αναστροφή στη μικρή πλευρά
Printer default Προεπιλογή εκτυπωτή
Paper: Χαρτί:
Pages to Print Σελίδες προς εκτύπωση
More options: Περισσότερες επιλογές:
Can't render this file because it contains an unexpected character in line 477 and column 9.
+1
View File
@@ -1214,6 +1214,7 @@
Off Desactivado
Flip on long edge Voltear por el borde largo
Flip on short edge Voltear por el borde corto
Printer default Predeterminado de la impresora
Paper: Papel:
Pages to Print Páginas que imprimir
More options: Más opciones:
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -1200,6 +1200,7 @@
Off Désactivé
Flip on long edge Retourner sur le bord long
Flip on short edge Retourner sur le bord court
Printer default Par défaut de l'imprimante
Paper: Papier :
Pages to Print Pages à imprimer
More options: Plus d'options :
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -1190,6 +1190,7 @@
Off Ki
Flip on long edge Hosszú él mentén
Flip on short edge Rövid él mentén
Printer default Nyomtató alapértéke
Paper: Papír:
Pages to Print Nyomtatandó oldalak
More options: További beállítások:
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -1216,6 +1216,7 @@
Off Disattivato
Flip on long edge Rilegatura sul lato lungo
Flip on short edge Rilegatura sul lato corto
Printer default Come da stampante
Paper: Carta:
Pages to Print Pagine da stampare
More options: Altre opzioni:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1220,6 +1220,7 @@
Off オフ
Flip on long edge 長辺とじ
Flip on short edge 短辺とじ
Printer default プリンターの既定
Paper: 用紙:
Pages to Print 印刷するページ
More options: 詳細オプション:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1164,6 +1164,7 @@
Off Өшірулі
Flip on long edge Ұзын жиек бойынша аудару
Flip on short edge Қысқа жиек бойынша аудару
Printer default Принтер әдепкісі
Paper: Қағаз:
Pages to Print Басып шығарылатын беттер
More options: Қосымша параметрлер:
Can't render this file because it contains an unexpected character in line 856 and column 9.
+1
View File
@@ -592,6 +592,7 @@
Flip horizontal Apversti horizontaliai
Flip on long edge Apversti per ilgąją kraštinę
Flip on short edge Apversti per trumpąją kraštinę
Printer default Pagal spausdintuvą
Flip vertical Apversti vertikaliai
Folders Aplankai
Font Size: Šrifto dydis:
Can't render this file because it contains an unexpected character in line 404 and column 9.
+1
View File
@@ -1203,6 +1203,7 @@
Off Wyłączony
Flip on long edge Odwracaj wzdłuż długiej krawędzi
Flip on short edge Odwracaj wzdłuż krótkiej krawędzi
Printer default Domyślne drukarki
Paper: Papier:
Pages to Print Strony do wydrukowania
More options: Więcej opcji:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -845,6 +845,7 @@
Off Desativado
Flip on long edge Virar na borda longa
Flip on short edge Virar na borda curta
Printer default Padrão da impressora
Paper: Papel:
Pages to Print Páginas a imprimir
More options: Mais opções:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1220,6 +1220,7 @@
Off Выкл
Flip on long edge Переворот по длинной стороне
Flip on short edge Переворот по короткой стороне
Printer default По умолчанию для принтера
Paper: Бумага:
Pages to Print Страницы для печати
More options: Дополнительные параметры:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -839,6 +839,7 @@
Flip image Prezrcali sliko
Flip on long edge Obrni po dolgem robu
Flip on short edge Obrni po kratkem robu
Printer default Privzeto tiskalnika
Flip vertical Prezrcali navpično
Folders Mape
Font Size: Velikost pisave:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1190,6 +1190,7 @@ comment menu Edit సవరించు
Off ఆఫ్
Flip on long edge పొడవైన అంచున తిరగేయి
Flip on short edge పొట్టి అంచున తిరగేయి
Printer default ప్రింటర్ డిఫాల్ట్
Paper: కాగితం:
Pages to Print ముద్రించాల్సిన పేజీలు
More options: మరిన్ని ఎంపికలు:
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -1177,6 +1177,7 @@
Off Kapalı
Flip on long edge Uzun kenardan çevir
Flip on short edge Kısa kenardan çevir
Printer default Yazıcı varsayılanı
Paper: Kağıt:
Pages to Print Yazdırılacak Sayfalar
More options: Diğer seçenekler:
Can't render this file because it contains an unexpected character in line 855 and column 9.
+1
View File
@@ -1217,6 +1217,7 @@
Off Вимкнено
Flip on long edge Перевертати за довгим краєм
Flip on short edge Перевертати за коротким краєм
Printer default За замовчуванням принтера
Paper: Папір:
Pages to Print Сторінки для друку
More options: Додаткові параметри:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -589,6 +589,7 @@ theme Appearance Giao diện
Flip horizontal Lật ngang
Flip on long edge Lật theo cạnh dài
Flip on short edge Lật theo cạnh ngắn
Printer default Mặc định của máy in
Flip vertical Lật dọc
Folders Thư mục
Font Size: Cỡ chữ:
Can't render this file because it contains an unexpected character in line 491 and column 12.
+1
View File
@@ -906,6 +906,7 @@
Off 关
Flip on long edge 沿长边翻转
Flip on short edge 沿短边翻转
Printer default 打印机默认
Paper: 纸张:
Pages to Print 要打印的页面
More options: 更多选项:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -850,6 +850,7 @@
Off 關
Flip on long edge 沿長邊翻轉
Flip on short edge 沿短邊翻轉
Printer default 印表機預設
Paper: 紙張:
Pages to Print 要列印的頁面
More options: 更多選項:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+30 -9
View File
@@ -51,7 +51,8 @@ pub struct PrintDraft {
pub copies: u32,
pub collate: bool,
pub grayscale: bool,
pub duplex: spool::Duplex,
/// `None` = the printer's own default (its Properties), as Acrobat does.
pub duplex: Option<spool::Duplex>,
pub which: Which,
pub range: String,
/// The pages `Which::Selected` prints (0-based, in page order); empty when none were picked.
@@ -108,7 +109,7 @@ impl Default for PrintDraft {
copies: 1,
collate: true,
grayscale: false,
duplex: spool::Duplex::Off,
duplex: None,
which: Which::All,
range: String::new(),
selected: Vec::new(),
@@ -168,7 +169,7 @@ impl PrintDraft {
/// The engine settings for this draft (page count and labels from the document).
pub fn settings(&self, count: usize, labels: &[String]) -> Result<print::Settings, String> {
if self.handling == Handling::Multiple && self.order == PageOrder::CutStack && self.duplex != spool::Duplex::Off {
if self.cut_and_stack() && matches!(self.duplex, Some(spool::Duplex::LongEdge | spool::Duplex::ShortEdge)) {
return Err("Cut and stack needs Two-sided: Off. Print single-sided sheets.".into());
}
let range = match self.which {
@@ -211,12 +212,17 @@ impl PrintDraft {
})
}
fn cut_and_stack(&self) -> bool {
self.handling == Handling::Multiple && self.order == PageOrder::CutStack
}
pub fn job(&self, title: &str) -> spool::Job {
spool::Job {
printer: self.printer.clone(),
copies: self.copies.max(1),
collate: self.collate,
duplex: Some(self.duplex),
// 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 },
scaling: spool::Scaling::Exact,
grayscale: self.grayscale,
title: title.to_string(),
@@ -928,9 +934,10 @@ pub(crate) fn body(
"duplex",
&mut d.duplex,
&[
(spool::Duplex::Off, tl!("Off")),
(spool::Duplex::LongEdge, tl!("Flip on long edge")),
(spool::Duplex::ShortEdge, tl!("Flip on short edge")),
(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,
);
@@ -1305,11 +1312,25 @@ mod tests {
#[test]
fn composed_sheets_keep_exact_scaling_and_the_dialogs_duplex_choice() {
let draft = PrintDraft { duplex: spool::Duplex::LongEdge, ..PrintDraft::default() };
let draft = PrintDraft { duplex: Some(spool::Duplex::LongEdge), ..PrintDraft::default() };
let job = draft.job("report.pdf");
assert_eq!(job.scaling, spool::Scaling::Exact);
assert_eq!(job.duplex, Some(spool::Duplex::LongEdge));
assert_eq!(PrintDraft::default().job("report.pdf").duplex, Some(spool::Duplex::Off));
let off = PrintDraft { duplex: Some(spool::Duplex::Off), ..PrintDraft::default() };
assert_eq!(off.job("report.pdf").duplex, Some(spool::Duplex::Off));
}
/// Untouched, Two-sided sends nothing, so the duplex set in the printer's Properties applies
/// (#2422); cut and stack still asks for single-sided sheets.
#[test]
fn two_sided_follows_the_printer_until_chosen() {
assert_eq!(PrintDraft::default().duplex, None);
assert_eq!(PrintDraft::default().job("report.pdf").duplex, None);
let mut cut = PrintDraft { handling: Handling::Multiple, order: PageOrder::CutStack, ..PrintDraft::default() };
assert_eq!(cut.job("report.pdf").duplex, Some(spool::Duplex::Off));
assert!(cut.settings(4, &[]).is_ok(), "the printer's default doesn't block cut and stack");
cut.duplex = Some(spool::Duplex::ShortEdge);
assert!(cut.settings(4, &[]).is_err());
}
/// A job being spooled holds the progress card, counting its sheets as the spooler reports
+2 -2
View File
@@ -182,11 +182,11 @@ fn cut_stack_dialog_previews_saves_and_refuses_duplex() {
h.get_by_label("›").click();
h.run_steps(2);
h.get_by_label("Sheet 2 of 3");
h.state_mut().print_draft.duplex = Duplex::LongEdge;
h.state_mut().print_draft.duplex = Some(Duplex::LongEdge);
h.run_steps(2);
h.get_by_label_contains("Cut and stack needs Two-sided: Off");
assert!(!h.state_mut().print_now());
h.state_mut().print_draft.duplex = Duplex::Off;
h.state_mut().print_draft.duplex = Some(Duplex::Off);
h.state_mut().print_draft.printer = None;
let out = std::env::temp_dir().join(format!("pdfcraft-cut-stack-ui-{}.pdf", std::process::id()));
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
+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"]
notes = "long/short edge via the spooler: lp -o sides=…, and the Windows driver's Duplex setting"
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"