fix(print): a black and white chosen in the printer's preferences applies

The Windows job ticket always named a colour: Monochrome when the job
asked for grayscale, Color otherwise. Merged over the user's printing
preferences, that Color replaced a black and white the user had picked
in the driver's own Properties dialog, so the job came out in colour
(storytold/pdfcraft#2422).

The ticket now names a colour only when grayscale is asked for. Without
it the driver validates the ticket from the user's preferences, as the
CUPS backend and `verify_job_options` already assumed.

Checked on an HP OfficeJet 250 (PCL-3 driver) with its preferences set
to black and white: the validated ticket had psk:Color and the DEVMODE
dmColor 2 before, psk:Monochrome and dmColor 1 after. With colour
preferences it is psk:Color either way.

- `the_ticket_carries_the_job_options`: no PageOutputColor without
  grayscale (fails on the old ticket).
- `the_preferences_colour_reaches_a_job_without_grayscale` (ignored,
  needs CRAFT_PRINT_TEST_PRINTER; sends no job): the job's validated
  colour equals the preferences' colour. Fails on the old ticket with the
  queue set to black and white; passes on HP OfficeJet 250, Microsoft
  Print to PDF and OneNote.
- The Windows command preview says `color=(driver default)`, like
  `duplex=(queue default)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
magdale76andClaude Opus 5.5 committed 2026-10-15 10:17:48 +02:00
1 parent 8f5a283161
commit bfbf82b360
5 files changed
+38 -8

No files matched your search

+4 -2
View File
@@ -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
+1 -1
View File
@@ -558,7 +558,7 @@ fn windows_preview(job: &Job) -> Vec<String> {
Scaling::ShrinkToFit => "shrink-to-fit",
}
),
format!("color={}", if job.grayscale { "monochrome" } else { "color" }),
format!("color={}", if job.grayscale { "monochrome" } else { "(driver default)" }),
]
}
+1 -1
View File
@@ -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)"]
);
}
}
+6 -4
View File
@@ -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 <psf:Feature name=\"psk:PageOutputColor\"><psf:Option name=\"psk:Monochrome\"/></psf:Feature>" } else { "" };
let copies = job.copies.clamp(1, 999);
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<psf:PrintTicket version="1" xmlns:psf="{NS_PSF}" xmlns:psk="{NS_PSK}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<psf:ParameterInit name="psk:JobCopiesAllDocuments"><psf:Value xsi:type="xsd:integer">{copies}</psf:Value></psf:ParameterInit>
<psf:Feature name="psk:DocumentCollate"><psf:Option name="psk:{collate}"/></psf:Feature>{duplex}
<psf:Feature name="psk:PageOutputColor"><psf:Option name="psk:{color}"/></psf:Feature>
<psf:Feature name="psk:DocumentCollate"><psf:Option name="psk:{collate}"/></psf:Feature>{duplex}{color}
<psf:Feature name="psk:PageScaling"><psf:Option name="psk:None"/></psf:Feature>
<psf:Feature name="psk:PageOrientation"><psf:Option name="psk:{orientation}"/></psf:Feature>
</psf:PrintTicket>"#
@@ -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"));
+26
View File
@@ -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<String> {
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");
}
}