From 0c0f0b6b56ba2e444fd691197403d83a5836b72b Mon Sep 17 00:00:00 2001 From: austin62jake Date: Mon, 9 Nov 2026 17:43:00 -0600 Subject: [PATCH] M10: grayscale with Print as image prints on a driver that keeps no grayscale (#5806) (#5882) * M10: grayscale with Print as image prints on a driver that keeps no grayscale (#5806) With Print as image and grayscale, PdfCraft renders the pages as grey pictures itself, yet the job still asked the driver for grayscale. A driver that keeps no grayscale setting, Microsoft Print to PDF among them, made craft-print refuse the job ("the printer's driver did not retain the requested grayscale; nothing was sent"), so even the grey pictures never printed. The job now asks the driver for grayscale only when the pages aren't grey pictures already (print::driver_needs_grayscale), in the dialog and in doc_print. The print.grayscale parity note and the doc_print as_image description say so, and the note names what is still missing: vector pages to such a driver are refused rather than printed in colour. Co-Authored-By: Claude Opus 5.5 * M10: the grey-pictures test checks that the pictures are grey too (#5806) The test checked that grey pictures don't ask the driver for grayscale, but not that the pictures are grey. A change that dropped grayscale from the dialog's raster settings, while the job still skipped the driver, would have printed in colour with the test passing. It now asserts both. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> --- crates/automation/src/printing.rs | 39 +++++++++++++++---- crates/automation/src/tools.rs | 2 +- crates/print/src/lib.rs | 2 +- crates/print/src/settings.rs | 8 ++++ crates/ui-egui/src/workflows/print_ui.rs | 12 ++++++ .../ui-egui/src/workflows/print_ui/draft.rs | 9 ++++- parity/acrobat-features.toml | 4 +- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index dd3f949db2..0af923ec25 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -62,8 +62,17 @@ fn paper_named(name: Option<&str>) -> Result<(usize, (f64, f64))> { /// (`PAPERS[paper]`, 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), and tells /// CUPS which way the laid-out `sheets` run, as the Print dialog does (#2713): an orientation the -/// caller chose in `options` stands. -fn spool_job(a: &Args, printer: &str, title: String, duplex: Option, paper: usize, sheets: &[print::Sheet]) -> Result { +/// caller chose in `options` stands. It asks the driver for grayscale only when the pages aren't +/// grey pictures already ([`print::driver_needs_grayscale`]). +fn spool_job( + a: &Args, + printer: &str, + title: String, + duplex: Option, + paper: usize, + raster: Option, + sheets: &[print::Sheet], +) -> Result { let options = match a.get("options") { None => Vec::new(), Some(Value::Object(m)) => m @@ -81,7 +90,7 @@ fn spool_job(a: &Args, printer: &str, title: String, duplex: Option = options.into_iter().chain(print::media_option(paper)).collect(); @@ -266,7 +275,7 @@ impl Automation { out["path"] = json!(path.to_string_lossy()); } (None, Some(printer)) => { - let job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index, &laid_out)?; + let job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index, settings.raster, &laid_out)?; 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\")")), @@ -312,7 +321,7 @@ mod tests { #[test] fn printing_landscape_sheets_to_a_printer_asks_cups_to_turn_them() { let sheets = |sizes: &[(f64, f64)]| sizes.iter().map(|&size| print::Sheet { size, ..Default::default() }).collect::>(); - let job = |args: Value, s: &[print::Sheet]| spool_job(&Args(&args), "Office", "a.pdf".into(), None, 0, s).unwrap().options; + let job = |args: Value, s: &[print::Sheet]| spool_job(&Args(&args), "Office", "a.pdf".into(), None, 0, None, s).unwrap().options; let media = ("media".to_string(), "Letter".to_string()); let landscape = sheets(&[(792.0, 612.0), (792.0, 612.0)]); let turned = if cfg!(windows) { vec![media.clone()] } else { vec![media.clone(), ("orientation-requested".to_string(), "4".to_string())] }; @@ -329,7 +338,7 @@ mod tests { fn printing_to_a_printer_feeds_the_laid_out_paper() { let job = |args: Value, paper_name: Option<&str>| { let (index, _) = paper_named(paper_name).unwrap(); - spool_job(&Args(&args), "Office", "a.pdf".into(), None, index, &[]).unwrap() + spool_job(&Args(&args), "Office", "a.pdf".into(), None, index, None, &[]).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. @@ -345,7 +354,21 @@ mod tests { assert_eq!(job(json!({}), Some("letter")).options, [media("Letter")]); assert_eq!(job(json!({}), Some("ARCH D")).options, [media("na_arch-d_24x36in")]); assert!(matches!(paper_named(Some("Quarto")), Err(ToolError::InvalidArgs(m)) if m.contains("US Letter"))); - assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, 0, &[]).ok().map(|j| j.printer), Some(None)); - assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, 0, &[]).is_err()); + assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, 0, None, &[]).ok().map(|j| j.printer), Some(None)); + assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, 0, None, &[]).is_err()); + } + + /// #5806: grayscale with Print as image sends grey pictures, so the driver isn't asked for + /// grayscale, which Microsoft Print to PDF keeps no setting for and the job was refused. + #[test] + fn grey_pictures_dont_ask_the_driver_for_grayscale() { + let grey = print::Raster { dpi: 300.0, grayscale: true }; + let colour = print::Raster { dpi: 300.0, grayscale: false }; + let job = + |args: Value, raster: Option| spool_job(&Args(&args), "Office", String::new(), None, 0, raster, &[]).unwrap().grayscale; + assert!(job(json!({ "grayscale": true }), None), "vector pages: the driver makes them grey"); + assert!(!job(json!({ "grayscale": true }), Some(grey)), "grey pictures: nothing for the driver to do"); + assert!(job(json!({ "grayscale": true }), Some(colour)), "colour pictures still need the driver"); + assert!(!job(json!({}), None)); } } diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index e1320972b8..46a3368a13 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -775,7 +775,7 @@ pub fn tools() -> Vec { "collate": { "type": "boolean" }, "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." }, + "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, so the printer isn't asked for grayscale (a driver without that setting still prints them)." }, "dpi": { "type": "number", "minimum": 72, "maximum": 600, "description": "Resolution for as_image (default 300)." }, "options": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Printer driver options from printer_options: key → choice value." }, }), diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index 54ae81cbdc..fca266b870 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, MAX_GRID, Orientation, PAPERS, PageOrder, Raster, Settings, SizeMode, booklet_duplex, - cups_media, media_option, + cups_media, driver_needs_grayscale, media_option, }; #[cfg(test)] use sheets::text_direction; diff --git a/crates/print/src/settings.rs b/crates/print/src/settings.rs index 1873f20ca8..a182948f57 100644 --- a/crates/print/src/settings.rs +++ b/crates/print/src/settings.rs @@ -178,6 +178,14 @@ impl Default for Raster { } } +/// Whether a print asked to be `grayscale` needs the printer's driver to print in grayscale. +/// Not when the pages go as grey pictures (`raster` with [`Raster::grayscale`]): they are grey +/// already, so a driver that keeps no grayscale setting (Microsoft Print to PDF, many inkjets) +/// still prints them instead of the job being refused (#5806). +pub fn driver_needs_grayscale(grayscale: bool, raster: Option) -> bool { + grayscale && !raster.is_some_and(|r| r.grayscale) +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct Crop { pub rect: [f64; 4], diff --git a/crates/ui-egui/src/workflows/print_ui.rs b/crates/ui-egui/src/workflows/print_ui.rs index 0629ffaeba..c30e76a3ba 100644 --- a/crates/ui-egui/src/workflows/print_ui.rs +++ b/crates/ui-egui/src/workflows/print_ui.rs @@ -709,6 +709,18 @@ mod tests { } } + /// #5806: with Print as image, grayscale makes the pictures grey, so the job doesn't ask the + /// driver for it as well; a driver that keeps no grayscale setting then still prints. + #[test] + fn grey_pictures_dont_ask_the_driver_for_grayscale() { + let vector = PrintDraft { grayscale: true, ..PrintDraft::default() }; + assert!(vector.job("a.pdf").grayscale, "vector pages: the driver makes them grey"); + let pictures = PrintDraft { grayscale: true, as_image: true, ..PrintDraft::default() }; + assert!(!pictures.job("a.pdf").grayscale, "grey pictures: nothing for the driver to do"); + assert!(pictures.settings(1, &[]).unwrap().raster.is_some_and(|r| r.grayscale), "grey pictures: the pages are rendered grey"); + assert!(!PrintDraft::default().job("a.pdf").grayscale); + } + #[test] fn composed_sheets_keep_exact_scaling_and_the_dialogs_duplex_choice() { let draft = PrintDraft { duplex: Some(spool::Duplex::LongEdge), ..PrintDraft::default() }; diff --git a/crates/ui-egui/src/workflows/print_ui/draft.rs b/crates/ui-egui/src/workflows/print_ui/draft.rs index d041bdaf7c..bbba844e0b 100644 --- a/crates/ui-egui/src/workflows/print_ui/draft.rs +++ b/crates/ui-egui/src/workflows/print_ui/draft.rs @@ -216,11 +216,16 @@ impl PrintDraft { orientation: self.orientation, layout, content: self.content, - raster: self.as_image.then_some(print::Raster { dpi: self.dpi, grayscale: self.grayscale }), + raster: self.raster(), crops: self.crops.clone(), }) } + /// Print as image: pictures at the chosen resolution, grey when grayscale is asked for. + fn raster(&self) -> Option { + self.as_image.then_some(print::Raster { dpi: self.dpi, grayscale: self.grayscale }) + } + fn cut_and_stack(&self) -> bool { self.handling == Handling::Multiple && self.order == PageOrder::CutStack } @@ -242,7 +247,7 @@ impl PrintDraft { self.duplex }, scaling: spool::Scaling::Exact, - grayscale: self.grayscale, + grayscale: print::driver_needs_grayscale(self.grayscale, self.raster()), title: title.to_string(), // Only for the printer the options were read from. options: match (&self.printer, &self.driver_options) { diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index df2dae6f63..9701ca5f5d 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8369,8 +8369,8 @@ milestone = "M10" status = "shipped" commands = ["print.dialog"] tools = ["doc_print"] -evidence = ["crates/engine/src/tests.rs::printing_as_image_sends_pictures_instead_of_pages", "crates/ui-egui/tests/it/print.rs::print_as_image_is_an_advanced_choice_that_saves_pictures", "crates/automation/tests/it/automation.rs::doc_print_as_image"] -notes = "asks the printer for monochrome (lp -o, and the Windows page settings as the job's); with Print as image the pictures themselves are rendered grey (Rec. 601 luma), so the print-ready PDF is grayscale too. Still missing against Acrobat: a grey conversion of the vector pages without rendering them." +evidence = ["crates/engine/src/tests.rs::printing_as_image_sends_pictures_instead_of_pages", "crates/ui-egui/tests/it/print.rs::print_as_image_is_an_advanced_choice_that_saves_pictures", "crates/automation/tests/it/automation.rs::doc_print_as_image", "crates/automation/src/printing.rs::grey_pictures_dont_ask_the_driver_for_grayscale", "crates/ui-egui/src/workflows/print_ui.rs::grey_pictures_dont_ask_the_driver_for_grayscale"] +notes = "asks the printer for monochrome (lp -o, and the Windows page settings as the job's); with Print as image the pictures themselves are rendered grey (Rec. 601 luma), so the print-ready PDF is grayscale too and the printer isn't asked for it: a driver that keeps no grayscale setting (Microsoft Print to PDF) prints them (#5806). Still missing against Acrobat: a grey conversion of the vector pages without rendering them, so vector pages to such a driver are refused rather than printed in colour." [[feature]] id = "print.print-as-image"