fix(print): preserve cached DEVMODE printer preferences on Windows (#36) #37

Merged
gronod merged 1 commits from bugfix/issue-36-windows-devmode-preferences into development 2026-08-24 09:13:00 +01:00
7 changed files with 112 additions and 47 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "tmp_tauri", "name": "tmp_tauri",
"version": "0.1.0", "version": "0.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tmp_tauri", "name": "tmp_tauri",
"version": "0.1.0", "version": "0.1.3",
"dependencies": { "dependencies": {
"@tauri-apps/plugin-dialog": "^2.7.2" "@tauri-apps/plugin-dialog": "^2.7.2"
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "tmp_tauri", "name": "tmp_tauri",
"private": true, "private": true,
"version": "0.1.2", "version": "0.1.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"tauri": "tauri" "tauri": "tauri"
+1 -1
View File
@@ -1606,7 +1606,7 @@ dependencies = [
[[package]] [[package]]
name = "iccery" name = "iccery"
version = "0.1.2" version = "0.1.3"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image", "image",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "iccery" name = "iccery"
version = "0.1.2" version = "0.1.3"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
+56
View File
@@ -186,5 +186,61 @@ printer Custom_Queue unknown state\n\
let err = res.unwrap_err(); let err = res.unwrap_err();
assert!(err.contains("Target TIFF file not found")); 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::<DEVMODEW>() + 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::<DEVMODEW>() + 10] = 0xAA;
buffer[std::mem::size_of::<DEVMODEW>() + 11] = 0x55;
let p_devmode = buffer.as_mut_ptr() as *mut DEVMODEW;
unsafe {
(*p_devmode).dmSize = std::mem::size_of::<DEVMODEW>() 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::<DEVMODEW>() + 10], 0xAA);
assert_eq!(retrieved[std::mem::size_of::<DEVMODEW>() + 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::<DEVMODEW>() + 10], 0xAA);
assert_eq!(work_buf[std::mem::size_of::<DEVMODEW>() + 11], 0x55);
}
}
+42 -33
View File
@@ -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 /// Print target TIFF file to printer with GDI ICM bypass, DEVMODE overrides, and auto-fit scaling
pub fn print_target( pub fn print_target(
printer_name: &str, printer_name: &str,
@@ -407,16 +436,16 @@ pub fn print_target(
); );
let mut devmode_buf = if devmode_size > 0 { let mut devmode_buf = if devmode_size > 0 {
let mut buf = devmode_store let mut buf = if let Some(cached) = devmode_store.and_then(|s| s.get(printer_name)) {
.and_then(|s| s.get(printer_name)) let mut b = cached;
.unwrap_or_else(|| vec![0u8; devmode_size as usize]); if b.len() < devmode_size as usize {
b.resize(devmode_size as usize, 0);
if buf.len() < devmode_size as usize {
buf.resize(devmode_size as usize, 0);
} }
b
let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW; } else {
let res = DocumentPropertiesW( let mut b = vec![0u8; devmode_size as usize];
let p_devmode = b.as_mut_ptr() as *mut DEVMODEW;
let _ = DocumentPropertiesW(
HWND::default(), HWND::default(),
h_printer, h_printer,
PCWSTR(printer_wide.as_ptr()), PCWSTR(printer_wide.as_ptr()),
@@ -424,35 +453,15 @@ pub fn print_target(
None, None,
DM_OUT_BUFFER, DM_OUT_BUFFER,
); );
b
};
if res >= 0 { let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW;
// Apply strict ICM bypass apply_print_options_to_devmode(p_devmode, options);
(*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) Some(buf)
} else { } else {
None None
}
} else {
None
}; };
let p_devmode_ptr = devmode_buf let p_devmode_ptr = devmode_buf
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery", "productName": "ICCery",
"version": "0.1.2", "version": "0.1.3",
"identifier": "com.gronod.iccery", "identifier": "com.gronod.iccery",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"