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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-10 00:43:00 +01:00
1 parent efd7c9fc24
commit 0c0f0b6b56
7 files changed
+62 -14

No files matched your search

+31 -8
View File
@@ -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<spool::Duplex>, paper: usize, sheets: &[print::Sheet]) -> Result<spool::Job> {
/// 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<spool::Duplex>,
paper: usize,
raster: Option<print::Raster>,
sheets: &[print::Sheet],
) -> Result<spool::Job> {
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<spool::Duple
collate: a.opt_bool("collate")?.unwrap_or(true),
duplex,
scaling: spool::Scaling::Exact,
grayscale: a.opt_bool("grayscale")?.unwrap_or(false),
grayscale: print::driver_needs_grayscale(a.opt_bool("grayscale")?.unwrap_or(false), raster),
title,
options: {
let mut options: Vec<(String, String)> = 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::<Vec<_>>();
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<print::Raster>| 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));
}
}
+1 -1
View File
@@ -775,7 +775,7 @@ pub fn tools() -> Vec<ToolDef> {
"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." },
}),
+1 -1
View File
@@ -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;
+8
View File
@@ -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<Raster>) -> bool {
grayscale && !raster.is_some_and(|r| r.grayscale)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Crop {
pub rect: [f64; 4],
+12
View File
@@ -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() };
@@ -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<print::Raster> {
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) {
+2 -2
View File
@@ -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"