diff --git a/crates/print/README.md b/crates/print/README.md index 27b5a72f..bda49a76 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -40,7 +40,8 @@ opens Windows printer preferences and submits jobs with copies, collation, duple `GetDefaultPrinter`. `printers` also reads each driver's PrintCapabilities to report two-sided support, which can wait on the device (a networked printer in power save); `printer_queues` lists the same queues without asking any driver, so a Print dialog's list never waits on a - device. A job asks the driver too before it is sent; `submit_cancellable` with a + device, and `two_sided(name)` asks the driver of just the printer picked. A job asks the + driver too before it is sent; `submit_cancellable` with a `Cancel` token lets the app stop a job still waiting on it, and a cancelled job is never sent. A job is drawn as vectors by the in-box `Windows.Data.Pdf` with Direct2D into the XPS print API (the `winprint` crate), and copies, collation, duplex, colour, paper, diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index a4849bde..0af3eb4e 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -438,6 +438,23 @@ pub fn printer_queues() -> Vec { } } +/// Whether `printer`'s driver takes two-sided settings, read for that one printer: the answer +/// [`printers`] gives for it, without asking every other driver too. On Windows that is its +/// PrintCapabilities; `None` when they can't be read or no queue has that name. Elsewhere `None`, +/// as [`printers`] reports. It can wait on the device, so a Print dialog listing from +/// [`printer_queues`] asks it for the printer picked, off the UI thread. +pub fn two_sided(printer: &str) -> Option { + #[cfg(windows)] + { + windows::two_sided_named(printer) + } + #[cfg(not(windows))] + { + let _ = printer; + None + } +} + /// The Print dialog's printers from lpstat's two answers: the spooler's queues (`-p -d`), then /// the driverless destinations of `available` (`-e`) that have no queue — CUPS builds a temporary /// queue when `lp` sends them a job. Each destination once, queues first; the default still comes diff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs index ec9cd1d0..feae2bb8 100644 --- a/crates/print/src/tests/windows_native.rs +++ b/crates/print/src/tests/windows_native.rs @@ -322,6 +322,14 @@ fn an_encrypted_file_is_turned_but_not_reframed() { assert!(matches!(sheets.update(&shrunk), Err(Error::Spool(ref m)) if m.contains("encrypted"))); } +#[test] +fn two_sided_is_unknown_for_a_printer_that_is_not_there() { + // A name no queue has, as when a printer was removed after the list was read: unknown, at + // once, never an error or a guess. + assert_eq!(spool::two_sided("craft-print: no such printer \u{1F5A8}"), None); + assert_eq!(spool::two_sided(""), None); +} + #[test] fn unreadable_sheets_are_errors_never_panics() { for bad in [&b""[..], b"not a pdf", b"%PDF-1.7\nstartxref\n999999\n%%EOF", b"%PDF-1.7\nxref\n0 1\ntrailer\n<< >>\nstartxref\n9\n%%EOF"] { @@ -354,6 +362,22 @@ fn unreadable_sheets_are_errors_never_panics() { } } +/// What `f` returns, and how many driver capability queries it made on this thread. +#[cfg(windows)] +fn driver_queries(f: impl FnOnce() -> T) -> (T, usize) { + let queries = || crate::windows::CAPABILITY_QUERIES.with(std::cell::Cell::get); + let start = queries(); + let out = f(); + (out, queries().saturating_sub(start)) +} + +/// `two_sided(name)` and the driver queries it made, beside what it should be: the answer +/// `printers()` listed for that queue, from one query. +#[cfg(windows)] +fn picked_printer(name: &str, listed: Option<&Printer>) -> ((Option, usize), (Option, usize)) { + (driver_queries(|| spool::two_sided(name)), (listed.and_then(|p| p.two_sided), 1)) +} + /// Windows: the printer list comes from the spooler. A bare machine may have no printer; the /// list that exists has real names and at most one default. #[cfg(windows)] @@ -449,15 +473,12 @@ fn windows_jobs_reach_the_spooler() { eprintln!("skipping the real print: the test printer could not be prepared: {why}"); return; } - let queries = || crate::windows::CAPABILITY_QUERIES.with(std::cell::Cell::get); - let start = queries(); - let listed = spool::printers().into_iter().find(|p| p.name == name); - let probed = queries().saturating_sub(start); + let (listed, probed) = driver_queries(|| spool::printers().into_iter().find(|p| p.name == name)); // The list a Print dialog shows first: the same queue, named without asking its driver (a // networked printer in power save can hold that query, storytold/wordcraft#5710). - let start = queries(); - let queue = spool::printer_queues().into_iter().find(|p| p.name == name); - let unprobed = queries().saturating_sub(start); + let (queue, unprobed) = driver_queries(|| spool::printer_queues().into_iter().find(|p| p.name == name)); + // Then the one printer picked: its driver alone, the same answer printers() gave. + let (picked, expected) = picked_printer(name, listed.as_ref()); let mixed = test_pdf(&[LETTER, LETTER_LANDSCAPE, LETTER], true); let job = Job { printer: Some(name.into()), title: "mixed.pdf".into(), ..Job::default() }; let caller = std::thread::current().id(); @@ -502,7 +523,8 @@ fn windows_jobs_reach_the_spooler() { assert!(listed.as_ref().is_some_and(|p| p.two_sided.is_some()), "the new printer is listed, its duplex support known: {listed:?}"); assert!(queue.as_ref().is_some_and(|p| p.two_sided.is_none()), "the new printer is listed, its duplex support unknown: {queue:?}"); assert!(probed > 0, "printers() asks each driver, so the count sees driver queries: {probed}"); - assert_eq!(unprobed, 0, "printer_queues() asks no driver"); + // printer_queues() asks no driver; two_sided(name) answers as printers() did, asking that one only. + assert_eq!((unprobed, picked), (0, expected)); assert_eq!(sent.as_deref(), Ok("spooled 3 sheet(s)"), "{sent:?}"); if let Some(refused) = duplex_refused { assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("simplex/duplex") && m.contains("nothing was sent")), "{refused:?}"); diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs index 01ad171e..d5d13fcf 100644 --- a/crates/print/src/windows.rs +++ b/crates/print/src/windows.rs @@ -63,6 +63,11 @@ fn spooler_list(two_sided: impl Fn(&PrinterDevice) -> Option) -> Vec Option { + PrinterDevice::all().ok()?.iter().find(|d| d.name() == name).and_then(two_sided) +} + #[cfg(test)] thread_local! { /// The driver capability queries printer discovery made on this thread, so a test can show