nx_phototwin.nx source
↩ module page · 509 lines · 26393 B
1// nx_phototwin.nx -- THE PHOTOGRAPH-TO-SUBSTRATE TWIN (2026-08-24, /compare/phototwin PT2 PT3 PT7).
2// license_tier: ORIGINAL No hw writes (Rule 26).
3//
4// nx_phototwin report <image> <image.icc> <substrate.icc> [conf] gamut-coverage report of a photograph
5// nx_phototwin proof <image> <image.icc> <substrate.icc> <outdir> [conf] twin.png (chroma-clipped) + heat.png (dE00 map) + proof.txt
6// nx_phototwin wash <ledger.tsv> <sku> [conf] wash-fastness ledger verdict for one SKU
7// nx_phototwin bench <ledger.tsv> [conf] the swatch bench over EVERY sku, refusing partial coverage
8// EXIT: 0 OK | 1 REFUSED (tolerance exceeded, wash failed) | 2 usage | 3 UNOBSERVABLE (undecodable image, unreadable
9// profile or conf, no ledger rows) -- the third state is never a pass and never a failure of the subject.
10//
11// TWO PROFILES, TWO ROLES. Pixels are interpreted through the IMAGE profile (RGB -> TRC -> matrix -> XYZ(D50) -> CIELAB,
12// one ruler, nx_colorsci_lib) and tested against the SUBSTRATE profile's gamut -- EXACTLY for a matrix/TRC profile
13// (linear RGB through its inverse in [0,1]; the cube is the gamut). Out-of-gamut colours are chroma-clipped at constant
14// L and hue (the twin), then rendered back through the image profile so the soft proof displays in the image's own
15// space. dE00 between photograph and twin is the proof: mean and max against knowledge/colorsci.conf (printing.org bar).
16// MEASURED 2026-08-24 before this shape: one profile for both roles made the narrow-substrate control VACUOUS (the
17// image's codes were read as substrate codes and were inside by construction), and a segment-maxima descriptor read
18// 18 percent of an sRGB photograph outside sRGB -- both retired here. The descriptor stays in the lib for MEASURED
19// swatch gamuts (PT7), which have no matrix.
20// The mapping is MEMOISED PER EXACT 24-BIT COLOUR (a photograph has ~1e5-1e6 distinct colours, never 4e6); the
21// unique-colour count is printed so the cost is on the record.
22// SUBSTRATE HONESTY: until a swatch bench measures the pigment-on-Supima profile, a proof whose substrate is the image
23// profile itself is a positive CONTROL and says so in proof.txt.
24// WASH LEDGER (knowledge/phototwin_wash.tsv): rows swatch sku blank line cycle L a b, Lab measured before (cycle 0)
25// and after declared ISO 105-C06 cycles; dE00 per cycle against cycle 0; a SKU with no rows is UNMEASURED (exit 3).
26import "nx_syscalls.nx"
27import "nx_colorsci_lib.nx"
28import "nx_img_to_rgb.nx"
29import "nx_png_write.nx"
30
31const PT_EXIT_OK: i64 = 0
32const PT_EXIT_REFUSED: i64 = 1
33const PT_EXIT_USAGE: i64 = 2
34const PT_EXIT_UNOBSERVABLE: i64 = 3
35const PT_CONF_DEFAULT: *u8 = "knowledge/colorsci.conf"
36const PT_CHANNELS: i64 = 3
37const PT_BYTE: i64 = 256
38const PT_BYTE_MAX: i64 = 255
39const PT_MEMO_SLOTS: i64 = 2097152 // 2^21 slots: above any photograph's distinct-colour count
40const PT_MEMO_MASK: i64 = 2097151
41const PT_HASH_KNUTH: i64 = 2654435761 // Knuth multiplicative hashing constant (TAOCP vol 3, 6.4)
42const PT_HASH_SHIFT: i64 = 11 // 32 - 21: take the top bits of the 32-bit product
43const PT_U32_MASK: i64 = 4294967295
44const PT_MEMO_EMPTY: i64 = 0 - 1
45const PT_MEMO_ARRAYS: i64 = 4 // key, permil, sector-or-codes, de
46const PT_SLOT: i64 = 8
47const PT_PATH_CAP: i64 = 4096
48const PT_LINE_CAP: i64 = 4096
49const PT_PERMIL: i64 = 1000
50const PT_DEG_FULL: i64 = 360
51const PT_TAB: i64 = 9
52const PT_NL: i64 = 10
53const PT_HASH: i64 = 35
54const PT_LF_SWATCH: i64 = 0
55const PT_LF_SKU: i64 = 1
56const PT_LF_CYCLE: i64 = 4
57const PT_LF_L: i64 = 5
58const PT_LF_A: i64 = 6
59const PT_LF_B: i64 = 7
60const PT_LEDGER_MAX_ROWS: i64 = 65536
61const PT_ROW_STRIDE: i64 = 8 // swatch_off swatch_len sku_off sku_len cycle L a b
62const PT_MIN_CYCLES: i64 = 2 // a swatch proves fastness only with cycle 0 AND at least one later cycle
63const PT_HEAT_MAX: i64 = 255
64const PT_MODE_0644: i64 = 420
65const PT_R_PIXELS: i64 = 0
66const PT_R_UNIQUE: i64 = 1
67const PT_R_INSIDE: i64 = 2
68const PT_R_OUTSIDE: i64 = 3
69const PT_R_WORST_PERMIL: i64 = 4
70const PT_R_SECT0: i64 = 8
71const PT_STATS_SLOTS: i64 = 4
72const PT_OUT_SLOTS: i64 = 8
73
74func 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 }
75func pt_wn(v: i64) -> i64 {
76 var m: i64 = v
77 if m < 0 { pt_w("-" as *u8); m = 0 - m }
78 let t: *u8 = sys_mmap(32)
79 var k: i64 = 0
80 if m == 0 { t[0] = 48 as u8; k = 1 }
81 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
82 let o: *u8 = sys_mmap(32)
83 var i: i64 = 0
84 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
85 sys_write(1, o, k)
86 return 0
87}
88func 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 }
89func pt_catn(d: *u8, o: i64, v: i64) -> i64 {
90 var m: i64 = v
91 var p: i64 = o
92 if m < 0 { d[p] = 45 as u8; p = p + 1; m = 0 - m }
93 let t: *u8 = sys_mmap(32)
94 var k: i64 = 0
95 if m == 0 { t[0] = 48 as u8; k = 1 }
96 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
97 var i: i64 = 0
98 while i < k { d[p] = t[k - 1 - i]; p = p + 1; i = i + 1 }
99 d[p] = 0 as u8
100 return p
101}
102func 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 }
103func pt_write_text(path: *u8, s: *u8, n: i64) -> i64 {
104 let fd: i64 = sys_openat_wr(path, PT_MODE_0644)
105 if fd < 0 { return 0 - 1 }
106 let wr: i64 = sys_write(fd, s, n)
107 sys_close(fd)
108 return wr
109}
110// conf: a missing key is named and the run is UNOBSERVABLE
111func pt_conf(path: *u8, key: *u8, missing: *i64) -> i64 {
112 let v: i64 = cs_conf_int(path, key)
113 if v == CS_CONF_MISS { pt_w(" CONF-MISSING key=" as *u8); pt_w(key); pt_w(" in " as *u8); pt_w(path); pt_w("\n" as *u8); missing[0] = missing[0] + 1; return 0 }
114 return v
115}
116
117// ---- memo: exact 24-bit colour -> (permil, sector or mapped codes, dE00 micro) ----
118func pt_memo_alloc() -> *i64 {
119 let m: *i64 = sys_mmap(PT_MEMO_SLOTS * PT_SLOT * PT_MEMO_ARRAYS) as *i64
120 var i: i64 = 0
121 while i < PT_MEMO_SLOTS { m[i] = PT_MEMO_EMPTY; i = i + 1 }
122 return m
123}
124func pt_memo_arr(m: *i64, k: i64) -> *i64 { return (m as i64 + PT_MEMO_SLOTS * PT_SLOT * k) as *i64 }
125func pt_memo_slot(m: *i64, key: i64) -> i64 {
126 var h: i64 = ((key * PT_HASH_KNUTH) & PT_U32_MASK) >> PT_HASH_SHIFT
127 var probes: i64 = 0
128 while probes < PT_MEMO_SLOTS {
129 let k: i64 = m[h]
130 if k == key { return h }
131 if k == PT_MEMO_EMPTY { return h }
132 h = (h + 1) & PT_MEMO_MASK
133 probes = probes + 1
134 }
135 return PT_MEMO_EMPTY
136}
137
138// pt_gamut_report: coverage of the photograph (through img) against the substrate's EXACT gamut (sub).
139// out: pixels unique inside outside worst_permil, then per-hue-sector outside counts at out[PT_R_SECT0 + s].
140func pt_gamut_report(cs: *i64, img: *i64, sub: *i64, sect: *i64, rgb: *u8, w: i64, h: i64, memo: *i64, out: *i64) -> i64 {
141 let lab: *i64 = sys_mmap(PT_SLOT * PT_CHANNELS) as *i64
142 let sec: *i64 = sys_mmap(PT_SLOT * PT_CHANNELS) as *i64
143 let npix: i64 = w * h
144 let perm: *i64 = pt_memo_arr(memo, 1)
145 let hsec: *i64 = pt_memo_arr(memo, 2)
146 var unique: i64 = 0
147 var inside: i64 = 0
148 var outside: i64 = 0
149 var worst: i64 = 0
150 var i: i64 = 0
151 while i < npix {
152 let r: i64 = rgb[i * PT_CHANNELS] as i64
153 let gg: i64 = rgb[i * PT_CHANNELS + 1] as i64
154 let b: i64 = rgb[i * PT_CHANNELS + 2] as i64
155 let key: i64 = (r * PT_BYTE + gg) * PT_BYTE + b
156 let s: i64 = pt_memo_slot(memo, key)
157 if s == PT_MEMO_EMPTY { return 0 - 1 }
158 if memo[s] != key {
159 memo[s] = key
160 cs_prof_rgb_to_lab(cs, img, r, gg, b, lab)
161 perm[s] = cs_prof_gamut_permil(cs, sub, lab[0], lab[1], lab[2])
162 cs_gbd_sector(cs, sect, lab[0], lab[1], lab[2], sec)
163 hsec[s] = sec[1]
164 unique = unique + 1
165 }
166 let pm: i64 = perm[s]
167 if pm <= PT_PERMIL { inside = inside + 1 }
168 else { outside = outside + 1; out[PT_R_SECT0 + hsec[s]] = out[PT_R_SECT0 + hsec[s]] + 1; if pm > worst { worst = pm } }
169 i = i + 1
170 }
171 out[PT_R_PIXELS] = npix
172 out[PT_R_UNIQUE] = unique
173 out[PT_R_INSIDE] = inside
174 out[PT_R_OUTSIDE] = outside
175 out[PT_R_WORST_PERMIL] = worst
176 return unique
177}
178func pt_print_report(out: *i64, hs: i64) -> i64 {
179 pt_w(" pixels=" as *u8); pt_wn(out[PT_R_PIXELS]); pt_w(" unique_colours=" as *u8); pt_wn(out[PT_R_UNIQUE])
180 pt_w(" inside=" as *u8); pt_wn(out[PT_R_INSIDE]); pt_w(" outside=" as *u8); pt_wn(out[PT_R_OUTSIDE])
181 if out[PT_R_INSIDE] + out[PT_R_OUTSIDE] == out[PT_R_PIXELS] { pt_w(" partition=SUMS" as *u8) } else { pt_w(" partition=LEAK" as *u8) }
182 pt_w(" out_permil=" as *u8)
183 if out[PT_R_PIXELS] > 0 { pt_wn((out[PT_R_OUTSIDE] * PT_PERMIL) / out[PT_R_PIXELS]) } else { pt_wn(0) }
184 pt_w(" worst_permil_of_gamut=" as *u8); pt_wn(out[PT_R_WORST_PERMIL]); pt_w("\n" as *u8)
185 var s: i64 = 0
186 while s < hs {
187 if out[PT_R_SECT0 + s] > 0 {
188 pt_w(" sector hue_deg=" as *u8); pt_wn((s * PT_DEG_FULL) / hs); pt_w("-" as *u8); pt_wn(((s + 1) * PT_DEG_FULL) / hs)
189 pt_w(" out_pixels=" as *u8); pt_wn(out[PT_R_SECT0 + s]); pt_w("\n" as *u8)
190 }
191 s = s + 1
192 }
193 return 0
194}
195
196// pt_gamut_map: chroma-clip every out-of-gamut colour against the substrate (memoised), render the twin through the
197// image profile, and record dE00 per pixel. stats: [0] sum dE micro [1] max dE micro [2] pixels above max_tol [3] moved
198func 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 {
199 let lab: *i64 = sys_mmap(PT_SLOT * PT_CHANNELS) as *i64
200 let mapped: *i64 = sys_mmap(PT_SLOT * PT_CHANNELS) as *i64
201 let codes: *i64 = sys_mmap(PT_SLOT * PT_CHANNELS) as *i64
202 let mcode: *i64 = pt_memo_arr(memo, 2)
203 let mde: *i64 = pt_memo_arr(memo, 3)
204 let npix: i64 = w * h
205 var i: i64 = 0
206 while i < PT_MEMO_SLOTS { mde[i] = PT_MEMO_EMPTY; i = i + 1 }
207 var sum: i64 = 0
208 var mx: i64 = 0
209 var above: i64 = 0
210 var moved: i64 = 0
211 i = 0
212 while i < npix {
213 let r: i64 = rgb[i * PT_CHANNELS] as i64
214 let gg: i64 = rgb[i * PT_CHANNELS + 1] as i64
215 let b: i64 = rgb[i * PT_CHANNELS + 2] as i64
216 let key: i64 = (r * PT_BYTE + gg) * PT_BYTE + b
217 let s: i64 = pt_memo_slot(memo, key)
218 if s == PT_MEMO_EMPTY { return 0 - 1 }
219 if memo[s] != key { memo[s] = key; mde[s] = PT_MEMO_EMPTY }
220 if mde[s] == PT_MEMO_EMPTY {
221 cs_prof_rgb_to_lab(cs, img, r, gg, b, lab)
222 let before: i64 = cs_prof_gamut_clip(cs, sub, lab[0], lab[1], lab[2], iters, mapped)
223 var dmicro: i64 = 0
224 var packed: i64 = key
225 if before > PT_PERMIL {
226 dmicro = fq_to_micro(cs_de2000(cs, lab[0], lab[1], lab[2], mapped[0], mapped[1], mapped[2]))
227 cs_prof_lab_to_rgb(cs, img, mapped[0], mapped[1], mapped[2], codes)
228 packed = (codes[0] * PT_BYTE + codes[1]) * PT_BYTE + codes[2]
229 }
230 mcode[s] = packed
231 mde[s] = dmicro
232 }
233 let pk: i64 = mcode[s]
234 twin[i * PT_CHANNELS] = (pk / (PT_BYTE * PT_BYTE)) as u8
235 twin[i * PT_CHANNELS + 1] = ((pk / PT_BYTE) % PT_BYTE) as u8
236 twin[i * PT_CHANNELS + 2] = (pk % PT_BYTE) as u8
237 let d: i64 = mde[s]
238 de[i] = d
239 sum = sum + d
240 if d > mx { mx = d }
241 if d > max_tol_micro { above = above + 1 }
242 if pk != key { moved = moved + 1 }
243 i = i + 1
244 }
245 stats[0] = sum
246 stats[1] = mx
247 stats[2] = above
248 stats[3] = moved
249 return 0
250}
251
252// pt_soft_proof: twin.png + heat.png + proof.txt; returns the exit code (0 OK, 1 REFUSED, 3 UNOBSERVABLE)
253func 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 {
254 let iters: i64 = pt_conf(conf, "clip_iters" as *u8, missing)
255 let mean_tol: i64 = pt_conf(conf, "soft_proof_mean_tol_de00_micro" as *u8, missing)
256 let max_tol: i64 = pt_conf(conf, "soft_proof_max_tol_de00_micro" as *u8, missing)
257 let heat_fs: i64 = pt_conf(conf, "heat_full_scale_de00_micro" as *u8, missing)
258 if missing[0] > 0 { return PT_EXIT_UNOBSERVABLE }
259 let npix: i64 = w * h
260 let twin: *u8 = sys_mmap(npix * PT_CHANNELS)
261 let de: *i64 = sys_mmap(npix * PT_SLOT) as *i64
262 let stats: *i64 = sys_mmap(PT_SLOT * PT_STATS_SLOTS) as *i64
263 if pt_gamut_map(cs, img, sub, rgb, w, h, memo, iters, twin, de, max_tol, stats) < 0 { pt_w(" MEMO-FULL: more distinct colours than the memo holds\n" as *u8); return PT_EXIT_UNOBSERVABLE }
264 let heat: *u8 = sys_mmap(npix * PT_CHANNELS)
265 var i: i64 = 0
266 while i < npix {
267 var v: i64 = 0
268 if heat_fs > 0 { v = (de[i] * PT_HEAT_MAX) / heat_fs }
269 if v > PT_HEAT_MAX { v = PT_HEAT_MAX }
270 heat[i * PT_CHANNELS] = v as u8
271 heat[i * PT_CHANNELS + 1] = v as u8
272 heat[i * PT_CHANNELS + 2] = v as u8
273 i = i + 1
274 }
275 let p1: *u8 = sys_mmap(PT_PATH_CAP)
276 var o: i64 = pt_cat(p1, 0, outdir)
277 o = pt_cat(p1, o, "/twin.png" as *u8)
278 let p2: *u8 = sys_mmap(PT_PATH_CAP)
279 o = pt_cat(p2, 0, outdir)
280 o = pt_cat(p2, o, "/heat.png" as *u8)
281 let p3: *u8 = sys_mmap(PT_PATH_CAP)
282 o = pt_cat(p3, 0, outdir)
283 o = pt_cat(p3, o, "/proof.txt" as *u8)
284 let w1: i64 = nx_png_write_rgb(p1, twin, w, h)
285 let w2: i64 = nx_png_write_rgb(p2, heat, w, h)
286 var mean: i64 = 0
287 if npix > 0 { mean = stats[0] / npix }
288 var verdict: *u8 = "PROOF-OK" as *u8
289 var rc: i64 = PT_EXIT_OK
290 if mean > mean_tol { verdict = "PROOF-REFUSED-MEAN" as *u8; rc = PT_EXIT_REFUSED }
291 if stats[1] > max_tol { verdict = "PROOF-REFUSED-MAX" as *u8; rc = PT_EXIT_REFUSED }
292 let txt: *u8 = sys_mmap(PT_LINE_CAP)
293 var t: i64 = pt_cat(txt, 0, "pixels=" as *u8); t = pt_catn(txt, t, npix)
294 t = pt_cat(txt, t, " moved=" as *u8); t = pt_catn(txt, t, stats[3])
295 t = pt_cat(txt, t, " mean_de00_micro=" as *u8); t = pt_catn(txt, t, mean)
296 t = pt_cat(txt, t, " max_de00_micro=" as *u8); t = pt_catn(txt, t, stats[1])
297 t = pt_cat(txt, t, " above_max_tol=" as *u8); t = pt_catn(txt, t, stats[2])
298 t = pt_cat(txt, t, " mean_tol_micro=" as *u8); t = pt_catn(txt, t, mean_tol)
299 t = pt_cat(txt, t, " max_tol_micro=" as *u8); t = pt_catn(txt, t, max_tol)
300 t = pt_cat(txt, t, " twin_png=" as *u8); t = pt_catn(txt, t, w1)
301 t = pt_cat(txt, t, " heat_png=" as *u8); t = pt_catn(txt, t, w2)
302 if control == 1 { t = pt_cat(txt, t, " substrate=CONTROL-image-profile-is-the-substrate-not-a-measured-one" as *u8) } else { t = pt_cat(txt, t, " substrate=PROFILE-exact-matrix-gamut" as *u8) }
303 t = pt_cat(txt, t, "\nverdict=" as *u8); t = pt_cat(txt, t, verdict); t = pt_cat(txt, t, "\n" as *u8)
304 pt_write_text(p3, txt, t)
305 sys_write(1, txt, t)
306 return rc
307}
308
309// ---- wash ledger ----
310func pt_field(buf: *u8, s: i64, e: i64, idx: i64, box: *i64) -> i64 {
311 var p: i64 = s
312 var f: i64 = 0
313 while f < idx {
314 var go: i64 = 1
315 while go == 1 { if p >= e { go = 0 } else { if (buf[p] as i64) == PT_TAB { p = p + 1; go = 0 } else { p = p + 1 } } }
316 f = f + 1
317 }
318 if p >= e { box[0] = 0; box[1] = 0; return 0 }
319 var q: i64 = p
320 var g2: i64 = 1
321 while g2 == 1 { if q >= e { g2 = 0 } else { if (buf[q] as i64) == PT_TAB { g2 = 0 } else { q = q + 1 } } }
322 box[0] = p
323 box[1] = q - p
324 return 1
325}
326func pt_span_eq(buf: *u8, off: i64, len: i64, s: *u8) -> i64 {
327 var i: i64 = 0
328 while i < len { if s[i] == (0 as u8) { return 0 } if buf[off + i] != s[i] { return 0 } i = i + 1 }
329 if s[len] != (0 as u8) { return 0 }
330 return 1
331}
332func pt_span_same(buf: *u8, a: i64, an: i64, b: i64, bn: i64) -> i64 {
333 if an != bn { return 0 }
334 var i: i64 = 0
335 while i < an { if buf[a + i] != buf[b + i] { return 0 } i = i + 1 }
336 return 1
337}
338func pt_ledger_load(buf: *u8, n: i64, rows: *i64, bad: *i64) -> i64 {
339 let box: *i64 = sys_mmap(PT_SLOT * 2) as *i64
340 var cnt: i64 = 0
341 var p: i64 = 0
342 while p < n {
343 var e: i64 = p
344 while e < n { if (buf[e] as i64) == PT_NL { break } e = e + 1 }
345 if e > p { if (buf[p] as i64) != PT_HASH {
346 if cnt < PT_LEDGER_MAX_ROWS {
347 var ok: i64 = 1
348 if pt_field(buf, p, e, PT_LF_B, box) == 0 { ok = 0 }
349 if ok == 1 {
350 let base: i64 = cnt * PT_ROW_STRIDE
351 pt_field(buf, p, e, PT_LF_SWATCH, box); rows[base] = box[0]; rows[base + 1] = box[1]
352 pt_field(buf, p, e, PT_LF_SKU, box); rows[base + 2] = box[0]; rows[base + 3] = box[1]
353 pt_field(buf, p, e, PT_LF_CYCLE, box); rows[base + 4] = fq_parse_micro(buf, box[0], box[1]) / FQ_MICRO
354 pt_field(buf, p, e, PT_LF_L, box); rows[base + 5] = fq_from_micro(fq_parse_micro(buf, box[0], box[1]))
355 pt_field(buf, p, e, PT_LF_A, box); rows[base + 6] = fq_from_micro(fq_parse_micro(buf, box[0], box[1]))
356 pt_field(buf, p, e, PT_LF_B, box); rows[base + 7] = fq_from_micro(fq_parse_micro(buf, box[0], box[1]))
357 cnt = cnt + 1
358 } else { bad[0] = bad[0] + 1 }
359 } else { bad[0] = bad[0] + 1 }
360 } }
361 p = e + 1
362 }
363 return cnt
364}
365// pt_wash_ledger: verdict for one sku (or every sku when sku is 0). out: swatches complete ok failed worst rows_used.
366func pt_wash_ledger(cs: *i64, buf: *u8, rows: *i64, nrows: i64, sku: *u8, tol_micro: i64, out: *i64) -> i64 {
367 var swatches: i64 = 0
368 var complete: i64 = 0
369 var okn: i64 = 0
370 var failn: i64 = 0
371 var worst: i64 = 0
372 var used: i64 = 0
373 var i: i64 = 0
374 while i < nrows {
375 let bi: i64 = i * PT_ROW_STRIDE
376 var mine: i64 = 1
377 if (sku as i64) != 0 { mine = pt_span_eq(buf, rows[bi + 2], rows[bi + 3], sku) }
378 if mine == 1 { if rows[bi + 4] == 0 {
379 var first: i64 = 1
380 var j: i64 = 0
381 while j < i { let bj: i64 = j * PT_ROW_STRIDE; if rows[bj + 4] == 0 { if pt_span_same(buf, rows[bi], rows[bi + 1], rows[bj], rows[bj + 1]) == 1 { if pt_span_same(buf, rows[bi + 2], rows[bi + 3], rows[bj + 2], rows[bj + 3]) == 1 { first = 0 } } } j = j + 1 }
382 if first == 1 {
383 swatches = swatches + 1
384 used = used + 1
385 var cycles: i64 = 0
386 var smax: i64 = 0
387 var k: i64 = 0
388 while k < nrows {
389 let bk: i64 = k * PT_ROW_STRIDE
390 if rows[bk + 4] > 0 { if pt_span_same(buf, rows[bi], rows[bi + 1], rows[bk], rows[bk + 1]) == 1 { if pt_span_same(buf, rows[bi + 2], rows[bi + 3], rows[bk + 2], rows[bk + 3]) == 1 {
391 let d: i64 = fq_to_micro(cs_de2000(cs, rows[bi + 5], rows[bi + 6], rows[bi + 7], rows[bk + 5], rows[bk + 6], rows[bk + 7]))
392 cycles = cycles + 1
393 used = used + 1
394 if d > smax { smax = d }
395 pt_w(" swatch=" as *u8); sys_write(1, (buf as i64 + rows[bi]) as *u8, rows[bi + 1]); pt_w(" sku=" as *u8); sys_write(1, (buf as i64 + rows[bi + 2]) as *u8, rows[bi + 3])
396 pt_w(" cycle=" as *u8); pt_wn(rows[bk + 4]); pt_w(" de00_micro=" as *u8); pt_wn(d); pt_w("\n" as *u8)
397 } } }
398 k = k + 1
399 }
400 if cycles + 1 >= PT_MIN_CYCLES {
401 complete = complete + 1
402 if smax <= tol_micro { okn = okn + 1 } else { failn = failn + 1 }
403 }
404 if smax > worst { worst = smax }
405 }
406 } }
407 i = i + 1
408 }
409 out[0] = swatches
410 out[1] = complete
411 out[2] = okn
412 out[3] = failn
413 out[4] = worst
414 out[5] = used
415 if swatches == 0 { return PT_EXIT_UNOBSERVABLE }
416 if complete < swatches { return PT_EXIT_UNOBSERVABLE }
417 if failn > 0 { return PT_EXIT_REFUSED }
418 return PT_EXIT_OK
419}
420// pt_bench_gate: the swatch bench over every sku; REFUSES partial coverage as UNOBSERVABLE
421func pt_bench_gate(cs: *i64, buf: *u8, rows: *i64, nrows: i64, tol_micro: i64, out: *i64) -> i64 {
422 return pt_wash_ledger(cs, buf, rows, nrows, 0 as *u8, tol_micro, out)
423}
424func pt_print_wash(out: *i64, rc: i64) -> i64 {
425 pt_w(" swatches=" as *u8); pt_wn(out[0]); pt_w(" complete=" as *u8); pt_wn(out[1]); pt_w(" ok=" as *u8); pt_wn(out[2])
426 pt_w(" failed=" as *u8); pt_wn(out[3]); pt_w(" worst_de00_micro=" as *u8); pt_wn(out[4]); pt_w(" rows_used=" as *u8); pt_wn(out[5]); pt_w("\n" as *u8)
427 if rc == PT_EXIT_OK { pt_w("verdict=WASH-OK\n" as *u8) }
428 if rc == PT_EXIT_REFUSED { pt_w("verdict=WASH-FAIL\n" as *u8) }
429 if rc == PT_EXIT_UNOBSERVABLE { if out[0] == 0 { pt_w("verdict=UNMEASURED (no cycle-0 swatch rows)\n" as *u8) } else { pt_w("verdict=UNMEASURED-PARTIAL-COVERAGE (a swatch lacks a later cycle)\n" as *u8) } }
430 return 0
431}
432func pt_run_wash(cs: *i64, ledger: *u8, sku: *u8, conf: *u8, missing: *i64, bench: i64) -> i64 {
433 let tol: i64 = pt_conf(conf, "wash_tol_de00_micro" as *u8, missing)
434 if missing[0] > 0 { pt_w("verdict=UNOBSERVABLE conf incomplete\n" as *u8); return PT_EXIT_UNOBSERVABLE }
435 let lp: *i64 = sys_mmap(PT_SLOT * 2) as *i64
436 let buf: *u8 = sys_read_file(ledger, lp)
437 if (buf as i64) == 0 { pt_w("verdict=UNMEASURED ledger unreadable: " as *u8); pt_w(ledger); pt_w("\n" as *u8); return PT_EXIT_UNOBSERVABLE }
438 let rows: *i64 = sys_mmap(PT_LEDGER_MAX_ROWS * PT_ROW_STRIDE * PT_SLOT) as *i64
439 let bad: *i64 = sys_mmap(PT_SLOT) as *i64
440 bad[0] = 0
441 let n: i64 = pt_ledger_load(buf, lp[0], rows, bad)
442 pt_w("=== NX-PHOTOTWIN " as *u8)
443 if bench == 1 { pt_w("bench" as *u8) } else { pt_w("wash sku=" as *u8); pt_w(sku) }
444 pt_w(" ledger=" as *u8); pt_w(ledger); pt_w(" rows=" as *u8); pt_wn(n); pt_w(" malformed=" as *u8); pt_wn(bad[0]); pt_w(" tol_micro=" as *u8); pt_wn(tol); pt_w("\n" as *u8)
445 let out: *i64 = sys_mmap(PT_SLOT * PT_OUT_SLOTS) as *i64
446 var rc: i64 = 0
447 if bench == 1 { rc = pt_bench_gate(cs, buf, rows, n, tol, out) } else { rc = pt_wash_ledger(cs, buf, rows, n, sku, tol, out) }
448 pt_print_wash(out, rc)
449 return rc
450}
451
452func pt_usage() -> i64 {
453 pt_w("usage: nx_phototwin report <image> <image.icc> <substrate.icc> [conf] | proof <image> <image.icc> <substrate.icc> <outdir> [conf] | wash <ledger.tsv> <sku> [conf] | bench <ledger.tsv> [conf]\n" as *u8)
454 return PT_EXIT_USAGE
455}
456
457func main(argc: i64, argv: *i64) -> i64 {
458 if argc < 3 { return pt_usage() }
459 let verb: *u8 = argv[1] as *u8
460 let cs: *i64 = cs_ctx()
461 let missing: *i64 = sys_mmap(PT_SLOT) as *i64
462 missing[0] = 0
463 var conf: *u8 = PT_CONF_DEFAULT
464 if pt_streq(verb, "wash" as *u8) == 1 { if argc < 4 { return pt_usage() } if argc >= 5 { conf = argv[4] as *u8 } return pt_run_wash(cs, argv[2] as *u8, argv[3] as *u8, conf, missing, 0) }
465 if pt_streq(verb, "bench" as *u8) == 1 { if argc >= 4 { conf = argv[3] as *u8 } return pt_run_wash(cs, argv[2] as *u8, 0 as *u8, conf, missing, 1) }
466 var is_proof: i64 = 0
467 if pt_streq(verb, "proof" as *u8) == 1 { is_proof = 1 }
468 if is_proof == 0 { if pt_streq(verb, "report" as *u8) == 0 { return pt_usage() } }
469 if argc < 5 { return pt_usage() }
470 if is_proof == 1 { if argc < 6 { return pt_usage() } if argc >= 7 { conf = argv[6] as *u8 } } else { if argc >= 6 { conf = argv[5] as *u8 } }
471 let image: *u8 = argv[2] as *u8
472 let imgprof: *u8 = argv[3] as *u8
473 let subprof: *u8 = argv[4] as *u8
474 pt_w("=== NX-PHOTOTWIN " as *u8); pt_w(verb); pt_w(" image=" as *u8); pt_w(image); pt_w(" image_profile=" as *u8); pt_w(imgprof); pt_w(" substrate_profile=" as *u8); pt_w(subprof); pt_w("\n" as *u8)
475 let hs: i64 = pt_conf(conf, "gbd_hue_sectors" as *u8, missing)
476 let es: i64 = pt_conf(conf, "gbd_elev_sectors" as *u8, missing)
477 if missing[0] > 0 { pt_w("verdict=UNOBSERVABLE conf incomplete\n" as *u8); return PT_EXIT_UNOBSERVABLE }
478 let img: *i64 = cs_icc_load(cs, imgprof)
479 if img[CS_P_OK] != 1 { pt_w("verdict=UNOBSERVABLE image profile unreadable or not matrix/TRC (tags_parsed=" as *u8); pt_wn(img[CS_P_HAVE]); pt_w(")\n" as *u8); return PT_EXIT_UNOBSERVABLE }
480 let sub: *i64 = cs_icc_load(cs, subprof)
481 if sub[CS_P_OK] != 1 { pt_w("verdict=UNOBSERVABLE substrate profile unreadable or not matrix/TRC (tags_parsed=" as *u8); pt_wn(sub[CS_P_HAVE]); pt_w(")\n" as *u8); return PT_EXIT_UNOBSERVABLE }
482 let wh: *i64 = sys_mmap(PT_SLOT * 2) as *i64
483 wh[0] = 0
484 wh[1] = 0
485 let rgb: *u8 = nx_img_to_rgb(image, wh)
486 if (rgb as i64) == 0 { pt_w("verdict=UNOBSERVABLE image undecodable (baseline JPEG, PNG, GIF, BMP, TIFF, TGA, PCX, PNM only)\n" as *u8); return PT_EXIT_UNOBSERVABLE }
487 let w: i64 = wh[0]
488 let h: i64 = wh[1]
489 if w <= 0 { return PT_EXIT_UNOBSERVABLE }
490 if h <= 0 { return PT_EXIT_UNOBSERVABLE }
491 pt_w(" decoded w=" as *u8); pt_wn(w); pt_w(" h=" as *u8); pt_wn(h); pt_w(" image_profile_b64=" as *u8); pt_wn(img[CS_P_B64]); pt_w(" substrate_profile_b64=" as *u8); pt_wn(sub[CS_P_B64]); pt_w("\n" as *u8)
492 let sect: *i64 = cs_gbd_alloc(hs, es)
493 let memo: *i64 = pt_memo_alloc()
494 let out: *i64 = sys_mmap(PT_SLOT * (PT_R_SECT0 + hs)) as *i64
495 var z: i64 = 0
496 while z < PT_R_SECT0 + hs { out[z] = 0; z = z + 1 }
497 if pt_gamut_report(cs, img, sub, sect, rgb, w, h, memo, out) < 0 { pt_w("verdict=UNOBSERVABLE memo full\n" as *u8); return PT_EXIT_UNOBSERVABLE }
498 pt_print_report(out, hs)
499 var control: i64 = 0
500 if pt_streq(imgprof, subprof) == 1 { control = 1 }
501 if is_proof == 0 {
502 pt_w("verdict=REPORT out_permil=" as *u8)
503 if out[PT_R_PIXELS] > 0 { pt_wn((out[PT_R_OUTSIDE] * PT_PERMIL) / out[PT_R_PIXELS]) } else { pt_wn(0) }
504 if control == 1 { pt_w(" substrate=CONTROL" as *u8) }
505 pt_w("\n" as *u8)
506 return PT_EXIT_OK
507 }
508 return pt_soft_proof(cs, img, sub, rgb, w, h, memo, argv[5] as *u8, conf, missing, control)
509}