diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 03b2ede..9d324d8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -684,7 +684,11 @@ pub async fn get_printers() -> Result, String> { { crate::print::windows::get_printers() } - #[cfg(unix)] + #[cfg(target_os = "macos")] + { + crate::print::macos::get_printers() + } + #[cfg(all(unix, not(target_os = "macos")))] { crate::print::unix::get_printers() } @@ -702,7 +706,11 @@ pub async fn get_printer_capabilities( { crate::print::windows::get_printer_capabilities(&printer_name) } - #[cfg(unix)] + #[cfg(target_os = "macos")] + { + crate::print::macos::get_printer_capabilities(&printer_name) + } + #[cfg(all(unix, not(target_os = "macos")))] { crate::print::unix::get_printer_capabilities(&printer_name) } @@ -722,7 +730,12 @@ pub async fn show_printer_properties( { crate::print::windows::show_printer_properties(&printer_name, &state) } - #[cfg(unix)] + #[cfg(target_os = "macos")] + { + let _ = state; + crate::print::macos::show_printer_properties(&printer_name) + } + #[cfg(all(unix, not(target_os = "macos")))] { let _ = (state, printer_name); Ok(()) @@ -750,7 +763,16 @@ pub async fn print_target_native( Some(&state), ) } - #[cfg(unix)] + #[cfg(target_os = "macos")] + { + let _ = state; + crate::print::macos::print_target( + &printer_name, + &tiff_path, + options.as_ref(), + ) + } + #[cfg(all(unix, not(target_os = "macos")))] { let _ = state; crate::print::unix::print_target( diff --git a/src-tauri/src/print/macos.rs b/src-tauri/src/print/macos.rs new file mode 100644 index 0000000..f1c92a7 --- /dev/null +++ b/src-tauri/src/print/macos.rs @@ -0,0 +1,114 @@ +#![allow(dead_code)] + +use std::path::Path; +use crate::print::{ + PrintOptions, Printer, PrinterCapabilities, +}; +pub use crate::print::unix::{ + get_printer_capabilities, get_printers, merge_printer_info, parse_lpoptions_l, + parse_lpstat_d, parse_lpstat_e, parse_lpstat_p, +}; + +/// Construct `lp` command line arguments for target printing on macOS with ColorSync bypass flags. +pub fn build_lp_args( + printer_name: &str, + tiff_path: &str, + options: Option<&PrintOptions>, +) -> Vec { + let path = Path::new(tiff_path); + let title = format!( + "ICCery Target - {}", + path.file_name().and_then(|n| n.to_str()).unwrap_or("Profiling Target") + ); + + let mut args = vec![ + "-d".to_string(), + printer_name.to_string(), + "-t".to_string(), + title, + ]; + + let ppd_fallback = options + .and_then(|o| o.ppd_uncorrected_passthrough) + .unwrap_or(false); + + if ppd_fallback { + args.push("-o".to_string()); + args.push("ColorModel=Gray".to_string()); + args.push("-o".to_string()); + args.push("cm-calibration".to_string()); + } else { + args.push("-o".to_string()); + args.push("raw".to_string()); + } + + // macOS Apple ColorSync suppression flags + args.push("-o".to_string()); + args.push("AP_ColorMatchingMode=AP_ApplicationColorMatching".to_string()); + + if let Some(opts) = options { + if let Some(ref orient) = opts.orientation { + args.push("-o".to_string()); + if orient.eq_ignore_ascii_case("landscape") { + args.push("orientation-requested=4".to_string()); + } else { + args.push("orientation-requested=3".to_string()); + } + } + + if let Some(ref page_size) = opts.paper_size { + if !page_size.trim().is_empty() { + args.push("-o".to_string()); + args.push(format!("PageSize={}", page_size.trim())); + } + } + } + + args.push(tiff_path.to_string()); + args +} + +/// Spool target TIFF to macOS CUPS printer bypassing ColorSync and PPD color management. +pub fn print_target( + printer_name: &str, + tiff_path: &str, + options: Option<&PrintOptions>, +) -> Result<(), String> { + let path = Path::new(tiff_path); + if !path.exists() { + return Err(format!("Target TIFF file not found: {}", tiff_path)); + } + + let args = build_lp_args(printer_name, tiff_path, options); + + let output = std::process::Command::new("lp") + .args(&args) + .output() + .map_err(|e| format!("Failed to execute 'lp' command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let msg = if !stderr.trim().is_empty() { + stderr.trim().to_string() + } else if !stdout.trim().is_empty() { + stdout.trim().to_string() + } else { + format!("exited with status code {}", output.status.code().unwrap_or(-1)) + }; + return Err(format!("macOS CUPS print job failed: {}", msg)); + } + + Ok(()) +} + +/// Open macOS printer queue / properties management or inspect queue options. +pub fn show_printer_properties(printer_name: &str) -> Result<(), String> { + // On macOS, open Print & Scan preference pane or query printer status via lpstat + let _ = std::process::Command::new("open") + .args(["x-apple.systempreferences:com.apple.preference.printfax"]) + .spawn(); + + let _ = printer_name; + Ok(()) +} diff --git a/src-tauri/src/print/mod.rs b/src-tauri/src/print/mod.rs index 2c09492..17cc2d8 100644 --- a/src-tauri/src/print/mod.rs +++ b/src-tauri/src/print/mod.rs @@ -65,8 +65,12 @@ impl PrinterDevModeStore { #[cfg(windows)] pub mod windows; +#[cfg(any(target_os = "macos", test))] +pub mod macos; + #[cfg(any(unix, test))] pub mod unix; #[cfg(test)] mod tests; + diff --git a/src-tauri/src/print/tests.rs b/src-tauri/src/print/tests.rs index b0deef7..0c325a0 100644 --- a/src-tauri/src/print/tests.rs +++ b/src-tauri/src/print/tests.rs @@ -241,6 +241,55 @@ printer Custom_Queue unknown state\n\ assert_eq!(work_buf[std::mem::size_of::() + 10], 0xAA); assert_eq!(work_buf[std::mem::size_of::() + 11], 0x55); } + + #[test] + fn test_macos_build_lp_args_colorsync_suppression() { + use crate::print::macos::build_lp_args as macos_build_lp_args; + + let printer = "Epson_SureColor_P900"; + let path = "/tmp/targets/target_page_1.tif"; + + // Raw mode + let args_raw = macos_build_lp_args(printer, path, None); + assert_eq!(args_raw[0], "-d"); + assert_eq!(args_raw[1], printer); + assert_eq!(args_raw[2], "-t"); + assert_eq!(args_raw[3], "ICCery Target - target_page_1.tif"); + assert_eq!(args_raw[4], "-o"); + assert_eq!(args_raw[5], "raw"); + assert_eq!(args_raw[6], "-o"); + assert_eq!(args_raw[7], "AP_ColorMatchingMode=AP_ApplicationColorMatching"); + assert_eq!(args_raw[8], path); + + // PPD uncorrected passthrough mode with options + let opts = PrintOptions { + orientation: Some("landscape".to_string()), + paper_size: Some("A4".to_string()), + ppd_uncorrected_passthrough: Some(true), + ..Default::default() + }; + let args_ppd = macos_build_lp_args(printer, path, Some(&opts)); + assert_eq!(args_ppd[4], "-o"); + assert_eq!(args_ppd[5], "ColorModel=Gray"); + assert_eq!(args_ppd[6], "-o"); + assert_eq!(args_ppd[7], "cm-calibration"); + assert_eq!(args_ppd[8], "-o"); + assert_eq!(args_ppd[9], "AP_ColorMatchingMode=AP_ApplicationColorMatching"); + assert_eq!(args_ppd[10], "-o"); + assert_eq!(args_ppd[11], "orientation-requested=4"); + assert_eq!(args_ppd[12], "-o"); + assert_eq!(args_ppd[13], "PageSize=A4"); + assert_eq!(args_ppd[14], path); + } + + #[test] + fn test_macos_print_target_nonexistent_file() { + use crate::print::macos::print_target as macos_print_target; + let res = macos_print_target("Test_Mac_Printer", "/non/existent/target.tif", None); + assert!(res.is_err()); + assert!(res.unwrap_err().contains("Target TIFF file not found")); + } } +