fix(raw): open Olympus ORF at the colour of its own preview via rgb_cam

The develop preferred cam_xyz, whose rows are the XYZ of each camera channel and
which nothing normalised: on the ORF this was reported on it left the frame
green and blue (R/G 0.921, B/G 0.886) where the file's own preview sits at
1.013 / 0.841, and saturated reds came back as the dark purple the frame was
reported for — a red pixel's green ran negative through a row that carries
-2.64, so it clipped to 0 while the knee pulled red down with it.

LibRaw hands back dcraw's own rgb_cam, already the camera -> sRGB transform with
rows summing to one, and it is now applied as handed back: R/G 1.020 B/G 0.920,
and the fit against the embedded preview follows to mean 167.5,164.8,138.5
against the preview's 167.0,164.8,138.6 and the camera's own JPEG's 165,162,133.
Dividing rgb_cam by pre_mul — which carries that row normalisation — is what the
frame before this one did instead, and it undoes it.

The cam_xyz chain stays as the fallback for a file with no rgb_cam, with its
rows normalised so a neutral frame opens neutral. invert3x3, unused since the
frame stopped going through the chain, is dropped.
This commit is contained in:
2026-10-05 22:51:10 +07:00
parent cef7868db8
commit 2e36acd18b
2 changed files with 79 additions and 40 deletions
@@ -119,4 +119,48 @@ assert.match(src, /^ *uniforms\[21\] = f\[0\]; uniforms\[22\] = f\[1\]; uniforms
// 6020x4024, and every one of the eight bodies checked doubled its frame.
assert.match(src, /^ *halfSize: false,$/m, 'the develop is half-size again');
// The matrix the develop applies is the camera->sRGB one LibRaw already hands
// back, not one derived from `cam_xyz` beside it. They disagree on the Olympus
// ORF this was reported on: rgb_cam x cam_mul lands on R/G 1.021 B/G 0.920
// against the file's own preview's 1.013 / 0.841, the cam_xyz chain on
// 0.921 / 0.886 — green, and blue for a scene the camera left neutral, which is
// the cast the frame opened with. And the matrix must not be divided by `pre_mul`
// again: that undoes the row-sum-to-one normalisation dcraw built into rgb_cam.
{
const body = src.match(/const XYZ_TO_SRGB = \[[\s\S]*?\n\}\n/)?.[0];
assert.ok(body, 'getCamToSrgbMatrix not found');
// The file is TypeScript; the two annotations in this block are all that stop
// `new Function` from reading it.
const js = body.replace(/: any/g, '').replace(/: number\[\]\[\]/g, '');
const getCamToSrgbMatrix = new Function(`${js}; return getCamToSrgbMatrix;`)();
const rgb_cam = [
[1.631760597229004, -0.39896339178085327, -0.23279713094234467],
[-0.298429936170578, 1.7056032419204712, -0.4071732759475708],
[0.02502557262778282, -0.45518237352371216, 1.4301568269729614],
];
const cam_xyz = [
[0.9422, -0.3258, -0.0711],
[-0.2655, 1.0898, 0.2015],
[-0.0512, 0.1354, 0.5512],
];
const pre_mul = [2.0312116146087646, 0.9462084174156189, 1.4558180570602417];
assert.deepEqual(
getCamToSrgbMatrix({ rgb_cam, cam_xyz, pre_mul }),
rgb_cam.map((row) => row.slice(0, 3)),
'rgb_cam is no longer applied as LibRaw handed it back'
);
// A neutral camera triple stays neutral through what is left: every row of the
// matrix has to sum to one, or the frame opens with a cast of its own.
for (const [name, cd] of [
['rgb_cam', { rgb_cam }],
['cam_xyz', { cam_xyz }],
['nothing', {}],
]) {
const rows = getCamToSrgbMatrix(cd);
for (const row of rows) {
assert.ok(Math.abs(row[0] + row[1] + row[2] - 1) < 1e-6, `${name}: row sums to what white is not`);
}
}
}
console.log('raw-develop-check ok');