Files
iccery-v2-mac/docs/05-argyll-fork.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

33 KiB
Raw Permalink Blame History

05 — Gronod ArgyllCMS fork protocols

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

Source tree analysed: /tmp/argyllcms (fork of upstream ArgyllCMS V3.5.0, 21 Aug 2026). Related: /tmp/ICCery, /tmp/iccery-research/argyll-issues.json, /tmp/iccery-research/issues/111.md.

This document is the fork-vs-upstream delta that ICCery actually depends on. Upstream Graeme Gill ArgyllCMS 3.5.0 has none of the -u JSON streams, instlist, printtarg -d, chartread -Y l, or the Windows PeekNamedPipe stdin fix.


1. Fork identity, licence, subprocess isolation

Field Value
Upstream ArgyllCMS V3.5.0 (Graeme W. Gill)
Fork gronod/argyllcms — Gordon Bolton <gordon@i3omb.com>
Repo https://git.i3omb.com/gronod/argyllcms
Version string ARGYLL_VERSION_STR "3.5.0" (h/aconfig.h)
Release tags v3.5.0-ICCery.1.x (e.g. 1.0, 1.2, 1.5)
Licence GNU Affero GPL v3 (License.txt, ReadMe.txt)

ReadMe.txt states the fork's purpose:

this fork adds structured JSON output capabilities (via a common -u switch) to several tools so they can be driven as isolated subprocesses by external UIs while preserving AGPLv3 licence isolation (no library linking; communication over stdin / stdout / stderr only).

ArgyllCMS is AGPLv3. Linking libinst / libicc / libcgats into the MIT/proprietary host would taint ICCery. The integration contract is:

  1. Never #include or link Argyll libraries.
  2. Spawn each tool as a child process with piped stdin/stdout/stderr.
  3. Treat the binaries as an external OS utility.
  4. Host parses line-oriented stdout; never shares address space.

ICCery implements this in src-tauri/src/process_manager.rs: Command::new(binary) + Stdio::piped() on all three FDs, plus ARGYLL_NOT_INTERACTIVE=1.

The fork ships a dedicated spec at doc/chartread_integration_guide.md covering AGPL isolation, ROW_COLORS_JSON framing, and host-side parsers.


2. Each tool's -u JSON protocol

Prefix rule (critical): only chartread prefixes. Every other tool emits bare JSON on stdout.

Tool Flag Prefix Framing When Progress vs final
chartread -u ROW_COLORS_JSON: (space after colon) one compact line + fflush after each completed row (strip / XY sheet rows / whole-chart rows / per-patch in -p) progress (one event per row); no final summary JSON
instlist (always) none pretty-printed multi-line object once at exit final only
printtarg -u none pretty-printed multi-line object once after all pages written final only (event: manifest)
targen -u none one compact line per tick during OFPS/grid generation progress (generating / seeding / optimising)
colprof -u none one compact line per tick during A2B/B2A/gamut progress (no final report JSON)
profcheck -u none one compact line after all patches scored final only (event: report)

ICCery's process manager only special-cases the chartread prefix:

                    const JSON_ROW_PREFIX: &str = "ROW_COLORS_JSON: ";
                    let mut reader = BufReader::new(stdout).lines();
                    while let Ok(Some(line)) = reader.next_line().await {
                        if line.starts_with(JSON_ROW_PREFIX) {
                            let json_str = line[JSON_ROW_PREFIX.len()..].to_string();
                            crate::events::emit_json_row(&app_clone, &id_clone, json_str);
                        } else {
                            log::info!(target: "subprocess", "[{id_clone}] {line}");
                            emit_stdout(&app_clone, &id_clone, line);
                        }
                    }

Everything else (instlist, printtarg, profcheck, colprof, targen) arrives as ordinary process:stdout lines. instlist is reassembled client-side and JSON.parse'd as a whole document.

2.1 chartread -u

Flag parser:

			/* Enable UI JSON output */
			} else if (argv[fa][1] == 'u') {
				json_ui_out = 1;

Emitter (the only place ROW_COLORS_JSON: is written):

static void emit_row_json_colors(const char *row_id, int row_index, int total_rows, int patch_count, int nchan, chcol **scbs) {
	int i, j;
	if (!json_ui_out) return;
	fprintf(stdout, "ROW_COLORS_JSON: {\"event\": \"row_complete\", \"row_id\": \"%s\", \"row_index\": %d, \"total_rows\": %d, \"patch_count\": %d, \"patches\": [", row_id ? row_id : "", row_index, total_rows, patch_count);
	/* ... per-patch objects ... */
	fprintf(stdout, "]}\n");
	fflush(stdout);
}

Call sites:

rmode Mode When emitted
3 whole-chart saved readings one event per row after all patches transferred
2 XY table one event per row of the just-read sheet
1 strip (handheld i1Pro etc.) one event per successfully accepted strip, after bi-di reversal + DTP51 offset, patches left-to-right
0 spot / -p one event per patch, patch_count=1

Without -u, stdout is 100% unmodified (the early if (!json_ui_out) return).

2.2 targen -u

int json_progress = 0;

void emit_json_progress(const char *stage, int percent) {
	if (!json_progress) return;
	printf("{\"event\": \"progress\", \"stage\": \"%s\", \"percent\": %d}\n", stage, percent);
	fflush(stdout);
}

Stages observed in source:

  • "generating" — cube / full-spread fill (targen.c, ifarp.c)
  • "seeding" — OFPS seed (ofps.c)
  • "optimising" — OFPS / display-delay iteration (ofps.c, targen.c); last tick is percent: 100

ICCery does not currently pass -u to targen (build_targen_args in commands.rs). Progress is inferred from verbose text.

2.3 printtarg -u

Final-only, pretty-printed, no prefix:

	if (json_manifest) {
		int pi;
		printf("{\n");
		printf("  \"event\": \"manifest\",\n");
		printf("  \"pages\": [\n");
		for (pi = 0; pi < npages; pi++) {
			printf("    {\"filename\": \"%s\", \"patches\": %d, \"width_mm\": %g, \"height_mm\": %g}%s\n",
				page_filenames[pi] != NULL ? page_filenames[pi] : "",
				page_patches[pi],
				pw,
				ph,
				(pi < npages - 1) ? "," : "");
			if (page_filenames[pi] != NULL)
				free(page_filenames[pi]);
		}
		printf("  ]\n");
		printf("}\n");
		fflush(stdout);

Schema:

{
  "event": "manifest",
  "pages": [
    {"filename": "target.tif", "patches": 800, "width_mm": 210, "height_mm": 297},
    {"filename": "target_02.tif", "patches": 250, "width_mm": 210, "height_mm": 297}
  ]
}
  • filename is the basename actually written (%s.tif if one page, %s_%02d.tif if more).
  • width_mm / height_mm are paper size in millimetres (pw/ph).
  • patches is a running count of TID/real patches assigned to that page (includes some padding/TID cells — not strictly "user patches only").
  • ICCery does pass -u (build_printtarg_args).

2.4 colprof -u

Same compact progress line as targen:

int json_progress = 0;

void emit_json_progress(const char *stage, int percent) {
	if (!json_progress) return;
	printf("{\"event\": \"progress\", \"stage\": \"%s\", \"percent\": %d}\n", stage, percent);
	fflush(stdout);
}

Stages: "gamut_mapping" (10/25/40/75/100), "a2b_table", "b2a_table", "gamut_table". Percent ticks also fire from LUT fill callbacks in profout.c / profin.c.

Flag collision with upstream -u. Parser (colprof.c:503-521):

  • -ua / -uc / -u <number> → original input-profile white-point flags
  • -u with no extra token → json_progress = 1

For ICCery's output/printer profiles this is safe (bare -u means JSON). For scanner/camera input profiles it is not drop-in. ICCery currently does not pass -u to colprof; colprof.mock emits only human text.

2.5 profcheck -u

		if (json_report) {
			if (cie2k) {
				printf("{\"event\": \"report\", \"peak_de2000\": %.2f, \"avg_de2000\": %.2f, \"rms\": %.2f}\n",
				       merr, aerr/nsamps, sqrt(rerr/nsamps));
			} else if (cie94) {
				printf("{\"event\": \"report\", \"peak_de94\": %.2f, \"avg_de94\": %.2f, \"rms\": %.2f}\n",
				       merr, aerr/nsamps, sqrt(rerr/nsamps));
			} else {
				printf("{\"event\": \"report\", \"peak_de\": %.2f, \"avg_de\": %.2f, \"rms\": %.2f}\n",
				       merr, aerr/nsamps, sqrt(rerr/nsamps));
			}
			fflush(stdout);
		}

Field names change with -k (CIEDE2000, ICCery default) vs -c (CIE94) vs default CIE76. ICCery always sends -v -k -s -u. The human line Profile check complete, errors(CIEDE2000): ... is still printed unless -v is off and -u is on.

profcheck.mock emits exactly:

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

3. chartread ROW_COLORS_JSON schema (field-by-field)

Exact prefix: ROW_COLORS_JSON: (17 chars including the trailing space). Payload is compact JSON, no newlines inside. Host must strip the prefix then JSON.parse.

Top-level

Field Type Source Notes
event string literal always "row_complete"
row_id string paix->aix(paix, row_index) strip letter, e.g. "A"; "" if NULL
row_index int 0-based overall row
total_rows int totpa total passes in the chart
patch_count int stipa (or 1 in -p) length of patches[]
patches array left-to-right after bi-di reversal

Patch object

Field Type Source Notes
id string scb->id "0" ⇒ spacer
loc string scb->loc e.g. "A1"
is_pad bool strcmp(scb->id, "0") == 0 JSON true/false (unquoted)
device [float] scb->dev[j] * 100.0 %.4f, 0100%, length = nchan (RGB=3, CMYK=4)
expected object, optional omitted if eXYZ is all zeros present only when .ti2 has a reference
expected.XYZ [3] scb->eXYZ scale 0100
expected.Lab [3] icmXYZ2Lab(&icmD50, …, eXYZ/100) D50
measured object always present even for pads
measured.XYZ [3] scb->XYZ scale 0100
measured.Lab [3] icmXYZ2Lab(&icmD50, …, XYZ/100) D50; L* ~0100
measured.spectral object, optional omitted if sp.spec_n == 0 omitted by -n or colorimeter
measured.spectral.bands int sp.spec_n
measured.spectral.start_nm float %.1f sp.spec_wl_short typically 380
measured.spectral.end_nm float %.1f sp.spec_wl_long typically 730
measured.spectral.norm float %.1f sp.norm typically 100.0
measured.spectral.values [float] sp.spec[j] %.4f length = bands

Lab conversion (XYZ must be divided by 100 before icmXYZ2Lab):

static void compute_patch_metrics(chcol *scb, double *eLab, double *mLab) {
	if (scb->eXYZ[0] != 0.0 || scb->eXYZ[1] != 0.0 || scb->eXYZ[2] != 0.0) {
		double scaled_exyz[3];
		scaled_exyz[0] = scb->eXYZ[0] / 100.0;
		/* ... */
		icmXYZ2Lab(&icmD50, eLab, scaled_exyz);
	} else {
		eLab[0] = eLab[1] = eLab[2] = 0.0;
	}
	{
		double scaled_mxyz[3];
		scaled_mxyz[0] = scb->XYZ[0] / 100.0;
		/* ... */
		icmXYZ2Lab(&icmD50, mLab, scaled_mxyz);
	}
}

Worked example (from chartread.mock, which matches the C serializer):

ROW_COLORS_JSON: {"event": "row_complete", "row_id": "A", "row_index": 0, "total_rows": 2, "patch_count": 3, "patches": [{"id": "1", "loc": "A1", "is_pad": false, "device": [0.0, 50.0, 100.0], "expected": {"XYZ": [18.4210, 20.1234, 15.6789], "Lab": [51.98, -8.45, 12.32]}, "measured": {"XYZ": [18.5120, 20.0451, 15.7100], "Lab": [51.89, -8.31, 12.15]}}, {"id": "2", "loc": "A2", "is_pad": false, "device": [10.0, 60.0, 90.0], "expected": {"Lab": [60.0, 10.0, -20.0]}, "measured": {"Lab": [60.1, 10.5, -19.5]}}, {"id": "3", "loc": "A3", "is_pad": true, "device": [100.0, 100.0, 100.0]}]}

Pad patches still have device + measured; they usually lack expected. UIs must skip is_pad==true for ΔE stats (ICCery swatch_grid.js does).


4. instlist JSON device enumeration (ICCery bug #111)

New binary spectro/instlist.c. Always JSON; no -u. Usage: instlist [-v] [-D [level]].

	if ((icmps = new_icompaths(g_log)) == NULL) {
		printf("{\n  \"event\": \"instruments\",\n  \"devices\": []\n}\n");
		fflush(stdout);
		return 1;
	}
	paths = icmps->paths;
	printf("{\n  \"event\": \"instruments\",\n  \"devices\": [\n");
	if (paths != NULL) {
		for (i = 0; paths[i] != NULL; i++) {
			char *tname = inst_name(paths[i]->dtype);
			char *name = paths[i]->name ? paths[i]->name : "Unknown";
			/* ... */
			printf("      \"port\": %d,\n", i + 1);
			printf("      \"name\": \""); /* escaped " and \ */
			printf("\",\n");
			printf("      \"type\": \"%s\"\n", (tname != NULL && tname[0] != '\000') ? tname : "Unknown");
			printf("    }");
			count++;
		}
	}
	printf("\n  ]\n}\n");
	fflush(stdout);

Schema (from doc/instlist.html + source):

{
  "event": "instruments",
  "devices": [
    {
      "port": 1,
      "name": "usb:/bus0/dev1 (X-Rite i1Pro)",
      "type": "i1Pro"
    }
  ]
}
Field Type Meaning
port int, 1-based i+1 over icompaths->paths[]the same integer as chartread -c N
name string icompath->name (USB path + product string). " and \ escaped.
type string inst_name(dtype) e.g. "i1Pro", "ColorMunki", "SpectroScan"

Empty enumeration (no USB perms / no devices / new_icompaths fail) still emits {"event":"instruments","devices":[]} (exit 1 only if new_icompaths itself fails).

Bug #111 — ports vs device index

ICCery originally treated instlist's ordinal as chartread -c. That is correct in this fork if you use devices[i].port (which is the comm-port index). It is wrong if you:

  • use 0-based array index,
  • parse chartread -?? separately (different listing / extra serial junk),
  • or pass -c 1 when the UI meant "auto".

ICCery's current fix (chartread.js):

  • JSON path: opt.value = inst.port && inst.port !== "1" ? inst.port : ""
  • empty value → build_chartread_args omits -c (Argyll default port 1)
  • regex fallback requires a known instrument token or on '…' clause

doc/instlist.html explicitly: The port number directly corresponds to the -c parameter accepted by chartread, spotread, and dispread.


5. chartread -Y l LED protocol / colours

Not -L. Issue #37 proposed -L; the implementation is -Y l (also accepts -Y L).

			else if (argv[fa][1] == 'Y') {
				fa = nfa;
				if (na == NULL)
					usage();
				if (na[0] == 'l' || na[0] == 'L') {
					g_use_leds = 1;
				} else {
					usage();
				}
			}

ICCery: enable_i1pro2_leds["-Y", "l"]. Usage text: -Y l Enable i1Pro 2 visual LED feedback. Not documented in doc/chartread.html yet (only usage()).

Dispatch is capability-silent: missing set_led_state or non-i1Pro2 returns inst_unsupported and is ignored.

static int g_use_leds = 0;
static void update_led_state(inst *it, inst_led_state state) {
	if (!g_use_leds || it == NULL)
		return;
	if (it->set_led_state != NULL) {
		it->set_led_state(it, state);
	}
}

Enum (spectro/inst.h):

typedef enum {
    inst_led_off           = 0,
    inst_led_cal_wait      = 1, /* Flashing White */
    inst_led_row_ready     = 2, /* Flashing Blue  */
    inst_led_row_fail      = 3, /* Flashing Red   */
    inst_led_row_success   = 4  /* Solid/Flash Green */
} inst_led_state;

Hardware: X-Rite i1Pro 2 (Rev E) only (p->dtype != instI1Pro2inst_unsupported). Default inst.c stub returns inst_unsupported. Implementation is a background thread in i1pro_imp.c driving i1pro2_indLEDset bitmasks:

State Visual Timing Mask
inst_led_cal_wait White flash 500 ms on / 500 ms off 0x3F (L+R R+G+B)
inst_led_row_ready Blue pulse 300 ms on / 700 ms off 0x24 (L+R Blue)
inst_led_row_fail Red strobe 3× (100 ms on / 100 ms off), then auto-off 0x09 (L+R Red)
inst_led_row_success Green confirm 400 ms solid, then auto-off 0x12 (L+R Green)
inst_led_off Off i1pro2_indLEDoff

Lifecycle hooks in chartread.c:

  • before inst_handle_calibratecal_wait; after → off
  • before read_strip / read_sample wait → row_ready
  • misread / coms fail / unexpected error → row_fail
  • accepted strip → row_success
  • abort / session end → off

6. printtarg -d custom label

Issue #19. -d was unused (TIFF 8-bit dither is -D).

			else if (argv[fa][1] == 'd') {
				fa = nfa;
				if (na == NULL) usage("Expected argument to -d");
				custom_label = na;
				custom_label_set = 1;
			}
	if (custom_label_set) {
		if (custom_label[0] != '\000') {
			strncpy(label, custom_label, sizeof(label) - 1);
			label[sizeof(label) - 1] = '\000';
			ocg->add_kword(ocg, 0, "CHART_LABEL", custom_label, NULL);
		} else {
			label[0] = '\000';
		}
	} else {
		sprintf(label, "ArgyllCMS - Chart \"%s\" (%s %d) %s",
		               psname, rand ? "Random Start" : "Chart ID", rstart, atm);
	}

And inside layout:

	if (label == NULL || label[0] == '\000')
		dopglabel = 0;		/* Omit per-page labelling */

Behaviour:

Invocation Border text .ti2 keyword
no -d ArgyllCMS - Chart "<psname>" (Chart ID|Random Start N) <datetime> none (CHART_ID / RANDOM_START still written)
-d "My Media 1440dpi" that string (truncated to 1023 chars) CHART_LABEL
-d "" omitted (dopglabel=0) not written

ICCery always passes -d when custom_label is Some(...) (test: "ICCery - Pro900 - Luster - 29/08/2026 12:00").


7. Windows anonymous-pipe stdin deadlock (argyllcms #24)

Symptom: ICCery/Tauri on Windows, chartread as child with redirected stdin. Calibration works; holding the i1Pro button never lights the lamp. Process stuck in stdin poll.

Root cause (upstream ARGYLL_NOT_INTERACTIVE):

  1. numlib/numsup.c check_if_not_interactive() calls SetNamedPipeHandleState(PIPE_NOWAIT) on stdin.
  2. That fails on Win32 anonymous pipes (CreatePipe / Rust Stdio::piped()). Pipe stays blocking.
  3. spectro/conv.c con_char(wait=0) then ReadFile()'s the pipe, blocks forever.
  4. def_uicallbackpoll_con_charcon_char(0) runs from i1pro_imp_measure()'s UI callback, so USB EP 0x84 switch polling never resumes.

Fork fix in spectro/conv.c: PeekNamedPipe before ReadFile:

		} else if (stdin_type == FILE_TYPE_PIPE) {
			int i, bib;
			DWORD bytes_avail = 0;

			if (!PeekNamedPipe(stdinh, NULL, 0, NULL, &bytes_avail, NULL) || bytes_avail == 0) {
				if (!wait) {
					return 0;
				}
			}

			for (bib = 0; bib < 10;) {
				if (!wait) {
					if (!PeekNamedPipe(stdinh, NULL, 0, NULL, &bytes_avail, NULL) || bytes_avail == 0) {
						break;
					}
				}
				if ((!ReadFile(stdinh, buf + bib, 10 - bib, &bread, NULL) || bread == 0)
				 && !wait) {
					break;
				}
				/* ... wait for \n / \r / ^C ... */
			}
			rv = buf[0];

SetNamedPipeHandleState is still attempted (harmless failure). PeekNamedPipe is valid on both anonymous and named pipes.

Shipped in v3.5.0-ICCery.1.2. ICCery always sets ARGYLL_NOT_INTERACTIVE=1 and CREATE_NO_WINDOW on Windows, so this fix is load-bearing.

Doc (doc/Environment.html): On MSWin systems, the character and return or line feed characters must be written to stdin in a single operation. ICCery honours this (" \n", "d\n", "q\n").


8. USB drivers packaging

Windows release zip must contain usb/ (issue #21: CRLF in binfiles / doc/afiles / usb/binfiles.msw made cp miss every manifest entry). Fork sanitises with tr -d '\r' in makepackagebin.sh and copies missing ArgyllCMS_{x64,arm64}.cat from ArgyllCMS.cat.

usb/binfiles.msw:

ArgyllCMS_install_USB.exe
ArgyllCMS_uninstall_USB.exe
ArgyllCMS.cat
ArgyllCMS_x64.cat
ArgyllCMS_arm64.cat
ArgyllCMS.inf
bin/libusb-win32-bin-README.txt
bin/x86/libusb0.sys
bin/amd64/libusb0.sys
bin/arm64/libusb0.sys

ICCery scripts/fetch-argyll.mjs copies usb/ from the Windows archive into src-tauri/argyll/usb/ and fails the fetch if ArgyllCMS_install_USB.exe or ArgyllCMS.inf is missing.

NSIS (src-tauri/windows/hooks.nsh): admin install prompts "Install ArgyllCMS USB instrument drivers?" then ExecWait ArgyllCMS_install_USB.exe. Uninstall does not auto-run ArgyllCMS_uninstall_USB.exe (would break other Argyll apps).

Linux: udev rules live in the Argyll tarball usb/ (binfiles.lx); ICCery does not currently stage them (USB access is via user-installed udev / plugdev).


9. macOS ad-hoc signing of Mach-O sidecars

Issue #32, release v3.5.0-ICCery.1.5 (1eb72e8, cf93305).

makepackagebin.sh:

# Apply ad-hoc code signatures to macOS Mach-O binaries before staging
if [ "${OSTYPE#*darwin*}" != "$OSTYPE" ] ; then
	echo "=== Applying ad-hoc code signatures to macOS Mach-O binaries ==="
	for f in bin/* ; do
		if [ -f "$f" ] && file "$f" | grep -q "Mach-O" ; then
			echo "Signing $f..."
			codesign -f -s - "$f" || true
		fi
	done
fi

Also signs lipo universal (x86_64 + arm64) binaries (issue #27: Argyll_V*_macOS_universal_bin.tgz). CI verifies with codesign -dvv.

ICCery prefers argyll/macos-universal/instlist when present (commands.rs / build.rs); otherwise macos-aarch64 / macos-x86_64. Unsigned Mach-O sidecars are killed by Gatekeeper / killed: 9 on Apple Silicon.


10. Which binaries ICCery actually ships vs uses

fetch-argyll.mjs copies the entire bin/ of the Gronod release (full Argyll suite: dispcal, dispread, spotread, collink, cctiff, oeminst, … plus fork instlist). Marker binary: instlist / instlist.exe. Plus License.txt. Plus Windows usb/.

Invoked by ICCery (resolve_binary call sites):

Binary Stage / feature
instlist instrument dropdown
targen Stage 1 + calibration charts
printtarg Stage 2
chartread Stage 3
average multi-pass .ti3 merge
colprof Stage 4
profcheck Stage 4 QA
iccgamut gamut viewer
printcal Stage 0 linearization (#224)
applycal embed/apply .cal into ICC

The rest of the suite is on disk as AGPL corresponding source/binary distribution but unused.

Mocks used by tests (src-tauri/argyll/mocks/): chartread.mock, colprof.mock, profcheck.mock only. chartread.mock speaks ROW_COLORS_JSON; profcheck.mock speaks bare {"event":"report",...}; colprof.mock does not speak JSON.


11. Environment variables the fork / tools honour

Fork-critical (ICCery always sets)

Var Set by ICCery Effect
ARGYLL_NOT_INTERACTIVE "1" in process_manager.rs and calibration.rs LF instead of CR on progress; stdin is "char + return" not raw key; Windows pipe NOWAIT + (fork) PeekNamedPipe; unbuffered/line-buffered stdout

Upstream vars still live in this tree (host may set)

Var Consumer
ARGYLL_3D_DISP_FORMAT VRML / X3D / X3DOM
ARGYLL_COLMTER_CAL_SPEC_SET / ARGYLL_COLMTER_COR_MATRIX default CCSS/CCMX (-X)
ARGYLL_MIN_DISPLAY_UPDATE_DELAY_MS display tools
ARGYLL_DISPLAY_SETTLE_TIME_MULT display tools
ARGYLL_DISPLAY_FAKE_RAND_SEED dispsup.c
ARGYLL_CREATE_WRONG_VON_KRIES_OUTPUT_CLASS_REL_WP ICC writer
ARGYLL_CREATE_DISPLAY_PROFILE_WITH_CHAD / _WITHOUT_CHAD ICC writer
ARGYLL_CREATE_OUTPUT_PROFILE_WITH_CHAD ICC writer
ARGYLL_CREATE_V2COLORANT_TABLE ICC writer
ARGYLL_PLATFORM_OVERRIDE icc.c
ARGYLL_CCAST_DEFAULT_RECEIVER / ARGYLL_CCAST_TEST_PATTERN Chromecast
ARGYLL_IGNORE_XRANDR1_2 / ARGYLL_IGNORE_XINERAMA Linux display
ARGYLL_USE_COLORD Linux profile store
ARGYLL_DISABLE_I1PRO2_DRIVER force i1Pro2 legacy
ARGYLL_EXCLUDE_SERIAL_SCAN skip COM/tty fast-scan
ARGYLL_XRGA XRGA / XRDI / GMDI reflective conversion
ARGYLL_XCALSTD per-driver X-Rite cal standard (i1pro, munki, dtp*, ss)
ARGYLL_UNTWIST_GAMUT_SURFACE B2A / collink clip
ARGYLL_REV_CACHE_MULT / ARGYLL_REV_ACC_GRID_RES_MULT rspl invert
ARGYLL_SUPPRESS_PLOT plot.c
XDG_{DATA,CONFIG,CACHE}_{HOME,DIRS} Linux paths
SPYD2024_LOWLEV_MEASURE Spyder 2024
I1D3_DISABLE_AIO i1d3 (upstream)
DISPLAY / SUDO_UID / SUDO_GID X11 / privilege drop

ICCery does not currently set any of these besides ARGYLL_NOT_INTERACTIVE.


12. chartread interactive stdin commands and UI classifier

12.1 Real fork chartread (strip mode, rmode==1)

UIH table (chartread.c:1547-1561):

inst_set_uih(0x00, 0xff, DUIH_TRIG);   /* every other key starts a read */
inst_set_uih('f'/'F'/'b'/'B'/'n'/'N'/'g'/'G'/'d'/'D', DUIH_CMND);
inst_set_uih('q'/'Q'/^C/Esc, DUIH_ABORT);
Key Class Action
any other key, Space, Return, instrument switch TRIG start strip read
f / F CMND next row / +10 (spot: F=+10)
b / B CMND previous row / 10
n / N CMND next unread row
g / G CMND goto (spot mode; registered in strip too)
d / D CMND done & save .ti3. If unread patches remain → confirm [y/n]
q / Q / Esc / ^C ABORT quit without saving (retry prompt first)
y / Y confirm "Are you sure [y/n]" (done-with-unread, abort)
n (at [y/n]) confirm stay in session
s / S optional calibration only inst_handle_calibrate: "or S to skip" when inst_calc_optional_flag
k CMND (spot only) force calibrate

There is no strip-mode s = skip row, and no u = undo. Those exist only in ICCery's mock / UI. Sending s or u during "Ready to read strip" is DUIH_TRIG → starts a measurement.

12.2 Spot mode (-p) extra keys

f/F/b/B/n/N/g/G/d/D/k as above; Return/Space/0 = take reading.

12.3 XY table (rmode==2)

Return = continue; q/Esc/^C = give up (parks head if q sent before kill — ICCery does this).

12.4 Exact prompt strings the C code prints

(With ARGYLL_NOT_INTERACTIVE=1, prompt lines still have no trailing LF unless noted; they are flushed via do_fflush(). ICCery's line reader may not see a prompt until a later \n.)

Calibration (instappsup.c) — real i1Pro:

Place the instrument on its reflective white reference S/N <id>,
 and then hit any key to continue,
 or hit Esc or Q to abort: 

(optional cal appends or S to skip)

Other cal variants: "Do a reflective white calibration,", "Place the instrument on light trap...", "Click the instrument on its reflective white reference...", "Hit any key to retry, or Esc or Q to abort:".

Strip ready:

Ready to read strip pass <row>[ (!! ALL ROWS READ !!)| (This row has been read)]
Press 'f' to move forward, 'b' to move back, 'n' for next unread,
 'd' when done, Esc or 'q' to quit without saving.
Trigger instrument switch to start reading.          # uswitch==1
Trigger instrument switch or any other key to start: # uswitch==2
Press any other key to start:                        # uswitch==0

Success / fail:

 Strip read OK
(Warning) Seem to have read strip pass <X> rather than <Y>!
Hit Return to use it anyway, any other key to retry, Esc or 'q' to give up:
There is at least one patch with an very unexpected response! (DeltaE <n>)
Hit Return to use it anyway, any other key to retry, Esc or  'q' to give up:
Strip read failed due to misread (<reason>)
Hit Esc to give up, any other key to retry:
Strip read failed due to communication problem.
Hit Esc or 'q' to give up, any other key to retry:
Done ? - At least one unread patch (<id>, <loc>), Are you sure [y/n]:

XY table:

Please place sheet <n> of <N> on table, then
hit return to continue, Esc or 'q' to give up
Please remove previous sheet, then place sheet <n> of <N> on table, then
hit return to continue, Esc or 'q' to give up
Using the XY table controls, locate patch <id> with the sight,
then hit return to continue, Esc or 'q' to give up
Sheet <n> of <N> read OK
Please remove last sheet from table

Done / abort:

Chart read OK
Abort ? - Are you sure ? [y/n]:

12.5 What ICCery actually sends (chartread.js)

UI button stdin bytes Intended
Calibrate / Retry " \n" space + NL (single WriteFile)
Accept / Continue "\n" Return (use-it-anyway, XY continue)
Done & Save "d\n" done
Undo "u\n" not a chartread command — will TRIG a read
Skip "s\n" only skips optional cal; otherwise TRIG a read
Cancel (XY) "q\n" then kill after 500 ms park head
Cancel (strip) kill process

12.6 classifyChartreadLine (tests in chartread.test.js)

Priority order:

  1. "remove last sheet from table" → info, no state change (isRemoveSheetNotice)
  2. /sheet\s+(\d+)\s+of\s+(\d+)\s+read\s+ok/isheetOk
  3. "locate patch … with … sight"TABLE_ALIGN (meta.patch)
  4. "place sheet N of M" / "remove previous sheet"TABLE_PLACE_SHEET
  5. "hit return to continue" (sticky if already TABLE_*) → PROMPT_CONTINUE otherwise
  6. "'d' if/when done", "all strips/patches read", "d to finish/save"ALL_STRIPS_READ
  7. "(warning)", "use it anyway", "seem to have read strip pass", "unexpected response"WARNING
  8. place + (reference|white|calibrat|standard) / "hit any key to continue" / "calibration"CALIBRATING
  9. hit+read+strip / "ready to read"AWAITING_STRIP
  10. "reading strip" / "processing" / "scanning" / "reading sheet"READING
  11. "error" / "too fast" / "too slow" / "misread" / "failed to read"ERROR

12.7 Mock vs real (tests rely on mock phrasing)

chartread.mock (and classifier tests) use synthetic lines that real chartread never prints:

Mock / test line Real chartread
Place instrument on calibration tile and hit [Space] to calibrate. Place the instrument on its reflective white reference S/N … + hit any key to continue
Hit [Space] to read strip A (or 's' to skip). Ready to read strip pass A + f/b/n/d menu
Reading strip A... no such line; success is Strip read OK
Calibration successful. no such line
Ready to read... done. Chart read OK

Classifier still matches real prompts because of substring rules ("ready to read", "hit any key to continue", "use it anyway", "place sheet", "locate patch"). Rewrite UIs should classify the C strings above, not only the mock.


Appendix A — Fork vs upstream 3.5.0 checklist

Feature Upstream 3.5.0 Gronod fork
chartread -u / ROW_COLORS_JSON no yes
instlist no yes
printtarg -u JSON manifest no yes
printtarg -d custom label no yes
targen -u progress JSON no yes
colprof -u progress JSON (bare -u) -u is WP-scale for input profiles same, plus bare -u → JSON
profcheck -u ΔE JSON no yes
chartread -Y l i1Pro2 LEDs no yes
Windows PeekNamedPipe stdin no (deadlocks on anon pipes) yes
makepackagebin.sh CRLF sanitize + cat copies no yes
macOS codesign -s - + universal lipo archive no yes
doc/chartread_integration_guide.md no yes
doc/instlist.html no yes

Appendix B — ICCery -u usage vs fork capability

Tool Fork -u ICCery currently passes -u
chartread yes yes (-v -u [-c port] [-Y l] basename)
printtarg yes yes (-v -u …)
profcheck yes yes (-v -k -s -u)
targen yes no
colprof yes no
instlist always JSON spawned with no args

Appendix C — Issues mapped

Repo # Topic
argyllcms 1 chartread -u ROW_COLORS_JSON
argyllcms 2 targen -u
argyllcms 3 printtarg -u
argyllcms 4 colprof -u
argyllcms 5 profcheck -u
argyllcms 6 instlist
argyllcms 19 printtarg -d
argyllcms 21 Windows zip missing usb/doc (CRLF)
argyllcms 24 PeekNamedPipe deadlock
argyllcms 27 macOS universal lipo
argyllcms 32 ad-hoc codesign
argyllcms 37 i1Pro2 LEDs (spec said -L, code is -Y l)
ICCery 111 instlist port vs -c index