Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
40 KiB
04 — Argyll binary invocations
Line numbers refer to ICCery v0.8.5 (
/tmp/ICCeryat 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:1–4). 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:40–106. Public Tauri command and internal helper.
Order of search:
- Settings
argyll_binary_dir(Settings dialog,AppSettings.argyll_binary_dir). If non-empty, join each candidate name; first existing path wins. - Bundled sidecar at
argyll/{platform}/{name}under TauriBaseDirectory::Resource. - If the resource path does not exist, still return the constructed resource path (does not search
$PATH). Missing binaries surface later as spawnprocess:error.
get_binary_candidates (commands.rs:40–46):
- Windows:
["{name}.exe", "{name}"]unlessnamealready ends with.exe. - Unix:
["{name}"].
Platform directory selection (commands.rs:65–83, mirrored in build.rs:26–44):
| 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:203–217. If cwd_input is a real directory, use it. Else: document_dir → home_dir → app_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.iccgamutis invoked without any 3D-display env; the.gamfile is parsed in JS (gamut_viewer.js).PATH— not mutated. Binaries are always absolute paths fromresolve_binary. Inherited PATH is whatever the OS/session provides (needed only if a user-suppliedargyll_binary_dirbinary 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:99–103 run_captured:calibration.rs:849–853
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:128–139):
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:28–60, 105–119). 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:38–40: "resources": ["argyll/**/*"].
build.rs:19–64 panics the compile if the platform marker is missing (npm run fetch-argyll required).
NSIS (windows/hooks.nsh:133–158): 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:952–964. generate_calibration_target: calibration.rs:598–613. 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:788–905)
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:295–314. On success (code === 0) advances to Stage 2 via setStage1Result + wizardState.navigateToStage(2).
1.3 Calibration argv — build_calibration_targen_args (calibration.rs:146–189)
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:399–410.
1.4 Env / cwd / stdin
- cwd =
resolve_safe_cwd(config.cwd)(Stage 1 browse dir / wizard cwd). ARGYLL_NOT_INTERACTIVE=1, WindowsCREATE_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:1481–1628 (RGB, CMYK, total_patches fallback, all-advanced, RGB ignores -l). Calibration: calibration.rs:903–936 (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:966–978). 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:108–116, AGENTS.md:7).
2.2 argv — build_printtarg_args (commands.rs:907–950)
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:917–922). Default random_seed: Some(1) (commands.rs:762–764) 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:680–691):
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}.tifor{basename}_NN.tif). 8-bit (-t) vs 16-bit (-T). - If
-K/-I: calibration is applied to / embedded in the printed patches;.calis not copied.
2.7 Tests
commands.rs:1631–1802: 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:1045–1061). 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:1051–1054). 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:1023–1043)
-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:488–489). 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:95–116. 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:137–141) 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:16–23 → ProcessManager::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:70–286)
Pure function. Priority order:
- “remove last sheet” → info,
isRemoveSheetNotice, state unchanged (Argyll emits this just before writing.ti3). /sheet\s+(\d+)\s+of\s+(\d+)\s+read\s+ok/i→sheetOkmeta./locate\s+patch\s+([A-Za-z0-9_]+)\s+with\s+(?:the\s+)?sight/i→TABLE_ALIGN./place\s+sheet\s+(\d+)\s+of\s+(\d+)/ior “place sheet” / “remove previous sheet” →TABLE_PLACE_SHEET.- “hit return to continue” (and not “use it anyway”) → sticky
TABLE_*if already there, elsePROMPT_CONTINUE. 'd' if/when done, “all strips/patches read”, “done reading” →ALL_STRIPS_READ.- “(warning)”, “use it anyway”, “seem to have read strip pass”, “unexpected response” →
WARNING. - place + (reference|white|calibrat|standard) or “hit any key to continue” or “calibration”, excluding place-sheet/locate-patch →
CALIBRATING. - “hit … read … strip”, “ready to read”, “read … strip … key” →
AWAITING_STRIP. - “reading strip/sheet”, “processing”, “scanning” →
READING. - “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 === 0 → snapshot_ti3 copies {basename}.ti3 → {basename}_pass{N}.ti3 and deletes the canonical .ti3 so Stage 4 stays locked (ICCery#109/#110, commands.rs:1112–1131). 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:1805–1874 (auto, empty port, -c 1, -Y l, both, leds disabled). Snapshot roundtrip: 1898–1934. 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:615–619).
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 fork’s instlist prints JSON on stdout by default (or ICCery treats the whole stdout as JSON).
4.2 stdout parsing (chartread.js:438–503)
Accumulate stdout. On exit:
JSON.parse(trimmed)looking for{ devices: [ { port, name, type } ] }.- Fallback regex:
/^(\d+)[\s:=]+'?([^'\n]+)'?(?:\s+on\s+'?([^'\n]+)'?)?/iplusKNOWN_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:864–947) → run_average (commands.rs:1079–1091). Process id average_{output} e.g. average_job.ti3.
5.2 argv — build_average_args (commands.rs:1070–1077)
-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 === 0 → setStage3Result, 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:1877–1896.
6. colprof — Stage 4 profile calculation
6.1 When
Stage 4 Calculate Profile (#btnCreateProfile) → run_colprof (commands.rs:1243–1255). 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:1176–1241)
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:95–107. Custom FWA: fwa: colprofCustomSpPath.value \|\| "none".
6.3 stdout “progress”
colprof.js:119–126 (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 === 0 → get_profile_path (.icm if it exists and .icc does not; else .icc; Windows default extension .icm if neither exists, Unix .icc — commands.rs:621–639). 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:1937–2099 (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:703–762) uses run_captured.
Callers:
- Automatically after successful colprof (
colprof.js:163–171) ifcal.applyEnabled && cal.calPath.output_path: null→ in-place replace via{input}.applycal.tmpthenrename. - Any future caller of the Tauri command.
7.2 argv — build_applycal_args (calibration.rs:237–261)
-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 tool’s -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:987–1015.
8. printcal — compute .cal from measured calibration chart
8.1 When
Stage 0 Compute Curves (#btnCalCompute) → compute_calibration_curves (calibration.rs:615–701). 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:418–437, 485–492). Rename uses {basename}_{ISO-stamp}.cal.
8.2 argv — build_printcal_args (calibration.rs:191–235)
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 printcal’s even/estimate switch (always on), not targen white patches.
8.3 stdout parsing — parse_printcal_stdout (calibration.rs:274–307)
Line-oriented, not JSON:
- line matching
ideal power/device power/power value→ first number →recommended_power(feeds Stage 1 targen-phint in the dashboard). - line containing
totaland (ink|tac|limit) → TAC. - line starting Cyan/Magenta/Yellow/Black/Red/Green/Blue or
C:/Cetc. → 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:939–984, 1018–1072.
9. profcheck — Stage 5 verification
9.1 When
Stage 5 Verify (#btnVerify) → run_profcheck (commands.rs:1275–1302). 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:1283–1295).
9.2 argv — build_profcheck_args (commands.rs:1264–1273)
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:27–139)
Order:
No of test patches = (\d+).- All
{...}objects in stdout; keep those withevent==="report"or any ofavg_de,avg_de2000,peak_de,peak_de2000,rms,rms_de. Prefer object with*de2000keys. - Else text:
Profile check complete, errors…: max. = X, avg. = Y, RMS = Z. - Else regex families for avg/max/rms. Missing metrics →
warnings[]and cards show 0.00 (AGENTS.md:23–24).
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:2102–2110. 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:197–214) → extract_gamut (commands.rs:693–731). Process id iccgamut_{stem}.
10.2 argv — build_iccgamut_args (commands.rs:684–691)
-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 Argyll’s 3D viewer; it reads the .gam file in-process.
10.3 Exit
code === 0 → loadGamutMesh("{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:1475–1478.
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:6–14) 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:166–194):
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:237–267:
stdins.clear()(close every pipe).- Drain
killersandsend(())each. - Sleep 50 ms so wait tasks can
start_kill(). - Return count.
Invoked:
- Tauri command
kill_all_processes(commands.rs:33–38) — no JS caller. RunEvent::Exit/ExitRequested(lib.rs:124–129).WindowEvent::CloseRequested(lib.rs:136–141).
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_waitpolling withchild.wait().awaitwhile holdingMutex<Child>. - Effect 1:
send_stdin/killblocked until the child exited (Calibrate/Retry/Skip/Cancel dead). - Effect 2: Tokio’s
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 onlyChildStdinin 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/ RustStdio::piped()).con_char(wait=0)thenReadFile()s a blocking pipe →poll_con_charnever returns → instrument switch (USB EP0x84) is never polled → lamp never lights after calibration.- Fix in the fork:
PeekNamedPipebeforeReadFile(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:270–325: occupancy helper (does not actually spawn), kill_all drains 2 fake killers, path sanitise. No integration test that execs a real binary.
13. Every Tauri command related to processes
From lib.rs:54–119 plus the command bodies:
Direct process control
| Command | File:line | Role |
|---|---|---|
spawn_process(id, binary, args) |
commands.rs:6–14 |
Generic spawn, cwd=None, unused by UI |
send_stdin(id, input) |
commands.rs:16–23 |
Write bytes to ChildStdin |
kill_process(id) |
commands.rs:25–31 |
Close stdin + start_kill one id |
kill_all_processes() |
commands.rs:33–38 |
Kill all; returns count |
resolve_binary(binary_name) |
commands.rs:48–106 |
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.ti3 → CAL_*.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:650–676) checks existence only (no content). .ti2 missing while .ti3 present still reports stage2_complete: false.
parse_ti2_header (commands.rs:366–424) reads TARGET_INSTRUMENT, NUMBER_OF_FIELDS (stored as colorant_count — this is CGATS field count, not ink count), NUMBER_OF_SETS → patch_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.