fix(library): land a reading's rows as they are read, and keep the copy the reader reads offline
- scan: rows go down on a clock as well as on a full batch. The reader's folder of 26 RAWs is under one batch of 50, so nothing of it reached the screen until the reading was over — a wall that stood empty for the whole scan and then filled in one go. Frames now reach the screen every 400ms while the roll is still being read, so the count climbs and the first tiles are drawn as the frames land. - tiles: a HEIC or a RAW has its tile written the first time it is drawn, so every later visit reads the tile back instead of the file. - resume: a reading a reload cut off is offered by the next visit with a chip instead of being run behind the reader's back. - offline listing: the desk's listing is kept whole in IndexedDB under `recipescam-offline`/`reads`, past the 200 000 characters localStorage held, so an offline visit opens on the listing it kept instead of on nothing. - offline writes: a vote or a view cast with no server is held and goes out when one answers again, with a bar that says how many are waiting. - sw: the shell precaches its own bundle, stylesheet and icon, read out of the document — a worker that precached only the document answered an offline launch with a page whose JavaScript the machine did not have. - checks in docker/frontend/scripts: tree-scan-check (counts while reading, resume after a reload, tiles of a long roll), offline-read-check, offline-queue-check.
This commit is contained in:
@@ -567,21 +567,57 @@ function getCamToSrgbMatrix(cd: any, brand?: CameraBrand): number[][] {
|
||||
}
|
||||
}
|
||||
|
||||
export async function developRaw(bytes: Uint8Array, fileName?: string): Promise<Uint8Array> {
|
||||
// 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 (Fujifilm, Sony, Olympus, Nikon, Canon,
|
||||
// Ricoh, Lumix, Leica, iPhone ProRAW, DNG).
|
||||
const preview = extractEmbeddedJpeg(bytes);
|
||||
if (preview && preview.length > 10000) {
|
||||
return preview;
|
||||
// The size a JPEG declares for itself, off its SOF segment — a header read, no
|
||||
// decoder. Null when the bytes carry no readable frame header.
|
||||
export function jpegFrameSize(jpeg: Uint8Array): { width: number; height: number } | null {
|
||||
for (let i = 2; i + 9 < jpeg.length; ) {
|
||||
if (jpeg[i] !== 0xff) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const marker = jpeg[i + 1];
|
||||
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
||||
return { height: (jpeg[i + 5] << 8) | jpeg[i + 6], width: (jpeg[i + 7] << 8) | jpeg[i + 8] };
|
||||
}
|
||||
i += 2 + ((jpeg[i + 2] << 8) | jpeg[i + 3]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Is the embedded preview the frame itself? Bodies that write one at the frame
|
||||
// size (Nikon NEF 6016x4016 out of 6032x4032, Sony ARW, an iPhone ProRAW DNG)
|
||||
// differ from the sensor by a margin row or two; a thumbnail out of the same
|
||||
// sensor is 1920x1280, a third of the long side, so 90% divides them cleanly.
|
||||
// Unreadable sizes keep the old behaviour and trust the preview.
|
||||
function previewIsFrame(preview: Uint8Array, meta: any): boolean {
|
||||
const size = jpegFrameSize(preview);
|
||||
const frame = Math.max(
|
||||
Number(meta?.iwidth) || 0,
|
||||
Number(meta?.iheight) || 0,
|
||||
Number(meta?.width) || 0,
|
||||
Number(meta?.height) || 0
|
||||
);
|
||||
if (!size || !frame) return true;
|
||||
return Math.max(size.width, size.height) >= frame * 0.9;
|
||||
}
|
||||
|
||||
export async function developRaw(bytes: Uint8Array, fileName?: string): Promise<Uint8Array> {
|
||||
// The embedded JPEG is the camera's own rendering — exact colour, tone, white
|
||||
// balance and picture profile, generated by the body itself. It is the frame
|
||||
// only when it is the size of the frame; on most bodies it is a thumbnail
|
||||
// (1920x1280 out of a 24MP sensor, measured on a Fuji RAF and a Lumix RW2), so
|
||||
// handing it back is what opened a 24MP file as a 1920 wide one. Whether it is
|
||||
// the frame is known from the sensor size, which costs one LibRaw open (91ms
|
||||
// measured, metadata only — no demosaic) against seconds for the develop.
|
||||
const preview = extractEmbeddedJpeg(bytes);
|
||||
|
||||
const raw = new LibRaw();
|
||||
try {
|
||||
await raw.open(bytes.slice() as unknown as BufferSource, SETTINGS);
|
||||
const meta = await raw.metadata(true);
|
||||
if (preview && preview.length > 10000 && previewIsFrame(preview, meta)) {
|
||||
return preview;
|
||||
}
|
||||
const img = await raw.imageData();
|
||||
const cd = meta?.color_data;
|
||||
if (!img || (!cd?.cam_mul && !cd?.pre_mul) || (!cd.rgb_cam && !cd.cam_xyz)) {
|
||||
@@ -657,7 +693,7 @@ export async function developRaw(bytes: Uint8Array, fileName?: string): Promise<
|
||||
if (!first) throw new Error('no surface for the develop');
|
||||
|
||||
let finalShot = first;
|
||||
let refJpeg = extractEmbeddedJpeg(bytes);
|
||||
let refJpeg = preview;
|
||||
if (!refJpeg) {
|
||||
refJpeg = await cameraPreview(raw).catch(() => null);
|
||||
}
|
||||
@@ -691,6 +727,9 @@ export async function developRaw(bytes: Uint8Array, fileName?: string): Promise<
|
||||
if (!jpeg?.length) throw new Error('develop produced no bytes');
|
||||
return jpeg;
|
||||
} catch (err) {
|
||||
// The preview is also the fallback, as it always was: a file whose sensor
|
||||
// will not decode still opens as the camera rendered it.
|
||||
if (preview && preview.length > 10000) return preview;
|
||||
const fallback = await rawThumbnail(bytes).catch(() => null);
|
||||
if (fallback) return fallback;
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user