From 725018d253f6b14394b0d2ac4dae2c512e84a263 Mon Sep 17 00:00:00 2001 From: gronod Date: Tue, 25 Aug 2026 12:18:06 +0100 Subject: [PATCH 01/18] docs: fix mermaid architecture diagram syntax and license badge/link (fixes #80) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ec26c01..b015510 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Release](https://img.shields.io/badge/version-v0.2.0-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-MIT-green.svg)](LICENSE) +[![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md) **ICCery** is a native GUI frontend designed to make creating custom ICC/ICM printer profiles seamless, visual, and reliable. It wraps the powerful color management capabilities of [ArgyllCMS](https://www.argyllcms.com/) within an intuitive, artefact-gated 5-stage wizard. @@ -35,7 +35,7 @@ flowchart TD UI[Wizard UI & Swatch Grid] ThreeJS[3D CIELAB Gamut Viewer] State[Wizard State & Artefact Verifier] - PrintEngine[Raw Print Subsystem (GDI / CUPS)] + PrintEngine["Raw Print Subsystem (GDI / CUPS)"] ProcMgr[Async Subprocess IPC Manager] UI <--> State @@ -95,4 +95,4 @@ npm run tauri build ## Licence -ICCery is licensed under the [MIT Licence](LICENSE). ArgyllCMS binaries and source code are licensed under the GNU Affero General Public License (AGPLv3). +The ICCery GUI application is proprietary software licensed under the terms of the [EULA](LICENCE.md). ArgyllCMS binaries and source code are licensed under the GNU Affero General Public License (AGPLv3). -- 2.39.5 From f07c8ae2388127256957a9902d33c8c694d78bb5 Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:02:23 +0100 Subject: [PATCH 02/18] fix(backend): resolve .exe binary candidates on Windows (fixes #85) --- src-tauri/src/commands.rs | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7d3bdfc..089e9bb 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -30,14 +30,27 @@ pub async fn kill_process( state.kill(&id).await } +pub fn get_binary_candidates(binary_name: &str) -> Vec { + if cfg!(windows) && !binary_name.to_lowercase().ends_with(".exe") { + vec![format!("{}.exe", binary_name), binary_name.to_string()] + } else { + vec![binary_name.to_string()] + } +} + #[tauri::command] pub async fn resolve_binary(app: AppHandle, binary_name: String) -> Result { let settings = crate::settings::load_settings(app.clone()).unwrap_or_default(); + let candidates = get_binary_candidates(&binary_name); + if let Some(dir) = settings.argyll_binary_dir { if !dir.trim().is_empty() { - let custom_path = std::path::Path::new(&dir).join(&binary_name); - if custom_path.exists() { - return Ok(custom_path.to_string_lossy().to_string()); + let base_dir = std::path::Path::new(&dir); + for name in &candidates { + let custom_path = base_dir.join(name); + if custom_path.exists() { + return Ok(custom_path.to_string_lossy().to_string()); + } } } } @@ -50,10 +63,22 @@ pub async fn resolve_binary(app: AppHandle, binary_name: String) -> Result "linux-x86_64", }; + for name in &candidates { + if let Ok(resource_path) = app.path().resolve( + format!("argyll/{}/{}", platform, name), + tauri::path::BaseDirectory::Resource, + ) { + if resource_path.exists() { + return Ok(resource_path.to_string_lossy().to_string()); + } + } + } + + let primary_name = &candidates[0]; let resource_path = app .path() .resolve( - format!("argyll/{}/{}", platform, binary_name), + format!("argyll/{}/{}", platform, primary_name), tauri::path::BaseDirectory::Resource, ) .map_err(|e| e.to_string())?; @@ -707,4 +732,17 @@ mod tests { let args = build_profcheck_args(&config); assert_eq!(args, vec!["-v", "-k", "-s", "my_profile.ti3", "my_profile.icc"]); } -} + + #[test] + fn test_get_binary_candidates() { + let candidates = get_binary_candidates("targen"); + if cfg!(windows) { + assert_eq!(candidates, vec!["targen.exe", "targen"]); + } else { + assert_eq!(candidates, vec!["targen"]); + } + + let candidates_exe = get_binary_candidates("targen.exe"); + assert_eq!(candidates_exe, vec!["targen.exe"]); + } +} \ No newline at end of file -- 2.39.5 From def7aeb317b5cf3403b1c6240b638df10e506e28 Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:16:19 +0100 Subject: [PATCH 03/18] feat(backend): support port flag in ChartreadConfig (fixes #86) --- src-tauri/src/commands.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 089e9bb..cde492b 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -386,14 +386,24 @@ pub async fn read_tiff_preview_png(path: String) -> Result { pub struct ChartreadConfig { pub basename: String, pub cwd: String, + pub port: Option, } pub fn build_chartread_args(config: &ChartreadConfig) -> Vec { - vec![ + let mut args = vec![ "-v".to_string(), "-u".to_string(), - config.basename.clone(), - ] + ]; + + if let Some(ref port) = config.port { + if !port.trim().is_empty() { + args.push("-c".to_string()); + args.push(port.trim().to_string()); + } + } + + args.push(config.basename.clone()); + args } #[tauri::command] @@ -699,11 +709,23 @@ mod tests { let config = ChartreadConfig { basename: "my_profile".to_string(), cwd: "/home/user".to_string(), + port: None, }; let args = build_chartread_args(&config); assert_eq!(args, vec!["-v", "-u", "my_profile"]); } + #[test] + fn test_build_chartread_args_with_port() { + let config = ChartreadConfig { + basename: "my_profile".to_string(), + cwd: "/home/user".to_string(), + port: Some("1".to_string()), + }; + let args = build_chartread_args(&config); + assert_eq!(args, vec!["-v", "-u", "-c", "1", "my_profile"]); + } + #[test] fn test_build_colprof_args() { let config = ColprofConfig { -- 2.39.5 From ccdb4f54cd1ce069443ed4ce2d40a3deb6637d56 Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:16:46 +0100 Subject: [PATCH 04/18] feat(ui): add instrument selector and detect button in Stage 3 (fixes #86) --- src/index.html | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index 4af00bd..104de42 100644 --- a/src/index.html +++ b/src/index.html @@ -249,6 +249,19 @@

Measure Target

Read the printed target patches with your spectrophotometer.

+ +
+
+ +
+ + +
+
+
+
State: IDLE
@@ -485,4 +498,4 @@
- + \ No newline at end of file -- 2.39.5 From cb468673e4306a1bb696efd001dcd0775a4b9bf4 Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:17:08 +0100 Subject: [PATCH 05/18] feat(stage3): instrument autodetection via instlist and port selection (fixes #86) --- src/js/chartread.js | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/js/chartread.js b/src/js/chartread.js index 9b78b92..3020dd4 100644 --- a/src/js/chartread.js +++ b/src/js/chartread.js @@ -36,11 +36,70 @@ export function initChartread() { const btnRetry = document.getElementById("btnRetry"); const btnSkip = document.getElementById("btnSkip"); const btnCancel = document.getElementById("btnCancel"); + const btnDetectInstruments = document.getElementById("btnDetectInstruments"); + const instrumentSelect = document.getElementById("chartreadInstrumentSelect"); const promptText = document.getElementById("chartreadPrompt"); const stateLabel = document.getElementById("chartreadState"); const logContainer = document.getElementById("chartreadLogContainer"); const logPre = document.getElementById("chartreadLog"); + // Instrument detection logic + if (btnDetectInstruments && instrumentSelect) { + btnDetectInstruments.addEventListener("click", async () => { + btnDetectInstruments.disabled = true; + btnDetectInstruments.textContent = "Detecting..."; + setPrompt("Querying connected spectrophotometers and colorimeters via instlist..."); + + const detected = []; + + try { + const unlistenStdout = await listen("process:stdout", (event) => { + if (event.payload.id !== "instlist" || !event.payload.line) return; + const line = event.payload.line.trim(); + + // Matches Argyll instlist output e.g. "1: 'i1Pro' on 'USB'" or "1: 'ColorMunki'" or "1 = 'i1Display'" + const match = line.match(/^(\d+)[\s:=]+'?([^'\n]+)'?(?:\s+on\s+'?([^'\n]+)'?)?/i); + if (match) { + const index = match[1]; + const name = match[2].trim(); + const port = match[3] ? match[3].trim() : ""; + detected.push({ index, name, port }); + } + }); + + const unlistenExit = await listen("process:exit", (event) => { + if (event.payload.id !== "instlist") return; + unlistenStdout(); + unlistenExit(); + btnDetectInstruments.disabled = false; + btnDetectInstruments.textContent = "↻ Detect"; + + instrumentSelect.innerHTML = ``; + + if (detected.length > 0) { + detected.forEach((inst) => { + const opt = document.createElement("option"); + opt.value = inst.index; + opt.textContent = `${inst.index}: ${inst.name} ${inst.port ? `(${inst.port})` : ""}`; + instrumentSelect.appendChild(opt); + }); + instrumentSelect.value = detected[0].index; + setPrompt(`Found ${detected.length} instrument(s): ${detected.map(d => d.name).join(", ")}`); + } else { + setPrompt("No instruments found via instlist. Ensure USB cable is plugged in."); + } + }); + + await invoke("detect_instruments"); + } catch (err) { + console.error("detect_instruments error:", err); + btnDetectInstruments.disabled = false; + btnDetectInstruments.textContent = "↻ Detect"; + setPrompt(`Instrument detection error: ${err}`); + } + }); + } + function setState(newState) { currentState = newState; if (stateLabel) stateLabel.textContent = newState; @@ -103,9 +162,12 @@ export function initChartread() { setState(STATE.CALIBRATING); setPrompt("Starting chartread... waiting for instrument calibration prompt."); + const selectedPort = instrumentSelect && instrumentSelect.value ? instrumentSelect.value : null; + const config = { basename: basename, cwd: cwd, + port: selectedPort, }; currentProcessId = `chartread_${basename}`; -- 2.39.5 From 454ee3488de4ffe7a10f9c510223b6b367d6e014 Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:29:17 +0100 Subject: [PATCH 06/18] feat(backend): add run_average command and AverageConfig (fixes #87) --- src-tauri/src/commands.rs | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index cde492b..ec7b5c9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -420,6 +420,36 @@ pub async fn run_chartread( state.spawn(app, id, binary, args, cwd).await } +#[derive(Debug, Deserialize, Serialize)] +pub struct AverageConfig { + pub inputs: Vec, + pub output: String, + pub cwd: String, +} + +pub fn build_average_args(config: &AverageConfig) -> Vec { + let mut args = vec!["-v".to_string()]; + for input in &config.inputs { + args.push(input.clone()); + } + args.push(config.output.clone()); + args +} + +#[tauri::command] +pub async fn run_average( + app: AppHandle, + state: State<'_, ProcessManager>, + config: AverageConfig, +) -> Result<(), String> { + let binary = resolve_binary(app.clone(), "average".to_string()).await?; + let args = build_average_args(&config); + let id = format!("average_{}", config.output); + let cwd = Some(resolve_safe_cwd(&app, &config.cwd)?); + + state.spawn(app, id, binary, args, cwd).await +} + #[derive(Debug, Deserialize, Serialize)] pub struct ColprofConfig { pub algorithm: String, @@ -726,6 +756,17 @@ mod tests { assert_eq!(args, vec!["-v", "-u", "-c", "1", "my_profile"]); } + #[test] + fn test_build_average_args() { + let config = AverageConfig { + inputs: vec!["pass1.ti3".to_string(), "pass2.ti3".to_string()], + output: "avg.ti3".to_string(), + cwd: "/home/user".to_string(), + }; + let args = build_average_args(&config); + assert_eq!(args, vec!["-v", "pass1.ti3", "pass2.ti3", "avg.ti3"]); + } + #[test] fn test_build_colprof_args() { let config = ColprofConfig { -- 2.39.5 From b215c7216263e1315c9b5cce24798c9adf87001a Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:29:27 +0100 Subject: [PATCH 07/18] feat(backend): register run_average command in lib.rs (fixes #87) --- src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 31cda44..f6e5ef4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ pub fn run() { commands::read_file_base64, commands::read_tiff_preview_png, commands::run_chartread, + commands::run_average, commands::run_colprof, commands::run_profcheck, commands::get_printers, -- 2.39.5 From a5840baa76f3305fdf1e21e3c0456e83aee6fbda Mon Sep 17 00:00:00 2001 From: gronod <1+gronod@noreply@i3omb.com> Date: Tue, 25 Aug 2026 17:29:50 +0100 Subject: [PATCH 08/18] feat(ui): add multi-pass averaging controls to Stage 3 HTML (fixes #87) --- src/index.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index 104de42..10eb2b9 100644 --- a/src/index.html +++ b/src/index.html @@ -291,6 +291,20 @@
+ + +