diff --git a/crates/print/README.md b/crates/print/README.md index 00b12689..a490efdf 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -83,8 +83,10 @@ Native Windows PDF submission checks the driver's validated copy count and expli simplex/duplex and grayscale requests before sending. Collation must be retained when both the page count and copy count exceed one. A driver that drops or changes these options is refused; select supported options or flatten grayscale into the print-ready page content. -Without an explicit duplex request, the queue default applies. Normal color output retains -the driver's color policy. Single-page or single-copy jobs have no collating order to check. +Without an explicit duplex request, the queue default applies. Without a grayscale request the +ticket names no colour, so the colour in the user's printing preferences (Properties…, or +Windows Settings) applies, black and white included. Single-page or single-copy jobs have no +collating order to check. `cargo test -p craft-print` tests CUPS transport (including pipe-sized jobs and refusal), printer parsing, PPD options and argument mapping, and on every platform the Windows pieces that diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index 8fbea52b..32c0fc6a 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -558,7 +558,7 @@ fn windows_preview(job: &Job) -> Vec { Scaling::ShrinkToFit => "shrink-to-fit", } ), - format!("color={}", if job.grayscale { "monochrome" } else { "color" }), + format!("color={}", if job.grayscale { "monochrome" } else { "(driver default)" }), ] } diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index fd5499e9..2ebaa949 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -257,7 +257,7 @@ fn command_previews_do_not_require_pdf_files() { } else { assert_eq!( preview, - ["xps-print", "printer=Office (east)", "copies=2", "collate=true", "duplex=(queue default)", "scaling=exact", "color=color"] + ["xps-print", "printer=Office (east)", "copies=2", "collate=true", "duplex=(queue default)", "scaling=exact", "color=(driver default)"] ); } } diff --git a/crates/print/src/ticket.rs b/crates/print/src/ticket.rs index 5f8c914b..45b8a06c 100644 --- a/crates/print/src/ticket.rs +++ b/crates/print/src/ticket.rs @@ -43,14 +43,15 @@ pub(crate) fn ticket_xml(job: &Job, landscape: bool) -> String { } }; let collate = if job.collate { "Collated" } else { "Uncollated" }; - let color = if job.grayscale { "Monochrome" } else { "Color" }; + // Only grayscale is asked for. Without it the ticket says nothing about colour, so a black and + // white chosen in the driver's own preferences (Properties…) still applies (storytold/pdfcraft#2422). + let color = if job.grayscale { "\n " } else { "" }; let copies = job.copies.clamp(1, 999); format!( r#" {copies} - {duplex} - + {duplex}{color} "# @@ -256,10 +257,11 @@ mod tests { assert!(!xml.contains("<&>"), "the title never enters the XML"); assert!(roxmltree::Document::parse(&xml).is_ok()); let xml = ticket_xml(&Job { copies: 5000, ..Job::default() }, false); - for value in [">999<", "psk:Collated", "psk:Color", "psk:Portrait"] { + for value in [">999<", "psk:Collated", "psk:Portrait"] { assert!(xml.contains(value), "missing {value}"); } assert!(!xml.contains("JobDuplex"), "no duplex asked: the queue's default applies"); + assert!(!xml.contains("PageOutputColor"), "no grayscale asked: the driver's own colour setting applies"); assert!(ticket_xml(&Job { duplex: Some(Duplex::Off), ..Job::default() }, false).contains("psk:OneSided")); assert!(ticket_xml(&Job { duplex: Some(Duplex::LongEdge), ..Job::default() }, false).contains("psk:TwoSidedLongEdge")); assert!(!ticket_xml(&Job { printer: Some("Office \"east\" <1>".into()), ..Job::default() }, false).contains("east")); diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs index 45a23aa7..126e6bb7 100644 --- a/crates/print/src/windows.rs +++ b/crates/print/src/windows.rs @@ -242,4 +242,30 @@ mod tests { eprintln!("{sent:?}"); assert!(sent.is_ok()); } + + /// A colour chosen in the driver's own preferences (Properties…) reaches the job when the job + /// doesn't ask for grayscale: the validated ticket has the colour of the user's preferences, + /// whatever they are (storytold/pdfcraft#2422; it was always Color before). Set the queue to + /// black and white in its preferences to see the case that failed. Sends no job. + #[test] + #[ignore = "needs CRAFT_PRINT_TEST_PRINTER naming an installed printer; sends no job"] + fn the_preferences_colour_reaches_a_job_without_grayscale() { + fn colour(ticket: &PrintTicket) -> Option { + let xml = std::str::from_utf8(ticket.get_xml()).ok()?; + let document = roxmltree::Document::parse(xml).ok()?; + let feature = document + .root_element() + .children() + .find(|n| n.has_tag_name((ticket::NS_PSF, "Feature")) && n.attribute("name").is_some_and(|a| a.ends_with(":PageOutputColor")))?; + feature.children().find(|n| n.has_tag_name((ticket::NS_PSF, "Option")))?.attribute("name").map(str::to_owned) + } + let name = std::env::var("CRAFT_PRINT_TEST_PRINTER").unwrap(); + let device = select_device(PrinterDevice::all().unwrap(), Some(&name), None).unwrap(); + let mut builder = PrintTicketBuilder::new(&device).unwrap(); + builder.merge(PrintTicket::from_xml(winprint::ticket::DEFAULT_PRINT_TICKET_XML.as_bytes().to_vec())).unwrap(); + let preferences = colour(&builder.build().unwrap()); + eprintln!("the preferences' colour: {preferences:?}"); + let (ticket, _) = driver_ticket(&device, &Job::default(), (595.28, 841.89), false).unwrap(); + assert_eq!(colour(&ticket), preferences, "a job without grayscale keeps the preferences' colour"); + } }