Files
iccery-v2-mac/docs/04-argyll-binaries.md
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 4c2863e52c Add bootstrap docs: AGENTS.md, BUILD-PLAN.md, LICENCE.md, vendored spec (docs/), brand assets
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 17:36:50 +01:00

40 KiB
Raw Permalink Blame History

04 — Argyll binary invocations

Line numbers refer to ICCery v0.8.5 (/tmp/ICCery at analysis time) and the Gronod ArgyllCMS 3.5.0 fork.

Source tree: /tmp/ICCery (v0.8.5). ICCery never links Argyll; every binary is spawned as an isolated subprocess (calibration.rs:14). Sidecars come from the ICCery-patched fork gronod/argyllcms (GitHub Gronod/argyllcms), not stock Graeme Gill builds.

Two spawn paths exist:

Path Used by Streams Events
ProcessManager::spawn targen, printtarg, chartread, average, colprof, profcheck, iccgamut, instlist piped stdin/stdout/stderr, line-by-line process:stdout, process:stderr, process:exit, process:error, process:json_row
calibration::run_captured printcal, applycal .output().await (full capture) none — returns (exit_code, stdout, stderr) to the Tauri command

0. Shared infrastructure

0.1 Binary resolution — resolve_binary

commands.rs:40106. Public Tauri command and internal helper.

Order of search:

  1. Settings argyll_binary_dir (Settings dialog, AppSettings.argyll_binary_dir). If non-empty, join each candidate name; first existing path wins.
  2. Bundled sidecar at argyll/{platform}/{name} under Tauri BaseDirectory::Resource.
  3. If the resource path does not exist, still return the constructed resource path (does not search $PATH). Missing binaries surface later as spawn process:error.

get_binary_candidates (commands.rs:4046):

  • Windows: ["{name}.exe", "{name}"] unless name already ends with .exe.
  • Unix: ["{name}"].

Platform directory selection (commands.rs:6583, mirrored in build.rs:2644):

OS / arch Resource dir
linux x86_64 linux-x86_64
windows x86_64 windows-x86_64
macos aarch64 macos-universal if argyll/macos-universal/instlist exists, else macos-aarch64
macos x86_64 macos-universal if that marker exists, else macos-x86_64
anything else linux-x86_64 (fallback)

default_instrument is stored in settings (settings.rs:76) and shown in the Settings dialog, but is never read when building any Argyll argv. Instrument comes from Stage 2 #instrumentSelect.

0.2 Working directory — resolve_safe_cwd

commands.rs:203217. If cwd_input is a real directory, use it. Else: document_dirhome_dirapp_data_dir.

All specialised run_* commands pass Some(resolve_safe_cwd(&app, &config.cwd)). Exceptions:

  • detect_instruments / spawn_process: cwd = None (inherit parent cwd).
  • extract_gamut: parent of the ICC file, or default cwd if empty.
  • apply_calibration: parent of the input profile.

0.3 Environment variables

Set on every spawn (ProcessManager and run_captured):

ARGYLL_NOT_INTERACTIVE=1
  • ProcessManager: process_manager.rs:97
  • run_captured: calibration.rs:848

This is the ICCery-side half of gronod/argyllcms#24 (Windows anonymous-pipe deadlock). It forces Argyll's check_if_not_interactive() path so con_char() uses pipe I/O instead of a console. Added in ICCery#134 (3d514b6).

Never set:

  • ARGYLL_3D_DISP — grep of the tree is empty. iccgamut is invoked without any 3D-display env; the .gam file is parsed in JS (gamut_viewer.js).
  • PATH — not mutated. Binaries are always absolute paths from resolve_binary. Inherited PATH is whatever the OS/session provides (needed only if a user-supplied argyll_binary_dir binary dlopens something).

0.4 Windows CREATE_NO_WINDOW

Both spawn paths:

const CREATE_NO_WINDOW: u32 = 0x08000000; // 0x08000000
command.creation_flags(CREATE_NO_WINDOW);
  • ProcessManager: process_manager.rs:99103
  • run_captured: calibration.rs:849853

Fixes ICCery#46 (v0.1.6, ec6205b): Argyll tools are IMAGE_SUBSYSTEM_WINDOWS_CUI; without this flag a black console covers the UI.

CommandExt is pulled in via tokio::process::Command::creation_flags (tokio re-exports the Windows ext).

0.5 Event schema (ProcessManager only)

events.rs:

ProcessEventPayload { id: String, line: Option<String>, code: Option<i32>, error: Option<String> }
JsonRowPayload      { id: String, json: String }
Event When Fields
process:stdout every stdout line except those starting ROW_COLORS_JSON: id, line
process:stderr every stderr line id, line
process:json_row stdout line prefixed ROW_COLORS_JSON: (prefix stripped) id, json (raw JSON string)
process:exit child reaped id, code (status.code().unwrap_or(0) on natural exit; unwrap_or(1) on kill)
process:error Command::spawn failed id, error

Stdout reader (process_manager.rs:128139):

const JSON_ROW_PREFIX: &str = "ROW_COLORS_JSON: ";
if line.starts_with(JSON_ROW_PREFIX) {
    emit_json_row(..., line[JSON_ROW_PREFIX.len()..]);
} else {
    emit_stdout(...);
}

JSON-row lines are not forwarded as process:stdout and are not written to the subprocess log as info lines.

Logging: spawn logs sanitised argv (~ for $HOME/%USERPROFILE%) at info, raw argv at debug (process_manager.rs:2860, 105119). Each stdout line → log::info!, stderr → log::warn!, exit → log::info!.

0.6 Sidecar fetch / bundle

scripts/fetch-argyll.mjs downloads from https://github.com/Gronod/argyllcms/releases (override: ARGYLL_SERVER_URL, ARGYLL_REPO, ARGYLL_RELEASE_TAG). Marker binary is instlist / instlist.exe. Windows also copies usb/ (ArgyllCMS_install_USB.exe, ArgyllCMS.inf) to src-tauri/argyll/usb/.

tauri.conf.json:3840: "resources": ["argyll/**/*"].

build.rs:1964 panics the compile if the platform marker is missing (npm run fetch-argyll required).

NSIS (windows/hooks.nsh:133158): admin install prompts “Install ArgyllCMS USB instrument drivers?” and ExecWaits ArgyllCMS_install_USB.exe. Uninstall does not run ArgyllCMS_uninstall_USB.exe.


1. targen — Stage 1 patch generation and Stage 0 calibration chart

1.1 When

UI Command Process id
Stage 1 Generate (#btnGenerate) run_targen targen_{basename}
Stage 0 Generate Calibration Target (#btnCalGenerate) generate_calibration_target targen_{CAL_basename}

run_targen: commands.rs:952964. generate_calibration_target: calibration.rs:598613. Both go through ProcessManager.

Stage 0 prefixes the basename with CAL_ (calibration_basename, never double-prefix). Calibration charts must not collide with the profiling .ti1.

1.2 Profiling argv — build_targen_args (commands.rs:788905)

Always starts -v -d {2|4}. Colour space is the only discriminator for -d: RGB → 2, CMYK → 4. No other colourant counts.

UI field (#id / config) Flag Condition
#colourSpace radio (colour_space) -d 2 or -d 4 always
#patchCountPreset / #patchCountCustom (patch_count, else total_patches) -f N N > 0; JS default 800
#whitePatches (white_patches) -e N Some
#blackPatches (black_patches) -B N Some. JS: RGB default 4, CMYK default 0 on colour-space change
#targenGreySteps (grey_steps) -g N Some and N > 0
#targenSingleChannelSteps (single_channel_steps) -s N Some and N > 0
#targenPrecondProfile (preconditioning_profile) -c PATH non-empty trim
#targenNeutralSteps (neutral_steps) -n N Some and N > 0
#targenNeutralConcentration (neutral_concentration) -N x.xx Some and `
#targenHighQuality (ofps_high_quality) -G Some(true)
#targenAdaptation (ofps_adaptation) -A x.xx Some (even 0.10 — no default-skip)
#targenAlgorithm (full_spread_algorithm) -t -r -R -q -Q -i -I value in that set; "ofps" / default → no flag
#targenInkLimit (total_ink_limit) -l N CMYK only, 1..=400
#targenDarkEmphasis (dark_emphasis) -V x.xx Some and `
#targenDevicePower (device_power) -p x.xx Some and `
#targetBasename positional last arg, no extension

Not passed: -u (Argyll fork has targen -u JSON progress — ICCery never enables it). -v always.

JS config construction: targen.js:295314. On success (code === 0) advances to Stage 2 via setStage1Result + wizardState.navigateToStage(2).

1.3 Calibration argv — build_calibration_targen_args (calibration.rs:146189)

Hard-wired for a short per-channel wedge, not a full-spread profile chart:

-v -d {2|4} -s {steps} -g {steps} [-n {steps}] -e {white|4} [-l TAC] -f 0 {CAL_basename}
UI / config Flag Condition
#calColourSpace -d 2 / -d 4 rgb / cmyk
#calSteps (steps_per_channel) -s N and -g N clamped 11..=51 (DEFAULT_STEPS=21)
#calNeutralEmphasis -n N (same N) checked
#cal white_patches (JS always sends 4) -e 4 if None, code also defaults to -e 4
#calInkExplore -l N CMYK only, 200..=400
(hardcoded) -f 0 always — “Full-spread patches are not useful on a calibration wedge”
channels field unused in the builder

Basename is sanitised (no / \\ ..). JS: calibration.js:399410.

1.4 Env / cwd / stdin

  • cwd = resolve_safe_cwd(config.cwd) (Stage 1 browse dir / wizard cwd).
  • ARGYLL_NOT_INTERACTIVE=1, Windows CREATE_NO_WINDOW.
  • No stdin protocol. targen is batch.

1.5 stdout / exit

Frontend appends every process:stdout/stderr line into #targenLog / #calLog. Success = code === 0. No regex parsing.

1.6 Artefacts

Consumes: nothing required (optional -c ICC/ICM/MPP).

Produces in cwd:

  • {basename}.ti1 (always) — Stage 1 complete gate (verify_stage_artefacts).
  • Calibration: CAL_{name}.ti1.

1.7 Tests

Rust: commands.rs:14811628 (RGB, CMYK, total_patches fallback, all-advanced, RGB ignores -l). Calibration: calibration.rs:903936 (RGB no -l, CMYK -l 320 + -n, path-separator reject). JS: calibration.test.js basename prefix only (no argv).


2. printtarg — Stage 2 layout (also used by Stage 0 “Create Layout”)

2.1 When

Stage 2 Create Layout (#btnCreateLayout) → run_printtarg (commands.rs:966978). Process id printtarg_{basename}.

Stage 0 Create Layout & Print (#btnCalLayout) does not spawn printtarg itself: it sets wizardState.sessionMode = 'calibration', copies the CAL_ basename into Stage 2 via setStage1Result, and navigates to Stage 2. The user then hits Create Layout. getPrinttargCalibrationFields('CAL_…') returns {calibration_file: null} so -K is never applied to the calibration chart itself (calibration.js:108116, AGENTS.md:7).

2.2 argv — build_printtarg_args (commands.rs:907950)

Always:

-v -u -i {instrument} -p {page_size} [ -r | -R {seed} ] [-d {label}] {-t|-T} {dpi} [-K|-I {cal}] {basename}
UI field Flag Notes
#instrumentSelect -i {code} i1 (default), p3, CM, SS, 20, 22, 41, 51 (PrinttargConfig comment commands.rs:768)
#pageSizeSelect / custom W×H -p {size} A4, A4R, A3, A2, Letter, LetterR, Legal, 4x6, 11x17, or {W}x{H} mm (JS requires ≥50)
#printtargLayoutOrder -R 1 (default), -R {seed}, or -r no_randomize supersedes seed (commands.rs:917922). Default random_seed: Some(1) (commands.rs:762764) for ICCery#163 determinism
#targetLabelPreview (custom_label) -d {string} assembled ICCery - {run} - {printer} - {ink} - {driver paper} - {actual paper} - DD/MM/YYYY HH:MM. Argyll fork argyllcms#19
bit-depth radios -t {dpi} (8-bit) or -T {dpi} (16-bit) dpi from #tiffDpi, default 300
calibration (getPrinttargCalibrationFields) -K {cal} apply, or -I {cal} embed-only only if Apply Calibration on and basename is not CAL_*
Stage 1 basename positional last

-u is always on. That is the ICCery-patched JSON manifest (argyllcms#3).

2.3 JSON event schema (-u manifest)

Parsed in JS from the accumulated stdout after exit, not via process:json_row (printtarg.js:680691):

stdout.match(/\{[\s\S]*?"event"\s*:\s*"manifest"[\s\S]*?\n\}/)

Expected object (from Argyll issue #3 and gallery use):

{
  "event": "manifest",
  "pages": [
    { "filename": "target_01.tif", "patches": 800, "width_mm": 210, "height_mm": 297 }
  ]
}

renderTiffGallery uses page.filename, page.patches, pages[0].width_mm/height_mm. TIFF is previewed via read_tiff_preview_png (decode TIFF → PNG ≤1200px, base64).

2.4 stdin / env / cwd

No stdin. cwd = Stage 1 working dir. ARGYLL_NOT_INTERACTIVE=1, CREATE_NO_WINDOW.

2.5 Exit

code === 0 → parse manifest, show gallery + raw-print panel, setStage2Result. Non-zero → log error, stay on Stage 2. Native print (print_target_native) is not an Argyll call (GDI / CUPS / NSPrintPanel).

2.6 Artefacts

Consumes: {basename}.ti1 in cwd.

Produces:

  • {basename}.ti2 — Stage 2 gate.
  • One or more TIFF pages named in the manifest (typically {basename}.tif or {basename}_NN.tif). 8-bit (-t) vs 16-bit (-T).
  • If -K/-I: calibration is applied to / embedded in the printed patches; .cal is not copied.

2.7 Tests

commands.rs:16311802: i1/A4/8-bit, CM/Letter/16-bit, custom 200x400, custom label, custom seed 42, raster -r (seed ignored), -K, -I embed-only. JS: calibration.test.js asserts CAL_ charts skip -K.


3. chartread — Stage 3 measurement (and Stage 0 “Measure Chart”)

3.1 When

Stage 3 Start Measurement (#btnStartRead) → run_chartread (commands.rs:10451061). Process id chartread_{basename}.

Stage 0 Measure Chart navigates to Stage 3 with the CAL_ basename; the same run_chartread path is used.

enable_i1pro2_leds is not sent by JS. If the config field is None, the command loads AppSettings.enable_i1pro2_leds (commands.rs:10511054). Default false (stock Argyll compatibility; ICCery#204 / Argyll#37). The fork flag is -Y l (not the earlier proposed -L).

3.2 argv — build_chartread_args (commands.rs:10231043)

-v -u [-c {port}] [-Y l] {basename}
UI / state Flag Condition
(always) -v -u -u = ROW_COLORS_JSON stream (argyllcms#1)
#chartreadInstrumentSelect (port) -c {port} non-empty. Port "1" is stored as "" by the detector so default port is used (chartread.js:488489). ICCery#111: do not pass instlist device index as -c.
Settings enable_i1pro2_leds -Y l true. LEDs: white=cal, blue=ready, red=error, green=capture. Unpatched binaries reject -Y l; frontend captures lastStderrLine and opens the log.

No -p (spot), -t (transmissive), -N (skip cal), -H, -F, -r resume, -n.

3.3 JSON event schema (-u / ROW_COLORS_JSON)

Intercepted in ProcessManager, emitted as process:json_row. Frontend: swatch_grid.js:95116. Ignored unless data.event === "row_complete".

{
  "event": "row_complete",
  "row_id": "A",
  "row_index": 0,
  "total_rows": 12,
  "patch_count": 21,
  "patches": [
    {
      "id": "1",
      "loc": "A1",
      "is_pad": false,
      "device": [0.0, 50.0, 100.0],
      "expected": { "XYZ": [18.42, 20.12, 15.68], "Lab": [51.98, -8.45, 12.32] },
      "measured": { "XYZ": [...], "Lab": [...], "spectral": { "bands": 36, "start_nm": 380, "end_nm": 730, "norm": 100, "values": [...] } }
    }
  ]
}

is_pad patches are skipped only when they have no measured and all-zero device (swatch_grid.js:137141) so white-reference pads from targen -e still render.

On row_index + 1 >= total_rows the swatch listener forces STATE.ALL_STRIPS_READ.

Mock: src-tauri/argyll/mocks/chartread.mock (handheld + --xy / MOCK_XY_TABLE=1).

3.4 stdin protocol

All via send_stdin (commands.rs:1623ProcessManager::send_stdin). Bytes are written as-is and flushed. No extra newline is added by Rust — JS includes \n.

Button State(s) Bytes Why
#btnCalibrate CALIBRATING " \n" (space + LF) Argyll “hit any key / space to calibrate”
#btnAccept WARNING, PROMPT_CONTINUE, TABLE_PLACE_SHEET, TABLE_ALIGN "\n" Continue / accept strip / sheet placed / fiducial aligned. TABLE_* does not force READING
#btnRetry AWAITING_STRIP, ALL_STRIPS_READ, WARNING, ERROR " \n" Re-read strip
#btnDoneRead AWAITING_STRIP, ALL_STRIPS_READ "d\n" Write .ti3 and exit (ICCery#175)
#btnUndo strip states "u\n" Undo last strip
#btnSkip AWAITING_STRIP, ERROR "s\n" Skip current strip
#btnCancel any; XY extra "q\n" then 500 ms then kill_process Park XY head (AGENTS.md:170) then SIGKILL-equivalent

send_stdin errors with "Process not found or stdin not available" if the id is not in stdins.

3.5 stdout state machine — classifyChartreadLine (chartread.js:70286)

Pure function. Priority order:

  1. “remove last sheet” → info, isRemoveSheetNotice, state unchanged (Argyll emits this just before writing .ti3).
  2. /sheet\s+(\d+)\s+of\s+(\d+)\s+read\s+ok/isheetOk meta.
  3. /locate\s+patch\s+([A-Za-z0-9_]+)\s+with\s+(?:the\s+)?sight/iTABLE_ALIGN.
  4. /place\s+sheet\s+(\d+)\s+of\s+(\d+)/i or “place sheet” / “remove previous sheet” → TABLE_PLACE_SHEET.
  5. “hit return to continue” (and not “use it anyway”) → sticky TABLE_* if already there, else PROMPT_CONTINUE.
  6. 'd' if/when done, “all strips/patches read”, “done reading” → ALL_STRIPS_READ.
  7. “(warning)”, “use it anyway”, “seem to have read strip pass”, “unexpected response” → WARNING.
  8. place + (reference|white|calibrat|standard) or “hit any key to continue” or “calibration”, excluding place-sheet/locate-patch → CALIBRATING.
  9. “hit … read … strip”, “ready to read”, “read … strip … key” → AWAITING_STRIP.
  10. “reading strip/sheet”, “processing”, “scanning” → READING.
  11. “error”, “too fast/slow”, “misread”, “failed to read” → ERROR.

XY table is auto-detected from these prompts or from instlist data-xy="1" (/spectro\s?scan|i1io/i).

3.6 Exit / snapshot

code === 0snapshot_ti3 copies {basename}.ti3{basename}_pass{N}.ti3 and deletes the canonical .ti3 so Stage 4 stays locked (ICCery#109/#110, commands.rs:11121131). setStage3Result is not called until Finish.

code !== 0 → prompt shows last stderr line; log <details> opened.

Single pass Finish → promote_ti3 restores {basename}.ti3 from *_pass1.ti3 (average is not invoked). Multi-pass → run_average.

Cancel: kill_process after optional q\n.

3.7 Artefacts

Consumes: {basename}.ti2 (and the printed chart).

Produces: {basename}.ti3 (ephemeral) then {basename}_passN.ti3. Canonical .ti3 only after Finish/average.

3.8 Tests

Rust argv: commands.rs:18051874 (auto, empty port, -c 1, -Y l, both, leds disabled). Snapshot roundtrip: 18981934. JS classifier: chartread.test.js (39 cases, XY sticky continuation, strip mode, warnings). Mock script as above.


4. instlist — instrument detection

4.1 When

Stage 3 Detect (#btnDetectInstruments) → detect_instruments (commands.rs:615619).

let binary = resolve_binary(..., "instlist")?;
state.spawn(app, "instlist".to_string(), binary, vec![], None).await

Empty argv. cwd = None. Process id is the literal "instlist" (not namespaced). Duplicate Detect clicks while running → "Process 'instlist' is still running" (#116).

This is the Argyll fork USB enumeration API (argyllcms#6). ICCery does not pass -u; the forks instlist prints JSON on stdout by default (or ICCery treats the whole stdout as JSON).

4.2 stdout parsing (chartread.js:438503)

Accumulate stdout. On exit:

  1. JSON.parse(trimmed) looking for { devices: [ { port, name, type } ] }.
  2. Fallback regex: /^(\d+)[\s:=]+'?([^'\n]+)'?(?:\s+on\s+'?([^'\n]+)'?)?/i plus KNOWN_INST_TOKENS = /i1|ColorMunki|Spyder|spectro|Display|Huey|DTP|SpectroScan|Smile|Klein/i.

Each device → <option value="{port if port!=='1' else ''}">. SpectroScan / i1iO get data-xy="1" and · XY Table.

Default option: "Auto (First available port)" with empty value → no -c.

4.3 stdin / artefacts

None. No files.


5. average — multi-pass ti3 merge

5.1 When

Stage 3 Finish & Average with ≥2 recorded passes (chartread.js:864947) → run_average (commands.rs:10791091). Process id average_{output} e.g. average_job.ti3.

5.2 argv — build_average_args (commands.rs:10701077)

-v {input1} {input2} ... {output}

JS:

{ inputs: recordedPasses.map(p => p.filename),  // "job_pass1.ti3", ...
  output: `${basename}.ti3`,
  cwd }

No other flags. Inputs are relative names (cwd is the project dir).

5.3 Exit

code === 0setStage3Result, Stage 4. Else fallback promote_ti3 of pass 1.

No stdin. ARGYLL_NOT_INTERACTIVE=1.

5.4 Artefacts

Consumes: {basename}_passN.ti3. Produces: {basename}.ti3 (Stage 3 gate).

5.5 Tests

commands.rs:18771896.


6. colprof — Stage 4 profile calculation

6.1 When

Stage 4 Calculate Profile (#btnCreateProfile) → run_colprof (commands.rs:12431255). Process id colprof_{basename}.

-u is NOT passed even though the fork has colprof -u progress JSON (argyllcms#4). Progress is guessed from plaintext stdout.

6.2 argv — build_colprof_args (commands.rs:11761241)

Always -v -a {algorithm} -q {quality}.

UI Flag Condition
#colprofAlgorithm -a l|x|X|m l Lab cLUT (default), x XYZ cLUT, X Display XYZ+matrix, m matrix
#colprofQuality -q l|m|h|u m Medium default. u = Ultra
intent (preset field only) -t {intent} no Stage 4 control. Presets leave it None. Tests use "p" / "a"
#colprofFwa / custom .sp -f {val} "none" → omit; "D50"/"D65"/path.sp-f VAL; empty string → bare -f
#colprofIlluminant -i A|C|D50M2|D65|F5|F8|F10 empty = default D50, omit flag
#colprofObserver -o 1964_10|2015_2|2015_10 empty = 1931 2°, omit
#colprofInputViewCond -c pc|pp|pe|pm "none" omit
#colprofOutputViewCond -d mt|mb|md|jm|jd|tv "none" omit
#colprofDescription -D {text} non-empty; JS falls back to basename
#colprofCopyright -C {text} non-empty
basename positional last

JS config: colprof.js:95107. Custom FWA: fwa: colprofCustomSpPath.value \|\| "none".

6.3 stdout “progress”

colprof.js:119126 (case-insensitive):

  • contains "gamut mapping" → “Gamut mapping calculation in progress...”
  • "fitting" or "clut" → “Fitting cLUT grid points...”
  • "writing" or "icc profile" → “Writing ICC profile header & tags...”

Mock (colprof.mock) emits exactly those phrases.

6.4 Exit / follow-on

code === 0get_profile_path (.icm if it exists and .icc does not; else .icc; Windows default extension .icm if neither exists, Unix .icccommands.rs:621639). Then if Apply Calibration: apply_calibration (see §8) in-place. Then extract_gamut.

intent is in ColprofConfig and presets but the Stage 4 form never sets it.

6.5 Artefacts

Consumes: {basename}.ti3. Produces: {basename}.icc (Unix/macOS) or {basename}.icm (Windows). Stage 4 gate.

6.6 Tests

commands.rs:19372099 (base, FWA D50/none/custom .sp, illuminant+observer, viewing conditions, combined).


7. applycal — embed/unembed calibration curves

7.1 When

Not ProcessManager. apply_calibration (calibration.rs:703762) uses run_captured.

Callers:

  1. Automatically after successful colprof (colprof.js:163171) if cal.applyEnabled && cal.calPath. output_path: null → in-place replace via {input}.applycal.tmp then rename.
  2. Any future caller of the Tauri command.

7.2 argv — build_applycal_args (calibration.rs:237261)

-v {-a|-u} {cal_path} {input_path} [{output_path}]
Config Flag
always -v
unapply: false (JS always) -a (apply)
unapply: true -u (unapply, NOT JSON)
cal_path, input_path required; empty → Err
output_path optional positional

Note: applycal -u conflicts in meaning with every other tools -u (JSON). ICCery never sends unapply: true from JS.

cwd = parent of input profile. ARGYLL_NOT_INTERACTIVE=1, CREATE_NO_WINDOW. Exit ≠ 0 → delete tmp, return last stderr line.

7.3 Artefacts

Consumes: .cal + .icc/.icm. Produces: same path (in-place) or output_path. Temp extension .applycal.tmp.

7.4 Tests

calibration.rs:9871015.


8. printcal — compute .cal from measured calibration chart

8.1 When

Stage 0 Compute Curves (#btnCalCompute) → compute_calibration_curves (calibration.rs:615701). Captured, not streamed. Requires {ti3_basename}.ti3 already on disk.

Collision: if {basename}.cal exists and force_overwrite is false → error string containing "already exists"; JS dialog Overwrite / Rename / Cancel (calibration.js:418437, 485492). Rename uses {basename}_{ISO-stamp}.cal.

8.2 argv — build_printcal_args (calibration.rs:191235)

Always -v -e. Then:

Config / UI Flag Condition
no_ink_limit (JS always false) -I true
verify (JS always false) -z true
previous_cal (current calState.calPath) -a {path} non-empty
#calTacOverride (total_ink_limit) -m x.x Some and > 0
#calInkLimitControls (channel_limits) -x{C} {percent} first char of channel, e.g. -xC 95.0
output_cal -o {file.cal} default {basename}.cal
ti3_basename positional last

-e here is printcals even/estimate switch (always on), not targen white patches.

8.3 stdout parsing — parse_printcal_stdout (calibration.rs:274307)

Line-oriented, not JSON:

  • line matching ideal power / device power / power value → first number → recommended_power (feeds Stage 1 targen -p hint in the dashboard).
  • line containing total and (ink|tac|limit) → TAC.
  • line starting Cyan/Magenta/Yellow/Black/Red/Green/Blue or C:/C etc. → per-channel limit.

Then parse_cal_file on the written .cal (CGATS: COLOR_REP, CREATED, DESCRIPTOR, MAX_TAC/TOTAL_INK_LIMIT/INK_LIMIT, INK_LIMIT_*, BEGIN_DATA_FORMAT / BEGIN_DATA curves). If stdout limits empty, infer from curve max×100.

8.4 Artefacts

Consumes: CAL_*.ti3. Produces: {basename}.cal (or override). Also iccery-calibration.json project state and optional library copy under app_data_dir/calibrations/.

8.5 Tests

calibration.rs:939984, 10181072.


9. profcheck — Stage 5 verification

9.1 When

Stage 5 Verify (#btnVerify) → run_profcheck (commands.rs:12751302). Process id profcheck_{ti3_path} (full path, not basename — colprof.js uses basename; these must not be confused; #56 was exactly that class of bug for colprof).

ICC path auto-flips .icc.icm if the requested file is missing (commands.rs:12831295).

9.2 argv — build_profcheck_args (commands.rs:12641273)

Hard-coded:

-v -k -s -u {ti3_path} {icc_path}
Flag Meaning in this fork
-v verbose
-k CIEDE2000
-s (Argyll: “standard” / summary — kept for the text line parser)
-u JSON report (argyllcms#5)

No UI toggles. Paths may be absolute (JS builds cwd + sep + basename.ti3 / get_profile_path).

9.3 JSON + text parsing — parseProfcheckReport (profcheck.js:27139)

Order:

  1. No of test patches = (\d+).
  2. All {...} objects in stdout; keep those with event==="report" or any of avg_de, avg_de2000, peak_de, peak_de2000, rms, rms_de. Prefer object with *de2000 keys.
  3. Else text: Profile check complete, errors…: max. = X, avg. = Y, RMS = Z.
  4. Else regex families for avg/max/rms. Missing metrics → warnings[] and cards show 0.00 (AGENTS.md:2324).

JSON schema (mock + tests):

{"event": "report", "peak_de2000": 2.41, "avg_de2000": 0.85, "rms": 1.02}

Also accepted: peak_de / avg_de / max_de / rms_de.

Quality bands (ICCery#95, not Argyll): <1 Excellent, <2 Good, <3.5 Acceptable, else Poor. Saved via save_verification_record.

9.4 stdin / artefacts

No stdin. Consumes .ti3 + .icc/.icm. Produces no new Argyll files (verification_history.json is ICCery). Attempts to load {basename}.gam into the 3D viewer (created earlier by iccgamut).

9.5 Tests

Rust argv: commands.rs:21022110. JS: profcheck.test.js (real -u JSON, de2000 preference, text summary, legacy regex, breach alert). Mock: profcheck.mock.


10. iccgamut — 3D gamut mesh

10.1 When

Automatically after colprof success (colprof.js:197214) → extract_gamut (commands.rs:693731). Process id iccgamut_{stem}.

10.2 argv — build_iccgamut_args (commands.rs:684691)

-v -d 10 {resolved_icc_path}

-d 10 is surface density, not a directory (ICCery#112). Path is the real .icc/.icm (auto-flip if missing). cwd = parent of the profile.

No ARGYLL_3D_DISP. The GUI never launches Argylls 3D viewer; it reads the .gam file in-process.

10.3 Exit

code === 0loadGamutMesh("{cwd}/{basename}.gam"). Failures are console.warn only.

10.4 Artefacts

Consumes ICC/ICM. Produces {stem}.gam next to the profile (Argyll default). Bundled reference: src/assets/sRGB.gam and src-tauri/argyll/reference_gamuts/sRGB.gam.

10.5 Tests

commands.rs:14751478.


11. Binaries that are not invoked

Binary Status
dispwin Never spawned. ICCery#90 “Emissive display calibration (dispwin & dispread)” = Won't Fix
dispread Same
spotread, dispcal, collink, cctiff, spec2cie, illumread, synthacc Not referenced
Generic spawn_process Registered (lib.rs:55, commands.rs:614) but no JS caller. Always cwd=None. Exists as an escape hatch

12. ProcessManager internals

File: src-tauri/src/process_manager.rs.

12.1 Maps

stdins:  Arc<Mutex<HashMap<String, Arc<Mutex<ChildStdin>>>>>
killers: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>

The Child itself lives only in the wait task (not in a map) so wait() cannot hold a mutex that send_stdin needs.

12.2 Spawn ids (complete list)

Pattern Binary
targen_{basename} targen (profile + CAL_)
printtarg_{basename} printtarg
chartread_{basename} chartread
average_{output} average (output includes .ti3)
colprof_{basename} colprof
profcheck_{ti3_path} profcheck (full path)
iccgamut_{stem} iccgamut
instlist instlist (literal)
caller-supplied unused spawn_process

12.3 Duplicate rejection (ICCery#116, 07d28eb)

Before spawn, if stdins.contains_key(&id)Err("Process '{id}' is still running"). Prevents a second chartread_{basename} from replacing map entries while the old wait task still remove()s that id on exit (which would orphan the live stdin).

Frontend also sets measurementInProgress to disable Start / Measure Another / Finish.

12.4 Wait / reap

Wait task (process_manager.rs:166194):

let exit_code = tokio::select! {
    res = child.wait() => status.code().unwrap_or(0),
    _ = kill_rx => { child.start_kill(); child.wait().await; code.unwrap_or(1) }
};
stdins.remove(id); killers.remove(id);
emit_exit(id, exit_code);

Natural exit also reaps, so send_stdin after the child dies returns “Process not found”.

v0.1.16 (90012e4, ICCery#63) originally awaited child.wait() inside Mutex<Child> — that is the bug #84 fixed.

12.5 stdin write

stdin.write_all(input.as_bytes()).await?;
stdin.flush().await?;

No encoding transform. JS is responsible for \n.

kill first stdins.remove(id) (drops/closes the pipe, which is what Argyll sees as EOF) then fires the oneshot so the wait task start_kill()s.

12.6 kill_all

process_manager.rs:237267:

  1. stdins.clear() (close every pipe).
  2. Drain killers and send(()) each.
  3. Sleep 50 ms so wait tasks can start_kill().
  4. Return count.

Invoked:

  • Tauri command kill_all_processes (commands.rs:3338) — no JS caller.
  • RunEvent::Exit / ExitRequested (lib.rs:124129).
  • WindowEvent::CloseRequested (lib.rs:136141).

This is ICCery#147/#149: closing the window during chartread used to leave the instrument locked.

12.7 Windows pipe deadlock history

Two layers, two repos:

ICCery#84 (host, v0.2.1, PR #83, d6118a1) — Tokio Child::wait() deadlock:

  • PR #77 / #63 replaced try_wait polling with child.wait().await while holding Mutex<Child>.
  • Effect 1: send_stdin / kill blocked until the child exited (Calibrate/Retry/Skip/Cancel dead).
  • Effect 2: Tokios wait() drops stdin to avoid parent/child pipe deadlock, so the pipe closed immediately after spawn — chartread never received keystrokes.
  • Fix: take() all three stdio handles; store only ChildStdin in the map; select! wait vs oneshot kill.

gronod/argyllcms#24 (child, Windows anonymous pipes)ReadFile deadlock inside Argyll:

  • SetNamedPipeHandleState(PIPE_NOWAIT) fails on anonymous pipes (CreatePipe / Rust Stdio::piped()).
  • con_char(wait=0) then ReadFile()s a blocking pipe → poll_con_char never returns → instrument switch (USB EP 0x84) is never polled → lamp never lights after calibration.
  • Fix in the fork: PeekNamedPipe before ReadFile (spectro/conv.c).
  • ICCery-side companion: ARGYLL_NOT_INTERACTIVE=1 (#134) so Argyll takes the pipe path at all. Stock Graeme Gill binaries will still hang on Windows interactive chartread.

ROADMAP.md:59: “Resolved P0 process manager deadlock and premature stdin pipe closure affecting interactive chartread instrument workflows.” / “Decoupled ChildStdin mutex management from child process wait/reap tasks.”

12.8 ProcessManager tests

process_manager.rs:270325: occupancy helper (does not actually spawn), kill_all drains 2 fake killers, path sanitise. No integration test that execs a real binary.


From lib.rs:54119 plus the command bodies:

Direct process control

Command File:line Role
spawn_process(id, binary, args) commands.rs:614 Generic spawn, cwd=None, unused by UI
send_stdin(id, input) commands.rs:1623 Write bytes to ChildStdin
kill_process(id) commands.rs:2531 Close stdin + start_kill one id
kill_all_processes() commands.rs:3338 Kill all; returns count
resolve_binary(binary_name) commands.rs:48106 Path lookup

Argyll runners (ProcessManager)

Command Binary id
run_targen targen targen_{basename}
run_printtarg printtarg printtarg_{basename}
run_chartread chartread chartread_{basename}
run_average average average_{output}
run_colprof colprof colprof_{basename}
run_profcheck profcheck profcheck_{ti3_path}
extract_gamut iccgamut iccgamut_{stem}
detect_instruments instlist instlist
generate_calibration_target targen targen_{CAL_basename}

Argyll runners (captured, no events)

Command Binary
compute_calibration_curves printcal
apply_calibration applycal

Artefact helpers used around those processes

parse_ti2_header, snapshot_ti3, promote_ti3, verify_stage_artefacts, get_profile_path, read_tiff_preview_png, read_file_base64, select_* dialogs, parse_cal_file_cmd, list_saved_calibrations, save_calibration_to_library, select_cal_file, load/save_project_calibration.


14. File artefacts by OS / stage

Stage Consumes Produces Gate
0 cal chart CAL_*.ti1
0 print CAL_*.ti1 CAL_*.ti2, CAL_*.tif
0 measure CAL_*.ti2 CAL_*_passN.ti3CAL_*.ti3
0 compute CAL_*.ti3 CAL_*.cal, iccery-calibration.json
1 optional -c ICC {base}.ti1 stage1_complete
2 .ti1 .ti2, .tif/.tiff pages stage2_complete
3 .ti2 _passN.ti3 then .ti3 stage3_complete = .ti3 exists
4 .ti3 .icc (macOS/Linux) or .icm (Windows); optional in-place applycal stage4_complete
4b ICC {stem}.gam not a gate
5 .ti3 + ICC none (history JSON is ICCery)

verify_stage_artefacts (commands.rs:650676) checks existence only (no content). .ti2 missing while .ti3 present still reports stage2_complete: false.

parse_ti2_header (commands.rs:366424) reads TARGET_INSTRUMENT, NUMBER_OF_FIELDS (stored as colorant_count — this is CGATS field count, not ink count), NUMBER_OF_SETSpatch_count, NUMBER_OF_PAGES/PAGES.


15. Flag cheat-sheet (-u, -Y, -K, -f, -c, -d)

Flag Binary Meaning in ICCery
-u printtarg JSON page manifest (always)
-u chartread ROW_COLORS_JSON per row (always)
-u profcheck JSON ΔE report (always)
-u applycal unapply curves (never sent from JS)
-u targen / colprof not used (fork supports JSON progress)
-Y l chartread i1Pro2 LED feedback (settings, default off)
-K printtarg apply .cal to printed patches (profiling only)
-I printtarg embed .cal without applying (builder supports; UI always calibration_embed_only: false)
-I printcal no ink limit (UI never sets)
-f N targen full-spread patch count; cal charts force -f 0
-f VAL colprof FWA/OBA (D50/D65/.sp/bare)
-c PATH targen preconditioning profile
-c PORT chartread comm port
-c COND colprof input viewing condition
-d 2|4 targen RGB / CMYK
-d 10 iccgamut surface density
-d STR printtarg custom chart label
-d COND colprof output viewing condition

16. Tests covering arg builders (index)

Builder Tests
build_targen_args commands.rs test_build_targen_args_{rgb,cmyk,total_patches_fallback,all_advanced_flags,rgb_ignores_ink_limit}
build_calibration_targen_args calibration.rs test_build_calibration_targen_{rgb,cmyk_ink_limit,rejects_path}
build_printtarg_args commands.rs i1/A4/8bit, CM/Letter/16bit, custom page, label, seed, raster, -K, -I
build_chartread_args auto, empty port, with port, leds on/off, port+leds
build_average_args two-input, pass-file names
build_colprof_args base, FWA D50/none/sp, illuminant+observer, viewing, combined
build_profcheck_args -v -k -s -u + paths
build_iccgamut_args -v -d 10 path
build_printcal_args defaults, overrides+prev
build_applycal_args apply+out, unapply+empty-err
get_binary_candidates exe on Windows
JS parsers chartread.test.js, profcheck.test.js, calibration.test.js
ProcessManager duplicate-id occupancy, kill_all, path sanitise

npm test runs the JS suites. Rust: cd src-tauri && CARGO_INCREMENTAL=0 cargo test.