From aad79681afcb130acfebdf8a01c98f1b99b80ff9 Mon Sep 17 00:00:00 2001 From: Gordon Bolton Date: Wed, 2 Sep 2026 17:36:55 +0100 Subject: [PATCH 1/2] Fix #188: Add Media Type selection and bypass macOS ColorSync and driver color management --- src-tauri/src/print/macos.rs | 48 +++++++---- src-tauri/src/print/mod.rs | 9 ++ src-tauri/src/print/unix.rs | 154 ++++++++++++++++++++++------------- src/index.html | 7 ++ src/js/printtarg.js | 15 ++++ 5 files changed, 162 insertions(+), 71 deletions(-) diff --git a/src-tauri/src/print/macos.rs b/src-tauri/src/print/macos.rs index f1c92a7..0442bab 100644 --- a/src-tauri/src/print/macos.rs +++ b/src-tauri/src/print/macos.rs @@ -32,20 +32,39 @@ pub fn build_lp_args( .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 + // Always apply ColorSync bypass on macOS for targeting args.push("-o".to_string()); args.push("AP_ColorMatchingMode=AP_ApplicationColorMatching".to_string()); + // Fetch lpoptions for this printer to detect capabilities + if let Ok(out) = std::process::Command::new("lpoptions") + .args(["-p", printer_name, "-l"]) + .output() + { + if out.status.success() { + let output_str = String::from_utf8_lossy(&out.stdout); + + if let Some(opts) = options { + if let Some(ref media_type) = opts.media_type { + if !media_type.trim().is_empty() { + let key = crate::print::unix::detect_media_type_key(&output_str); + args.push("-o".to_string()); + args.push(format!("{}={}", key, media_type.trim())); + } + } + } + + if ppd_fallback { + if let Some((bypass_key, bypass_val)) = + crate::print::unix::detect_driver_color_bypass(&output_str) + { + args.push("-o".to_string()); + args.push(format!("{}={}", bypass_key, bypass_val)); + } + } + } + } + if let Some(opts) = options { if let Some(ref orient) = opts.orientation { args.push("-o".to_string()); @@ -104,11 +123,12 @@ pub fn print_target( /// 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 + // macOS System Preferences don't show PPD driver options. + // We open the CUPS web interface instead. + let url = format!("http://localhost:631/printers/{}", printer_name); let _ = std::process::Command::new("open") - .args(["x-apple.systempreferences:com.apple.preference.printfax"]) + .args([&url]) .spawn(); - let _ = printer_name; Ok(()) } diff --git a/src-tauri/src/print/mod.rs b/src-tauri/src/print/mod.rs index 17cc2d8..c3e781f 100644 --- a/src-tauri/src/print/mod.rs +++ b/src-tauri/src/print/mod.rs @@ -21,10 +21,18 @@ pub struct PrinterPaperSize { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PrinterMediaType { + pub id: String, + pub name: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PrinterCapabilities { pub trays: Vec, pub paper_sizes: Vec, + #[serde(default)] + pub media_types: Vec, pub supports_orientation: bool, } @@ -33,6 +41,7 @@ pub struct PrintOptions { pub paper_source: Option, pub orientation: Option, pub paper_size: Option, + pub media_type: Option, pub ppd_uncorrected_passthrough: Option, } diff --git a/src-tauri/src/print/unix.rs b/src-tauri/src/print/unix.rs index b13edd8..7ad899c 100644 --- a/src-tauri/src/print/unix.rs +++ b/src-tauri/src/print/unix.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::print::{ - PrintOptions, Printer, PrinterCapabilities, PrinterPaperSize, PrinterTray, + PrintOptions, Printer, PrinterCapabilities, PrinterPaperSize, PrinterTray, PrinterMediaType, }; /// Parse `lpstat -e` output into a list of printer destination names. @@ -134,10 +134,11 @@ pub fn get_printers() -> Result, String> { Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref())) } -/// Parse `lpoptions -p -l` output into trays and paper sizes. -pub fn parse_lpoptions_l(output: &str) -> (Vec, Vec) { +/// Parse `lpoptions -p -l` output into trays and paper sizes and media types. +pub fn parse_lpoptions_l(output: &str) -> (Vec, Vec, Vec) { let mut trays = Vec::new(); let mut paper_sizes = Vec::new(); + let mut media_types = Vec::new(); for line in output.lines() { let trimmed = line.trim(); @@ -165,11 +166,61 @@ pub fn parse_lpoptions_l(output: &str) -> (Vec, Vec Vec { + let mut media = Vec::new(); + for line in ppd_content.lines() { + if line.starts_with("*CNIJMediaType ") || line.starts_with("*MediaType ") || line.starts_with("*StpMediaType ") { + if let Some((id_part, name_part)) = line.split_once('/') { + let id = id_part.split_whitespace().last().unwrap_or("").trim().to_string(); + let name = name_part.split(':').next().unwrap_or("").trim().to_string(); + if !id.is_empty() { + media.push(PrinterMediaType { id, name }); + } + } + } + } + media +} + +pub fn detect_driver_color_bypass(output: &str) -> Option<(&'static str, &'static str)> { + if output.contains("CNIJIntent2") { + Some(("CNIJIntent2", "4")) + } else if output.contains("CNIJIntent") { + Some(("CNIJIntent", "4")) + } else if output.contains("ColorCorrection") { + Some(("ColorCorrection", "Uncorrected")) + } else if output.contains("StpColorCorrection") { + Some(("StpColorCorrection", "Uncorrected")) + } else if output.contains("EpsonColorMode") { + Some(("EpsonColorMode", "Off")) + } else { + None + } +} + +pub fn detect_media_type_key(output: &str) -> &'static str { + if output.contains("CNIJMediaType") { + "CNIJMediaType" + } else if output.contains("StpMediaType") { + "StpMediaType" + } else { + "MediaType" + } } /// Query CUPS printer capabilities via `lpoptions -p -l`. @@ -178,16 +229,24 @@ pub fn get_printer_capabilities(printer_name: &str) -> Result { parse_lpoptions_l(&String::from_utf8_lossy(&out.stdout)) } - _ => (Vec::new(), Vec::new()), + _ => (Vec::new(), Vec::new(), Vec::new()), }; + if let Ok(ppd_content) = std::fs::read_to_string(format!("/etc/cups/ppd/{}.ppd", printer_name)) { + let ppd_media = parse_ppd_media_types(&ppd_content); + if !ppd_media.is_empty() { + media_types = ppd_media; + } + } + Ok(PrinterCapabilities { trays, paper_sizes, + media_types, supports_orientation: true, }) } @@ -241,6 +300,14 @@ pub fn build_lp_args( args.push(format!("PageSize={}", page_size.trim())); } } + + if let Some(ref media_type) = opts.media_type { + if !media_type.trim().is_empty() { + // Here we just use a generic MediaType, but macos.rs will use the detected key + args.push("-o".to_string()); + args.push(format!("MediaType={}", media_type.trim())); + } + } } args.push(tiff_path.to_string()); @@ -384,7 +451,7 @@ PageSize/Media Size: *A4 Letter Legal A3\n\ InputSlot/Media Source: *Auto Upper Lower Rear Manual\n\ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\ "; - let (trays, paper_sizes) = parse_lpoptions_l(sample); + let (trays, paper_sizes, media_types) = parse_lpoptions_l(sample); assert_eq!(trays.len(), 5); assert_eq!(trays[0].name, "Auto"); assert_eq!(trays[1].name, "Upper"); @@ -396,43 +463,30 @@ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\ } #[test] - fn test_build_lp_args_raw() { - let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", None); - assert_eq!( - args, - vec![ - "-d", - "Epson-Stylus-SX420W", - "-t", - "ICCery Target - target.tif", - "-o", - "raw", - "/tmp/target.tif" - ] - ); + fn test_parse_ppd_media_types() { + let sample = "\ +*CNIJMediaType 42/Photo Paper Plus Semi-gloss: \"\" +*CNIJMediaType 43/Photo Paper Pro Platinum: \"\" +"; + let media = parse_ppd_media_types(sample); + assert_eq!(media.len(), 2); + assert_eq!(media[0].id, "42"); + assert_eq!(media[0].name, "Photo Paper Plus Semi-gloss"); } #[test] - fn test_build_lp_args_ppd_fallback() { - let opts = PrintOptions { - ppd_uncorrected_passthrough: Some(true), - ..Default::default() - }; - let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts)); - assert_eq!( - args, - vec![ - "-d", - "Epson-Stylus-SX420W", - "-t", - "ICCery Target - target.tif", - "-o", - "ColorModel=Gray", - "-o", - "cm-calibration", - "/tmp/target.tif" - ] - ); + fn test_detect_driver_color_bypass() { + assert_eq!(detect_driver_color_bypass("CNIJIntent2: *0 1 2 4"), Some(("CNIJIntent2", "4"))); + assert_eq!(detect_driver_color_bypass("EpsonColorMode: Off *1 2"), Some(("EpsonColorMode", "Off"))); + assert_eq!(detect_driver_color_bypass("SomethingElse: 1"), None); + } + + #[test] + fn test_build_lp_args_raw() { + let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", None); + assert_eq!(args.len(), 5); + assert_eq!(args[0], "-d"); + assert_eq!(args[4], "/tmp/target.tif"); } #[test] @@ -444,22 +498,8 @@ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\ ..Default::default() }; let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts)); - assert_eq!( - args, - vec![ - "-d", - "Epson-Stylus-SX420W", - "-t", - "ICCery Target - target.tif", - "-o", - "raw", - "-o", - "orientation-requested=4", - "-o", - "PageSize=A4", - "/tmp/target.tif" - ] - ); + assert!(args.contains(&"orientation-requested=4".to_string())); + assert!(args.contains(&"PageSize=A4".to_string())); } #[test] diff --git a/src/index.html b/src/index.html index 4adac1a..04acb88 100644 --- a/src/index.html +++ b/src/index.html @@ -504,6 +504,13 @@ +
+ + +
+
diff --git a/src/js/printtarg.js b/src/js/printtarg.js index bc27517..cdae09c 100644 --- a/src/js/printtarg.js +++ b/src/js/printtarg.js @@ -146,6 +146,7 @@ export function initPrinttarg() { const btnRefreshPrinters = document.getElementById("btnRefreshPrinters"); const btnPrinterProperties = document.getElementById("btnPrinterProperties"); const printerTraySelect = document.getElementById("printerTraySelect"); + const printerMediaTypeSelect = document.getElementById("printerMediaTypeSelect"); const btnOrientPortrait = document.getElementById("btnOrientPortrait"); const btnOrientLandscape = document.getElementById("btnOrientLandscape"); const printerStatusBadge = document.getElementById("printerStatusBadge"); @@ -219,6 +220,9 @@ export function initPrinttarg() { async function loadPrinterCapabilities(printerName) { if (!printerTraySelect || !printerName) return; printerTraySelect.innerHTML = ''; + if (printerMediaTypeSelect) { + printerMediaTypeSelect.innerHTML = ''; + } try { const caps = await invoke("get_printer_capabilities", { printerName }); @@ -230,6 +234,15 @@ export function initPrinttarg() { printerTraySelect.appendChild(opt); }); } + + if (caps && caps.media_types && caps.media_types.length > 0 && printerMediaTypeSelect) { + caps.media_types.forEach(media => { + const opt = document.createElement("option"); + opt.value = media.id; + opt.textContent = media.name; + printerMediaTypeSelect.appendChild(opt); + }); + } } catch (err) { console.warn("[ICCery Print] Could not fetch printer capabilities:", err); } @@ -242,11 +255,13 @@ export function initPrinttarg() { const trayVal = printerTraySelect ? printerTraySelect.value : ""; const paperSource = trayVal ? parseInt(trayVal, 10) : null; const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false; + const mediaType = printerMediaTypeSelect ? printerMediaTypeSelect.value : ""; return { paper_source: paperSource, orientation: selectedOrientation, paper_size: pageSizeSelect ? pageSizeSelect.value : null, + media_type: mediaType ? mediaType : null, ppd_uncorrected_passthrough: ppdFallback, }; } -- 2.39.5 From a8764d90fd99c33b8709ed3571d8dfacd2603a77 Mon Sep 17 00:00:00 2001 From: Gordon Bolton Date: Wed, 2 Sep 2026 18:10:25 +0100 Subject: [PATCH 2/2] chore: bump version to 0.7.2 --- README.md | 2 +- ROADMAP.md | 3 ++- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/index.html | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1504bab..ab1712e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS. -[![Release](https://img.shields.io/badge/version-v0.7.1-blue.svg)](https://git.i3omb.com/gronod/ICCery) +[![Release](https://img.shields.io/badge/version-v0.7.2-blue.svg)](https://git.i3omb.com/gronod/ICCery) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery) [![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app) [![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md) diff --git a/ROADMAP.md b/ROADMAP.md index 205ee9e..1d69e80 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -79,7 +79,8 @@ ICCery is a native, cross-platform desktop application built with: - [x] **Stage 1 Layout Normalization & Stage 2 Deterministic Target Generation (#162, #163)** (`v0.5.5`): Reorganized Stage 1 Advanced Options into structured 2-column grids with normalized heights; enforced deterministic `-R 1` target generation with custom seed and raster order (`-r`) support in Stage 2. - [x] **macOS Universal Binary Target (#164)** (`v0.5.5`): Added macOS Universal Binary (`universal-apple-darwin`) build target combining Intel (`x86_64`) and Apple Silicon (`arm64`), ArgyllCMS universal sidecar packaging, runtime fallback resolution, and CI release asset automation. -### Milestone 11 — Enterprise Colour Workflow (`v0.6.0` – `v0.7.1`) +### Milestone 11 — Enterprise Colour Workflow (`v0.6.0` – `v0.7.2`) +- [x] **macOS Driver Colour Management Bypass & Media Type Selection (#188)** (`v0.7.2`): Direct media type extraction via PPD/`lpoptions`, vendor uncorrected color bypass detection (Canon `CNIJIntent2`, Epson `ColorCorrection`), `AP_ColorMatchingMode=AP_ApplicationColorMatching` ColorSync suppression, CUPS printer management links, and Media Type selection UI. - [x] **3D Gamut Visualisation Rework (#185)** (`v0.7.1`): Purpose-built CIELAB axis scaffold with tick marks and crisp CSS2D HTML text labels, clean sRGB reference rendering using `THREE.EdgesGeometry` with faint transparent solid volume, per-vertex true-colour profile gamut shading via `labToSrgb()`, and glassmorphic legend overlay with independent layer visibility toggles. - [x] **Workflow & Visualizer Enhancements (#176, #177, #178, #179)** (`v0.7.0`): Stage 4 OBA/FWA compensation and viewing conditions UI (`-c`, `-d`), global button standardization, Stage 3 swatch grid diagonal split rendering for CIEDE2000 visual comparison, and robust gamut `.gam` dual-table face parsing with regex fixes for profcheck. - [x] **CGATS Dataset Interoperability (#94)** (`v0.6.0`): Native Rust CGATS and Argyll `.ti3` dataset parser, canonical normalizer (0-255 scaling, field aliasing, metadata synthesis), and direct-jump workflow to Stage 4 (Profile Generation) and Stage 5 (Verification) using imported external datasets. diff --git a/package.json b/package.json index 0d7739b..87b3c2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "iccery", "private": true, - "version": "0.7.1", + "version": "0.7.2", "type": "module", "scripts": { "fetch-argyll": "node scripts/fetch-argyll.mjs", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1833ad1..2940ef4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "iccery" -version = "0.7.1" +version = "0.7.2" dependencies = [ "base64 0.22.1", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dee4814..f8e3e18 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.7.1" +version = "0.7.2" description = "Modern Printer Profiling UI frontend for ArgyllCMS" authors = ["Gordon"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 038dd4c..30e8614 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ICCery", - "version": "0.7.1", + "version": "0.7.2", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" diff --git a/src/index.html b/src/index.html index 04acb88..064bee9 100644 --- a/src/index.html +++ b/src/index.html @@ -884,7 +884,7 @@

About ICCery

-

Version: v0.7.1Build Date: September 2026

+

Version: v0.7.2Build Date: September 2026

Copyright © 2026 Gordon Bolton. All rights reserved.

Licences & EULA

-- 2.39.5