feat(print): macOS native CUPS raw print spooler integration (resolves #92) #130
@@ -684,7 +684,11 @@ pub async fn get_printers() -> Result<Vec<crate::print::Printer>, 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(
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>() + 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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user