// The per-file colour fit is pure arithmetic, so it can be checked here rather // than in a browser: hand fitMatch a grid and the same grid through a known 3x3, // and it has to hand that 3x3 back. Done that way because the function only ever // runs inside the RAW develop, which needs LibRaw and CanvasKit — neither of // which a node check has. The module is TypeScript, so it is transpiled on the // fly out of the installed compiler (the repo's convention for checks: see // raw-develop-check.mjs, which reads the shader source instead). // // node scripts/preview-match-check.mjs import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import ts from 'typescript'; const file = new URL('../src/engine/previewMatch.ts', import.meta.url); const js = ts.transpileModule(readFileSync(file, 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, }).outputText; const { fitMatch, MATCH_GRID } = await import( `data:text/javascript;base64,${Buffer.from(js).toString('base64')}` ); // A grid with colour in it: three independent ramps, none of them clipped and // none at the floor, so every block carries something to fit. Independent on // purpose — three correlated channels leave the 3x3 ill-conditioned, and a fit // that comes back a percent off on such a scene says nothing about the code. function scene() { let s = 1; const rnd = () => ((s = (s * 1103515245 + 12345) & 0x7fffffff) % 4096) / 4096; const g = new Uint8Array(MATCH_GRID * MATCH_GRID * 4); for (let i = 0; i < MATCH_GRID * MATCH_GRID; i++) { for (let c = 0; c < 3; c++) g[i * 4 + c] = 25 + Math.round(rnd() * 190); g[i * 4 + 3] = 255; } return g; } const M = [1.06, -0.05, 0.01, 0.02, 0.98, 0.03, -0.04, 0.06, 1.02]; const dev = scene(); const ref = new Uint8Array(dev.length); for (let i = 0; i < MATCH_GRID * MATCH_GRID; i++) { const e = [dev[i * 4], dev[i * 4 + 1], dev[i * 4 + 2]]; for (let r = 0; r < 3; r++) { const v = M[r * 3] * e[0] + M[r * 3 + 1] * e[1] + M[r * 3 + 2] * e[2]; ref[i * 4 + r] = Math.max(0, Math.min(255, Math.round(v))); } ref[i * 4 + 3] = 255; } const fit = fitMatch(dev, ref); assert.ok(fit, 'a fitted grid has to fit'); for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { assert.ok(Math.abs(fit.m[r * 3 + c] - M[r * 3 + c]) < 0.01, `recovered ${fit.m[r * 3 + c]} for ${M[r * 3 + c]}`); } } // This scene was made by a 3x3 and nothing else, so the curve fitted on what the // 3x3 leaves has to come back flat — the identity, up to the rounding the scene // carries. assert.equal(fit.tone.length, 12); for (let i = 0; i < 12; i++) { const flat = i % 4 === 1 ? 1 : 0; assert.ok(Math.abs(fit.tone[i] - flat) < 0.15, `tone ${i} came back ${fit.tone[i]}, not ${flat}`); } // Nothing to fit against: the camera clipped the whole frame, so no block says // anything about its rendering, and the caller develops as the sensor left it. assert.equal(fitMatch(dev, new Uint8Array(dev.length).fill(255)), null); // A near-neutral frame through a matrix whose rows carry a negative element — what // the bodies the fit used to refuse looked like (X-T3 -0.04, X100V -1.67 on rows // that also carry +1.05 and +1.63). Three channels that share nearly all their // content leave the normal equations near-singular, and the plain solve came back // with a diagonal element at or below zero, which the caller read as a degeneracy // and dropped the whole fit for. It is a colour: the ridge lifts the system back // to definite and the fit has to come back a matrix. function neutral() { let s = 7; const rnd = () => ((s = (s * 1103515245 + 12345) & 0x7fffffff) % 4096) / 4096; const g = new Uint8Array(MATCH_GRID * MATCH_GRID * 4); for (let i = 0; i < MATCH_GRID * MATCH_GRID; i++) { const x = 40 + Math.round(rnd() * 180); for (let c = 0; c < 3; c++) g[i * 4 + c] = Math.max(0, Math.min(255, x + Math.round((rnd() - 0.5) * 8))); g[i * 4 + 3] = 255; } return g; } const N = [1.05, -1.67, 1.63, 0.02, 0.95, 0.03, -0.04, 0.06, 1.02]; const nDev = neutral(); const nRef = new Uint8Array(nDev.length); for (let i = 0; i < MATCH_GRID * MATCH_GRID; i++) { const e = [nDev[i * 4], nDev[i * 4 + 1], nDev[i * 4 + 2]]; for (let r = 0; r < 3; r++) { const v = N[r * 3] * e[0] + N[r * 3 + 1] * e[1] + N[r * 3 + 2] * e[2]; nRef[i * 4 + r] = Math.max(0, Math.min(255, Math.round(v))); } nRef[i * 4 + 3] = 255; } const nFit = fitMatch(nDev, nRef); assert.ok(nFit, 'a near-neutral frame has to fit, not come back null'); for (const v of nFit.m) assert.ok(Number.isFinite(v) && Math.abs(v) <= 4, `degenerate fit ${v}`); console.log('preview-match-check ok');