mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
* 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:
7 files changed
+62
-14
No files matched your search
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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." },
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user