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:
2026-10-09 15:03:53 +07:00
parent 29759721cb
commit c3d46b0d15
5 changed files with 695 additions and 37 deletions
+3 -4
View File
@@ -4,7 +4,7 @@ import { go } from './nav';
import { api } from './api';
import { keep, recall } from './remember';
import { readGps, readSpecs, specsLine } from './engine/imageOps';
import { isRawName, rawThumbnail, tiffThumbnail } from './engine/rawDevelop';
import { isRawName, rawThumbnail } from './engine/rawDevelop';
import { heicThumbnail, heicToJpeg, isHeicName } from './engine/heicDevelop';
import {
cachedFolders,
@@ -1643,7 +1643,7 @@ function Catalogue({ admin }: { admin: boolean }) {
} else if (isRawName(file.name)) {
const bytes = new Uint8Array(await file.arrayBuffer());
if (!live) return;
const previewBytes = await rawThumbnail(bytes);
const previewBytes = await rawThumbnail(bytes, file.name);
if (previewBytes && live) {
const blob = new Blob([previewBytes as BlobPart], { type: 'image/jpeg' });
url = URL.createObjectURL(blob);
@@ -2777,8 +2777,7 @@ function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null): Recor
if (prevBytes) blob = await tileOf(photo.id, prevBytes);
} else if (isRawName(name) || /\.(tiff?)$/i.test(name)) {
const fullBytes = new Uint8Array(await file.arrayBuffer());
let prevBytes = await rawThumbnail(fullBytes, name);
if (!prevBytes && /\.(tiff?)$/i.test(name)) prevBytes = tiffThumbnail(fullBytes);
const prevBytes = await rawThumbnail(fullBytes, name);
if (prevBytes) blob = await tileOf(photo.id, prevBytes);
} else {
// A frame the wall can decode itself: the tile is made here, at the
+8 -1
View File
@@ -24,7 +24,7 @@ const PHOTOS = 'photos';
const THUMBS = 'thumbs';
const EDITS = 'edits';
const DIRS = 'dirs';
const DB_VERSION = 3;
const DB_VERSION = 4;
// The tile the catalogue keeps is the same tile the grid cell and the strip both
// paint, and there is one of them per frame of a roll that reaches six figures.
@@ -236,6 +236,13 @@ function openDb(): Promise<IDBDatabase> {
req.onblocked = () => blockedSink?.(true);
req.onupgradeneeded = () => {
const db = req.result;
// v4: every catalogue kept a tile for every frame, and a `.tif`'s tile was
// the frame's own pixel data read as if it were a JPEG — the bytes are not
// a picture, so the tile drew as nothing and, being kept, went on drawing
// as nothing on every visit after. The tiles are dropped; each is made
// again the first time its frame is looked at, which is when it was going
// to be made anyway.
if (db.objectStoreNames.contains(THUMBS)) db.deleteObjectStore(THUMBS);
if (!db.objectStoreNames.contains(FOLDERS)) db.createObjectStore(FOLDERS, { keyPath: 'name' });
if (!db.objectStoreNames.contains(PHOTOS)) {
const photos = db.createObjectStore(PHOTOS, { keyPath: 'id' });
+65 -32
View File
@@ -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
+491
View File
@@ -0,0 +1,491 @@
// TIFF → RGBA, for the catalogue's tiles.
//
// A `.tif` in an album had no tile, and none of the three decoders the app
// already carries will make one. Measured on a baseline 5472x3648 scan written
// by vips: `createImageBitmap` answers InvalidStateError, CanvasKit's
// `MakeImageFromEncoded` answers null — so `tiffThumbnail` never returned
// anything for a TIFF at all — and LibRaw opens the file only to report a 0x0
// frame with no thumbnail inside it.
//
// So the frame is read here instead: the header, the strips or tiles with their
// compression, the photometric, and one row in `step` out of the image. A 640px
// tile shows no more of a 5472px row than that, which is why the full-size row
// is never built — a scan album is hundreds of megabytes a frame and the tile
// is the only thing anyone asked for.
//
// What is covered: little and big endian, strips and tiles, chunky (planar 1),
// 1/8/16-bit, grey / RGB / palette, compression none, LZW (5), PackBits
// (32773) and both JPEG-in-TIFF flavours (6 and 7, through CanvasKit, which is
// the one thing in here that does decode a JPEG). YCbCr outside a JPEG and CMYK
// are not decoded and answer null — the row still lists the frame by name.
import { Skia } from './skiaShim';
export interface TiffFrame {
width: number;
height: number;
rgba: Uint8Array;
}
// An IFD longer than this is not a camera file, it is something reading one.
const MAX_ENTRIES = 4096;
interface Entry {
type: number;
count: number;
/** Where the value's bytes are, inline in the entry or out at an offset. */
at: number;
}
interface Chunk {
/** Position in the image this chunk's pixels start at. */
x0: number;
y0: number;
/** Width of a stored row, which a tile pads out to its own tile width. */
stride: number;
width: number;
height: number;
offset: number;
length: number;
/** Set for the two JPEG flavours: the stream itself, tables already joined. */
jpeg?: Uint8Array;
}
interface Samples {
kind: 'samples';
width: number;
height: number;
spp: number;
depth: number;
data: Uint8Array | Uint16Array;
}
interface Rgba {
kind: 'rgba';
width: number;
height: number;
data: Uint8Array;
}
type Decoded = Samples | Rgba;
export function decodeTiff(bytes: Uint8Array, maxSide: number): TiffFrame | null {
if (bytes.length < 8) return null;
const little = bytes[0] === 0x49 && bytes[1] === 0x49;
if (!little && !(bytes[0] === 0x4d && bytes[1] === 0x4d)) return null;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const u16 = (at: number) => view.getUint16(at, little);
const u32 = (at: number) => view.getUint32(at, little);
if (u16(2) !== 42) return null;
const valueSize = (type: number) => {
if (type === 1 || type === 2 || type === 6 || type === 7) return 1;
if (type === 3 || type === 8) return 2;
if (type === 4 || type === 9 || type === 11) return 4;
if (type === 5 || type === 10 || type === 12) return 8;
return 0;
};
const readIfd = (offset: number): Map<number, Entry> | null => {
if (offset < 8 || offset + 2 > bytes.length) return null;
const count = u16(offset);
if (count > MAX_ENTRIES || offset + 2 + count * 12 > bytes.length) return null;
const tags = new Map<number, Entry>();
for (let i = 0; i < count; i++) {
const entry = offset + 2 + i * 12;
const type = u16(entry + 2);
const n = u32(entry + 4);
const size = valueSize(type) * n;
if (!size || n > 1 << 22) continue;
const at = size <= 4 ? entry + 8 : u32(entry + 8);
if (at < 0 || at + size > bytes.length) continue;
tags.set(u16(entry), { type, count: n, at });
}
return tags;
};
const num = (tags: Map<number, Entry>, tag: number, fallback = 0) => {
const entry = tags.get(tag);
if (!entry) return fallback;
if (entry.type === 3) return u16(entry.at);
if (entry.type === 4 || entry.type === 9) return u32(entry.at);
if (entry.type === 1 || entry.type === 6 || entry.type === 7) return bytes[entry.at];
return fallback;
};
const nums = (tags: Map<number, Entry>, tag: number) => {
const entry = tags.get(tag);
if (!entry) return [];
const out: number[] = [];
for (let i = 0; i < entry.count; i++) {
if (entry.type === 3) out.push(u16(entry.at + i * 2));
else if (entry.type === 4) out.push(u32(entry.at + i * 4));
else out.push(bytes[entry.at + i]);
}
return out;
};
const tags = readIfd(u32(4));
if (!tags) return null;
const width = num(tags, 256);
const height = num(tags, 257);
if (width < 1 || height < 1) return null;
const compression = num(tags, 259, 1);
const photometric = num(tags, 262, 2);
const spp = num(tags, 277, 1);
if (num(tags, 284, 1) !== 1) return null;
if (spp < 1 || spp > 4) return null;
if (photometric === 5) return null;
if (photometric === 6 && compression !== 6 && compression !== 7) return null;
if (photometric === 2 && spp < 3) return null;
let depths = nums(tags, 258);
if (!depths.length) depths = [1];
if (depths.length === 1 && spp > 1) depths = new Array(spp).fill(depths[0]);
if (depths.length !== spp) return null;
const depth = depths[0];
if (depths.some((d) => d !== depth)) return null;
if (depth !== 1 && depth !== 8 && depth !== 16) return null;
const predictor = num(tags, 317, 1);
if (predictor !== 1 && predictor !== 2) return null;
const tileWidth = num(tags, 322);
const tileHeight = num(tags, 323);
const tiled = tileWidth > 0 && tileHeight > 0 && tags.has(324);
const rowsPerStrip = Math.max(1, num(tags, 278, height) || height);
const chunks = new Map<number, Chunk>();
const tables = tags.get(347);
const jpegTables = tables ? bytes.subarray(tables.at, tables.at + tables.count) : null;
if (compression === 6) {
const offsets = nums(tags, 273);
const lengths = nums(tags, 279);
const offset = num(tags, 513, offsets[0] ?? -1);
const length = num(tags, 514, lengths[0] ?? 0);
if (offset < 0 || offset + length > bytes.length || !length) return null;
chunks.set(0, {
x0: 0, y0: 0, stride: width, width, height, offset, length,
jpeg: bytes.subarray(offset, offset + length),
});
} else if (tiled) {
const offsets = nums(tags, 324);
const lengths = nums(tags, 325);
const across = Math.ceil(width / tileWidth);
const down = Math.ceil(height / tileHeight);
for (let ty = 0; ty < down; ty++) {
for (let tx = 0; tx < across; tx++) {
const key = ty * across + tx;
const offset = offsets[key];
const length = lengths[key];
if (offset == null || !length || offset + length > bytes.length) continue;
const x0 = tx * tileWidth;
const y0 = ty * tileHeight;
const chunk: Chunk = {
x0, y0, stride: tileWidth,
width: Math.min(tileWidth, width - x0),
height: Math.min(tileHeight, height - y0),
offset, length,
};
if (compression === 7) {
const scan = bytes.subarray(offset, offset + length);
chunk.jpeg = jpegTables ? jpegWithTables(jpegTables, scan) : scan;
}
chunks.set(key, chunk);
}
}
} else {
const offsets = nums(tags, 273);
const lengths = nums(tags, 279);
for (let i = 0; i < offsets.length; i++) {
const y0 = i * rowsPerStrip;
if (y0 >= height) break;
const offset = offsets[i];
const length = lengths[i] ?? 0;
if (offset < 0 || !length || offset + length > bytes.length) continue;
const chunk: Chunk = {
x0: 0, y0, stride: width, width,
height: Math.min(rowsPerStrip, height - y0),
offset, length,
};
if (compression === 7) {
const scan = bytes.subarray(offset, offset + length);
chunk.jpeg = jpegTables ? jpegWithTables(jpegTables, scan) : scan;
}
chunks.set(i, chunk);
}
}
if (!chunks.size) return null;
const keyOf = tiled
? (sx: number, sy: number) => Math.floor(sy / tileHeight) * Math.ceil(width / tileWidth) + Math.floor(sx / tileWidth)
: (_sx: number, sy: number) => Math.floor(sy / rowsPerStrip);
const value = (s: Samples, i: number) => {
if (i < 0 || i >= s.data.length) return 0;
const raw = s.data[i];
return s.depth === 16 ? raw >> 8 : raw;
};
const decode = (chunk: Chunk): Decoded | null => {
if (chunk.jpeg) {
const image = Skia.Image.MakeImageFromEncoded(chunk.jpeg);
if (!image) return null;
try {
const w = image.width();
const h = image.height();
if (!w || !h) return null;
const data = new Uint8Array(w * h * 4);
const done = image.readPixels(
0, 0,
{
width: w,
height: h,
colorType: Skia.ColorType.RGBA_8888,
alphaType: Skia.AlphaType.Unpremul,
colorSpace: Skia.ColorSpace.SRGB,
},
data, w * 4,
);
return done ? { kind: 'rgba', width: w, height: h, data } : null;
} finally {
image.delete();
}
}
const stride = Math.ceil((chunk.stride * spp * depth) / 8);
const want = stride * chunk.height;
let raw: Uint8Array;
if (compression === 1) raw = bytes.subarray(chunk.offset, Math.min(bytes.length, chunk.offset + want));
else if (compression === 5) raw = lzwDecode(bytes.subarray(chunk.offset, chunk.offset + chunk.length), want);
else if (compression === 32773) raw = packBits(bytes.subarray(chunk.offset, chunk.offset + chunk.length), want);
else return null;
if (raw.length < want) return null;
if (depth === 1) {
const data = new Uint8Array(chunk.height * chunk.width * spp);
for (let y = 0; y < chunk.height; y++) {
for (let x = 0; x < chunk.width; x++) {
const at = y * stride + (x >> 3);
const bit = at < raw.length ? (raw[at] >> (7 - (x & 7))) & 1 : 0;
data[y * chunk.width * spp + x] = bit * 255;
}
}
return { kind: 'samples', width: chunk.width, height: chunk.height, spp, depth: 1, data };
}
if (depth === 16) {
const data = new Uint16Array(chunk.height * chunk.width * spp);
for (let i = 0; i < data.length; i++) {
const at = i * 2;
data[i] = little ? raw[at] | (raw[at + 1] << 8) : (raw[at] << 8) | raw[at + 1];
}
if (predictor === 2) differencing(data, chunk, spp);
return { kind: 'samples', width: chunk.width, height: chunk.height, spp, depth: 16, data };
}
const data = new Uint8Array(want).fill(0);
data.set(raw.subarray(0, want));
if (predictor === 2) differencing(data, chunk, spp);
return { kind: 'samples', width: chunk.width, height: chunk.height, spp, depth: 8, data };
};
const scale = Math.min(1, maxSide / Math.max(width, height));
const outW = Math.max(1, Math.round(width * scale));
const outH = Math.max(1, Math.round(height * scale));
const out = new Uint8Array(outW * outH * 4);
const palette = photometric === 3 ? nums(tags, 320) : null;
const half = palette ? palette.length / 3 : 0;
let cachedKey = -1;
let cached: Decoded | null = null;
for (let oy = 0; oy < outH; oy++) {
const sy = Math.min(height - 1, Math.floor((oy * height) / outH));
for (let ox = 0; ox < outW; ox++) {
const sx = Math.min(width - 1, Math.floor((ox * width) / outW));
const key = keyOf(sx, sy);
if (key !== cachedKey) {
cachedKey = key;
const chunk = chunks.get(key);
cached = chunk ? decode(chunk) : null;
}
const chunk = chunks.get(key);
if (!cached || !chunk) continue;
const chunkX = sx - chunk.x0;
const chunkY = sy - chunk.y0;
if (chunkX < 0 || chunkY < 0 || chunkX >= chunk.width || chunkY >= chunk.height) continue;
const at = (oy * outW + ox) * 4;
let r: number;
let g: number;
let b: number;
if (cached.kind === 'rgba') {
const i = (chunkY * cached.width + chunkX) * 4;
if (i + 3 >= cached.data.length) continue;
r = cached.data[i];
g = cached.data[i + 1];
b = cached.data[i + 2];
} else {
const i = chunkY * cached.width * spp + chunkX * spp;
if (palette && half) {
const index = value(cached, i);
r = (palette[index] ?? 0) >> 8;
g = (palette[half + index] ?? 0) >> 8;
b = (palette[2 * half + index] ?? 0) >> 8;
} else if (photometric === 2 || photometric === 6) {
r = value(cached, i);
g = value(cached, i + 1);
b = value(cached, i + 2);
} else {
const gray = value(cached, i);
r = g = b = photometric === 0 ? 255 - gray : gray;
}
}
out[at] = r;
out[at + 1] = g;
out[at + 2] = b;
out[at + 3] = 255;
}
}
return { width: outW, height: outH, rgba: out };
}
// Horizontal differencing, undone in place: every sample is the one before it
// plus the difference stored. Bounded by the type, which is the point — a 16-bit
// sample wrapped at 65536 and an 8-bit one at 256, and the file wrapped them the
// same way.
function differencing(data: Uint8Array | Uint16Array, chunk: Chunk, spp: number): void {
const row = chunk.width * spp;
for (let y = 0; y < chunk.height; y++) {
const base = y * row;
for (let i = spp; i < row; i++) data[base + i] += data[base + i - spp];
}
}
// A JPEG-compressed strip carries the frame's own tables — the whole stream
// minus the entropy-coded scan — and the strip is the scan. Joining the two is
// the only way either decodes: a strip on its own is missing its tables, and the
// tables on their own are not a picture.
function jpegWithTables(tables: Uint8Array, strip: Uint8Array): Uint8Array {
const tableFrom = tables[0] === 0xff && tables[1] === 0xd8 ? 2 : 0;
const tableTo = tables.length > 4 && tables[tables.length - 2] === 0xff && tables[tables.length - 1] === 0xd9
? tables.length - 2
: tables.length;
const scanFrom = strip[0] === 0xff && strip[1] === 0xd8 ? 2 : 0;
const complete = strip.length > 4 && strip[strip.length - 2] === 0xff && strip[strip.length - 1] === 0xd9;
const body = tableTo - tableFrom;
const out = new Uint8Array(2 + body + (strip.length - scanFrom) + (complete ? 0 : 2));
out[0] = 0xff;
out[1] = 0xd8;
out.set(tables.subarray(tableFrom, tableTo), 2);
out.set(strip.subarray(scanFrom), 2 + body);
if (!complete) {
out[out.length - 2] = 0xff;
out[out.length - 1] = 0xd9;
}
return out;
}
// TIFF's LZW: MSB-first codes, 9 to 12 bits, and the code width steps up one
// code early — at 511, not 512 — which is the quirk every other implementation
// trips on.
function lzwDecode(src: Uint8Array, expected: number): Uint8Array {
const out = new Uint8Array(expected);
const prefix = new Int32Array(4096);
const suffix = new Uint8Array(4096);
const stack = new Uint8Array(4096);
let taken = 0;
let read = 0;
let bitCount = 0;
let bitBuffer = 0;
let next = 258;
let codeSize = 9;
let previous = -1;
let first = 0;
const code = () => {
while (bitCount < codeSize) {
if (read >= src.length) return -1;
bitBuffer = (bitBuffer << 8) | src[read++];
bitCount += 8;
}
bitCount -= codeSize;
return (bitBuffer >> bitCount) & ((1 << codeSize) - 1);
};
const emit = (at: number) => {
let depth = 0;
let walk = at;
while (walk >= 256) {
if (depth >= 4096 || prefix[walk] < 0) return;
stack[depth++] = suffix[walk];
walk = prefix[walk];
}
stack[depth++] = walk;
first = walk;
while (depth > 0) {
if (taken >= out.length) return;
out[taken++] = stack[--depth];
}
};
let current = code();
while (current >= 0 && current !== 257) {
if (current === 256) {
next = 258;
codeSize = 9;
previous = -1;
current = code();
continue;
}
if (previous < 0) {
if (current > 255) return out;
emit(current);
} else if (current < next) {
emit(current);
if (next < 4096) {
prefix[next] = previous;
suffix[next] = first;
next++;
}
} else {
if (next >= 4096) return out;
prefix[next] = previous;
suffix[next] = first;
emit(next);
next++;
}
previous = current;
if (next === (1 << codeSize) - 1 && codeSize < 12) codeSize++;
current = code();
}
return out;
}
function packBits(src: Uint8Array, expected: number): Uint8Array {
const out = new Uint8Array(expected);
let taken = 0;
let read = 0;
while (read < src.length && taken < expected) {
let run = src[read++];
if (run > 127) run -= 256;
if (run >= 0) {
const end = Math.min(src.length, read + run + 1);
while (read < end && taken < expected) out[taken++] = src[read++];
read = end;
} else if (run !== -128) {
const value = src[read++];
const times = Math.min(1 - run, expected - taken);
for (let i = 0; i < times; i++) out[taken++] = value;
}
}
return out;
}