fix(library): stop calling a .tif a RAW, and stop the wall's render loop

Three things, all from one look at a folder of TIFF scans.

A `.tif` is no longer a RAW. Nothing here develops one, and `tif`/`tiff` sat in
`RAW_EXT` for no decoder's sake: opening a TIFF asked for PRO and its tile wore
a RAW badge, for a file the engine has nothing to hand back. Both extensions
are dropped, and a TIFF keeps its own route instead — the tile reads the file's
own pixels (`useThumbUrls`, via `tiffDevelop`) and so does the studio — so a
`.tif` still opens and still draws, it just is not a RAW any more.

The wall's render loop. `painted` was a fresh `.slice()` on every render, so
`useThumbUrls` saw a new list each time round and its effect ran again, and
again: about twenty thousand renders a second while the page sat idle, and the
file reads frozen at the first screenful, so a tile scrolled to never came up
(7 of 24 cards drawn). `painted` is memoised on `listed, wall.from, wall.to`
now — no date formats in eight idle seconds, 28 of 28 cards after a scroll.

Scrolling between the two modes. SINGLE → ALL THUMBS lands the wall on the
frame the strip was showing, centred in the window; ALL THUMBS → SINGLE swipes
the strip to the frame nearest the wall's middle, centred in the row. Each way
the frame the other mode was standing on is the one you come back to.
This commit is contained in:
2026-10-09 16:12:18 +07:00
parent 6e11da7e9a
commit 88d97432bd
4 changed files with 169 additions and 39 deletions
+55 -31
View File
@@ -188,11 +188,12 @@ half4 main(float2 pos) {
`;
// The extensions LibRaw is compiled for. Not `tif`: a TIFF in a camera folder is
// as likely to be an export as a RAW.
// as likely to be an export as a RAW, and opening one asked for PRO and wore a
// RAW badge for a file the engine has nothing to develop in. It is read for its
// own pixels instead — see `tiffDevelop`.
const RAW_EXT = [
'arw', 'srf', 'sr2', 'cr2', 'cr3', 'crw', 'nef', 'nrw', 'dng', 'orf', 'raf',
'rw2', 'pef', 'srw', 'raw', 'rwl', '3fr', 'fff', 'iiq', 'mos', 'mrw', 'x3f',
'tif', 'tiff',
];
export const RAW_ACCEPT = RAW_EXT.map((e) => `.${e}`).join(',');
@@ -202,6 +203,12 @@ export function isRawName(name: string): boolean {
return name.includes('.') && RAW_EXT.includes(ext);
}
// A TIFF is not a RAW: it is a picture no decoder here draws by itself, and the
// one test for it, so the tile's route and the studio's route cannot drift apart.
export function isTiffName(name: string): boolean {
return /\.(tiff?)$/i.test(name);
}
// The camera's own preview, when the file carries one: the colour reference the
// curve below is fitted to, and what the file opens as when the sensor does not
// decode.
@@ -296,48 +303,65 @@ export function extractEmbeddedJpeg(buf: Uint8Array): Uint8Array | null {
// What CanvasKit draws, and only that: a TIFF goes to `decodeTiff` below, since
// `MakeImageFromEncoded` answers null to one (measured on a baseline 5472x3648
// scan) and the file has to be read by hand.
export function tiffThumbnail(bytes: Uint8Array): Uint8Array | null {
try {
const img = Skia.Image.MakeImageFromEncoded(bytes);
if (img) {
try {
return shrinkToJpeg(img);
} finally {
img.delete();
}
}
function tiffImage(bytes: Uint8Array, maxSide: number): any {
const img = Skia.Image.MakeImageFromEncoded(bytes);
if (img) return img;
const frame = decodeTiff(bytes, 640);
if (!frame) return null;
const image = Skia.Image.MakeImage(
{
width: frame.width,
height: frame.height,
colorType: Skia.ColorType.RGBA_8888,
alphaType: Skia.AlphaType.Unpremul,
colorSpace: Skia.ColorSpace.SRGB,
},
frame.rgba,
frame.width * 4
);
if (!image) return null;
const frame = decodeTiff(bytes, maxSide);
if (!frame) return null;
return Skia.Image.MakeImage(
{
width: frame.width,
height: frame.height,
colorType: Skia.ColorType.RGBA_8888,
alphaType: Skia.AlphaType.Unpremul,
colorSpace: Skia.ColorSpace.SRGB,
},
frame.rgba,
frame.width * 4
);
}
// The same read, encoded to JPEG. `max` is the long side it comes out at, 0 for
// the file's own size.
function tiffJpeg(bytes: Uint8Array, max: number): Uint8Array | null {
try {
const img = tiffImage(bytes, max || Infinity);
if (!img) return null;
try {
return shrinkToJpeg(image);
return shrinkToJpeg(img, max || Math.max(img.width(), img.height()));
} finally {
image.delete();
img.delete();
}
} catch {
return null;
}
}
export function tiffThumbnail(bytes: Uint8Array): Uint8Array | null {
return tiffJpeg(bytes, 640);
}
// A `.tif` read for the screen, now that it is no file the engine develops and
// no decoder the browser has: the frame is the picture, so it is decoded here —
// by the studio at the file's own size, by the catalogue's stage at what a screen
// shows (`max`, 0 for the file's own). CanvasKit is what draws it, and neither
// caller has booted it: a visitor who only ever opens the wall never does.
//
// ponytail: at the file's own size this is the frame's RGBA in memory (80MB for a
// baseline 5472x3648 scan) plus the canvas it is drawn to. Cap it if a plate ever
// arrives that does not fit; the develop above is the same order already.
export async function tiffDevelop(bytes: Uint8Array, max = 0): Promise<Uint8Array | null> {
if (!Skia) await initSkia({ locateFile: (file) => `/wasm/${file}` });
return tiffJpeg(bytes, max);
}
// A frame shrunk to the square the wall paints and encoded to JPEG — the one
// step of a tile that is CanvasKit's whatever the frame came out of.
function shrinkToJpeg(img: any): Uint8Array | null {
function shrinkToJpeg(img: any, n = 640): Uint8Array | null {
const w = img.width();
const h = img.height();
if (!w || !h) return null;
const n = 640;
const wide = w >= h;
const targetW = wide ? n : Math.max(1, Math.round((w / h) * n));
const targetH = wide ? Math.max(1, Math.round((h / w) * n)) : n;
@@ -371,7 +395,7 @@ export async function rawThumbnail(bytes: Uint8Array, fileName?: string): Promis
// hiding in it — scanning the bytes for one finds the frame's own pixel data
// and hands back three hundred kilobytes of it dressed as a JPEG, which is
// what the wall used to keep as the frame's tile and draw as nothing.
if (fileName && /\.(tiff?)$/i.test(fileName)) {
if (fileName && isTiffName(fileName)) {
// CanvasKit is what draws the JPEG inside one, and the catalogue has a route
// of its own: the studio boots the engine, a visitor who only ever opens the
// wall never does, and a tile read before it is up draws as nothing.