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()
|
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(
|
||||||
|
|||||||
@@ -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)]
|
#[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;
|
||||||
|
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user