diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -743,7 +743,7 @@ dependencies = [
 [[package]]
 name = "craft-winprint-gdi"
 version = "0.1.0"
-source = "git+https://github.com/storytold/winprint.rs?rev=417ac3c4d14d3da5485a99c7d5ce8f05664ec312#417ac3c4d14d3da5485a99c7d5ce8f05664ec312"
+source = "git+https://github.com/MaxFairwinds/winprint.rs?rev=3fc968789cf6a7e43c546d8113ef3850bc82dd7a#3fc968789cf6a7e43c546d8113ef3850bc82dd7a"
 dependencies = [
  "windows-sys 0.61.2",
 ]
@@ -5758,7 +5758,7 @@ dependencies = [
 [[package]]
 name = "winprint"
 version = "0.2.1"
-source = "git+https://github.com/storytold/winprint.rs?rev=5f80b9baf6b58937c401656634e42d33f00ddf49#5f80b9baf6b58937c401656634e42d33f00ddf49"
+source = "git+https://github.com/MaxFairwinds/winprint.rs?rev=3fc968789cf6a7e43c546d8113ef3850bc82dd7a#3fc968789cf6a7e43c546d8113ef3850bc82dd7a"
 dependencies = [
  "flate2",
  "fmt-derive",
diff --git a/crates/print/Cargo.toml b/crates/print/Cargo.toml
--- a/crates/print/Cargo.toml
+++ b/crates/print/Cargo.toml
@@ -9,7 +9,7 @@ description = "System PDF print spooler for the Craft apps: CUPS and Windows"
 
 [dependencies]
 # Safe API; Win32 calls are isolated in this maintained MIT OR Apache-2.0 dependency.
-craft-winprint-gdi = { git = "https://github.com/storytold/winprint.rs", rev = "417ac3c4d14d3da5485a99c7d5ce8f05664ec312" }
+craft-winprint-gdi = { git = "https://github.com/MaxFairwinds/winprint.rs", rev = "3fc968789cf6a7e43c546d8113ef3850bc82dd7a" }
 
 [lints]
 workspace = true
@@ -21,7 +21,9 @@ workspace = true
 # feature downloads PDFium at build time and is off. The maintained BSD-3-Clause fork
 # preserves terminal failures/cancellation and bounds the completion notification wait.
 # Pinned to an immutable revision; review before bumping.
-winprint = { git = "https://github.com/storytold/winprint.rs", rev = "5f80b9baf6b58937c401656634e42d33f00ddf49", default-features = false }
+# storytold/winprint.rs main plus PrintTicket::user_default and PrintCapabilities::fetch_for_ticket
+# (MaxFairwinds/winprint.rs feat/user-default-ticket; to move to storytold/winprint.rs once merged).
+winprint = { git = "https://github.com/MaxFairwinds/winprint.rs", rev = "3fc968789cf6a7e43c546d8113ef3850bc82dd7a", default-features = false }
 # GetDefaultPrinter (MIT).
 winsafe = { version = "0.0.29", default-features = false, features = ["winspool"] }
 # The job file's private folder.
diff --git a/crates/print/README.md b/crates/print/README.md
--- a/crates/print/README.md
+++ b/crates/print/README.md
@@ -22,7 +22,8 @@ opens Windows printer preferences and submits jobs with copies, collation, duple
   into the XPS print API (the `winprint` crate), and copies, collation, duplex, colour, paper,
   orientation and "no scaling" travel in a PrintTicket that the driver validates first; a
   driver that would change the paper, orientation or scaling is refused before anything is
-  sent. Paper is the driver's own option for the sheet's size (to 0.25 mm), preferring the one
+  sent. A driver that offers no PageScaling feature at all (Zebra's ZDesigner label drivers)
+  is not asked for one and not held to its answer: asked for None, it answers CustomSquare. Paper is the driver's own option for the sheet's size (to 0.25 mm), preferring the one
   named like the standard size ("ARCH D" over a custom 24 × 36 in entry). This backend comes
   from storytold/pdfcraft#599 and #1132.
   A job has one ticket, so one paper and orientation. Sheets of the other orientation are
@@ -45,6 +46,22 @@ opens Windows printer preferences and submits jobs with copies, collation, duple
 - **Browser/other targets:** discovery is empty and submission reports an unsupported
   platform; callers can offer PDF export instead.
 
+The paper a job comes out on is checked on the DEVMODE winprint prints with (the ticket
+converted on the user's own settings, then settled by the driver), not only on the merged
+ticket, which Windows builds on the printer's Printing Defaults: a label driver's one
+"User defined" stock is whatever each dialog says, and when a user's Printing Preferences
+and the printer's Printing Defaults disagree the merged ticket names the defaults' size
+while the job prints on the preferences'. When the driver settles on the paper asked for,
+the ticket asked for is printed with. A driver with no two-sided feature at all (a label
+printer) is not asked to confirm one-sided printing: it can print nothing else; two-sided
+asked of it is refused. Every ticket and capabilities request starts from the signed-in user's own settings
+(winprint's `PrintTicket::user_default`, their Printing Preferences), not the printer's
+Printing Defaults, which Windows' Print Ticket calls fall back to: on a Zebra ZD500 the
+defaults held another stock, another darkness and tear-off where the user had set cutting,
+and the driver's private settings ride in the ticket (`PageDevmodeSnapshot`). A sheet is turned (landscape) only when its shape differs from the
+paper's as the driver feeds it: a wide sheet on upright office paper is, a wide sheet on a
+label stock that is itself wider than tall is not.
+
 Windows jobs briefly use a file in a private temporary folder (random name, this user's only),
 named after the job's title because the spooler shows it as the document name; the folder is
 removed once the spooler has the job, on success and failure. Failed and canceled terminal
@@ -65,7 +82,15 @@ change an application's UI.
 Receipts are backend messages, not portable job IDs. `Job::duplex = None` preserves the queue's
 duplex default; `Some(Duplex::Off)` forces simplex. `Job::scaling` defaults to `Scaling::Exact`;
 callers exporting arbitrary document sizes can choose `Scaling::ShrinkToFit`. Exact mode reports
-a driver paper-size mismatch. On Windows `command_preview` describes the in-process job
+a driver paper-size mismatch. `printer_papers` lists the sizes a driver offers (Windows:
+its PrintCapabilities page media sizes, keyed by Print Schema or vendor name, in points, as
+the driver has them: a label's width is across the feed; the driver's current default marked;
+only the driver's own sizes, so a form added only in Print Server Properties is not among them
+unless the driver offers it),
+so an app's Print dialog can offer them, start on the driver's default and lay the sheets out
+on one the driver has; on CUPS it is empty, the queue's PPD
+(`*PaperDimension`) holding them instead.
+On Windows `command_preview` describes the in-process job
 (`xps-print`, then the printer and options as `key=value`) instead of a command line. The crate
 does not provide PostScript output, separations, trapping or colour-managed device rendering.
 
diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs
--- a/crates/print/src/lib.rs
+++ b/crates/print/src/lib.rs
@@ -323,6 +323,59 @@ pub fn printer_options(printer: &str) -> Vec<PrinterOption> {
     }
 }
 
+/// A paper size a printer's driver offers, as a Print dialog lists it beside the standard sizes.
+#[derive(Clone, Debug, PartialEq)]
+pub struct PrinterPaper {
+    /// The driver's keyword for it: on Windows the Print Schema name (`NorthAmericaLetter`,
+    /// `NorthAmerica4x6`) or the vendor's own.
+    pub key: String,
+    /// What the driver calls it (`Letter`, `4 x 6 in`); the keyword when it gives no name.
+    pub label: String,
+    /// Width and height in points as the driver has them: a label's width is across the feed,
+    /// so a 2.25 × 1.25 in label and a 1.25 × 2.25 in one are different stock.
+    pub size: (f64, f64),
+    /// Whether it is the driver's current default: the stock a label printer is set up for, an
+    /// office printer's tray paper. A Print dialog starts on it.
+    pub default: bool,
+}
+
+/// How many papers a driver may put in the list.
+pub const MAX_PRINTER_PAPERS: usize = 400;
+
+/// The paper sizes a printer's driver offers, in the driver's order: on Windows its
+/// PrintCapabilities page media sizes (roll media and sizes beyond 200 inches left out), the
+/// driver's current default marked. Empty
+/// when the printer or its capabilities can't be read, and on other platforms, where the CUPS
+/// queue's PPD holds them (`*PaperDimension`; the apps read it with their driver options).
+/// Reads the driver, so call it when a printer is chosen, not every frame.
+pub fn printer_papers(printer: &str) -> Vec<PrinterPaper> {
+    #[cfg(windows)]
+    {
+        windows::printer_papers(printer)
+    }
+    #[cfg(not(windows))]
+    {
+        let _ = printer;
+        Vec::new()
+    }
+}
+
+/// Whether `printer` is a thermal label printer: its driver offers only monochrome output and the
+/// paper in the user's settings is label-sized (at most 4.5 in on its short side). Such drivers
+/// dither every grey they are given, so a caller sends them black-and-white pictures at the
+/// printer's resolution through [`rgb::print_pages`] instead of a PDF (Windows; false elsewhere).
+pub fn is_label_printer(printer: &str) -> bool {
+    #[cfg(windows)]
+    {
+        windows::is_label_printer(printer)
+    }
+    #[cfg(not(windows))]
+    {
+        let _ = printer;
+        false
+    }
+}
+
 /// Whether [`open_printer_preferences`] can show the driver's own settings window (Windows).
 pub const HAS_PRINTER_PREFERENCES: bool = cfg!(windows);
 
diff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs
--- a/crates/print/src/tests/windows_native.rs
+++ b/crates/print/src/tests/windows_native.rs
@@ -31,6 +31,27 @@ fn windows_printers_are_sorted_once_with_their_default() {
     assert!(spool::printer_list([("Office".to_string(), None)], None).iter().all(|p| !p.default));
 }
 
+/// The driver's paper sizes come from its capabilities (Windows: the in-box PDF printer offers
+/// Letter and A4, keyed by Print Schema names, in points, as the driver has them); an unknown
+/// printer and the other platforms list none.
+#[test]
+fn printer_papers_come_from_the_drivers_capabilities() {
+    assert!(spool::printer_papers("No Such Printer (craft-print test)").is_empty());
+    assert!(spool::printer_papers("").is_empty());
+    #[cfg(windows)]
+    {
+        let Some(printer) = spool::printers().into_iter().find(|p| p.name == "Microsoft Print to PDF") else { return };
+        let papers = spool::printer_papers(&printer.name);
+        assert!(papers.len() > 1, "{papers:?}");
+        let letter = papers.iter().find(|p| p.key == "NorthAmericaLetter").expect("Letter by its Print Schema name");
+        assert!((letter.size.0 - 612.0).abs() < 1.0 && (letter.size.1 - 792.0).abs() < 1.0 && !letter.label.is_empty(), "{letter:?}");
+        assert!(papers.iter().all(|p| !p.key.is_empty() && p.size.0 >= 1.0 && p.size.1 >= 1.0), "no roll media");
+        let keys: std::collections::BTreeSet<&str> = papers.iter().map(|p| p.key.as_str()).collect();
+        assert_eq!(keys.len(), papers.len(), "no repeated keyword");
+        assert_eq!(papers.iter().filter(|p| p.default).count(), 1, "the driver's default paper is marked, once: {papers:?}");
+    }
+}
+
 /// One page of a test PDF: its MediaBox (`None` = inherited from the page tree's root, which is
 /// Letter), CropBox and `/Rotate`.
 #[derive(Clone, Copy)]
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
@@ -144,7 +144,9 @@ fn integer(node: roxmltree::Node<'_, '_>, tag: &str, name: &str) -> Option<u32>
 /// paper (when `paper` is given, that one, to a quarter of a millimetre). Returns the paper's
 /// width and height in micrometres as the driver lists it. Only those schema fields are read:
 /// some drivers return empty vendor parameters that a stricter parser would refuse.
-pub(crate) fn verify_ticket(xml: &str, paper: Option<(u32, u32)>, landscape: bool) -> Result<(u32, u32), Error> {
+/// `scaling_offered`: whether the driver's capabilities have a PageScaling feature; only then is its
+/// answer about scaling checked.
+pub(crate) fn verify_ticket(xml: &str, paper: Option<(u32, u32)>, landscape: bool, scaling_offered: bool) -> Result<(u32, u32), Error> {
     let document = roxmltree::Document::parse(xml).map_err(spool_error)?;
     let feature = |name: &str| document.root_element().children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, name));
     let media = feature("PageMediaSize").ok_or_else(|| spool_error("the printer's driver did not confirm the paper size; nothing was sent"))?;
@@ -162,7 +164,9 @@ pub(crate) fn verify_ticket(xml: &str, paper: Option<(u32, u32)>, landscape: boo
     if !orientation.is_some_and(|n| named(n, if landscape { "Landscape" } else { "Portrait" })) {
         return Err(spool_error("the printer's driver changed the orientation; nothing was sent"));
     }
-    if let Some(scaling) = feature("PageScaling") {
+    // Only a driver that offers PageScaling can scale by it; one that doesn't (Zebra's ZDesigner)
+    // was not asked, and whatever the merge put there is not a choice of its own.
+    if let Some(scaling) = feature("PageScaling").filter(|_| scaling_offered) {
         // Microsoft's IPP class driver names its "None" in a namespace of its own.
         if !scaling.children().any(|n| n.has_tag_name((NS_PSF, "Option")) && n.attribute("name").and_then(|v| v.rsplit(':').next()) == Some("None")) {
             return Err(spool_error("the printer's driver would scale the sheets; nothing was sent"));
@@ -173,8 +177,11 @@ pub(crate) fn verify_ticket(xml: &str, paper: Option<(u32, u32)>, landscape: boo
 
 /// Confirm explicit job options survived driver validation. Collation only changes output
 /// when both the document and copy count exceed one. Without a duplex request the queue's
-/// default applies; without forced grayscale the driver's normal color policy applies.
-pub(crate) fn verify_job_options(xml: &str, job: &Job, pages: usize) -> Result<(), Error> {
+/// default applies; without forced grayscale the driver's normal color policy applies. A
+/// driver that offers no two-sided feature at all (`duplex_offered` false: a label printer)
+/// can only print one side, so one-sided asked of it has nothing to confirm; two-sided asked
+/// of it is refused as any dropped option is.
+pub(crate) fn verify_job_options(xml: &str, job: &Job, pages: usize, duplex_offered: bool) -> Result<(), Error> {
     let document = roxmltree::Document::parse(xml).map_err(spool_error)?;
     let root = document.root_element();
     let option = |name: &str| {
@@ -196,7 +203,8 @@ pub(crate) fn verify_job_options(xml: &str, job: &Job, pages: usize) -> Result<(
             Duplex::LongEdge => "TwoSidedLongEdge",
             Duplex::ShortEdge => "TwoSidedShortEdge",
         };
-        if !option("JobDuplexAllDocumentsContiguously").is_some_and(|n| named(n, wanted)) {
+        let confirmed = option("JobDuplexAllDocumentsContiguously").is_some_and(|n| named(n, wanted));
+        if !confirmed && !(duplex == Duplex::Off && !duplex_offered) {
             return Err(refused("simplex/duplex setting"));
         }
     }
@@ -206,6 +214,69 @@ pub(crate) fn verify_job_options(xml: &str, job: &Job, pages: usize) -> Result<(
     Ok(())
 }
 
+/// Whether a sheet is turned on the paper: `sheet_landscape` says the sheet is wider than tall,
+/// `paper` is the driver's paper in micrometres as the driver feeds it. An upright paper (office
+/// sizes) turns a wide sheet; a paper that is itself wider than tall (a label stock defined across
+/// the feed) takes a wide sheet as it is and turns an upright one. Without a known paper the
+/// sheet's own shape decides, as for upright paper.
+pub(crate) fn turned(sheet_landscape: bool, paper: Option<(u32, u32)>) -> bool {
+    match paper {
+        Some((w, h)) if w > h => !sheet_landscape,
+        _ => sheet_landscape,
+    }
+}
+
+/// The paper a DEVMODE prints on, in micrometres, and whether it is landscape: what the driver
+/// settled on when the ticket was converted for printing (winprint's `PrintTicket::to_dev_mode`:
+/// the user's own settings as the base, then the driver's validation). `None` when the DEVMODE
+/// names no paper size (a roll, a driver that leaves the fields unset).
+pub(crate) fn devmode_paper(dev_mode: &[u8]) -> Option<((u32, u32), bool)> {
+    const DM_ORIENTATION: u32 = 0x1;
+    const DM_PAPERLENGTH: u32 = 0x4;
+    const DM_PAPERWIDTH: u32 = 0x8;
+    const DMORIENT_LANDSCAPE: i16 = 2;
+    let i16_at = |at: usize| dev_mode.get(at..at + 2).and_then(|b| <[u8; 2]>::try_from(b).ok()).map(i16::from_le_bytes);
+    let fields = dev_mode.get(72..76).and_then(|b| <[u8; 4]>::try_from(b).ok()).map(u32::from_le_bytes)?;
+    if fields & (DM_PAPERWIDTH | DM_PAPERLENGTH) != DM_PAPERWIDTH | DM_PAPERLENGTH {
+        return None;
+    }
+    let (width, length) = (i16_at(82)?, i16_at(80)?);
+    if width <= 0 || length <= 0 {
+        return None;
+    }
+    let landscape = fields & DM_ORIENTATION != 0 && i16_at(76)? == DMORIENT_LANDSCAPE;
+    // Tenths of a millimetre, as DEVMODE has them, to micrometres.
+    Some(((u32::from(width.unsigned_abs()) * 100, u32::from(length.unsigned_abs()) * 100), landscape))
+}
+
+/// The longest short side of a label printer's paper, in micrometres (4.5 in).
+pub(crate) const LABEL_MAX_SHORT_SIDE: u32 = 114_300;
+
+/// Whether a driver offering `colours` (PageOutputColor option keywords) on `paper` (micrometres)
+/// is a thermal label printer: monochrome only, label-sized paper. A monochrome office printer
+/// or a fax has Letter or A4; a label printer that also offers colour is left to its driver.
+pub(crate) fn label_shaped(colours: &[&str], paper: Option<(u32, u32)>) -> bool {
+    !colours.is_empty()
+        && colours.iter().all(|c| *c == "Monochrome")
+        && paper.is_some_and(|(w, h)| w > 0 && h > 0 && w.min(h) <= LABEL_MAX_SHORT_SIDE)
+}
+
+/// A ticket's PageMediaSize, in micrometres; `None` without one or for a roll.
+pub(crate) fn media_size(xml: &str) -> Option<(u32, u32)> {
+    let document = roxmltree::Document::parse(xml).ok()?;
+    let media = document.root_element().children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, "PageMediaSize"))?;
+    integer(media, "ScoredProperty", "MediaSizeWidth").zip(integer(media, "ScoredProperty", "MediaSizeHeight")).filter(|(w, h)| *w > 0 && *h > 0)
+}
+
+/// The keyword of a ticket's PageMediaSize option, without its namespace prefix
+/// (`NorthAmericaLetter`, a vendor's `User0000000256`): what [`crate::PrinterPaper::key`] holds.
+pub(crate) fn media_keyword(xml: &str) -> Option<String> {
+    let document = roxmltree::Document::parse(xml).ok()?;
+    let media = document.root_element().children().find(|n| n.has_tag_name((NS_PSF, "Feature")) && named(*n, "PageMediaSize"))?;
+    let option = media.children().find(|n| n.has_tag_name((NS_PSF, "Option")))?;
+    option.attribute("name").and_then(|v| v.rsplit(':').next()).filter(|k| !k.is_empty()).map(str::to_string)
+}
+
 /// The widest unprintable margin of the paper, in micrometres, from PrintCapabilities computed
 /// for the job's ticket (`PageImageableSize`). The largest of the four, so content centred inside
 /// it is clear of every edge whichever way the driver measures. `None` when the driver doesn't say.
@@ -265,6 +336,16 @@ mod tests {
         assert!(!ticket_xml(&Job { printer: Some("Office \"east\" <1>".into()), ..Job::default() }, false).contains("east"));
     }
 
+    #[test]
+    fn the_media_keyword_is_the_tickets_page_media_size_option() {
+        assert_eq!(media_keyword(&validated((215_900, 279_400), "psk:Portrait", None)).as_deref(), Some("NorthAmericaLetter"));
+        assert_eq!(
+            media_keyword(r#"<psf:PrintTicket version="1" xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"/>"#),
+            None
+        );
+        assert_eq!(media_keyword("not xml"), None);
+    }
+
     /// A validated ticket as a driver returns it.
     fn validated(paper: (u32, u32), orientation: &str, scaling: Option<&str>) -> String {
         let scaling = scaling.map_or(String::new(), |s| format!(r#"<psf:Feature name="psk:PageScaling"><psf:Option name="{s}"/></psf:Feature>"#));
@@ -285,47 +366,113 @@ mod tests {
     fn a_driver_that_changes_the_sheet_is_refused() {
         let letter = (215_900, 279_400);
         let ticket = validated(letter, "Portrait", Some("psk:None"));
-        assert_eq!(verify_ticket(&ticket, Some(letter), false), Ok(letter));
-        assert_eq!(verify_ticket(&ticket, Some((279_400, 215_900)), false), Ok(letter), "either way round");
-        assert_eq!(verify_ticket(&ticket, None, false), Ok(letter), "shrink-to-fit takes the driver's paper");
-        assert!(verify_ticket(&ticket, Some((210_000, 297_000)), false).is_err(), "A4 asked, Letter returned");
-        assert!(verify_ticket(&ticket, Some(letter), true).is_err(), "landscape asked, portrait returned");
-        assert!(verify_ticket(&validated(letter, "Portrait", Some("psk:FitApplicationBleedSizeToPageImageableSize")), Some(letter), false).is_err());
-        assert!(verify_ticket(&validated(letter, "Portrait", Some("ns0000:None")), Some(letter), false).is_ok(), "the IPP class driver's None");
-        assert!(verify_ticket(&validated(letter, "Portrait", None), Some(letter), false).is_ok(), "a driver without scaling");
-        assert!(verify_ticket(&validated((0, 279_400), "Portrait", None), None, false).is_err(), "a roll has no length");
-        assert!(verify_ticket("<not xml", Some(letter), false).is_err());
+        assert_eq!(verify_ticket(&ticket, Some(letter), false, true), Ok(letter));
+        assert_eq!(verify_ticket(&ticket, Some((279_400, 215_900)), false, true), Ok(letter), "either way round");
+        assert_eq!(verify_ticket(&ticket, None, false, true), Ok(letter), "shrink-to-fit takes the driver's paper");
+        assert!(verify_ticket(&ticket, Some((210_000, 297_000)), false, true).is_err(), "A4 asked, Letter returned");
+        assert!(verify_ticket(&ticket, Some(letter), true, true).is_err(), "landscape asked, portrait returned");
+        assert!(
+            verify_ticket(&validated(letter, "Portrait", Some("psk:FitApplicationBleedSizeToPageImageableSize")), Some(letter), false, true).is_err()
+        );
+        assert!(verify_ticket(&validated(letter, "Portrait", Some("ns0000:None")), Some(letter), false, true).is_ok(), "the IPP class driver's None");
+        assert!(verify_ticket(&validated(letter, "Portrait", None), Some(letter), false, true).is_ok(), "a driver without scaling");
+        // A driver with no PageScaling feature (Zebra's ZDesigner) answers CustomSquare when asked
+        // for None: not asked, its answer is not checked; a driver that offers the feature is held to it.
+        let custom = validated(letter, "Portrait", Some("psk:CustomSquare"));
+        assert_eq!(verify_ticket(&custom, Some(letter), false, false), Ok(letter), "not offered: not refused");
+        assert!(verify_ticket(&custom, Some(letter), false, true).is_err(), "offered: it would scale");
+        assert!(verify_ticket(&validated((0, 279_400), "Portrait", None), None, false, true).is_err(), "a roll has no length");
+        assert!(verify_ticket("<not xml", Some(letter), false, true).is_err());
     }
 
     #[test]
     fn a_driver_that_drops_explicit_job_options_is_refused() {
         let job = Job { copies: 2, duplex: Some(Duplex::LongEdge), grayscale: true, ..Job::default() };
         let xml = ticket_xml(&job, false);
-        assert!(verify_job_options(&xml, &job, 3).is_ok());
+        assert!(verify_job_options(&xml, &job, 3, true).is_ok());
         for (from, to) in
             [(">2<", ">1<"), ("psk:Collated", "psk:Uncollated"), ("psk:TwoSidedLongEdge", "psk:OneSided"), ("psk:Monochrome", "psk:Color")]
         {
-            assert!(verify_job_options(&xml.replace(from, to), &job, 3).is_err(), "{from}");
+            assert!(verify_job_options(&xml.replace(from, to), &job, 3, true).is_err(), "{from}");
         }
-        assert!(verify_job_options(&xml.replace("psk:Collated", "vendor:Collated"), &job, 3).is_err(), "namespace matters");
-        assert!(verify_job_options(&xml.replace("psk:Monochrome", "psk:Grayscale"), &job, 3).is_ok());
+        assert!(verify_job_options(&xml.replace("psk:Collated", "vendor:Collated"), &job, 3, true).is_err(), "namespace matters");
+        assert!(verify_job_options(&xml.replace("psk:Monochrome", "psk:Grayscale"), &job, 3, true).is_ok());
         for feature in ["DocumentCollate", "JobDuplexAllDocumentsContiguously", "PageOutputColor"] {
             let start = xml.find(&format!("<psf:Feature name=\"psk:{feature}\"")).unwrap();
             let end = start + xml[start..].find("</psf:Feature>").unwrap() + "</psf:Feature>".len();
             let mut dropped = xml.clone();
             dropped.replace_range(start..end, "");
-            assert!(verify_job_options(&dropped, &job, 3).is_err(), "{feature}");
+            assert!(verify_job_options(&dropped, &job, 3, true).is_err(), "{feature}");
             if feature == "DocumentCollate" {
-                assert!(verify_job_options(&dropped, &job, 1).is_ok(), "one page has no collating order");
+                assert!(verify_job_options(&dropped, &job, 1, true).is_ok(), "one page has no collating order");
+            }
+            if feature == "JobDuplexAllDocumentsContiguously" {
+                assert!(verify_job_options(&dropped, &job, 3, false).is_err(), "two-sided asked of a printer without it");
             }
         }
         let default = Job::default();
         let xml = ticket_xml(&default, false);
-        assert!(verify_job_options(&xml, &default, 3).is_ok());
+        assert!(verify_job_options(&xml, &default, 3, true).is_ok());
         let simplex = Job { duplex: Some(Duplex::Off), ..Job::default() };
-        assert!(verify_job_options(&ticket_xml(&simplex, false), &simplex, 3).is_ok());
-        assert!(verify_job_options(&xml, &simplex, 3).is_err(), "explicit simplex must be confirmed");
-        assert!(verify_job_options("<not xml", &default, 1).is_err());
+        assert!(verify_job_options(&ticket_xml(&simplex, false), &simplex, 3, true).is_ok());
+        assert!(verify_job_options(&xml, &simplex, 3, true).is_err(), "explicit simplex must be confirmed");
+        // A driver with no two-sided feature (Zebra's label drivers) drops the one-sided request:
+        // it can only print one side, so nothing is lost and nothing is refused.
+        assert!(verify_job_options(&xml, &simplex, 3, false).is_ok(), "one-sided on a printer without two-sided");
+        assert!(verify_job_options("<not xml", &default, 1, true).is_err());
+    }
+
+    #[test]
+    fn a_sheet_is_turned_only_when_its_shape_differs_from_the_papers() {
+        let letter = Some((215_900, 279_400));
+        let label = Some((69_850, 31_750)); // 2.75 × 1.25 in, wider than tall as the printer feeds it
+        assert!(turned(true, letter), "a wide sheet on upright paper is turned");
+        assert!(!turned(false, letter));
+        assert!(!turned(true, label), "a wide sheet on a wide label stock lies as it is");
+        assert!(turned(false, label), "an upright sheet on a wide label stock is turned");
+        assert!(turned(true, None) && !turned(false, None), "no paper known: the sheet's shape, as on upright paper");
+        assert!(!turned(false, Some((100_000, 100_000))) && turned(true, Some((100_000, 100_000))), "a square paper is upright");
+        // A MUNBYN ITPP941's native 60 × 40 mm form (storytold/pdfcraft#2673): a wide sheet is not
+        // turned a second time, and its upright 102.1 × 152.15 mm form turns a wide sheet as usual.
+        let munbyn = Some((60_000, 40_000));
+        assert!(!turned(true, munbyn) && turned(false, munbyn));
+        let upright_label = Some((102_100, 152_150));
+        assert!(turned(true, upright_label) && !turned(false, upright_label));
+    }
+
+    #[test]
+    fn a_label_printer_is_monochrome_only_on_label_sized_paper() {
+        let label = Some((38_100, 25_400)); // 1.5 × 1 in
+        let four_by_six = Some((101_600, 152_400));
+        let letter = Some((215_900, 279_400));
+        assert!(label_shaped(&["Monochrome"], label) && label_shaped(&["Monochrome"], four_by_six));
+        assert!(!label_shaped(&["Monochrome"], letter), "a fax or a monochrome laser");
+        assert!(!label_shaped(&["Monochrome", "Color"], label), "a colour label printer keeps its driver's way");
+        assert!(!label_shaped(&[], label) && !label_shaped(&["Monochrome"], None) && !label_shaped(&["Monochrome"], Some((0, 25_400))));
+        assert_eq!(media_size(&validated((38_100, 25_400), "psk:Portrait", None)), Some((38_100, 25_400)));
+        assert_eq!(media_size("not xml"), None);
+    }
+
+    #[test]
+    fn the_paper_a_devmode_prints_on_is_read_from_its_public_fields() {
+        // A DEVMODEW: dmFields at 72, dmOrientation at 76, dmPaperSize 78, dmPaperLength 80, dmPaperWidth 82 (tenths of mm).
+        let mut dm = vec![0u8; 220];
+        let put16 = |dm: &mut Vec<u8>, at: usize, v: i16| dm[at..at + 2].copy_from_slice(&v.to_le_bytes());
+        dm[72..76].copy_from_slice(&(0x1u32 | 0x2 | 0x4 | 0x8).to_le_bytes());
+        put16(&mut dm, 76, 1);
+        put16(&mut dm, 78, 256);
+        put16(&mut dm, 80, 318); // 1.25 in
+        put16(&mut dm, 82, 699); // 2.75 in
+        assert_eq!(devmode_paper(&dm), Some(((69_900, 31_800), false)));
+        put16(&mut dm, 76, 2);
+        assert_eq!(devmode_paper(&dm), Some(((69_900, 31_800), true)), "landscape");
+        dm[72..76].copy_from_slice(&(0x2u32).to_le_bytes());
+        assert_eq!(devmode_paper(&dm), None, "no width and length fields set");
+        dm[72..76].copy_from_slice(&(0x4u32 | 0x8).to_le_bytes());
+        put16(&mut dm, 80, 0);
+        assert_eq!(devmode_paper(&dm), None, "a roll has no length");
+        assert_eq!(devmode_paper(&dm[..60]), None, "too short");
+        assert_eq!(devmode_paper(&[]), None);
     }
 
     #[test]
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
@@ -18,7 +18,7 @@ use winprint::ticket::{FeatureOptionPack as _, PageMediaSize, PrintCapabilities,
 
 use crate::sheets::{self, Fit, SAME};
 use crate::ticket::{self, Paper};
-use crate::{Error, Job, Printer, Scaling};
+use crate::{Error, Job, Printer, PrinterPaper, Scaling};
 
 fn spool_error(e: impl std::fmt::Display) -> Error {
     Error::Spool(e.to_string())
@@ -40,11 +40,105 @@ pub(crate) fn printers() -> Vec<Printer> {
 /// option for the feature the job's ticket sets. `None` when the capabilities can't be read. A
 /// driver without one cannot confirm an explicit two-sided request, which is refused before submission.
 fn two_sided(device: &PrinterDevice) -> Option<bool> {
-    let capabilities = PrintCapabilities::fetch(device).ok()?;
+    let capabilities = capabilities_for(device, user_ticket(device).as_ref()).ok()?;
     let duplex = OwnedName::qualified("JobDuplexAllDocumentsContiguously", NS_PSK, Some("psk"));
     Some(capabilities.options_for_feature(duplex).any(|o| o.name.as_ref().is_some_and(|n| n.local_name.starts_with("TwoSided"))))
 }
 
+/// [`crate::printer_papers`]: the driver's page media sizes as it has them, keyed by their
+/// Print Schema or vendor name and named by their display name, the driver's current default
+/// marked. Roll media (a side of 0) and sizes beyond PDF's 200-inch page limit are left out, as
+/// are repeated keywords.
+pub(crate) fn printer_papers(printer: &str) -> Vec<PrinterPaper> {
+    const MAX_SIDE: f64 = 14_400.0;
+    let Ok(devices) = PrinterDevice::all() else { return Vec::new() };
+    let Some(device) = devices.iter().find(|d| d.name() == printer) else { return Vec::new() };
+    let base = user_ticket(device);
+    let Ok(capabilities) = capabilities_for(device, base.as_ref()) else { return Vec::new() };
+    let default_key = default_media_keyword(device, base.as_ref(), &capabilities);
+    let mut out: Vec<PrinterPaper> = Vec::new();
+    for media in capabilities.page_media_sizes() {
+        if out.len() >= crate::MAX_PRINTER_PAPERS {
+            break;
+        }
+        let Some(key) = media.option().name.as_ref().map(|n| n.local_name.clone()).filter(|k| !k.is_empty()) else { continue };
+        let size = media.size();
+        let (w, h) = (ticket::points(size.width_in_micron()), ticket::points(size.height_in_micron()));
+        if size.is_roll() || !(1.0..=MAX_SIDE).contains(&w) || !(1.0..=MAX_SIDE).contains(&h) || out.iter().any(|p| p.key == key) {
+            continue;
+        }
+        let label = media.display_name().filter(|l| !l.trim().is_empty()).map_or_else(|| key.clone(), |l| l.trim().to_string());
+        let default = default_key.as_deref() == Some(key.as_str());
+        out.push(PrinterPaper { key, label, size: (w, h), default });
+    }
+    out
+}
+
+/// The signed-in user's own settings for the printer as a job ticket: what they set in its
+/// Printing Preferences (`PrintTicket::user_default`). Windows' Print Ticket calls otherwise work
+/// from the printer's Printing Defaults, which can hold another label stock, another darkness and
+/// no cut (a Zebra ZD500 whose defaults said 4.09 × 3.00 in, darkness 10, while the user had set
+/// 2.25 × 1.25 in, darkness 22); every ticket and capabilities request here starts from this one.
+/// `None` when the driver won't say, and then the printer's defaults apply as before.
+fn user_ticket(device: &PrinterDevice) -> Option<PrintTicket> {
+    PrintTicket::user_default(device).ok()
+}
+
+/// The driver's capabilities for the user's settings, else for the printer's defaults.
+fn capabilities_for(device: &PrinterDevice, base: Option<&PrintTicket>) -> Result<PrintCapabilities, Error> {
+    match base {
+        Some(base) => PrintCapabilities::fetch_for_ticket(device, base).or_else(|_| PrintCapabilities::fetch(device)),
+        None => PrintCapabilities::fetch(device),
+    }
+    .map_err(spool_error)
+}
+
+/// [`crate::is_label_printer`]: the driver's PageOutputColor options and the paper of the user's
+/// settings ([`ticket::label_shaped`]).
+pub(crate) fn is_label_printer(printer: &str) -> bool {
+    let Ok(devices) = PrinterDevice::all() else { return false };
+    let Some(device) = devices.iter().find(|d| d.name() == printer) else { return false };
+    let base = user_ticket(device);
+    let Ok(capabilities) = capabilities_for(device, base.as_ref()) else { return false };
+    let colour = OwnedName::qualified("PageOutputColor", NS_PSK, Some("psk"));
+    let colours: Vec<String> = capabilities.options_for_feature(colour).filter_map(|o| o.name.as_ref().map(|n| n.local_name.clone())).collect();
+    let colours: Vec<&str> = colours.iter().map(String::as_str).collect();
+    let paper = base.as_ref().and_then(|t| std::str::from_utf8(t.get_xml()).ok()).and_then(ticket::media_size);
+    ticket::label_shaped(&colours, paper)
+}
+
+/// The keyword of the driver's current default paper among `capabilities`' papers: the user's
+/// settings' paper when the capabilities have it, else an empty ticket merged and validated, which
+/// comes back complete at the printer's defaults (the stock a label printer is set up for, an
+/// office printer's tray paper); the capabilities fall back to the printer's defaults when the
+/// user's can't be read, and then so does the default. `None` when the driver won't say.
+fn default_media_keyword(device: &PrinterDevice, base: Option<&PrintTicket>, capabilities: &PrintCapabilities) -> Option<String> {
+    let offered = |key: &str| capabilities.page_media_sizes().any(|m| m.option().name.as_ref().is_some_and(|n| n.local_name == key));
+    if let Some(key) = base.and_then(|t| std::str::from_utf8(t.get_xml()).ok()).and_then(ticket::media_keyword)
+        && offered(&key)
+    {
+        return Some(key);
+    }
+    let mut builder = PrintTicketBuilder::new(device).ok()?;
+    builder.merge(PrintTicket::from_xml(winprint::ticket::DEFAULT_PRINT_TICKET_XML)).ok()?;
+    let validated = builder.build().ok()?;
+    std::str::from_utf8(validated.get_xml()).ok().and_then(ticket::media_keyword)
+}
+
+/// What [`driver_ticket`] settled on.
+struct DriverTicket {
+    /// The ticket the job is printed with.
+    ticket: PrintTicket,
+    /// The driver's merged and validated ticket: what the driver kept of the job's options
+    /// (copies, collation, two-sided, colour), held to them by `ticket::verify_job_options`
+    /// whichever ticket is printed with.
+    validated: PrintTicket,
+    /// The paper, in micrometres.
+    paper: (u32, u32),
+    /// Whether the driver has a two-sided feature at all.
+    duplex_offered: bool,
+}
+
 /// The queue the job goes to: the one asked for, else the default. Never another one: a job
 /// for a queue that has gone must not come out of a different printer.
 fn select_device(devices: Vec<PrinterDevice>, requested: Option<&str>, default: Option<&str>) -> Result<PrinterDevice, Error> {
@@ -62,8 +156,13 @@ fn select_device(devices: Vec<PrinterDevice>, requested: Option<&str>, default:
 /// its default paper is used. Returns the ticket and its paper in micrometres. Refused when the
 /// driver changed the paper, the orientation or the scaling: then nothing is sent, rather than a
 /// job at the wrong size.
-fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape: bool) -> Result<(PrintTicket, (u32, u32)), Error> {
-    let capabilities = PrintCapabilities::fetch(device).map_err(spool_error)?;
+fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape: bool) -> Result<DriverTicket, Error> {
+    let base = user_ticket(device);
+    let capabilities = capabilities_for(device, base.as_ref())?;
+    // Whether the driver has a two-sided feature at all: a label printer's doesn't, and its
+    // answer to a one-sided request is then not judged (see `ticket::verify_job_options`).
+    let duplex_offered =
+        capabilities.options_for_feature(OwnedName::qualified("JobDuplexAllDocumentsContiguously", NS_PSK, Some("psk"))).next().is_some();
     let medias: Vec<PageMediaSize> = capabilities.page_media_sizes().collect();
     let papers: Vec<Paper<'_>> = medias
         .iter()
@@ -87,7 +186,19 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape
             )));
         }
     };
-    let requested = PrintTicket::from_xml(ticket::ticket_xml(job, landscape));
+    // Which way the sheet lies on the paper: turned when its shape differs from the paper's as the
+    // driver feeds it. Office papers are upright, so a sheet wider than tall is turned; a label
+    // stock defined across the feed (a Zebra's 2.75 × 1.25 in) is wider than tall itself, so a
+    // sheet of that shape lies on it as it is and must not be turned, and an upright sheet must.
+    // A sheet that matches none of the driver's papers (shrink to fit) is printed on the driver's
+    // default paper, so the turn is judged against that one.
+    let fed = wanted.or_else(|| {
+        let key = default_media_keyword(device, base.as_ref(), &capabilities)?;
+        let m = medias.iter().find(|m| m.option().name.as_ref().is_some_and(|n| n.local_name == key))?;
+        Some((m.size().width_in_micron(), m.size().height_in_micron()))
+    });
+    let turned = ticket::turned(landscape, fed);
+    let requested = PrintTicket::from_xml(ticket::ticket_xml(job, turned));
     let mut document = PrintTicketDocument::parse_from_bytes(requested.get_xml()).map_err(spool_error)?;
     if let Some(media) = media {
         let media_ticket: PrintTicket = media.clone().into();
@@ -96,17 +207,73 @@ fn driver_ticket(device: &PrinterDevice, job: &Job, sheet: (f64, f64), landscape
         document.parameter_inits.extend(media_document.parameter_inits);
     }
     let scaling_name = OwnedName::qualified("PageScaling", NS_PSK, Some("psk"));
-    if let Some(no_scaling) = capabilities.options_for_feature(scaling_name.clone()).find(|o| o.name.as_ref().is_some_and(|n| n.local_name == "None"))
-    {
-        document.features.retain(|f| !(f.name.local_name == "PageScaling" && f.name.namespace_ref() == Some(NS_PSK)));
-        document.features.push(PrintFeature { name: scaling_name, properties: Vec::new(), options: vec![no_scaling.clone()], features: Vec::new() });
+    let scaling_offered: Vec<_> = capabilities.options_for_feature(scaling_name.clone()).collect();
+    let is_page_scaling = |f: &PrintFeature| f.name.local_name == "PageScaling" && f.name.namespace_ref() == Some(NS_PSK);
+    if scaling_offered.is_empty() {
+        // A driver without a PageScaling feature (Zebra's ZDesigner label drivers) is not asked
+        // for one: asked for None, it answers CustomSquare, which the check below would refuse,
+        // though it cannot scale by a feature it doesn't have.
+        document.features.retain(|f| !is_page_scaling(f));
+    } else if let Some(no_scaling) = scaling_offered.iter().find(|o| o.name.as_ref().is_some_and(|n| n.local_name == "None")) {
+        document.features.retain(|f| !is_page_scaling(f));
+        document.features.push(PrintFeature {
+            name: scaling_name,
+            properties: Vec::new(),
+            options: vec![(*no_scaling).clone()],
+            features: Vec::new(),
+        });
     }
+    let requested_xml = PrintTicket::from(document).into_xml();
+    // The job starts from the user's own settings (stock, darkness, cutter: the driver's private
+    // features ride along) and the job's options are merged over them.
     let mut builder = PrintTicketBuilder::new(device).map_err(spool_error)?;
-    builder.merge(PrintTicket::from(document)).map_err(spool_error)?;
+    if let Some(base) = &base {
+        builder.merge(base.clone()).map_err(spool_error)?;
+    }
+    builder.merge(PrintTicket::from_xml(requested_xml.clone())).map_err(spool_error)?;
     let validated = builder.build().map_err(spool_error)?;
     let xml = std::str::from_utf8(validated.get_xml()).map_err(spool_error)?;
-    let paper = ticket::verify_ticket(xml, wanted, landscape)?;
-    Ok((validated, paper))
+    // The paper the job comes out on is the one in the DEVMODE winprint prints with: the ticket
+    // converted on the user's own settings and settled by the driver. The merged ticket is built
+    // on the printer's defaults instead, and a driver whose one "User defined" stock is set in
+    // the user's Printing Preferences but not in the printer's Printing Defaults (Zebra's) names
+    // the defaults' size there while printing on the preferences'. So when the driver settles on
+    // the paper asked for, the ticket asked for is the one printed with, and the merged ticket is
+    // only held to the rest; otherwise the merged ticket is held to the paper as well. The ticket
+    // asked for is laid over the user's settings, so their private features (a cutter, a
+    // darkness, a finisher) are printed with too: the spooler merges a job's ticket onto the
+    // printer's defaults, not the user's.
+    let requested = PrintTicket::from_xml(requested_xml);
+    let requested = match &base {
+        Some(base) => overlay(base, &requested)?,
+        None => requested,
+    };
+    let settled = requested.to_dev_mode(device).ok().and_then(|dev_mode| ticket::devmode_paper(&dev_mode));
+    let same = |a: (u32, u32), b: (u32, u32)| a.0.abs_diff(b.0) <= ticket::PAPER_TOLERANCE && a.1.abs_diff(b.1) <= ticket::PAPER_TOLERANCE;
+    if let (Some((size, settled_landscape)), Some(want)) = (settled, wanted)
+        && (same(size, want) || same(size, (want.1, want.0)))
+        && settled_landscape == turned
+    {
+        ticket::verify_ticket(xml, None, turned, !scaling_offered.is_empty())?;
+        return Ok(DriverTicket { ticket: requested, validated, paper: size, duplex_offered });
+    }
+    let paper = ticket::verify_ticket(xml, wanted, turned, !scaling_offered.is_empty())?;
+    Ok(DriverTicket { ticket: validated.clone(), validated, paper, duplex_offered })
+}
+
+/// `over` laid over `base`: each of `over`'s features, parameters and properties replaces
+/// `base`'s of the same name, and the rest of `base` is kept.
+fn overlay(base: &PrintTicket, over: &PrintTicket) -> Result<PrintTicket, Error> {
+    let same = |a: &OwnedName, b: &OwnedName| a.local_name == b.local_name && a.namespace == b.namespace;
+    let mut document = PrintTicketDocument::parse_from_bytes(base.get_xml()).map_err(spool_error)?;
+    let top = PrintTicketDocument::parse_from_bytes(over.get_xml()).map_err(spool_error)?;
+    document.features.retain(|f| !top.features.iter().any(|t| same(&t.name, &f.name)));
+    document.features.extend(top.features);
+    document.parameter_inits.retain(|p| !top.parameter_inits.iter().any(|t| same(&t.name, &p.name)));
+    document.parameter_inits.extend(top.parameter_inits);
+    document.properties.retain(|p| !top.properties.iter().any(|t| same(&t.name, &p.name)));
+    document.properties.extend(top.properties);
+    Ok(PrintTicket::from(document))
 }
 
 /// The widest unprintable margin of the ticket's paper, in points; 0 when the driver doesn't say.
@@ -142,9 +309,9 @@ pub(crate) fn submit_with_progress(pdf: &[u8], job: &Job, progress: &mut dyn FnM
     let read = &read;
     // The worker is joined before `dir` is dropped: the spooler reads the file.
     let result = crate::progress::run(progress, move |sender| {
-        let (ticket, paper) = driver_ticket(&device, job, first, landscape)?;
-        let xml = std::str::from_utf8(ticket.get_xml()).map_err(spool_error)?;
-        ticket::verify_job_options(xml, job, count)?;
+        let DriverTicket { ticket, validated, paper, duplex_offered } = driver_ticket(&device, job, first, landscape)?;
+        let xml = std::str::from_utf8(validated.get_xml()).map_err(spool_error)?;
+        ticket::verify_job_options(xml, job, count, duplex_offered)?;
         let (fit, paper) = match job.scaling {
             Scaling::Exact => (Fit::Exact, first),
             Scaling::ShrinkToFit => {
@@ -198,8 +365,8 @@ mod tests {
         let name = std::env::var("CRAFT_PRINT_TEST_PRINTER").unwrap();
         let device = select_device(PrinterDevice::all().unwrap(), Some(&name), None).unwrap();
         let job = Job { copies: 2, ..Job::default() };
-        let (ticket, paper) = driver_ticket(&device, &job, (612.0, 792.0), false).unwrap();
-        ticket::verify_job_options(std::str::from_utf8(ticket.get_xml()).unwrap(), &job, 1).unwrap();
+        let DriverTicket { ticket, validated, paper, duplex_offered } = driver_ticket(&device, &job, (612.0, 792.0), false).unwrap();
+        ticket::verify_job_options(std::str::from_utf8(validated.get_xml()).unwrap(), &job, 1, duplex_offered).unwrap();
         assert_eq!(paper, (215_900, 279_400));
         eprintln!("unprintable margin: {} pt", imageable_margin(&device, &ticket));
         if let Ok(path) = std::env::var("CRAFT_PRINT_TEST_TICKET_OUTPUT") {
@@ -211,8 +378,8 @@ mod tests {
             ("JobDuplexAllDocumentsContiguously", "TwoSidedLongEdge", Job { duplex: Some(crate::Duplex::LongEdge), ..Job::default() }),
             ("DocumentCollate", "Collated", Job { copies: 2, ..Job::default() }),
         ] {
-            let (ticket, _) = driver_ticket(&device, &requested, (612.0, 792.0), false).unwrap();
-            let retained = ticket::verify_job_options(std::str::from_utf8(ticket.get_xml()).unwrap(), &requested, 3);
+            let DriverTicket { validated, duplex_offered, .. } = driver_ticket(&device, &requested, (612.0, 792.0), false).unwrap();
+            let retained = ticket::verify_job_options(std::str::from_utf8(validated.get_xml()).unwrap(), &requested, 3, duplex_offered);
             eprintln!("{feature}={wanted}: {retained:?}");
             let offered = capabilities
                 .options_for_feature(OwnedName::qualified(feature, NS_PSK, Some("psk")))
