feat(print): macOS native CUPS raw print spooler integration (resolves #92) #130

Merged
gronod merged 1 commits from feat/92-macos-cups-raw-spooling into development 2026-08-29 11:48:42 +01:00
4 changed files with 193 additions and 4 deletions
+26 -4
View File
@@ -684,7 +684,11 @@ pub async fn get_printers() -> Result<Vec<crate::print::Printer>, String> {
{ {
crate::print::windows::get_printers() 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() crate::print::unix::get_printers()
} }
@@ -702,7 +706,11 @@ pub async fn get_printer_capabilities(
{ {
crate::print::windows::get_printer_capabilities(&printer_name) 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) 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) 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); let _ = (state, printer_name);
Ok(()) Ok(())
@@ -750,7 +763,16 @@ pub async fn print_target_native(
Some(&state), 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; let _ = state;
crate::print::unix::print_target( crate::print::unix::print_target(
+114
View File
@@ -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<String> {
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(())
}
+4
View File
@@ -65,8 +65,12 @@ impl PrinterDevModeStore {
#[cfg(windows)] #[cfg(windows)]
pub mod windows; pub mod windows;
#[cfg(any(target_os = "macos", test))]
pub mod macos;
#[cfg(any(unix, test))] #[cfg(any(unix, test))]
pub mod unix; pub mod unix;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
+49
View File
@@ -241,6 +241,55 @@ printer Custom_Queue unknown state\n\
assert_eq!(work_buf[std::mem::size_of::<DEVMODEW>() + 10], 0xAA); assert_eq!(work_buf[std::mem::size_of::<DEVMODEW>() + 10], 0xAA);
assert_eq!(work_buf[std::mem::size_of::<DEVMODEW>() + 11], 0x55); assert_eq!(work_buf[std::mem::size_of::<DEVMODEW>() + 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"));
}
} }