From e22845fd7300f56a214e844bc81833276ac73699 Mon Sep 17 00:00:00 2001 From: gronod Date: Mon, 24 Aug 2026 09:12:38 +0100 Subject: [PATCH] fix(print): preserve cached DEVMODE printer preferences on Windows (#36) --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/print/tests.rs | 56 +++++++++++++++++++++ src-tauri/src/print/windows.rs | 91 +++++++++++++++++++--------------- src-tauri/tauri.conf.json | 2 +- 7 files changed, 112 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e370dc..b2c28fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tmp_tauri", - "version": "0.1.0", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tmp_tauri", - "version": "0.1.0", + "version": "0.1.3", "dependencies": { "@tauri-apps/plugin-dialog": "^2.7.2" }, diff --git a/package.json b/package.json index 2db6556..1acc0f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tmp_tauri", "private": true, - "version": "0.1.2", + "version": "0.1.3", "type": "module", "scripts": { "tauri": "tauri" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d8c51b5..2a7239b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1606,7 +1606,7 @@ dependencies = [ [[package]] name = "iccery" -version = "0.1.2" +version = "0.1.3" dependencies = [ "base64 0.22.1", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dfd8b83..c6a8765 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.1.2" +version = "0.1.3" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/print/tests.rs b/src-tauri/src/print/tests.rs index 8cf24f5..b0deef7 100644 --- a/src-tauri/src/print/tests.rs +++ b/src-tauri/src/print/tests.rs @@ -186,5 +186,61 @@ printer Custom_Queue unknown state\n\ let err = res.unwrap_err(); assert!(err.contains("Target TIFF file not found")); } + + #[cfg(windows)] + #[test] + fn test_windows_devmode_preservation_and_overrides() { + use windows::Win32::Graphics::Gdi::DEVMODEW; + use crate::print::windows::apply_print_options_to_devmode; + + let devmode_size = std::mem::size_of::() + 128; // Public DEVMODE + 128 bytes private OEM extra + let mut buffer = vec![0u8; devmode_size]; + + // Simulate OEM private driver payload (e.g. Epson Print Preview flag in private offset) + buffer[std::mem::size_of::() + 10] = 0xAA; + buffer[std::mem::size_of::() + 11] = 0x55; + + let p_devmode = buffer.as_mut_ptr() as *mut DEVMODEW; + unsafe { + (*p_devmode).dmSize = std::mem::size_of::() as u16; + (*p_devmode).dmDriverExtra = 128; + } + + // Store into PrinterDevModeStore + let store = PrinterDevModeStore::new(); + store.set("EPSON-Test-Printer", buffer.clone()); + + let retrieved = store.get("EPSON-Test-Printer").expect("Failed to get cached DEVMODE"); + assert_eq!(retrieved.len(), devmode_size); + // Verify private OEM payload is preserved + assert_eq!(retrieved[std::mem::size_of::() + 10], 0xAA); + assert_eq!(retrieved[std::mem::size_of::() + 11], 0x55); + + // Apply UI overrides + let opts = PrintOptions { + paper_source: Some(3), + orientation: Some("landscape".to_string()), + paper_size: None, + ppd_uncorrected_passthrough: None, + }; + + let mut work_buf = retrieved; + let p_work_devmode = work_buf.as_mut_ptr() as *mut DEVMODEW; + apply_print_options_to_devmode(p_work_devmode, Some(&opts)); + + unsafe { + // Verify ICM bypass is applied + assert_eq!((*p_work_devmode).dmICMMethod, 1); // DMICMMETHOD_NONE = 1 + // Verify tray override + assert_eq!((*p_work_devmode).Anonymous1.Anonymous1.dmDefaultSource, 3); + // Verify orientation override + assert_eq!((*p_work_devmode).Anonymous1.Anonymous1.dmOrientation, 2); // DMORIENT_LANDSCAPE = 2 + } + + // Verify private OEM payload was NOT corrupted or overwritten + assert_eq!(work_buf[std::mem::size_of::() + 10], 0xAA); + assert_eq!(work_buf[std::mem::size_of::() + 11], 0x55); + } } + diff --git a/src-tauri/src/print/windows.rs b/src-tauri/src/print/windows.rs index 2d0328d..d8283df 100644 --- a/src-tauri/src/print/windows.rs +++ b/src-tauri/src/print/windows.rs @@ -345,6 +345,35 @@ pub fn show_printer_properties( } } +/// Apply ICM bypass and user options onto an active DEVMODE structure +pub fn apply_print_options_to_devmode( + p_devmode: *mut DEVMODEW, + options: Option<&PrintOptions>, +) { + unsafe { + // Apply strict ICM bypass + (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ICMMETHOD); + (*p_devmode).dmICMMethod = DMICMMETHOD_NONE; + + // Apply UI options if specified + if let Some(opts) = options { + if let Some(tray_id) = opts.paper_source { + (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_DEFAULTSOURCE); + (*p_devmode).Anonymous1.Anonymous1.dmDefaultSource = tray_id as i16; + } + + if let Some(ref orient) = opts.orientation { + (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ORIENTATION); + if orient.eq_ignore_ascii_case("landscape") { + (*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_LANDSCAPE; + } else { + (*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_PORTRAIT; + } + } + } + } +} + /// Print target TIFF file to printer with GDI ICM bypass, DEVMODE overrides, and auto-fit scaling pub fn print_target( printer_name: &str, @@ -407,50 +436,30 @@ pub fn print_target( ); let mut devmode_buf = if devmode_size > 0 { - let mut buf = devmode_store - .and_then(|s| s.get(printer_name)) - .unwrap_or_else(|| vec![0u8; devmode_size as usize]); - - if buf.len() < devmode_size as usize { - buf.resize(devmode_size as usize, 0); - } + let mut buf = if let Some(cached) = devmode_store.and_then(|s| s.get(printer_name)) { + let mut b = cached; + if b.len() < devmode_size as usize { + b.resize(devmode_size as usize, 0); + } + b + } else { + let mut b = vec![0u8; devmode_size as usize]; + let p_devmode = b.as_mut_ptr() as *mut DEVMODEW; + let _ = DocumentPropertiesW( + HWND::default(), + h_printer, + PCWSTR(printer_wide.as_ptr()), + Some(p_devmode), + None, + DM_OUT_BUFFER, + ); + b + }; let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW; - let res = DocumentPropertiesW( - HWND::default(), - h_printer, - PCWSTR(printer_wide.as_ptr()), - Some(p_devmode), - None, - DM_OUT_BUFFER, - ); + apply_print_options_to_devmode(p_devmode, options); - if res >= 0 { - // Apply strict ICM bypass - (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ICMMETHOD); - (*p_devmode).dmICMMethod = DMICMMETHOD_NONE; - - // Apply UI options if specified - if let Some(opts) = options { - if let Some(tray_id) = opts.paper_source { - (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_DEFAULTSOURCE); - (*p_devmode).Anonymous1.Anonymous1.dmDefaultSource = tray_id as i16; - } - - if let Some(ref orient) = opts.orientation { - (*p_devmode).dmFields |= DEVMODE_FIELD_FLAGS(DM_ORIENTATION); - if orient.eq_ignore_ascii_case("landscape") { - (*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_LANDSCAPE; - } else { - (*p_devmode).Anonymous1.Anonymous1.dmOrientation = DMORIENT_PORTRAIT; - } - } - } - - Some(buf) - } else { - None - } + Some(buf) } else { None }; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9b192b0..f7c6d99 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ICCery", - "version": "0.1.2", + "version": "0.1.3", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" -- 2.39.5