From 43f94aaaa5d95418b36a9cb7c1b56bf484c31cb5 Mon Sep 17 00:00:00 2001 From: Combiler01 Date: Mon, 9 Nov 2026 16:13:57 +0530 Subject: [PATCH] fix(print): carry media option for custom and detected driver papers in spool jobs --- crates/automation/src/printing.rs | 60 +++++++++---------- crates/print/src/lib.rs | 2 +- crates/print/src/settings.rs | 46 ++++++++++++++ crates/print/src/tests.rs | 22 +++++++ .../ui-egui/src/workflows/print_ui/draft.rs | 2 +- 5 files changed, 100 insertions(+), 32 deletions(-) diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index 919e8d958a..2a562a78d3 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -37,9 +37,9 @@ fn bad(m: impl Into) -> ToolError { ToolError::InvalidArgs(m.into()) } -/// Parse explicit custom dimensions like "300x500", "300 x 500", "300x500 pt". +/// Parse explicit custom dimensions like "300x500", "300 x 500", "300x500 pt", "Custom.300x500". fn parse_custom_dimensions(s: &str) -> Option<(f64, f64)> { - let s = s.trim().trim_end_matches("pt").trim_end_matches("points").trim(); + let s = s.trim().trim_start_matches("Custom.").trim_start_matches("custom.").trim_end_matches("pt").trim_end_matches("points").trim(); let (w_str, h_str) = s.split_once(['x', 'X', '*'])?; let w: f64 = w_str.trim().parse().ok()?; let h: f64 = h_str.trim().parse().ok()?; @@ -47,8 +47,8 @@ fn parse_custom_dimensions(s: &str) -> Option<(f64, f64)> { } /// `doc_print`'s `paper`, by its name, short form, driver-detected paper name, or custom dimensions. -/// Returns `(matching_papers_index, (width, height))`. -fn resolve_paper(name: Option<&str>, printer: Option<&str>) -> Result<(Option, (f64, f64))> { +/// Returns `((width, height), media_name)`. +fn resolve_paper(name: Option<&str>, printer: Option<&str>) -> Result<((f64, f64), String)> { if let Some(p) = name { // 1. Match standard PAPERS presets. for (index, (n, size)) in PAPERS.iter().enumerate() { @@ -56,26 +56,20 @@ fn resolve_paper(name: Option<&str>, printer: Option<&str>) -> Result<(Optionx"). + // 3. Match custom dimensions ("x"). if let Some(size) = parse_custom_dimensions(p) { - return Ok((print::matching_paper(size), size)); + return Ok((size, print::paper_media(None, size))); } return Err(bad(format!( "unknown paper {p:?}; choose one of {}, a paper from printer_papers, or dimensions \"x\"", @@ -85,19 +79,19 @@ fn resolve_paper(name: Option<&str>, printer: Option<&str>) -> Result<(Option, paper: Option) -> Result { +/// (as `media` after the driver's `options`) rather than the queue's default paper, which a job +/// naming none got (A4 under Letter sheets on the queue in #3752). +fn spool_job(a: &Args, printer: &str, title: String, duplex: Option, media: Option<&str>) -> Result { let options = match a.get("options") { None => Vec::new(), Some(Value::Object(m)) => m @@ -117,7 +111,7 @@ fn spool_job(a: &Args, printer: &str, title: String, duplex: Option { let actual_printer = resolved_printer.as_deref().unwrap_or(printer); - let job = spool_job(a, actual_printer, self.doc(a)?.name.clone(), duplex, paper_index)?; + let job = spool_job(a, actual_printer, self.doc(a)?.name.clone(), duplex, Some(&media))?; out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?); } _ => return Err(bad("pass either path (save the print-ready PDF) or printer (a name from printers, or \"default\")")), @@ -316,8 +310,8 @@ mod tests { #[test] fn printing_to_a_printer_feeds_the_laid_out_paper() { let job = |args: Value, paper_name: Option<&str>| { - let (index, _) = resolve_paper(paper_name, None).unwrap(); - spool_job(&Args(&args), "Office", "a.pdf".into(), None, index).unwrap() + let (_, media) = resolve_paper(paper_name, None).unwrap(); + spool_job(&Args(&args), "Office", "a.pdf".into(), None, Some(&media)).unwrap() }; let media = |m: &str| ("media".to_string(), m.to_string()); // US Letter when no paper is asked for, after the driver's own options. @@ -326,14 +320,20 @@ mod tests { assert_eq!((letter.printer.as_deref(), letter.title.as_str()), (Some("Office"), "a.pdf")); // Every paper, by its full name, goes out as its own media name. for (i, (name, size)) in PAPERS.iter().enumerate() { - assert_eq!(resolve_paper(Some(name), None).ok(), Some((Some(i), *size)), "{name}"); + assert_eq!(resolve_paper(Some(name), None).ok(), Some((*size, print::cups_media(i).unwrap().to_string())), "{name}"); assert_eq!(job(json!({}), Some(name)).options, print::media_option(i).into_iter().collect::>(), "{name}"); } // Short forms find the same paper. assert_eq!(job(json!({}), Some("letter")).options, [media("Letter")]); assert_eq!(job(json!({}), Some("ARCH D")).options, [media("na_arch-d_24x36in")]); + + // Non-preset dimensions and driver papers carry their media selection. + assert_eq!(job(json!({}), Some("300x500")).options, [media("Custom.300x500")]); + assert_eq!(job(json!({}), Some("300 x 500 pt")).options, [media("Custom.300x500")]); + assert_eq!(job(json!({}), Some("Custom.300x500")).options, [media("Custom.300x500")]); + assert!(matches!(resolve_paper(Some("Quarto"), None), Err(ToolError::InvalidArgs(m)) if m.contains("US Letter"))); - assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, Some(0)).ok().map(|j| j.printer), Some(None)); - assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, Some(0)).is_err()); + assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, Some("Letter")).ok().map(|j| j.printer), Some(None)); + assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, Some("Letter")).is_err()); } } diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index d4cecebd7a..9ee2820dcc 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -38,7 +38,7 @@ use imposition::{PAGES_LISTED, appearance, appearance_matrix, decoded_contents, pub use imposition::{impose, keep_printed, sheet_pages}; pub use settings::{ Binding, BookletSubset, Content, Crop, LABEL_PAPER, Layout, Orientation, PAPERS, PageOrder, Raster, Settings, SizeMode, booklet_duplex, - cups_media, media_option, + cups_media, custom_media, media_option, paper_media, paper_media_option, }; #[cfg(test)] use sheets::text_direction; diff --git a/crates/print/src/settings.rs b/crates/print/src/settings.rs index 0506eb5b18..1ed2665f74 100644 --- a/crates/print/src/settings.rs +++ b/crates/print/src/settings.rs @@ -1,4 +1,5 @@ use super::spool; +use crate::matching_paper; #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum Orientation { @@ -248,3 +249,48 @@ pub fn cups_media(index: usize) -> Option<&'static str> { pub fn media_option(index: usize) -> Option<(String, String)> { cups_media(index).map(|m| ("media".to_string(), m.to_string())) } + +/// Format custom dimensions in points as a CUPS custom media string (e.g. `Custom.300x500`). +pub fn custom_media(size: (f64, f64)) -> String { + let format_dim = |d: f64| { + if (d.round() - d).abs() < 1e-4 { + format!("{:.0}", d.round()) + } else { + let s = format!("{d:.2}"); + s.trim_end_matches('0').trim_end_matches('.').to_string() + } + }; + format!("Custom.{}x{}", format_dim(size.0), format_dim(size.1)) +} + +/// The CUPS media name for a paper by its name and dimensions. +/// +/// Matches standard [`PAPERS`] presets by size first, then a clean driver-reported keyword, +/// falling back to custom media dimensions (`Custom.x`). +pub fn paper_media(name: Option<&str>, size: (f64, f64)) -> String { + if let Some(index) = matching_paper(size) + && let Some(media) = cups_media(index) + { + return media.to_string(); + } + if let Some(n) = name { + let is_dim = n.split_once(['x', 'X', '*']).is_some_and(|(w, h)| { + w.trim().trim_start_matches("Custom.").trim_start_matches("custom.").parse::().is_ok() + && h.trim().trim_end_matches("pt").trim_end_matches("points").trim().parse::().is_ok() + }); + let is_clean = !n.is_empty() + && n.len() <= 64 + && !n.contains(' ') + && !is_dim + && n.chars().all(|c| c.is_ascii_graphic() && !matches!(c, '/' | ':' | '=' | '"' | '\'' | '\\' | '{')); + if is_clean { + return n.to_string(); + } + } + custom_media(size) +} + +/// The media job option (`("media", )`) for a paper by its optional name and dimensions. +pub fn paper_media_option(name: Option<&str>, size: (f64, f64)) -> (String, String) { + ("media".to_string(), paper_media(name, size)) +} diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index 6e9a2abe5a..8deaaccf89 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -852,6 +852,28 @@ fn every_paper_reaches_the_spooler_by_a_name_of_its_size() { ); } +#[test] +fn paper_media_and_custom_dimensions() { + assert_eq!(custom_media((300.0, 500.0)), "Custom.300x500"); + assert_eq!(custom_media((300.5, 500.25)), "Custom.300.5x500.25"); + + // Standard presets match by size regardless of name: + assert_eq!(paper_media(None, (612.0, 792.0)), "Letter"); + assert_eq!(paper_media(Some("Driver Letter"), (612.0, 792.0)), "Letter"); + assert_eq!(paper_media(Some("Shipping Label"), (288.0, 432.0)), "w288h432"); + + // Clean driver keyword that does not match PAPERS presets: + assert_eq!(paper_media(Some("Roll80mm"), (226.77, 800.0)), "Roll80mm"); + assert_eq!(paper_media(Some("Photo5x7"), (360.0, 504.0)), "Photo5x7"); + + // Driver paper with spaces or raw dimensions falls back to Custom: + assert_eq!(paper_media(Some("Custom 5x7"), (360.0, 504.0)), "Custom.360x504"); + assert_eq!(paper_media(Some("300x500"), (300.0, 500.0)), "Custom.300x500"); + assert_eq!(paper_media(None, (300.0, 500.0)), "Custom.300x500"); + + assert_eq!(paper_media_option(Some("300x500"), (300.0, 500.0)), ("media".to_string(), "Custom.300x500".to_string())); +} + /// #2734: the US #10 envelope (4.125 × 9.5 in) is offered, fed by its PPD name and laid out at /// its own size. #[test] diff --git a/crates/ui-egui/src/workflows/print_ui/draft.rs b/crates/ui-egui/src/workflows/print_ui/draft.rs index 560e8ae0ff..7939c4a047 100644 --- a/crates/ui-egui/src/workflows/print_ui/draft.rs +++ b/crates/ui-egui/src/workflows/print_ui/draft.rs @@ -421,7 +421,7 @@ impl PrintDraft { } .into_iter() // The sheet the PDF was laid out on, not the queue's default paper. - .chain(print::media_option(self.paper)) + .chain(self.papers.get(self.paper).map(|(name, size)| print::paper_media_option(Some(name), *size))) .collect(), } }