nx_phototwin.nx
buildroot/runtime/nx_phototwin.nx
about
nx_phototwin.nx -- THE PHOTOGRAPH-TO-SUBSTRATE TWIN (2026-08-24, /compare/phototwin PT2 PT3 PT7).
license_tier: ORIGINAL No hw writes (Rule 26).
nx_phototwin report <image> <image.icc> <substrate.icc> [conf] gamut-coverage report of a photograph
nx_phototwin proof <image> <image.icc> <substrate.icc> <outdir> [conf] twin.png (chroma-clipped) + heat.png (dE00 map) + proof.txt
nx_phototwin wash <ledger.tsv> <sku> [conf] wash-fastness ledger verdict for one SKU
nx_phototwin bench <ledger.tsv> [conf] the swatch bench over EVERY sku, refusing partial coverage
EXIT: 0 OK | 1 REFUSED (tolerance exceeded, wash failed) | 2 usage | 3 UNOBSERVABLE (undecodable image, unreadable
profile or conf, no ledger rows) -- the third state is never a pass and never a failure of the subject.
TWO PROFILES, TWO ROLES. Pixels are interpreted through the IMAGE profile (RGB -> TRC -> matrix -> XYZ(D50) -> CIELAB,
one ruler, nx_colorsci_lib) and tested against the SUBSTRATE profile's gamut -- EXACTLY for a matrix/TRC profile
(linear RGB through its inverse in [0,1]; the cube is the gamut). Out-of-gamut colours are chroma-clipped at constant
L and hue (the twin), then rendered back through the image profile so the soft proof displays in the image's own
space. dE00 between photograph and twin is the proof: mean and max against knowledge/colorsci.conf (printing.org bar).
MEASURED 2026-08-24 before this shape: one profile for both roles made the narrow-substrate control VACUOUS (the
image's codes were read as substrate codes and were inside by construction), and a segment-maxima descriptor read
18 percent of an sRGB photograph outside sRGB -- both retired here. The descriptor stays in the lib for MEASURED
swatch gamuts (PT7), which have no matrix.
The mapping is MEMOISED PER EXACT 24-BIT COLOUR (a photograph has ~1e5-1e6 distinct colours, never 4e6); the
unique-colour count is printed so the cost is on the record.
SUBSTRATE HONESTY: until a swatch bench measures the pigment-on-Supima profile, a proof whose substrate is the image
profile itself is a positive CONTROL and says so in proof.txt.
WASH LEDGER (knowledge/phototwin_wash.tsv): rows swatch sku blank line cycle L a b, Lab measured before (cycle 0)
and after declared ISO 105-C06 cycles; dE00 per cycle against cycle 0; a SKU with no rows is UNMEASURED (exit 3).
dependencies 4 imports · 0 importers
imports: nx_syscalls.nxnx_colorsci_lib.nxnx_img_to_rgb.nxnx_png_write.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 31 | const PT_EXIT_OK: i64 = 0 |
| 32 | const PT_EXIT_REFUSED: i64 = 1 |
| 33 | const PT_EXIT_USAGE: i64 = 2 |
| 34 | const PT_EXIT_UNOBSERVABLE: i64 = 3 |
| 35 | const PT_CONF_DEFAULT: *u8 = "knowledge/colorsci.conf" |
| 36 | const PT_CHANNELS: i64 = 3 |
| 37 | const PT_BYTE: i64 = 256 |
| 38 | const PT_BYTE_MAX: i64 = 255 |
| 39 | const PT_MEMO_SLOTS: i64 = 2097152 // 2^21 slots: above any photograph's distinct-colour count |
| 40 | const PT_MEMO_MASK: i64 = 2097151 |
| 41 | const PT_HASH_KNUTH: i64 = 2654435761 // Knuth multiplicative hashing constant (TAOCP vol 3, 6.4) |
| 42 | const PT_HASH_SHIFT: i64 = 11 // 32 - 21: take the top bits of the 32-bit product |
| 43 | const PT_U32_MASK: i64 = 4294967295 |
| 44 | const PT_MEMO_EMPTY: i64 = 0 - 1 |
| 45 | const PT_MEMO_ARRAYS: i64 = 4 // key, permil, sector-or-codes, de |
| 46 | const PT_SLOT: i64 = 8 |
| 47 | const PT_PATH_CAP: i64 = 4096 |
| 48 | const PT_LINE_CAP: i64 = 4096 |
| 49 | const PT_PERMIL: i64 = 1000 |
| 50 | const PT_DEG_FULL: i64 = 360 |
| 51 | const PT_TAB: i64 = 9 |
| 52 | const PT_NL: i64 = 10 |
| 53 | const PT_HASH: i64 = 35 |
| 54 | const PT_LF_SWATCH: i64 = 0 |
| 55 | const PT_LF_SKU: i64 = 1 |
| 56 | const PT_LF_CYCLE: i64 = 4 |
| 57 | const PT_LF_L: i64 = 5 |
| 58 | const PT_LF_A: i64 = 6 |
| 59 | const PT_LF_B: i64 = 7 |
| 60 | const PT_LEDGER_MAX_ROWS: i64 = 65536 |
| 61 | const PT_ROW_STRIDE: i64 = 8 // swatch_off swatch_len sku_off sku_len cycle L a b |
| 62 | const PT_MIN_CYCLES: i64 = 2 // a swatch proves fastness only with cycle 0 AND at least one later cycle |
| 63 | const PT_HEAT_MAX: i64 = 255 |
| 64 | const PT_MODE_0644: i64 = 420 |
| 65 | const PT_R_PIXELS: i64 = 0 |
| 66 | const PT_R_UNIQUE: i64 = 1 |
| 67 | const PT_R_INSIDE: i64 = 2 |
| 68 | const PT_R_OUTSIDE: i64 = 3 |
| 69 | const PT_R_WORST_PERMIL: i64 = 4 |
| 70 | const PT_R_SECT0: i64 = 8 |
| 71 | const PT_STATS_SLOTS: i64 = 4 |
| 72 | const PT_OUT_SLOTS: i64 = 8 |
functions
| 74 | func pt_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } called by 9: pt_wnpt_confpt_print_reportpt_soft_proofpt_wash_ledgerpt_print_wash+3 calls 1: sys_write |
| 75 | func pt_wn(v: i64) -> i64 called by 5: pt_print_reportpt_wash_ledgerpt_print_washpt_run_washmain calls 3: pt_wsys_mmapsys_write |
| 88 | func pt_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p } called by 1: pt_soft_proof |
| 89 | func pt_catn(d: *u8, o: i64, v: i64) -> i64 |
| 102 | func pt_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } called by 1: main |
| 103 | func pt_write_text(path: *u8, s: *u8, n: i64) -> i64 |
| 111 | func pt_conf(path: *u8, key: *u8, missing: *i64) -> i64 |
| 118 | func pt_memo_alloc() -> *i64 |
| 124 | func pt_memo_arr(m: *i64, k: i64) -> *i64 { return (m as i64 + PT_MEMO_SLOTS * PT_SLOT * k) as *i64 } |
| 125 | func pt_memo_slot(m: *i64, key: i64) -> i64 |
| 140 | func pt_gamut_report(cs: *i64, img: *i64, sub: *i64, sect: *i64, rgb: *u8, w: i64, h: i64, memo: *i64, out: *i64) -> i64 called by 1: main calls 6: sys_mmappt_memo_arrpt_memo_slotcs_prof_rgb_to_labcs_prof_gamut_permilcs_gbd_sector |
| 178 | func pt_print_report(out: *i64, hs: i64) -> i64 |
| 198 | func pt_gamut_map(cs: *i64, img: *i64, sub: *i64, rgb: *u8, w: i64, h: i64, memo: *i64, iters: i64, twin: *u8, de: *i64, max_tol_micro: i64, stats: *i64) -> i64 called by 1: pt_soft_proof calls 8: sys_mmappt_memo_arrpt_memo_slotcs_prof_rgb_to_labcs_prof_gamut_clipfq_to_micro+2 |
| 253 | func pt_soft_proof(cs: *i64, img: *i64, sub: *i64, rgb: *u8, w: i64, h: i64, memo: *i64, outdir: *u8, conf: *u8, missing: *i64, control: i64) -> i64 |
| 310 | func pt_field(buf: *u8, s: i64, e: i64, idx: i64, box: *i64) -> i64 called by 1: pt_ledger_load |
| 326 | func pt_span_eq(buf: *u8, off: i64, len: i64, s: *u8) -> i64 called by 1: pt_wash_ledger |
| 332 | func pt_span_same(buf: *u8, a: i64, an: i64, b: i64, bn: i64) -> i64 called by 1: pt_wash_ledger |
| 338 | func pt_ledger_load(buf: *u8, n: i64, rows: *i64, bad: *i64) -> i64 |
| 366 | func pt_wash_ledger(cs: *i64, buf: *u8, rows: *i64, nrows: i64, sku: *u8, tol_micro: i64, out: *i64) -> i64 called by 2: pt_bench_gatept_run_wash calls 7: pt_span_eqpt_span_samefq_to_microcs_de2000pt_wsys_write+1 |
| 421 | func pt_bench_gate(cs: *i64, buf: *u8, rows: *i64, nrows: i64, tol_micro: i64, out: *i64) -> i64 |
| 424 | func pt_print_wash(out: *i64, rc: i64) -> i64 |
| 432 | func pt_run_wash(cs: *i64, ledger: *u8, sku: *u8, conf: *u8, missing: *i64, bench: i64) -> i64 |
| 452 | func pt_usage() -> i64 |
| 457 | func main(argc: i64, argv: *i64) -> i64 |