collective

Browse / github.com/storytold/craft-libs / patch

print: keep the driver's namespace prefixes in the Windows PrintTicket

print: keep the driver's namespace prefixes in the Windows PrintTicket ## Problem On Windows, every print job to a **Brother MFC-T910DW** failed (PdfCraft showed a render/spool error), while other apps printed fine: ``` DXGI print context error: Failed to close print control: No such interface supported (0x80040003) ``` ## Cause `driver_ticket` builds the job's ticket partly from PrintCapabilities options (the driver's paper option). In PrintCapabilities, Windows names the Brother namespace `ns0000` instead of the driver's own `brpsk`, and that prefix carries through `PTMergeAndValidatePrintTicket` into the validated ticket (`ns0000:PageCustomHalftone`, `ns00

Status
open
Score
0.00
Adopters
0 other operators
Author
xoincare (imported from pull request #170, open)
Base
82a24b006eefa036b43c09a67fb9a3fbd17ffe63
Size
+72 −0 in 2 files
Review
pending
Shared
2026-10-28
Id
2a31cf9bfb2895fd79982a52

How it went

No other operator has reported using it yet.

Diff

raw · crates/print/src/ticket.rs crates/print/src/windows.rs

diff --git a/crates/print/src/ticket.rs b/crates/print/src/ticket.rs--- a/crates/print/src/ticket.rs+++ b/crates/print/src/ticket.rs@@ -81,6 +81,35 @@ const NAMED_PAPERS: &[((f64, f64), &[&str])] = &[     ((2448.0, 3168.0), &["ANSI E"]), ]; +/// `xml`, a ticket the driver validated, with each namespace prefix that the driver's own+/// `reference` ticket spells differently for the same namespace renamed to the driver's prefix.+///+/// Prefixes don't matter in XML, but some drivers compare them as text: a Brother MFC-T910DW+/// fails the job at `ID2D1PrintControl::Close` (0x80040003) when its own keywords arrive as+/// `ns0000:PageCustomHalftone`, the name Windows gives its namespace in PrintCapabilities,+/// instead of `brpsk:PageCustomHalftone`. A ticket that doesn't parse, and a prefix the ticket+/// already uses for another namespace, are left as they are.+pub(crate) fn driver_prefixes(xml: &str, reference: &str) -> String {+    let declared = |text: &str| -> Option<Vec<(String, String)>> {+        let doc = roxmltree::Document::parse(text).ok()?;+        Some(doc.root_element().namespaces().filter_map(|n| Some((n.name()?.to_owned(), n.uri().to_owned()))).collect())+    };+    let (Some(ours), Some(theirs)) = (declared(xml), declared(reference)) else { return xml.to_owned() };+    let mut out = xml.to_owned();+    for (prefix, uri) in &ours {+        let Some((wanted, _)) = theirs.iter().find(|(p, u)| u == uri && p != prefix) else { continue };+        if ours.iter().any(|(p, u)| p == wanted && u != uri) {+            continue;+        }+        // The declaration, names in attributes (`name="ns0000:…"`) and QName values (`>ns0000:…`).+        out = out.replace(&format!("xmlns:{prefix}="), &format!("xmlns:{wanted}="));+        for lead in ['"', '>'] {+            out = out.replace(&format!("{lead}{prefix}:"), &format!("{lead}{wanted}:"));+        }+    }+    out+}+ /// The usual names of a paper of this size (either way round, to half a point). pub(crate) fn paper_names(width: f64, height: f64) -> &'static [&'static str] {     let (w, h) = (width.min(height), width.max(height));@@ -246,6 +275,34 @@ pub(crate) fn job_filename(title: &str) -> String { mod tests {     use super::*; +    const BROTHER: &str = "http://schemas.brother.info/mfc/printing/2006/11/printschemakeywords";++    fn ticket(prefix: &str) -> String {+        format!(+            r#"<psf:PrintTicket xmlns:psf="{NS_PSF}" xmlns:psk="{NS_PSK}" xmlns:{prefix}="{BROTHER}" version="1"><psf:Feature name="{prefix}:PageCustomHalftone"><psf:Option name="{prefix}:Dither"/></psf:Feature><psf:Feature name="psk:PageMediaType"><psf:Option name="psk:Plain"/></psf:Feature></psf:PrintTicket>"#+        )+    }++    #[test]+    fn a_renamed_driver_namespace_gets_the_drivers_prefix_back() {+        assert_eq!(driver_prefixes(&ticket("ns0000"), &ticket("brpsk")), ticket("brpsk"));+    }++    #[test]+    fn prefixes_the_driver_shares_are_left_alone() {+        assert_eq!(driver_prefixes(&ticket("brpsk"), &ticket("brpsk")), ticket("brpsk"));+        // No reference to go by, or a ticket that doesn't parse: unchanged.+        assert_eq!(driver_prefixes(&ticket("ns0000"), "not xml"), ticket("ns0000"));+        assert_eq!(driver_prefixes("not xml", &ticket("brpsk")), "not xml");+    }++    #[test]+    fn a_prefix_taken_by_another_namespace_is_not_reused() {+        // The ticket already uses `psk` for the keywords: the driver's `psk` for its own+        // namespace must not take it over.+        assert_eq!(driver_prefixes(&ticket("ns0000"), &ticket("psk").replace(&format!("xmlns:psk=\"{NS_PSK}\" "), "")), ticket("ns0000"));+    }+     #[test]     fn the_ticket_carries_the_job_options() {         let job = Job { copies: 3, collate: false, duplex: Some(Duplex::ShortEdge), grayscale: true, title: "<&>".into(), ..Job::default() };diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs--- a/crates/print/src/windows.rs+++ b/crates/print/src/windows.rs@@ -104,6 +104,21 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape     let mut builder = PrintTicketBuilder::new(device).map_err(spool_error)?;     builder.merge(PrintTicket::from(document)).map_err(spool_error)?;     let validated = builder.build().map_err(spool_error)?;+    // The driver's own prefixes for its keywords: some drivers fail the job on Windows'+    // generated ones (`ns0000:`). Without the driver's default ticket, the validated one as it is.+    let reference = PrintTicketBuilder::new(device)+        .and_then(|mut defaults| {+            defaults.merge(PrintTicket::default())?;+            defaults.build()+        })+        .ok();+    let validated = match reference.as_ref().map(|r| std::str::from_utf8(r.get_xml())) {+        Some(Ok(reference)) => match std::str::from_utf8(validated.get_xml()) {+            Ok(xml) => PrintTicket::from_xml(ticket::driver_prefixes(xml, reference)),+            Err(_) => validated,+        },+        _ => validated,+    };     let xml = std::str::from_utf8(validated.get_xml()).map_err(spool_error)?;     let paper = ticket::verify_ticket(xml, wanted, landscape)?;     Ok((validated, paper))