Files
RecipesCam/docker/frontend/src/engine/rawStore.ts
T
3dtours 97bdf605e2 web: import the camera's RAW, and grade it like the phone
The studio took JPEG, PNG and HEIC and nothing else, so a photographer's own
negatives never reached it. A RAW now loads the way any other file does —
`isRawName` reads the extension off a 24-entry list, the file goes into OPFS
under one slot (`current_image.raw`, beside `current_image.name`, so a reload
finds it again) and `rawDevelop` runs it through LibRaw-wasm: half size, 16-bit
output, camera white balance and the camera's own 3x3 matrix, in bands of 2M
pixels so a 30MB file never holds a second copy of itself. `example-sony.ARW`
(30.3MB) lands as a 3120x2084 picture, no page error.

DEHAZE joins the FX tab, where Lightroom keeps it: a chip off the same
PARAM_DEFS entry (`dehaze`, 0..10) so nothing new renders chips, and the pass is
the dark channel prior — `atmosphericLight` reads A off a 32x32 draw of the
photo, `DEHAZE_SKSL` takes omega up to 0.95 over a floor of 0.1 — measured at
71.8% of the stage's pixels moved between 0 and 10.

The gradient mask grows the six knobs the phone's has: HIGHLIGHT, SHADOW,
WHITE, BLACK, CLARITY and DEHAZE. The mask's falloff is a smoothstep rather than
a line, and CLARITY/DEHAZE inside a mask get a blurred copy of the photo plus
the air A as a second child of the mask shader — so a mask's clarity is clarity
and not a flat brightness lift. The column shows all nine rulers; CLARITY 9
moves 42.2% of the stage, DEHAZE 9 moves 27.9%.

CLARITY stops reading the whole photo per pixel: the single pass that sampled a
15x15 box 225 times is now the three passes the same math wants — 1x15, then
15x1, then a blend, `orig + (orig - B) * 3.2` — about 30 reads. Both signs work
(77.4% of the stage moves at +10, 79.6% at -10), and the negative branch keeps
its mist as it was.

The pointer reviews a look before it is taken: resting on a PHOTO STYLE chip or
a recipe chip lays that look on the photo while it stays there and gives it back
the moment it leaves — byte-identical, measured on four of them (24.9%, 23.8%,
24.5%, 25.3% of the stage moves on, 0.00% off) — while the recipe, the UNDO
stack and the session stay on the look the click left. A hovered look brings its
colour alone: the masks, the dust spots and the mosaic of the photo being edited
ride along, or a pointer crossing a chip row would rub them off. A PRO sim is
left out, since a hover that showed its look would hand over what the click
gates.

Probes: e2e-raw-verify, e2e-dehaze-mask, e2e-mask-verify, e2e-clarity-verify,
e2e-hover-preview2.
2026-09-26 18:18:22 +07:00

61 lines
2.3 KiB
TypeScript

// The RAW the current photo came from, parked in the origin private file system
// so the studio can go back to it instead of asking for 30MB a second time (see
// native_raw_processing_opfs_architecture.md). One slot only: the studio holds
// one photo, and the previous RAW is stale the moment the next one lands.
//
// The session keeps the *developed* JPEG, so the ordinary reload path never
// touches this — it is the fallback for an empty IndexedDB, and the file a
// future full-resolution or different-settings develop would read.
//
// ponytail: OPFS's own sync-access handle would let LibRaw read in place, but
// LibRaw.open takes a BufferSource, so every path here reads a Blob anyway.
// Switch to createSyncAccessHandle if that API ever grows a worker-side reader.
const FILE = 'current_image.raw';
const NAME = 'current_image.name';
async function opfs(name: string, create: boolean): Promise<FileSystemFileHandle | null> {
const root = await navigator.storage?.getDirectory?.();
if (!root) return null;
return root.getFileHandle(name, { create });
}
async function write(name: string, data: BlobPart): Promise<void> {
const fh = await opfs(name, true);
if (!fh) return;
const w = await fh.createWritable();
await w.write(data);
await w.close();
}
export async function storeRaw(bytes: Uint8Array, fileName: string): Promise<void> {
try {
await write(FILE, bytes as unknown as BufferSource);
await write(NAME, fileName);
} catch {
// No OPFS (private mode, older browser): the photo still develops, it just
// cannot be recovered after a reload. Nothing here is worth failing a load.
}
}
export async function readRaw(): Promise<{ name: string; bytes: Uint8Array } | null> {
try {
const fh = await opfs(FILE, false);
if (!fh) return null;
const nameHandle = await opfs(NAME, false);
const name = nameHandle ? (await (await nameHandle.getFile()).text()).trim() : FILE;
return { name: name || FILE, bytes: new Uint8Array(await (await fh.getFile()).arrayBuffer()) };
} catch {
return null;
}
}
export async function clearRaw(): Promise<void> {
try {
const root = await navigator.storage?.getDirectory?.();
await root?.removeEntry(FILE).catch(() => undefined);
await root?.removeEntry(NAME).catch(() => undefined);
} catch {
// Absent is the desired state.
}
}