chore: release v0.7.2 #191
@@ -2,7 +2,7 @@
|
||||
|
||||
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
||||
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://tauri.app)
|
||||
[](LICENCE.md)
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -1423,7 +1423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iccery"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"image",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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<PrinterTray>,
|
||||
pub paper_sizes: Vec<PrinterPaperSize>,
|
||||
#[serde(default)]
|
||||
pub media_types: Vec<PrinterMediaType>,
|
||||
pub supports_orientation: bool,
|
||||
}
|
||||
|
||||
@@ -33,6 +41,7 @@ pub struct PrintOptions {
|
||||
pub paper_source: Option<u16>,
|
||||
pub orientation: Option<String>,
|
||||
pub paper_size: Option<String>,
|
||||
pub media_type: Option<String>,
|
||||
pub ppd_uncorrected_passthrough: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
+97
-57
@@ -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<Vec<Printer>, String> {
|
||||
Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref()))
|
||||
}
|
||||
|
||||
/// Parse `lpoptions -p <printer> -l` output into trays and paper sizes.
|
||||
pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSize>) {
|
||||
/// Parse `lpoptions -p <printer> -l` output into trays and paper sizes and media types.
|
||||
pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSize>, Vec<PrinterMediaType>) {
|
||||
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<PrinterTray>, Vec<PrinterPaperSiz
|
||||
name: clean_val.to_string(),
|
||||
});
|
||||
}
|
||||
} else if key_name.eq_ignore_ascii_case("CNIJMediaType") || key_name.eq_ignore_ascii_case("MediaType") || key_name.eq_ignore_ascii_case("StpMediaType") {
|
||||
for val in values {
|
||||
let clean_val = val.trim_start_matches('*');
|
||||
media_types.push(PrinterMediaType {
|
||||
id: clean_val.to_string(),
|
||||
name: clean_val.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(trays, paper_sizes)
|
||||
(trays, paper_sizes, media_types)
|
||||
}
|
||||
|
||||
pub fn parse_ppd_media_types(ppd_content: &str) -> Vec<PrinterMediaType> {
|
||||
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 <printer> -l`.
|
||||
@@ -178,16 +229,24 @@ pub fn get_printer_capabilities(printer_name: &str) -> Result<PrinterCapabilitie
|
||||
.args(["-p", printer_name, "-l"])
|
||||
.output();
|
||||
|
||||
let (trays, paper_sizes) = match output {
|
||||
let (trays, paper_sizes, mut media_types) = match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
+8
-1
@@ -504,6 +504,13 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group media-group" id="mediaTypeGroup">
|
||||
<label for="printerMediaTypeSelect">Media Type</label>
|
||||
<select id="printerMediaTypeSelect">
|
||||
<option value="" selected>Default / Auto Select</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group orientation-group">
|
||||
<label>Orientation</label>
|
||||
<div class="orientation-toggle-group">
|
||||
@@ -877,7 +884,7 @@
|
||||
<img src="./assets/ICCery-logo.svg" alt="ICCery Logo" class="about-logo" />
|
||||
</div>
|
||||
<h2>About ICCery</h2>
|
||||
<p><strong>Version:</strong> <span id="aboutVersion">v0.7.1</span> • <strong>Build Date:</strong> <span id="aboutBuildDate">September 2026</span></p>
|
||||
<p><strong>Version:</strong> <span id="aboutVersion">v0.7.2</span> • <strong>Build Date:</strong> <span id="aboutBuildDate">September 2026</span></p>
|
||||
<p><strong>Copyright © 2026 Gordon Bolton. All rights reserved.</strong></p>
|
||||
<h3>Licences & EULA</h3>
|
||||
<div class="license-text-container">
|
||||
|
||||
@@ -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 = '<option value="" selected>Default / Auto Select</option>';
|
||||
if (printerMediaTypeSelect) {
|
||||
printerMediaTypeSelect.innerHTML = '<option value="" selected>Default / Auto Select</option>';
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user