Author SHA1 Message Date
gronod 906b4f9104 ci: add npm test script and integrate frontend tests into CI workflows (#215) 2026-09-06 16:40:37 +01:00
gronod 9c7d563dbf chore: bump version to v0.8.4 2026-09-06 16:37:59 +01:00
gronod 743ad86f58 ci(macos): remove redundant cargo test step from release workflow 2026-09-06 16:22:57 +01:00
gronod ebbdbd4f55 chore: disable Apple Silicon and Universal build targets in macOS workflow 2026-09-06 16:06:24 +01:00
gronod ef0d65453e chore: configure git permissions in CI workflows and restrict crate source inclusion and build re-run triggers 2026-09-06 16:04:34 +01:00
gronod 9f2ff015d7 fix(cgats): support dataset file selection, sync wizard target state, and bump to v0.8.3 (#211) 2026-09-06 14:57:11 +01:00
gronod 5a1ad654b8 fix(colprof): add select_spectrum_file command for custom sp browse (#210) 2026-09-06 12:51:52 +01:00
gronod ec7eb2fcb4 test(gamut): fix gamut_viewer.test.js node execution and guard window globals (#212) 2026-09-06 12:48:43 +01:00
gronod c5ff3d1a49 docs: resolve documentation and license inaccuracies (#214) 2026-09-06 12:41:32 +01:00
gronod 8008d9bda4 ci(macos): drop redundant .zip archive from macOS release assets (#209) 2026-09-06 12:23:14 +01:00
gronod ebcb3cdc2a ci(macos): drop redundant .zip archive from macOS release assets 2026-09-06 12:22:37 +01:00
gronod 2641edecc1 Merge pull request 'ci(macos): enable DMG AppleScript background on Gitea runners (#189)' (#208) from fix/gitea-dmg-background into development
Build Windows Packages / Build Windows (push) Successful in 7m37s
Build Linux Packages / Build Linux (push) Successful in 8m56s
Build macOS Packages / Build macOS (Apple Silicon) (push) Successful in 18m6s
Build macOS Packages / Build macOS (Universal (Intel + Apple Silicon)) (push) Failing after 49s
Build macOS Packages / Build macOS (Intel) (push) Failing after 1m3s
2026-09-06 09:40:37 +01:00
gronod 4621d734ff ci(macos): enable DMG AppleScript background on Gitea runners (#189)
Port the GitHub runner fix (d51bae7) to .gitea/workflows/build-macos.yml:
warm Finder/System Events before tauri build, and set
TAURI_BUNDLER_DMG_IGNORE_CI=true so create-dmg does not skip
AppleScript under CI=true. Tighten DMG/.app discovery to the
release bundle paths.

Closes #189 for the Gitea workflow.
2026-09-06 09:40:18 +01:00
gronod 0b419702a1 docs: align README, ROADMAP, and AGENTS for v0.8.2 and Milestones 12/13/14
- Update README.md version to v0.8.2 and platforms to Windows | macOS | Linux
- Document XY automated scanning tables (#93) and i1Pro 2 LED feedback (#204)
- Document longitudinal printer drift tracking (#95) and CGATS interop (#94)
- Update Mermaid architecture diagram and macOS build prerequisites
- Move Milestones 12 and 13 and Release v0.8.1 to Completed Milestones in ROADMAP.md
- Add post-v0.8.2 future architecture considerations in ROADMAP.md
- Document i1Pro 2 LED switch, frontend test suites, and CI cross-compilation in AGENTS.md
2026-09-06 09:17:33 +01:00
20 changed files with 338 additions and 114 deletions
+3
View File
@@ -64,6 +64,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Set Release Environment
id: set_env
shell: bash
+42 -33
View File
@@ -9,6 +9,8 @@ jobs:
build-macos:
name: Build macOS (${{ matrix.platform.name }})
runs-on: ${{ matrix.platform.os }}
env:
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
strategy:
matrix:
platform:
@@ -17,21 +19,31 @@ jobs:
target: x86_64-apple-darwin
binary_dir: macos-x86_64
arch: x86_64
- name: Apple Silicon
os: macos
target: aarch64-apple-darwin
binary_dir: macos-aarch64
arch: arm64
- name: Universal (Intel + Apple Silicon)
os: macos
target: universal-apple-darwin
binary_dir: macos-universal
arch: universal
# - name: Apple Silicon
# os: macos
# target: aarch64-apple-darwin
# binary_dir: macos-aarch64
# arch: arm64
# - name: Universal (Intel + Apple Silicon)
# os: macos
# target: universal-apple-darwin
# binary_dir: macos-universal
# arch: universal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -47,6 +59,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries
env:
ARGYLL_SERVER_URL: ${{ github.server_url }}
@@ -59,21 +74,6 @@ jobs:
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
- name: Run Rust tests
shell: bash
env:
CARGO_INCREMENTAL: "0"
CARGO_TARGET_DIR: "${{ runner.temp }}/cargo-target"
run: |
cd src-tauri
if [ "${{ matrix.platform.target }}" = "universal-apple-darwin" ]; then
cargo test
elif [ "${{ matrix.platform.target }}" = "aarch64-apple-darwin" ] && [ "$(uname -m)" != "arm64" ]; then
cargo test --no-run --target ${{ matrix.platform.target }}
else
cargo test --target ${{ matrix.platform.target }}
fi
- name: Set Release Environment
id: set_env
shell: bash
@@ -84,7 +84,23 @@ jobs:
echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV
echo "PREFIX=ICCery_${TAG}-${SHORT_SHA}-macos-${{ matrix.platform.arch }}" >> $GITHUB_ENV
- name: Warm up macOS GUI for DMG layout
shell: bash
run: |
# Tauri's DMG bundler runs Finder AppleScript to set the background
# picture and icon positions. On CI Macs, Finder/Aqua may be idle,
# which causes AppleEvent timeouts (-1712). Wake them before build.
echo "WindowServer: $(pgrep -l WindowServer || echo 'NOT RUNNING')"
echo "console user: $(stat -f '%Su' /dev/console 2>/dev/null || echo unknown)"
echo "whoami: $(whoami)"
open -g -a Finder || true
open -g -a "System Events" || true
osascript -e 'tell application "Finder" to get name' || true
sleep 10
- name: Build Tauri App
env:
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
run: npm run tauri build -- --target ${{ matrix.platform.target }}
- name: Prepare Release Assets
@@ -93,18 +109,11 @@ jobs:
mkdir -p release-assets
# Stage DMG package
DMG_FILE=$(find src-tauri/target -type f -name "*.dmg" | head -n 1)
DMG_FILE=$(find src-tauri/target -type f -path "*/release/bundle/dmg/*.dmg" | head -n 1)
if [ -n "$DMG_FILE" ] && [ -f "$DMG_FILE" ]; then
cp "$DMG_FILE" "release-assets/${PREFIX}.dmg"
fi
# Stage ZIP archive of .app bundle
APP_DIR=$(find src-tauri/target -type d -name "*.app" | head -n 1)
if [ -n "$APP_DIR" ] && [ -d "$APP_DIR" ]; then
APP_PARENT=$(dirname "$APP_DIR")
APP_NAME=$(basename "$APP_DIR")
(cd "$APP_PARENT" && zip -r "${GITHUB_WORKSPACE}/release-assets/${PREFIX}.zip" "$APP_NAME")
fi
- name: Upload Artifacts
uses: actions/upload-artifact@v3
+3
View File
@@ -41,6 +41,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries
shell: powershell
env:
+10 -7
View File
@@ -35,6 +35,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -103,13 +113,6 @@ jobs:
cp "$DMG_FILE" "release-assets/${PREFIX}.dmg"
fi
# Stage ZIP archive of .app bundle
APP_DIR=$(find src-tauri/target -type d -path "*/release/bundle/macos/*.app" | head -n 1)
if [ -n "$APP_DIR" ] && [ -d "$APP_DIR" ]; then
APP_PARENT=$(dirname "$APP_DIR")
APP_NAME=$(basename "$APP_DIR")
(cd "$APP_PARENT" && zip -r "${GITHUB_WORKSPACE}/release-assets/${PREFIX}.zip" "$APP_NAME")
fi
- name: Upload Artifacts
uses: actions/upload-artifact@v4
+30 -6
View File
@@ -53,11 +53,17 @@ The frontend uses a tiered button sizing system defined in `src/styles/main.css`
- Valid threshold values must be non-negative and `delta_e_good_max < delta_e_warning_max`; both the frontend and backend enforce this.
- Saving settings dispatches a `settings-saved` custom event so live components (e.g. the swatch grid) can re-classify on the fly.
## Build Commands
## Build & Test Commands
- **Rust backend**: `cd src-tauri && CARGO_INCREMENTAL=0 cargo check` (the project lives on a network filesystem that doesn't support file locking, so `CARGO_INCREMENTAL=0` is required)
- **Rust tests**: `cd src-tauri && CARGO_INCREMENTAL=0 cargo test`
- **Frontend**: `cd src-tauri && npm run build` (or `npm run dev` for development)
- **Rust backend check**: `cd src-tauri && CARGO_INCREMENTAL=0 cargo check` (the project lives on a network filesystem that doesn't support file locking, so `CARGO_INCREMENTAL=0` is required)
- **Rust unit tests**: `cd src-tauri && CARGO_INCREMENTAL=0 cargo test`
- **Frontend test suites**:
- Verification & drift tests: `node src/js/profcheck.test.js` (21 tests)
- Chartread classifier & XY table tests: `node src/js/chartread.test.js` (39 tests)
- Gamut viewer tests: `node src/js/gamut_viewer.test.js`
- Browser devtools console: `import('./profcheck.test.js').then(m => m.runAll())`
- **Frontend development server**: `npm run tauri dev`
- **Production package build**: `npm run tauri build`
## Architecture Overview
@@ -109,7 +115,7 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- Record schema (`VerificationRecord` in `src-tauri/src/quality_store.rs`):
- `id`: unique record identifier in the format `vr-<epoch_millis>-<seq>`.
- `profile_name`: target profile filename.
- `printer`: device name captured at print spooling (`wizardState.printerName`), or "Unknown".
- `printer_name`: device name captured at print spooling (`wizardState.printerName`), or "Unknown".
- `avg_de`, `max_de`, `rms_de`: CIEDE2000 metrics from `profcheck` (using `-u` JSON summary).
- `patch_count`: number of test patches evaluated.
- `status`: classified status using **ICCery verification bands (issue #95)**:
@@ -118,7 +124,7 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- `< 3.5`: "Acceptable" (`badge-acceptable`)
- `>= 3.5`: "Warning" (`badge-poor`)
- `timestamp`: ISO-8601 UTC string.
- Max capacity is 500 records; oldest records evicted on overflow.
- Max capacity is 1,000 records; oldest records evicted on overflow.
- Atomic file writes (`.tmp` write followed by `rename`) prevent data corruption.
- Tauri IPC command casing:
- Nested struct fields (`VerificationRecord`) serialize with `snake_case`.
@@ -145,3 +151,21 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- Testing:
- Pure line classification unit tests live in `src/js/chartread.test.js` (executable directly in Node or browser console).
- Unix/macOS mock script `src-tauri/argyll/mocks/chartread.mock` supports `--xy` flag (or `MOCK_XY_TABLE=1`) with blocking `read` calls simulating calibration, sheet placement, fiducial alignment, and scanning.
## Stage 3 i1Pro 2 LED Status Feedback (#204)
- Supports the `-Y l` switch introduced in the ICCery ArgyllCMS fork to drive the dual RGB ring LEDs of the X-Rite i1Pro 2 (Rev E) for real-time visual status feedback during strip measurement:
- **Flashing White**: Awaiting baseline calibration on white tile.
- **Flashing Blue**: Ready for row swipe / awaiting strip read.
- **Flashing Red**: Strip scan error / misread.
- **Flashing Green**: Strip scan successfully captured.
- Controlled via `enable_i1pro2_leds: bool` in `AppSettings` (persisted in `settings.json`), exposed under Settings → Instrument & Measurement Preferences.
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
## CI & Cross-Compilation
- Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`).
- Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations.
- Local/CI cross-compilation test execution for Apple Silicon (`aarch64-apple-darwin`) on Intel hosts must use `cargo test --no-run --target aarch64-apple-darwin` to avoid executing ARM64 binaries on an x86_64 CPU (`Bad CPU type in executable (os error 86)`).
+34 -14
View File
@@ -2,8 +2,8 @@
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
[![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)
[![Release](https://img.shields.io/badge/version-v0.8.4-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%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)
@@ -14,16 +14,29 @@
## Key Features
- 🪄 **Linear 5-Stage Wizard Workflow**:
1. **Stage 1 — Patch Generation (`targen`)**: Configure RGB (driver-managed) or CMYK (RIP-managed) patch sets with custom counts, profiling presets, and neutral/grey axis boosting.
2. **Stage 2 — Target Creation & Raw Printing (`printtarg`)**: Format patch targets for spectrophotometers (i1Pro, i1Pro2, ColorMunki, SpyderPrint). View high-resolution downscaled TIFF previews and print directly using native OS raw unmanaged pathways (Windows GDI uncorrected / Linux CUPS `raw`).
3. **Stage 3 — Interactive Measurement (`chartread`) & Averaging (`average`)**: Instrument auto-detection (`instlist`), real-time calibration prompts, interactive strip reading state machine, live swatch grid with CIEDE2000 ($\Delta E_{00}$) quality indicators and user-configurable traffic-light thresholds, diagonally split intended-vs-measured colour swatches, white reference patch preservation, and multi-pass sheet averaging for measurement noise reduction.
4. **Stage 4 — Profile Calculation (`colprof`)**: Generate high-precision cLUT mathematical ICC/ICM profiles with configurable algorithm quality, OBA/FWA compensation, illuminant/observer selection, viewing-condition transforms, custom ambient spectrum support, descriptions, and copyright tagging.
5. **Stage 5 — Verification & 3D Gamut (`profcheck` + `iccgamut`)**: Comprehensive mathematical validation report (Peak, Average, RMS $\Delta E$) paired with an interactive 3D CIELAB convex hull color volume viewer, per-vertex true-colour rendering, layer opacity controls, camera reset, keyboard shortcut, touch controls, bundled sRGB reference wireframe comparison, and robust parsing of both JSON and legacy profcheck output formats.
1. **Stage 1 — Patch Generation (`targen`)**: Configure RGB (driver-managed) or CMYK (RIP-managed) patch sets with custom counts, profiling presets, neutral/grey axis boosting, and 11 advanced generation parameters with contextual guidance tooltips. Supports direct-resume from existing `.ti2` target files to jump straight to measurement.
2. **Stage 2 — Target Creation & Raw Printing (`printtarg`)**: Format patch targets for handheld spectrophotometers (i1Pro, i1Pro2, ColorMunki, SpyderPrint) and automated XY tables (i1iO, SpectroScan). View high-resolution downscaled TIFF previews and print directly using native OS unmanaged pathways:
- **macOS**: Native `NSPrintPanel` driver preferences with automatic ColorSync suppression (`AP_ColorMatchingMode=AP_ApplicationColorMatching`), CUPS media type selection, and driver-specific color adjustment bypass detection (Canon `CNIJIntent2`, Epson `ColorCorrection`, Gutenprint).
- **Windows**: GDI uncorrected raw printing and DEVMODE preferences.
- **Linux**: CUPS `raw` queue and PPD media option spooling.
3. **Stage 3 — Interactive Measurement (`chartread`) & Averaging (`average`)**:
- **Automated XY Scanning Tables (#93)**: Full automated sequence for GretagMacbeth SpectroScan and X-Rite i1iO tables with multi-line prompt classification, fiducial sight alignment prompts, 4-step sequence checklist UI, and graceful head parking (`q\n`).
- **Instrument Status Feedback (#204)**: Optional `-Y l` switch support driving the dual RGB ring LEDs of the X-Rite i1Pro 2 (flashing white for calibration, flashing blue for ready/swipe, flashing red for error, flashing green for capture).
- **Interactive Controls**: Dedicated `Done & Save .ti3` (`d\n`), `Undo Strip` (`u\n`), and `Skip Strip` (`s\n`) actions.
- **Live Swatch Grid**: 135° diagonally split intended-vs-measured colour patches with live CIEDE2000 ($\Delta E_{00}$) quality indicators, white reference patch preservation, and user-configurable good/warning traffic-light thresholds persisted across sessions.
- **Noise Reduction**: Multi-pass sheet averaging (`average`) to eliminate spectrophotometer noise.
4. **Stage 4 — Profile Calculation (`colprof`)**: Generate high-precision cLUT mathematical ICC/ICM profiles with configurable algorithm quality, OBA/FWA compensation (`-f`), illuminant (`-i`) and observer (`-o`) overrides, viewing-condition transforms (`-c`, `-d`), custom ambient spectrum support, descriptions, and copyright tagging.
5. **Stage 5 — Verification, Drift Analytics & 3D Gamut (`profcheck` + `iccgamut`)**:
- **Longitudinal Printer Drift Analytics (#95)**: Historical verification logging persisted to `verification_history.json` (up to 1,000 records), an interactive dual-series SVG trend chart with shaded ICCery verification reference bands, consecutive-breach alert recommendation card (detecting drift across distinct dates or $\ge 1$ hour apart), and RFC-4180 compliant CSV export.
- **Mathematical Accuracy Report**: Peak, Average, and RMS CIEDE2000 metrics with robust parsing of both Argyll JSON summaries (`-u`) and legacy plain-text reports.
- **Interactive 3D Gamut Viewer**: CIELAB coordinate scaffold with crisp CSS2D labels, per-vertex true-colour profile gamut shading, layer visibility toggles and opacity sliders, camera reset (press **R**), touch controls, and bundled sRGB reference wireframe comparison.
- 📊 **CGATS Dataset Interoperability (#94)**: Native parser for external CGATS and Argyll `.ti3` datasets with canonical normalization (0255 scaling, field aliasing, metadata synthesis) and direct-jump workflows to Stage 4 (Profile Calculation) and Stage 5 (Verification).
- 📋 **Profiling Presets**: One-click configuration presets (Standard RGB Photo, High-Gamut CMYK Proofing, Fast RGB Draft) with custom preset export/import and security validation.
- 🐧 **glibc Compatibility**: Pre-built Linux packages compiled with Ubuntu 22.04 LTS compatibility for Debian/Ubuntu environments.
- 🛡️ **Disk Artefact Gating**: Stepper navigation strictly verifies generated artefacts on disk (`.ti1`, `.ti2`, `.ti3`, `.icc`/`.icm`), preventing out-of-order execution while preserving backward navigation.
- 🌐 **Platform-Aware**: Automatic handling of platform profile conventions (`.icm` on Windows, `.icc` on Linux/macOS) and native OS printer subsystems.
- 🎛️ **Consistent UI Controls**: Tiered button sizing (`btn-sm`/`btn-md`/`btn-lg`/`btn-icon-sq`) and standardised action row classes provide a uniform, polished interface across all wizard stages and dialogs.
- 🍎 **macOS Universal Binary**: Native Apple Silicon (`arm64`) and Intel (`x86_64`) support with universal binary bundling and fallback resolution.
- 🐧 **Linux glibc Compatibility**: Pre-built Linux packages compiled with Ubuntu 22.04 LTS compatibility for Debian/Ubuntu environments.
- 🛡️ **Disk Artefact Gating**: Stepper navigation strictly verifies generated artefacts on disk (`.ti1``.ti2``.ti3``.icc`/`.icm`), preventing out-of-order execution while preserving backward navigation.
- 🌐 **Platform-Aware**: Automatic handling of platform profile conventions (`.icm` on Windows, `.icc` on macOS/Linux) and native OS printer subsystems.
- 🎛️ **Standardised UI Design**: Tiered button sizing (`.btn-sm`, `.btn-md`, `.btn-lg`, `.btn-icon-sq`) and consistent action row layouts provide a uniform, responsive interface across all stages.
- ⚖️ **Clean AGPL Boundary**: Complete isolation of AGPLv3 binaries via asynchronous tokio IPC process pipelines.
---
@@ -38,13 +51,16 @@ flowchart TD
UI[Wizard UI & Swatch Grid]
ThreeJS[3D CIELAB Gamut Viewer]
State[Wizard State & Artefact Verifier]
PrintEngine["Raw Print Subsystem (GDI / CUPS)"]
QualityStore[Verification History & Drift Analytics]
PrintEngine["Raw Print Subsystem (GDI / CUPS / NSPrintPanel)"]
ProcMgr[Async Subprocess IPC Manager]
UI <--> State
State <--> ProcMgr
State <--> QualityStore
ProcMgr --> ThreeJS
UI --> PrintEngine
QualityStore --> UI
end
subgraph Argyll ["ArgyllCMS Subprocesses (AGPLv3)"]
@@ -72,6 +88,7 @@ flowchart TD
- [Node.js](https://nodejs.org/) (v18 or newer)
- [Rust](https://www.rust-lang.org/) (1.78+ stable)
- Operating system dependencies:
- **macOS**: macOS 11.0 (Big Sur) or newer, Xcode Command Line Tools (`xcode-select --install`).
- **Windows**: Microsoft Visual Studio C++ Build Tools & WebView2 runtime.
- **Linux (Debian/Ubuntu)**: `libwebkit2gtk-4.1-dev`, `build-essential`, `curl`, `wget`, `file`, `libxdo-dev`, `libssl-dev`, `libayatana-appindicator3-dev`, `librsvg2-dev`, `libcups2-dev`.
@@ -91,7 +108,7 @@ npm run fetch-argyll
npm run tauri dev
```
> **Note:**
> **Notes:**
> - Sidecars are not stored in git; `tauri build` / `tauri dev` will fail until `npm run fetch-argyll` has been run at least once.
> - You can override the downloaded ArgyllCMS release version using `ARGYLL_RELEASE_TAG=vX.Y.Z npm run fetch-argyll`.
> - On Windows, the NSIS installer package bundles the ArgyllCMS USB instrument driver suite and offers an optional driver setup step when run with administrative privileges.
@@ -101,7 +118,10 @@ npm run tauri dev
# Download sidecars (if not already fetched)
npm run fetch-argyll
# Build desktop packages (MSI/NSIS on Windows, DEB/AppImage on Linux)
# Build desktop packages
# - macOS: .dmg / .app bundle (Intel, Apple Silicon, or Universal with --target universal-apple-darwin)
# - Windows: .exe (NSIS) / .msi installer
# - Linux: .AppImage / .deb package
npm run tauri build
```
+32 -14
View File
@@ -10,10 +10,11 @@ This document outlines the architectural roadmap, completed milestones, and upco
## 1. Architecture Summary
ICCery is a native, cross-platform desktop application built with:
- **Backend**: Rust + Tauri v2, managing asynchronous process pipes, native printer devmode configurations (Windows GDI & Linux CUPS), and filesystem operations.
- **Backend**: Rust + Tauri v2, managing asynchronous process pipes, native printer configurations (macOS Core Printing / `NSPrintPanel`, Windows GDI & DEVMODE, Linux CUPS), and filesystem operations.
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
- **Current Version**: `v0.8.4` (Production release).
---
@@ -90,16 +91,7 @@ ICCery is a native, cross-platform desktop application built with:
- [x] **Windows Authenticode Code Signing in Gitea CI** (`v0.6.4` `v0.6.6`): Integrated Tauri bundle signing hooks via `sign.cmd` batch wrapper with PATH resolution, Gitea Actions secret-based PFX materialization, and ephemeral signing pipeline.
- [x] **Stage 3 Chartread Completion & Snapshot IPC Fix (#175)** (`v0.6.7` `v0.6.8`): Added dedicated `Done & Save .ti3` action (`d\n`), `Undo Strip` action (`u\n`), automated completion state detection, and corrected Tauri IPC deserialization parameter (`passIndex`) in `snapshot_ti3`.
---
## 3. Future Roadmap
### Milestone 9 — macOS Native Support & Enhanced Print Spooling (`v0.4.0`)
- [ ] **macOS Platform Bundle**: Build and sign universal macOS `.dmg` bundles with notarization.
- [ ] **macOS Raw Spooling**: Native CoreGraphics/CUPS raw print dialog bypass.
### Milestone 13 — UI/UX & Workflow Polish
### Milestone 13 — UI/UX & Workflow Polish (`v0.7.0` `v0.7.4`, `v0.8.0`)
- [x] **Global Button Standardization (#177)**: Enforce `.btn-sm`/`.btn-md`/`.btn-lg`/`.btn-icon-sq` tiers across all stages, remove inline button styles, and add CSS custom properties for button metrics.
- [x] **Swatch Grid White Patch & Orientation Polish (#178)**: Finalise `is_pad` guard documentation, diagonally split swatch tooltips, and validate `printtarg` row/column ordering.
- [x] **Configurable CIEDE2000 Thresholds (#184)**: User-configurable good/warning ΔE₀₀ upper bounds in Settings, persisted across sessions and applied to the Stage 3 swatch grid.
@@ -107,7 +99,33 @@ ICCery is a native, cross-platform desktop application built with:
- [x] **3D Gamut Viewer Controls (#185)**: Camera reset, opacity sliders, keyboard shortcut, and full public-API JSDoc.
- [x] **Gamut / Profcheck Hardening (#179)**: Validate `.gam` vertex/face parsing, improved `profcheck` regex fallbacks for legacy text output, and user-visible parser warnings.
### Milestone 12 — Future Workflow & Advanced Analytics
- [x] **Printer Drift Tracking & Verification Analytics (#95)**: Track longitudinal printer drift in Stage 5 over time with CIEDE2000 trend charts, breach alert banners, RFC-4180 CSV export, and history storage.
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (SpectroScan, i1iO) with multi-line prompt classification, fiducial alignment, and sheet placement checklist.
### Hardware Status Feedback Release (`v0.8.1`)
- [x] **i1Pro 2 LED Status Feedback (#204)**: Added optional `-Y l` switch support to `chartread` driving the dual RGB ring LEDs of the X-Rite i1Pro 2 for visual status feedback (calibration, swipe ready, misread, capture success).
- [x] **macOS CI Cross-Compilation Testing**: Hardened `.gitea/workflows/build-macos.yml` by compiling Apple Silicon tests with `--no-run` on Intel runner hosts to avoid architecture execution mismatch.
### Milestone 12 — Future Workflow & Advanced Analytics (`v0.8.2`)
- [x] **Printer Drift Tracking & Verification Analytics (#95)**: Track longitudinal printer drift in Stage 5 over time with historical run logging in `verification_history.json` (1,000 records), interactive dual-series SVG trend chart with ICCery verification reference bands, consecutive-breach alert recommendation card, and RFC-4180 CSV export.
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
### Maintenance & Reliability Release (`v0.8.3`)
- [x] **Gamut Viewer Node Test Runner Support (#212)**: Guarded `window` and `window.__TAURI__` globals in `gamut_viewer.js` and added polyfill mock harness to `gamut_viewer.test.js` to enable automated headless test execution via `node src/js/gamut_viewer.test.js`.
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
### Maintenance & Reliability Release (`v0.8.4`)
- [x] **Atomic Verification History Persistence (#213)**: Hardened `quality_store.rs` with atomic temporary file writes (`.tmp`), explicit flush/sync, and atomic rename to prevent historical drift data loss or corruption upon unexpected system crashes.
- [x] **Frontend Unit Testing & CI Integration (#215)**: Added standard `npm test` script executing the 3 frontend test suites (`profcheck`, `chartread`, and `gamut_viewer`) and integrated automated frontend test validation into macOS, Linux, and Windows CI workflows.
---
## 3. Future Roadmap
### Milestone 9 — macOS Native Support & Enhanced Print Spooling (`v0.4.0` / Post-v0.8)
- [ ] **macOS Platform Bundle**: Build and sign universal macOS `.dmg` bundles with Apple Developer ID notarization and stapling.
- [ ] **macOS Raw Spooling**: Native CoreGraphics/CUPS raw print dialog bypass.
### Future Architecture Strategy (Post-v0.8.2)
- **Multi-Device Drift Overlays**: Overlay drift curves from multiple printers/media types on a shared timeline in Stage 5.
- **Direct Remote Target Dispatch**: Send `.ti2` target jobs to network print spoolers or remote print labs with automated token callbacks.
- **Embedded ICC Profile Inspector**: Direct inspection of cLUT tags, chromatic adaptation matrices, tone reproduction curves (TRC), and profile metadata from saved `.icc`/`.icm` files.
+18 -12
View File
@@ -23,20 +23,26 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Delaunator
Copyright (c) 2017, Mapbox
## quickhull3d
MIT License
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Tauri Framework & Rust Dependencies
Copyright (c) 2019-present, Tauri Programme within The Commons Conservancy.
+3 -2
View File
@@ -1,11 +1,12 @@
{
"name": "iccery",
"private": true,
"version": "0.8.2",
"version": "0.8.4",
"type": "module",
"scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs",
"tauri": "tauri"
"tauri": "tauri",
"test": "node src/js/profcheck.test.js && node src/js/chartread.test.js && node src/js/gamut_viewer.test.js"
},
"devDependencies": {
"@tauri-apps/cli": "^2"
+1 -1
View File
@@ -1423,7 +1423,7 @@ dependencies = [
[[package]]
name = "iccery"
version = "0.8.2"
version = "0.8.4"
dependencies = [
"base64 0.22.1",
"image",
+12 -1
View File
@@ -1,9 +1,20 @@
[package]
name = "iccery"
version = "0.8.2"
version = "0.8.4"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
edition = "2021"
include = [
"src/**/*",
"build.rs",
"Cargo.toml",
"Cargo.lock",
"tauri.conf.json",
"capabilities/**/*",
"gen/**/*",
"icons/**/*",
"windows/**/*",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+6
View File
@@ -10,6 +10,12 @@ fn main() {
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=argyll");
// Validate that ArgyllCMS sidecar binaries are staged before building
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
+55
View File
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_spectrum_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter("Argyll Spectrum File (*.sp)", &["sp"]);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_dataset_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter(
"Measurement Dataset (*.ti3, *.txt, *.cgats, *.csv)",
&["ti3", "txt", "cgats", "csv"],
);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_target_file(
app: AppHandle,
+2
View File
@@ -51,6 +51,8 @@ pub fn run() {
commands::inspect_dataset_preview,
commands::select_existing_target,
commands::select_profile_file,
commands::select_spectrum_file,
commands::select_dataset_file,
commands::select_target_file,
commands::select_directory,
commands::send_stdin,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.8.2",
"version": "0.8.4",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"
+3 -3
View File
@@ -1059,9 +1059,9 @@
<p>Copyright (c) 2010-2023 three.js authors</p>
<p class="license-legal-block">Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h5>Delaunator</h5>
<p>Copyright (c) 2017, Mapbox</p>
<p class="license-legal-block">Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
<h5>quickhull3d</h5>
<p>MIT License</p>
<p class="license-legal-block">Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h5>Tauri Framework &amp; Rust Dependencies</h5>
<p>Copyright (c) 2019-present, Tauri Programme within The Commons Conservancy.</p>
+40 -11
View File
@@ -1,4 +1,6 @@
const { invoke } = window.__TAURI__.core;
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke
? window.__TAURI__.core.invoke
: async () => {};
export class CgatsInterop {
constructor(appState) {
@@ -22,26 +24,53 @@ export class CgatsInterop {
async handleImport() {
try {
const filePath = await invoke('select_target_file');
const filePath = await invoke('select_dataset_file', {
defaultDir: this.appState?.cwd || null,
});
if (!filePath) return; // User cancelled
// Inspect first to show modal (optional, skipping for now, directly import)
// Since we are mocking the UI a bit for this branch, we will just import directly
const isWindows = filePath.includes('\\');
const sep = isWindows ? '\\' : '/';
const parts = filePath.split(sep);
const fileName = parts.pop();
const fileDir = parts.join(sep);
const fileStem = fileName.replace(/\.[^/.]+$/, '');
const targetCwd = this.appState?.cwd || fileDir;
const targetBasename = this.appState?.basename || fileStem;
const targetBasenameInput = document.getElementById('targetBasename');
if (targetBasenameInput && !targetBasenameInput.value.trim()) {
targetBasenameInput.value = targetBasename;
}
const selectedPathDisplay = document.getElementById('selectedPathDisplay');
if (selectedPathDisplay && (!selectedPathDisplay.textContent || selectedPathDisplay.textContent.includes('No directory') || !this.appState?.cwd)) {
selectedPathDisplay.textContent = `Directory: ${targetCwd}`;
}
if (this.appState?.setTarget) {
await this.appState.setTarget(targetBasename, targetCwd);
}
const summary = await invoke('import_measurement_dataset', {
filePath,
targetCwd: this.appState.cwd,
targetBasename: this.appState.basename,
targetCwd,
targetBasename,
});
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
this.appState?.showNotice?.(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
// Update state to jump to stage 4
await this.appState.updateGating();
this.appState.currentStage = 4;
this.appState.applyStageDOM(4);
if (this.appState?.updateGating) {
await this.appState.updateGating();
}
if (this.appState) {
this.appState.currentStage = 4;
this.appState.applyStageDOM?.(4);
}
} catch (e) {
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
}
}
+2 -2
View File
@@ -54,8 +54,8 @@ export function initColprof() {
if (btnBrowseCustomSp && colprofCustomSpPath) {
btnBrowseCustomSp.addEventListener("click", async () => {
try {
const selected = await window.__TAURI__.dialog.open({
filters: [{ name: 'Spectrum', extensions: ['sp'] }]
const selected = await invoke("select_spectrum_file", {
defaultDir: chartreadCwd || wizardState.cwd || null,
});
if (selected) {
colprofCustomSpPath.value = selected;
+2 -3
View File
@@ -1,8 +1,7 @@
import { computeQuickHull } from "./vendor/quickhull.js";
import { computeQuickHull } from "./vendor/quickhull.js";
import { labToSrgb } from "./color_convert.js";
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
let scene, camera, renderer, labelRenderer, controls;
let currentProfileMesh = null;
+39 -4
View File
@@ -1,20 +1,50 @@
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
// Run in a browser/devtools console after the app has loaded:
// Unit & console tests for gamut_viewer.js parsing.
// Can be run in browser devtools console:
// import('./gamut_viewer.test.js').then(m => m.runAll())
// Or in Node:
// node src/js/gamut_viewer.test.js
import { parseGamutFile } from './gamut_viewer.js';
// Node environment polyfill for browser globals
if (typeof window === 'undefined') {
globalThis.window = {
__TAURI__: {
core: { invoke: () => Promise.resolve() },
event: { listen: () => Promise.resolve(() => {}) }
},
addEventListener: () => {},
dispatchEvent: () => {}
};
globalThis.document = {
getElementById: () => null,
querySelectorAll: () => []
};
}
const { parseGamutFile } = await import('./gamut_viewer.js');
let passed = 0;
let total = 0;
export function runAll() {
console.group('gamut/profcheck parser tests');
console.group('Gamut Viewer Parser Tests');
passed = 0;
total = 0;
testParseGamutBasic();
testParseGamutDualTable();
testParseGamutWithComments();
console.log(`\nResults: ${passed} / ${total} tests passed.`);
console.groupEnd();
if (passed !== total) {
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
}
}
function assertEqual(actual, expected, message) {
total++;
const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (ok) {
passed++;
console.log('PASS:', message);
} else {
console.error('FAIL:', message, 'expected', expected, 'got', actual);
@@ -78,3 +108,8 @@ END_DATA`;
assertEqual(vertices.length, 4, 'commented gamut vertex count');
assertEqual(faces.length, 2, 'commented gamut face count');
}
// Auto-run if executed in Node.js
if (typeof process !== 'undefined' && process.argv && process.argv[1]?.endsWith('gamut_viewer.test.js')) {
runAll();
}