From 5c238e9db9979c22966d14b3b427cf67ca39b00b Mon Sep 17 00:00:00 2001 From: gronod Date: Sun, 23 Aug 2026 11:11:51 +0100 Subject: [PATCH 1/4] feat(print): implement Windows native raw print spooler and printer enumeration (#26) --- src-tauri/Cargo.lock | 216 +++++++++++++++++++++-- src-tauri/Cargo.toml | 9 + src-tauri/src/commands.rs | 10 ++ src-tauri/src/lib.rs | 3 + src-tauri/src/print/mod.rs | 16 ++ src-tauri/src/print/windows.rs | 309 +++++++++++++++++++++++++++++++++ 6 files changed, 549 insertions(+), 14 deletions(-) create mode 100644 src-tauri/src/print/mod.rs create mode 100644 src-tauri/src/print/windows.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bfeb766..867c792 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -334,6 +334,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.1" @@ -567,6 +573,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -965,6 +977,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1423,6 +1441,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1580,6 +1609,7 @@ name = "iccery" version = "0.1.1" dependencies = [ "base64 0.22.1", + "image", "serde", "serde_json", "tauri", @@ -1587,6 +1617,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-opener", "tokio", + "windows 0.58.0", ] [[package]] @@ -1709,6 +1740,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", + "zune-core", + "zune-jpeg", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2074,6 +2121,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.19.3" @@ -2678,6 +2735,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.41.0" @@ -3384,7 +3453,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3455,7 +3524,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3596,7 +3665,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.20", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -3622,7 +3691,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3647,7 +3716,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -3762,6 +3831,20 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.55" @@ -4409,10 +4492,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -4433,10 +4516,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.20", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -4483,6 +4572,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -4505,14 +4604,27 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -4524,8 +4636,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -4542,6 +4654,17 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4553,6 +4676,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -4586,6 +4720,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4604,6 +4747,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -4946,7 +5099,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -5066,6 +5219,26 @@ dependencies = [ "serde", ] +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -5126,6 +5299,21 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.15.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bb1b5ee..96cf89b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,4 +25,13 @@ serde_json = "1" tokio = { version = "1", features = ["full"] } tauri-plugin-dialog = "2.7.2" base64 = "0.22" +image = { version = "0.25", default-features = false, features = ["tiff", "png", "jpeg"] } +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_Graphics_Printing", + "Win32_Storage_FileSystem", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 65b4f2b..4d08de1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -342,6 +342,16 @@ pub async fn run_profcheck( state.spawn(app, id, binary, args, cwd).await } +#[tauri::command] +pub async fn get_windows_printers() -> Result, String> { + crate::print::get_printers() +} + +#[tauri::command] +pub async fn print_target_windows(printer_name: String, tiff_path: String) -> Result<(), String> { + crate::print::print_target(&printer_name, &tiff_path) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4b1c93d..b910590 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod commands; mod events; +mod print; mod process_manager; mod settings; @@ -23,6 +24,8 @@ pub fn run() { commands::run_chartread, commands::run_colprof, commands::run_profcheck, + commands::get_windows_printers, + commands::print_target_windows, settings::load_settings, settings::save_settings, ]) diff --git a/src-tauri/src/print/mod.rs b/src-tauri/src/print/mod.rs new file mode 100644 index 0000000..f468d79 --- /dev/null +++ b/src-tauri/src/print/mod.rs @@ -0,0 +1,16 @@ +#[cfg(windows)] +pub mod windows; + +#[cfg(windows)] +pub use windows::*; + +#[cfg(not(windows))] +pub fn get_printers() -> Result, String> { + Err("Direct native printer querying is only supported on Windows in this build.".to_string()) +} + +#[cfg(not(windows))] +pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> { + let _ = (printer_name, tiff_path); + Err("Direct native raw printing is currently implemented for Windows. macOS/Linux CUPS support is planned for the next sub-task.".to_string()) +} diff --git a/src-tauri/src/print/windows.rs b/src-tauri/src/print/windows.rs new file mode 100644 index 0000000..efcf261 --- /dev/null +++ b/src-tauri/src/print/windows.rs @@ -0,0 +1,309 @@ +use std::ffi::{c_void, OsStr}; +use std::os::windows::ffi::OsStrExt; +use std::path::Path; +use windows::core::{PCWSTR, PWSTR}; +use windows::Win32::Foundation::{HANDLE, HWND}; +use windows::Win32::Graphics::Gdi::{ + CreateDCW, DeleteDC, GetDeviceCaps, SetICMMode, StretchDIBits, + BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HDC, HORZRES, + ICM_OFF, LOGPIXELSX, LOGPIXELSY, SRCCOPY, VERTRES, +}; +use windows::Win32::Graphics::Printing::{ + ClosePrinter, DocumentPropertiesW, EndDoc, EndPage, EnumPrintersW, + OpenPrinterW, StartDocW, StartPage, DEVMODEW, DMICMMETHOD_NONE, + DM_ICMMETHOD, DM_OUT_BUFFER, DOCINFOW, PRINTER_ENUM_CONNECTIONS, + PRINTER_ENUM_LOCAL, PRINTER_INFO_2W, PRINTER_INFO_4W, +}; + +fn to_wide(s: &str) -> Vec { + OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect() +} + +/// Enumerate available printers on the system +pub fn get_printers() -> Result, String> { + unsafe { + let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS; + let mut bytes_needed = 0u32; + let mut count = 0u32; + + // Query buffer size using Level 4 (fast local & network connections) + let _ = EnumPrintersW( + flags, + PCWSTR::null(), + 4, + None, + 0, + &mut bytes_needed, + &mut count, + ); + + if bytes_needed == 0 { + // Fallback to Level 2 + let _ = EnumPrintersW( + flags, + PCWSTR::null(), + 2, + None, + 0, + &mut bytes_needed, + &mut count, + ); + + if bytes_needed == 0 { + return Ok(Vec::new()); + } + + let mut buffer = vec![0u8; bytes_needed as usize]; + let success = EnumPrintersW( + flags, + PCWSTR::null(), + 2, + Some(buffer.as_mut_ptr()), + bytes_needed, + &mut bytes_needed, + &mut count, + ); + + if !success.as_bool() { + return Err("Failed to enumerate printers (Level 2)".to_string()); + } + + let p_info = buffer.as_ptr() as *const PRINTER_INFO_2W; + let mut printers = Vec::new(); + for i in 0..count as usize { + let info = *p_info.add(i); + if !info.pPrinterName.is_null() { + let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?; + printers.push(name); + } + } + return Ok(printers); + } + + let mut buffer = vec![0u8; bytes_needed as usize]; + let success = EnumPrintersW( + flags, + PCWSTR::null(), + 4, + Some(buffer.as_mut_ptr()), + bytes_needed, + &mut bytes_needed, + &mut count, + ); + + if !success.as_bool() { + return Err("Failed to enumerate printers (Level 4)".to_string()); + } + + let p_info = buffer.as_ptr() as *const PRINTER_INFO_4W; + let mut printers = Vec::new(); + for i in 0..count as usize { + let info = *p_info.add(i); + if !info.pPrinterName.is_null() { + let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?; + printers.push(name); + } + } + Ok(printers) + } +} + +/// Print target TIFF file to printer with GDI ICM bypass and DIB drawing +pub fn print_target(printer_name: &str, tiff_path: &str) -> Result<(), String> { + let path = Path::new(tiff_path); + if !path.exists() { + return Err(format!("Target TIFF file not found: {}", tiff_path)); + } + + // Load and convert the TIFF image + let dyn_img = image::open(path).map_err(|e| format!("Failed to open TIFF image: {}", e))?; + let rgb_img = dyn_img.to_rgb8(); + let img_width = rgb_img.width(); + let img_height = rgb_img.height(); + + if img_width == 0 || img_height == 0 { + return Err("Image has zero dimensions".to_string()); + } + + // Prepare 24-bit BGR DIB buffer with 4-byte row stride alignment + let row_stride = ((img_width as usize * 3 + 3) / 4) * 4; + let mut dib_bytes = vec![0u8; row_stride * img_height as usize]; + + for y in 0..img_height as usize { + for x in 0..img_width as usize { + let pixel = rgb_img.get_pixel(x as u32, y as u32); + let dst_idx = y * row_stride + x * 3; + dib_bytes[dst_idx] = pixel[2]; // Blue + dib_bytes[dst_idx + 1] = pixel[1]; // Green + dib_bytes[dst_idx + 2] = pixel[0]; // Red + } + } + + let printer_wide = to_wide(printer_name); + let doc_name = format!("ICCery Target - {}", path.file_name().and_then(|n| n.to_str()).unwrap_or("Profiling Target")); + let doc_name_wide = to_wide(&doc_name); + + unsafe { + // Open printer handle + let mut h_printer = HANDLE::default(); + let open_res = OpenPrinterW(PCWSTR(printer_wide.as_ptr()), &mut h_printer, None); + if !open_res.as_bool() || h_printer.is_invalid() { + return Err(format!("Failed to open printer '{}'", printer_name)); + } + + // Configure DEVMODE if possible + let devmode_size = DocumentPropertiesW( + HWND::default(), + h_printer, + PCWSTR(printer_wide.as_ptr()), + None, + None, + 0, + ); + + let mut devmode_buf = if devmode_size > 0 { + let mut buf = vec![0u8; devmode_size as usize]; + let res = DocumentPropertiesW( + HWND::default(), + h_printer, + PCWSTR(printer_wide.as_ptr()), + Some(buf.as_mut_ptr() as *mut DEVMODEW), + None, + DM_OUT_BUFFER, + ); + if res >= 0 { + let p_devmode = buf.as_mut_ptr() as *mut DEVMODEW; + // Attempt to explicitly disable driver ICM if supported in DEVMODE + (*p_devmode).dmFields |= DM_ICMMETHOD; + (*p_devmode).dmICMMethod = DMICMMETHOD_NONE as i32; + Some(buf) + } else { + None + } + } else { + None + }; + + let p_devmode_ptr = devmode_buf + .as_mut() + .map(|b| b.as_mut_ptr() as *const DEVMODEW); + + // Create printer Device Context (DC) + let hdc = CreateDCW( + PCWSTR::null(), + PCWSTR(printer_wide.as_ptr()), + PCWSTR::null(), + p_devmode_ptr, + ); + + if hdc.is_invalid() { + let _ = ClosePrinter(h_printer); + return Err(format!("Failed to create Device Context for printer '{}'", printer_name)); + } + + // STRICT ICM BYPASS: Turn off Windows GDI Image Color Management + let prev_icm = SetICMMode(hdc, ICM_OFF as i32); + if prev_icm == 0 { + // Note: Some printer drivers report 0 if ICM is unsupported or already off + } + + // Start Document + let doc_info = DOCINFOW { + cbSize: std::mem::size_of::() as i32, + lpszDocName: PCWSTR(doc_name_wide.as_ptr()), + lpszOutput: PCWSTR::null(), + lpszDatatype: PCWSTR::null(), + fwType: 0, + }; + + let start_doc_res = StartDocW(hdc, &doc_info); + if start_doc_res <= 0 { + let _ = DeleteDC(hdc); + let _ = ClosePrinter(h_printer); + return Err("Failed to start print document (StartDocW)".to_string()); + } + + // Start Page + let start_page_res = StartPage(hdc); + if start_page_res <= 0 { + let _ = EndDoc(hdc); + let _ = DeleteDC(hdc); + let _ = ClosePrinter(h_printer); + return Err("Failed to start print page (StartPage)".to_string()); + } + + // Query DC dimensions and DPI + let dpi_x = GetDeviceCaps(hdc, LOGPIXELSX); + let dpi_y = GetDeviceCaps(hdc, LOGPIXELSY); + let page_width = GetDeviceCaps(hdc, HORZRES); + let page_height = GetDeviceCaps(hdc, VERTRES); + + // Calculate destination dimensions assuming standard 300 DPI target + let target_dpi = 300.0f64; + let mut dest_width = ((img_width as f64 / target_dpi) * dpi_x as f64).round() as i32; + let mut dest_height = ((img_height as f64 / target_dpi) * dpi_y as f64).round() as i32; + + // If target exceeds page bounds, scale proportionally to fit printable area + if dest_width > page_width || dest_height > page_height { + let scale_x = page_width as f64 / dest_width as f64; + let scale_y = page_height as f64 / dest_height as f64; + let scale = scale_x.min(scale_y); + dest_width = (dest_width as f64 * scale).round() as i32; + dest_height = (dest_height as f64 * scale).round() as i32; + } + + let dest_x = (page_width - dest_width).max(0) / 2; + let dest_y = (page_height - dest_height).max(0) / 2; + + // Build BITMAPINFO structure for top-down 24-bit DIB + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: img_width as i32, + biHeight: -(img_height as i32), // Negative height denotes top-down DIB + biPlanes: 1, + biBitCount: 24, + biCompression: BI_RGB.0, + biSizeImage: dib_bytes.len() as u32, + biXPelsPerMeter: ((dpi_x as f64 / 0.0254).round()) as i32, + biYPelsPerMeter: ((dpi_y as f64 / 0.0254).round()) as i32, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [windows::Win32::Graphics::Gdi::RGBQUAD::default()], + }; + + // Draw unmanaged DIB to printer device context + let lines_drawn = StretchDIBits( + hdc, + dest_x, + dest_y, + dest_width, + dest_height, + 0, + 0, + img_width as i32, + img_height as i32, + Some(dib_bytes.as_ptr() as *const c_void), + &bmi, + DIB_RGB_COLORS, + SRCCOPY, + ); + + if lines_drawn <= 0 { + let _ = EndPage(hdc); + let _ = EndDoc(hdc); + let _ = DeleteDC(hdc); + let _ = ClosePrinter(h_printer); + return Err("Failed to draw DIB bitmap to printer DC (StretchDIBits)".to_string()); + } + + // Finish Page and Document + let _ = EndPage(hdc); + let _ = EndDoc(hdc); + let _ = DeleteDC(hdc); + let _ = ClosePrinter(h_printer); + + Ok(()) + } +} -- 2.39.5 From 231c938eec97dd1fdd9ef79dac890f71c9f830a3 Mon Sep 17 00:00:00 2001 From: gronod Date: Sun, 23 Aug 2026 13:27:36 +0100 Subject: [PATCH 2/4] ci: add libfuse2 and squashfs-tools for AppImage bundling in Linux CI --- .github/workflows/build-linux.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index b0efdbd..ef63b92 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -14,6 +14,9 @@ jobs: build-linux: name: Build Linux runs-on: ubuntu-latest + env: + APPIMAGE_EXTRACT_AND_RUN: 1 + NO_STRIP: "true" steps: - name: Checkout repository @@ -37,7 +40,8 @@ jobs: - name: Install Tauri Linux dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils squashfs-tools + sudo apt-get install -y libfuse2 || sudo apt-get install -y libfuse2t64 || true - name: Install Node dependencies run: npm ci -- 2.39.5 From f52b24784aa8e0272b545b3e8a51423fe9c513e1 Mon Sep 17 00:00:00 2001 From: gronod Date: Sun, 23 Aug 2026 13:59:34 +0100 Subject: [PATCH 3/4] ci: enable verbose output and zsync in Linux build workflow --- .github/workflows/build-linux.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index ef63b92..f01ca1f 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -17,6 +17,7 @@ jobs: env: APPIMAGE_EXTRACT_AND_RUN: 1 NO_STRIP: "true" + RUST_BACKTRACE: "1" steps: - name: Checkout repository @@ -40,7 +41,7 @@ jobs: - name: Install Tauri Linux dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils squashfs-tools + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils squashfs-tools zsync sudo apt-get install -y libfuse2 || sudo apt-get install -y libfuse2t64 || true - name: Install Node dependencies @@ -60,7 +61,7 @@ jobs: - name: Build Tauri App env: APPIMAGE_EXTRACT_AND_RUN: 1 - run: npm run tauri build + run: npm run tauri build -- -v - name: Upload Debian Package uses: actions/upload-artifact@v4 -- 2.39.5 From d663311fc64824b18a940b7bb5b38956208614bb Mon Sep 17 00:00:00 2001 From: gronod Date: Sun, 23 Aug 2026 14:17:19 +0100 Subject: [PATCH 4/4] ci: add X11 libraries required by ArgyllCMS binaries for linuxdeploy bundling --- .github/workflows/build-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index f01ca1f..b1f7bac 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -41,7 +41,7 @@ jobs: - name: Install Tauri Linux dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils squashfs-tools zsync + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf xdg-utils squashfs-tools zsync libxss-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libxcursor-dev libxi-dev sudo apt-get install -y libfuse2 || sudo apt-get install -y libfuse2t64 || true - name: Install Node dependencies -- 2.39.5