Release v0.4.0: Milestone 9 - macOS Native Support & Enhanced Print Spooling #131

Merged
gronod merged 3 commits from development into main 2026-08-29 11:49:30 +01:00
7 changed files with 196 additions and 7 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "iccery",
"private": true,
"version": "0.3.3",
"version": "0.4.0",
"type": "module",
"scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "iccery"
version = "0.3.3"
version = "0.4.0"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
edition = "2021"
+26 -4
View File
@@ -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(
+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)]
pub mod windows;
#[cfg(any(target_os = "macos", test))]
pub mod macos;
#[cfg(any(unix, test))]
pub mod unix;
#[cfg(test)]
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>() + 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"));
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.3.3",
"version": "0.4.0",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"