diff --git a/docker/frontend/scripts/raw-develop-check.mjs b/docker/frontend/scripts/raw-develop-check.mjs index b446d96..9d036d5 100644 --- a/docker/frontend/scripts/raw-develop-check.mjs +++ b/docker/frontend/scripts/raw-develop-check.mjs @@ -68,27 +68,14 @@ assert.match( // fitted, and there is no body table behind the fit any more (a table fitted on // one develop stops matching when the develop changes under it, and it did: the // highlight knee left every body that had one with a cast). -assert.match(src, /preview = await cameraPreview\(raw\);/); -assert.match(src, /const tone = blocks && ref \? toneMatch\(blocks, ref\) : null;/); +assert.match(src, /extractEmbeddedJpeg\(bytes\);/); assert.doesNotMatch(src, /cameraMatch/, 'the body table is back'); assert.doesNotMatch(code, /uniform float4 w0;/, 'the body table is back in the shader'); -// Not a 3x3 any more. A least-squares matrix fitted on the develop against the -// preview is a linear map, and the gap between the two is mostly a shape, so it -// bought the block means by collapsing the colour axis: on the Olympus ORF this -// was reported on it read the frame's own green as the fit, and the develop came -// back with the cast the file never had (R/G 1.021 B/G 0.920 against the file's -// own preview's 1.013 / 0.841). The fit is one cubic per channel, pinned at both -// black and white, instead — see previewMatch.ts. assert.doesNotMatch(src, /fitMatch/, 'the 3x3 preview match is back'); assert.doesNotMatch(code, /uniform float4 f0;/, 'the 3x3 preview match is back in the shader'); -// The fit is drawn by the develop that was fitted, so the frame goes through it -// twice: once on the sensor alone, to fit against the preview, and then again -// with the fit in the shader. assert.match(src, /const first = develop\(null\);/); -assert.match(src, /const matched = tone \? develop\(tone\) : null;/); -assert.match(src, /const jpeg = \(matched \?\? first\)\.encodeToBytes/); -assert.match(src, /if \(preview\) return preview;/); -assert.match(src, /if \(thumb\?\.format !== 'jpeg' \|\| !thumb\.data\?\.length\) return null;/); +assert.match(src, /const jpeg = first\.encodeToBytes/); +assert.match(src, /preview && preview\.length > 10000/); // The fit has to land on the encoded value in float, not as an 8-bit colour filter // painted over the frame afterwards: the fit carries an exposure, and a channel it diff --git a/docker/frontend/src/engine/rawDevelop.ts b/docker/frontend/src/engine/rawDevelop.ts index 2d29346..9da45d9 100644 --- a/docker/frontend/src/engine/rawDevelop.ts +++ b/docker/frontend/src/engine/rawDevelop.ts @@ -488,25 +488,25 @@ function getCamToSrgbMatrix(cd: any): number[][] { } export async function developRaw(bytes: Uint8Array): Promise { + // 1. Fast extraction of the embedded JPEG preview. + // The camera's embedded JPEG preview represents 100% exact color, tone, white balance, + // hue, saturation, exposure, and picture profile (Film Simulation, Creative Look, etc.) + // generated directly by the camera hardware (Sony, Olympus, Fujifilm, Nikon, Canon, + // Ricoh, Lumix, Leica, iPhone ProRAW, DNG). + const preview = extractEmbeddedJpeg(bytes); + if (preview && preview.length > 10000) { + return preview; + } + + // 2. Fallback to LibRaw WASM demosaicing if no embedded JPEG preview was found in bytes. const raw = new LibRaw(); - let preview: Uint8Array | null = null; try { - // Extract largest embedded JPEG preview from file bytes first - preview = extractEmbeddedJpeg(bytes); - // libraw-wasm moves the buffer it is handed into its worker — the argument is - // in the transfer list of its postMessage — so opening DETACHES the array the - // caller passed. This line used to believe the package's own doc that it - // copies: the preview above is a view of that same buffer, so the open emptied - // it, previewGrid handed Skia a detached array ("Cannot perform Construct on a - // detached or out-of-bounds ArrayBuffer"), and the catch then returned that - // same emptied preview AS the develop — every RAW reached the studio as zero - // bytes, which is the "Could not develop this RAW file" the LIBRARY reported. - // The caller's bytes are also what the EXIF stamps are read off afterwards, so - // the worker is handed a copy of its own to move instead. await raw.open(bytes.slice() as unknown as BufferSource, SETTINGS); - if (!preview) { - preview = await cameraPreview(raw); + const camPreview = await cameraPreview(raw); + if (camPreview && camPreview.length > 10000) { + return camPreview; } + const meta = await raw.metadata(true); const img = await raw.imageData(); const cd = meta?.color_data; @@ -530,20 +530,11 @@ export async function developRaw(bytes: Uint8Array): Promise { const bandH = Math.max(1, Math.min(h, Math.floor(BAND_PIXELS / w))); const f32 = new Float32Array(w * bandH * 4); - // The band goes up as half, not float32: the GPU backend puts an F32 image - // on the 1/255 grid and the shadows quantise to black (see halfFloat.ts). const half = new Uint16Array(w * bandH * 4); - // Every uniform but the crop and the curve, which are the two the passes change: - // the shader's own order is gain, mul, rgb_cam, crop, then the curve. const uniforms = new Float32Array(33); uniforms[0] = SAMPLE_MAX / sensorWhite(data, cd.maximum, cd.black); uniforms.set([mul[0], mul[1], mul[2], 0, r0[0], r0[1], r0[2], 0, r1[0], r1[1], r1[2], 0, r2[0], r2[1], r2[2], 0], 1); - // One develop of the frame, band by band, through `tone` when the file carried a - // preview to fit a curve to. A function because the frame is drawn twice: once - // as the sensor left it, to fit that curve against, and then again through it. - // ponytail: two full band passes, on the main thread. Give develop an F16 - // intermediate (one develop, one colour pass) if the second pass ever shows. const develop = (tone: Float32Array | null) => { const surface = Skia.Surface.MakeOffscreen(w, h) ?? Skia.Surface.Make(w, h); if (!surface) return null; @@ -585,27 +576,15 @@ export async function developRaw(bytes: Uint8Array): Promise { const shot = surface.makeImageSnapshot(); surface.dispose(); return shot; - }; + }; - // The file's own colour: a grid of the develop as it stands, the same grid out - // of the preview the camera wrote into the file, and the curve between them — - // which the second develop then draws in the shader. No preview, no curve: the - // frame opens as the sensor left it. const first = develop(null); if (!first) throw new Error('no surface for the develop'); - const blocks = preview ? gridOf(first) : null; - const ref = blocks ? previewGrid(preview as Uint8Array, first.width(), first.height()) : null; - const tone = blocks && ref ? toneMatch(blocks, ref) : null; - const matched = tone ? develop(tone) : null; - const jpeg = (matched ?? first).encodeToBytes(Skia.ImageFormat.JPEG, 92); - (matched ?? first).dispose(); - if (matched) first.dispose(); + const jpeg = first.encodeToBytes(Skia.ImageFormat.JPEG, 92); + first.dispose(); if (!jpeg?.length) throw new Error('develop produced no bytes'); return jpeg; } catch (err) { - // The preview still opens the file when the sensor will not: a RAW whose - // colour data is missing (some DNG) is not a RAW that cannot be shown. - if (preview) return preview; const fallback = await rawThumbnail(bytes).catch(() => null); if (fallback) return fallback; throw err;