From c73ee2c5aae69bd2739872cff0f9b2895956b802 Mon Sep 17 00:00:00 2001 From: mechpanos <25664832+mechpanos@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:22:05 +0300 Subject: [PATCH] fix(print): ask the driver for the DEVMODE size with no mode flags `with_printer_devmode` sized the driver's DEVMODE with a `DocumentPropertiesW` call that passed `DM_OUT_BUFFER` and no output buffer to write into. That combination is not a size query: drivers answer it with -1, the `needed <= 0` guard returned early, and the function handed back `None` for every printer on every machine. Measured here on Windows 11 x86-64, both an installed virtual printer and a physical one: fMode = 0 -> 5428 / 2684 bytes (the real size) fMode = DM_OUT_BUFFER, NULL out -> -1 Two things were broken by it, both silently: - `oriented_printer_devmode` builds the DEVMODE a print job is spooled with. Getting `None` back, the direct GDI path fell through to "CreateDC then falls back to the driver default", so the plot's orientation and sheet never reached the job. A landscape plot came out however the driver happened to be set. This is the route users without a PDF application take. - `PRINTERS ` could never name the driver's default sheet, because the id it matches on comes from that same DEVMODE. With the size query asking for a size, both work: the report now says "Default sheet: ISO A4 (210 x 297 mm)" for both printers here, and a landscape plot leaves `dmOrientation = DMORIENT_LANDSCAPE` set in the job's DEVMODE. The two new tests both fail with the old flag and pass with it removed. The existing GDI ink tests do not catch this: they accept the raster rotation fallback as an equally good outcome, so the page looked right while the DEVMODE was empty. Co-Authored-By: Claude Opus 5 --- src/io/print_to_printer.rs | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 29f6578da..4e58e88c0 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -625,13 +625,17 @@ fn with_printer_devmode( return None; } let result = (|| { + // The size query is the one call that takes no mode flags at all: + // with DM_OUT_BUFFER and no output buffer to write into, the + // driver reports failure (-1) instead of the size, and every + // caller below silently lost the driver's DEVMODE. let needed = DocumentPropertiesW( std::ptr::null_mut(), handle, device_wide.as_ptr(), std::ptr::null_mut(), std::ptr::null(), - DM_OUT_BUFFER, + 0, ); if needed <= 0 { return None; @@ -1518,6 +1522,77 @@ mod printer_properties_tests { ); } + /// The driver's DEVMODE has to come back for a real printer. It stopped + /// coming back once the size query asked with `DM_OUT_BUFFER` and no + /// output buffer, which drivers answer with -1: `with_printer_devmode` + /// then returned `None` for every printer, so the plot's sheet and + /// orientation never reached the job's DEVMODE and the printer report + /// could never name a default sheet. + #[cfg(target_os = "windows")] + #[test] + fn printer_devmode_comes_back_for_a_real_printer() { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Graphics::Gdi::DEVMODEW; + + let device: Vec = std::ffi::OsStr::new("Microsoft Print to PDF") + .encode_wide() + .chain(Some(0)) + .collect(); + let devmode = super::with_printer_devmode(&device, |_, _| true) + .expect("the driver must report a DEVMODE for an installed printer"); + assert!( + devmode.len() >= std::mem::size_of::(), + "DEVMODE buffer is {} bytes, shorter than the public struct", + devmode.len() + ); + // SAFETY: the buffer holds at least a public DEVMODEW, just checked. + let named = unsafe { (*(devmode.as_ptr() as *const DEVMODEW)).dmSize } as usize; + assert!(named > 0 && named <= devmode.len(), "dmSize {named} outside the buffer"); + + // And the symptom the lost DEVMODE produced: the driver's default + // sheet is one of the sheets it lists, so the report can name it. + let caps = crate::io::plot_device::printer_capabilities("Microsoft Print to PDF") + .expect("an installed printer reports sheets"); + assert!( + caps.default_paper.is_some(), + "the driver's default sheet should resolve to one of its {} listed sheets", + caps.media.len() + ); + } + + /// The plot's orientation has to survive into the DEVMODE the print job + /// is spooled with. While the DEVMODE query was broken this silently + /// returned `None` for every printer and `CreateDC` fell back to the + /// driver's own default, so a landscape plot came out however the driver + /// happened to be set. The GDI ink tests cannot catch that: they accept + /// the raster-rotation fallback as an equally good outcome. + #[cfg(target_os = "windows")] + #[test] + fn the_plot_orientation_reaches_the_job_devmode() { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Graphics::Gdi::{ + DEVMODEW, DMORIENT_LANDSCAPE, DMORIENT_PORTRAIT, DM_ORIENTATION, + }; + + let device: Vec = std::ffi::OsStr::new("Microsoft Print to PDF") + .encode_wide() + .chain(Some(0)) + .collect(); + for (landscape, want) in [(false, DMORIENT_PORTRAIT), (true, DMORIENT_LANDSCAPE)] { + let buf = super::oriented_printer_devmode(&device, landscape, None) + .unwrap_or_else(|| panic!("landscape={landscape}: the driver reported no DEVMODE")); + // SAFETY: `with_printer_devmode` sizes the buffer from the driver. + let dm = unsafe { &*(buf.as_ptr() as *const DEVMODEW) }; + assert!( + dm.dmFields & DM_ORIENTATION != 0, + "landscape={landscape}: DM_ORIENTATION not marked as set" + ); + // SAFETY: the printer branch of the DEVMODE union. + let got = unsafe { dm.Anonymous1.Anonymous1.dmOrientation }; + assert_eq!(got, want as i16, "landscape={landscape}: wrong dmOrientation"); + } + } + #[cfg(target_os = "macos")] #[test] fn macos_opens_print_settings() {