fix(library): read a .tif's own pixels for its tile
A `.tif` in an album drew nothing: the tile took the file's embedded JPEG — which a TIFF has not got, so the search found the frame's own pixel data and kept three hundred kilobytes of it as the frame's tile — and nothing else in the app draws a TIFF either, `createImageBitmap` answering InvalidStateError and CanvasKit's `MakeImageFromEncoded` answering null to one. `src/engine/tiffDecode.ts` reads the file itself, strips or tiles, and hands back a shrunk frame, which is what the tile is made of now. The tiles that were kept are dropped once (v4) so a frame already looked at is drawn right.
This commit is contained in:
@@ -55,6 +55,7 @@ import { sensorWhite } from './sensorWhite';
|
||||
import { f32ToF16 } from './halfFloat';
|
||||
import { toneMatch, FLAT_TONE, MATCH_GRID } from './previewMatch';
|
||||
import { Skia } from './skiaShim';
|
||||
import { decodeTiff } from './tiffDecode';
|
||||
|
||||
// What `imageData()` returns for the settings below: 16-bit, 3 channels, black
|
||||
// level already gone — the post-process subtracts it whatever `noAutoScale`
|
||||
@@ -291,58 +292,90 @@ export function extractEmbeddedJpeg(buf: Uint8Array): Uint8Array | null {
|
||||
// above is a full decode of every pixel at full resolution. No preview inside
|
||||
// the file means no tile — the row still lists the frame by name, and the
|
||||
// studio develops it the moment it is opened.
|
||||
//
|
||||
// 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) return null;
|
||||
try {
|
||||
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;
|
||||
if (img) {
|
||||
try {
|
||||
return shrinkToJpeg(img);
|
||||
} finally {
|
||||
img.delete();
|
||||
}
|
||||
}
|
||||
|
||||
const surface = Skia.Surface.MakeOffscreen(targetW, targetH) ?? Skia.Surface.Make(targetW, targetH);
|
||||
if (!surface) return null;
|
||||
const canvas = surface.getCanvas();
|
||||
canvas.drawImageRectCubic(
|
||||
img,
|
||||
Skia.XYWHRect(0, 0, w, h),
|
||||
Skia.XYWHRect(0, 0, targetW, targetH),
|
||||
1 / 3,
|
||||
1 / 3
|
||||
);
|
||||
surface.flush();
|
||||
const snapshot = surface.makeImageSnapshot();
|
||||
surface.dispose();
|
||||
if (!snapshot) return null;
|
||||
const jpeg = snapshot.encodeToBytes(Skia.ImageFormat.JPEG, 80);
|
||||
snapshot.dispose();
|
||||
return jpeg ? new Uint8Array(jpeg) : null;
|
||||
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;
|
||||
try {
|
||||
return shrinkToJpeg(image);
|
||||
} finally {
|
||||
img.delete();
|
||||
image.delete();
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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;
|
||||
|
||||
const surface = Skia.Surface.MakeOffscreen(targetW, targetH) ?? Skia.Surface.Make(targetW, targetH);
|
||||
if (!surface) return null;
|
||||
const canvas = surface.getCanvas();
|
||||
canvas.drawImageRectCubic(
|
||||
img,
|
||||
Skia.XYWHRect(0, 0, w, h),
|
||||
Skia.XYWHRect(0, 0, targetW, targetH),
|
||||
1 / 3,
|
||||
1 / 3
|
||||
);
|
||||
surface.flush();
|
||||
const snapshot = surface.makeImageSnapshot();
|
||||
surface.dispose();
|
||||
if (!snapshot) return null;
|
||||
const jpeg = snapshot.encodeToBytes(Skia.ImageFormat.JPEG, 80);
|
||||
snapshot.dispose();
|
||||
return jpeg ? new Uint8Array(jpeg) : null;
|
||||
}
|
||||
|
||||
// The preview on its own, for the catalogue: a folder of RAW files has to show
|
||||
// a tile per frame, and unpack_thumb is a seek and a copy where the develop
|
||||
// above is a full decode of every pixel at full resolution. No preview inside
|
||||
// the file means no tile — the row still lists the frame by name, and the
|
||||
// studio develops it the moment it is opened.
|
||||
export async function rawThumbnail(bytes: Uint8Array, fileName?: string): Promise<Uint8Array | null> {
|
||||
// A `.tif` is not a RAW: the file IS the picture, and no camera preview is
|
||||
// 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)) return tiffThumbnail(bytes);
|
||||
|
||||
const fast = extractEmbeddedJpeg(bytes);
|
||||
if (fast) return fast;
|
||||
|
||||
if (fileName && /\.(tiff?)$/i.test(fileName)) {
|
||||
const tiff = tiffThumbnail(bytes);
|
||||
if (tiff) return tiff;
|
||||
}
|
||||
|
||||
const raw = new LibRaw();
|
||||
try {
|
||||
// The same transfer as in the develop: the worker is handed a copy, so the
|
||||
|
||||
Reference in New Issue
Block a user