YAPF/bindings/js/yapf.js
Claude 0ba1f0af17
JavaScript port decodes YAPF 2; VS Code and Figma open every file
yapf.js: decodeTexture (container, yapf-sum checks, code books with
context decoding, 16-bit / float planes, GPU block tiles), BC1-BC7 /
BC6H block decoding and textureToRGBA for display. Matches the C library
on 417 generated files (surfaces bit-exact) and survives fuzzing with
clean YapfErrors; fixtures in bindings/js/test-data keep it checked.

VS Code preview: pickers for mip, face, layer, frame and slice; storage,
normal-map convention and version in the info bar. Figma import:
textures converted to 8-bit, 8-bit images stay exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:52 +00:00

1168 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* yapf.js — YAPF encoder (v1) and decoder (v1 and v2) in plain JavaScript.
*
* Copyright 2026 Nexoniarz — Apache License 2.0.
*
* Works in browsers, Node.js, Figma plugins and VS Code webviews; no
* dependencies. Produces exactly the same bytes as the C library.
*
* const img = YAPF.decode(bytes); // Uint8Array → image
* const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
*
* const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
* const view = YAPF.textureToRGBA(tex, surface); // { width, height, rgba }
*
* An image is { width, height, channels (1–4), pixels: Uint8Array,
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
* Version 2 textures (GPU blocks, 16-bit / float, cubes, arrays, volumes,
* animation) are decoded by decodeTexture; encoding writes version 1.
* The specification: yapf.h (v1) and docs/FORMAT.md (v2).
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.YAPF = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const TILE = 64, TILE_PX = TILE * TILE, GROUP = 8, BAND = 8, RING = 16;
const MAX_GROUPS = TILE_PX / GROUP, MAX_DIM = 65535, MAX_MIPS = 16;
const STORED = 0x80, SUBGREEN = 0x10, NIB_REPEAT = 9;
const F_LEFT = 0, F_GRAD = 1, F_UPRIGHT = 2, F_UPAVG = 3, F_UP = 16;
const NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64];
const RUN_LENS = [2, 3, 4, 8, 16, 32, 64];
/* Encoder try order: cheapest to decode first, ties go to the earlier. */
const FILTERS = [F_UP, F_UP + 1, F_UP + 2, F_UP + 3, F_UP + 4, F_UP + 5,
F_UP + 6, F_UP + 7, F_UPRIGHT, F_UPAVG, F_LEFT, F_GRAD];
const GPU = { RGBA8: 0, RGB8: 1, RG8: 2, R8: 3, SRGB8_A8: 4, SRGB8: 5 };
const FLAG = { PREMULT_ALPHA: 1, SRGB: 2 };
const ZERO_ROW = new Uint8Array(TILE + 1);
const zz = (r) => ((r << 1) ^ ((r << 24) >> 31)) & 255; /* r is 0..255 */
const unzz = (z) => ((z >>> 1) ^ -(z & 1)) & 255;
const width = (z) => (z === 0 ? 0 : 32 - Math.clz32(z));
const s8 = (v) => (v << 24) >> 24;
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
function corrupt() { throw new YapfError('corrupt', 'corrupt file'); }
/* ── Version 2: textures (decoding) ───────────────────────────────── */
const ENTROPY = 0x20, NOXFORM = 0x40, BLOCK_TILE = 16;
const HUFF_BITS = 11, HUFF_SIZE = 1 << HUFF_BITS, SLOTS = 48, CTXS = 7;
const NEED = { WIDE: 1, BLOCKS: 2, ENTROPY: 4, LAYOUT: 8, FRAMES: 16, ALL: 31 };
const r16 = (b, p) => b[p] | (b[p + 1] << 8);
const r64 = (b, p) => r32(b, p) + r32(b, p + 4) * 4294967296;
class YapfError extends Error {
constructor(code, msg) { super('YAPF: ' + msg); this.code = code; }
}
const fail = (code, msg) => { throw new YapfError(code, msg); };
const bad = () => fail('corrupt', 'corrupt file');
/* yapf-sum: 64-bit lanes kept as [hi, lo] uint32 pairs. */
const KH = 0x9E3779B9, KL = 0x7F4A7C15;
function mul64(ah, al, bh, bl) { /* (a · b) mod 2^64 */
const a0 = al & 0xFFFF, a1 = al >>> 16, b0 = bl & 0xFFFF, b1 = bl >>> 16;
const p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
const mid = (p00 >>> 16) + (p01 & 0xFFFF) + (p10 & 0xFFFF);
const lo = ((mid & 0xFFFF) * 65536 + (p00 & 0xFFFF)) >>> 0;
const hi = (p11 + (p01 >>> 16) + (p10 >>> 16) + Math.floor(mid / 65536) +
Math.imul(ah, bl) + Math.imul(al, bh)) >>> 0;
return [hi, lo];
}
function checksum(b, p, n) {
const lanes = [];
for (let i = 0; i < 4; i++) {
const [h, l] = mul64(0, i + 1, KH, KL);
lanes.push([(h ^ Math.floor(n / 4294967296)) >>> 0, (l ^ (n >>> 0)) >>> 0]);
}
const words = Math.ceil(n / 8);
for (let j = 0; j < words; j++) {
const q = p + j * 8;
let wl, wh;
if (q + 8 <= p + n) { wl = r32(b, q); wh = r32(b, q + 4); }
else {
const t = new Uint8Array(8); t.set(b.subarray(q, p + n));
wl = r32(t, 0); wh = r32(t, 4);
}
const a = lanes[j & 3];
const [h, l] = mul64((a[0] ^ wh) >>> 0, (a[1] ^ wl) >>> 0, KH, KL);
a[0] = h; a[1] = (l ^ h) >>> 0; /* a ^= a >> 32 */
}
const rot = (x, r) => r === 32 ? [x[1], x[0]]
: [((x[0] << r) | (x[1] >>> (32 - r))) >>> 0, ((x[1] << r) | (x[0] >>> (32 - r))) >>> 0];
const r1 = rot(lanes[1], 16), r2 = rot(lanes[2], 32), r3 = rot(lanes[3], 48 - 32);
const r3b = [r3[1], r3[0]]; /* rotl 48 = rotl 16 then swap */
let hh = (lanes[0][0] ^ r1[0] ^ r2[0] ^ r3b[0]) >>> 0, hl = (lanes[0][1] ^ r1[1] ^ r2[1] ^ r3b[1]) >>> 0;
[hh, hl] = mul64(hh, hl, KH, KL);
return (hl ^ hh) >>> 0;
}
/* Huffman code table → lut[off .. off + 2048) (length << 8 | symbol). */
function huffTable(b, p, end, lut, off) {
if (p >= end) bad();
const n = b[p++] + 1, nbytes = (n + 1) >> 1;
if (end - p < nbytes) bad();
const len = new Uint8Array(n), count = new Uint32Array(HUFF_BITS + 2);
let kraft = 0;
for (let i = 0; i < n; i++) {
const l = (b[p + (i >> 1)] >> ((i & 1) * 4)) & 15;
if (l > HUFF_BITS) bad();
len[i] = l; count[l]++;
if (l) kraft += HUFF_SIZE >> l;
}
if ((n & 1) && (b[p + (n >> 1)] >> 4)) bad();
if (kraft === 0 || kraft > HUFF_SIZE) bad();
p += nbytes;
const next = new Uint32Array(HUFF_BITS + 2);
let code = 0;
count[0] = 0;
for (let l = 1; l <= HUFF_BITS; l++) { code = (code + count[l - 1]) << 1; next[l] = code; }
for (let s = 0; s < n; s++) {
const l = len[s];
if (!l) continue;
let c = next[l]++, r = 0;
for (let i = 0; i < l; i++) { r = (r << 1) | (c & 1); c >>= 1; }
for (let k = r; k < HUFF_SIZE; k += 1 << l) lut[off + k] = (l << 8) | s;
}
return p;
}
/* Code book → per slot one Uint16Array of 7 × 2048 entries. */
function readBook(b, p, len) {
const end = p + len;
if (len < 2) bad();
const n = r16(b, p);
p += 2;
if (n === 0 || n > SLOTS * CTXS) bad();
const book = new Array(SLOTS).fill(null), seen = new Uint8Array(SLOTS * CTXS);
for (let i = 0; i < n; i++) {
if (end - p < 3) bad();
const slot = b[p], ctx = b[p + 1];
if (slot >= SLOTS || ctx >= CTXS || seen[slot * CTXS + ctx]) bad();
seen[slot * CTXS + ctx] = 1;
if (!book[slot]) book[slot] = new Uint16Array(CTXS * HUFF_SIZE);
p = huffTable(b, p + 2, end, book[slot], ctx * HUFF_SIZE);
}
if (p !== end) bad();
return book;
}
/* Context of a residual from the folded residuals left + above. */
const ctxOf = (s) => (s > 0) + (s > 2) + (s > 6) + (s > 14) + (s > 30) + (s > 62);
/* Entropy plane payload: filters, u16 length, context-coded residuals. */
function huffPlane(b, p, end, z, tw, th, book, slot) {
const nb = (th + BAND - 1) >> 3;
if (!book || !book[slot] || end - p < nb + 2) bad();
const filt = b.subarray(p, p + nb);
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) bad();
p += nb;
const L = r16(b, p);
p += 2;
if (end - p < L) bad();
const lut = book[slot], se = p + L;
let buf = 0, bits = 0, pos = p, used = 0, i = 0;
for (let y = 0; y < th; y++)
for (let x = 0; x < tw; x++, i++) {
while (bits <= 24) { buf |= (pos < se ? b[pos] : 0) << bits; pos++; bits += 8; }
const s = (x ? z[i - 1] : 0) + (y ? z[i - tw] : 0);
const e = lut[(ctxOf(s) << HUFF_BITS) | (buf & (HUFF_SIZE - 1))];
if (!e) bad();
const n = e >> 8;
buf >>>= n; bits -= n; used += n;
z[i] = e & 255;
}
if (Math.ceil(used / 8) !== L) bad();
z.fill(0, tw * th, tw * th + GROUP);
return { p: se, filt };
}
/* Wide samples: byte planes, transforms at the sample width. */
function decodeTileWide(b, p, end, out, imgW, tx, ty, tw, th, ch, sample, book) {
const sb = sample === 3 ? 4 : 2, bits = 8 * sb, isf = sample >= 2, np = ch * sb;
const half = bits === 32 ? 0x80000000 : 0x8000;
const mod = bits === 32 ? (x) => x >>> 0 : (x) => x & 0xFFFF;
const sar1 = (v) => mod((v >>> 1) | (v & half));
if (end <= p) bad();
const hdr = b[p++];
if (hdr === STORED) {
if (end - p !== tw * th * ch * sb) bad();
for (let y = 0; y < th; y++) {
const d = ((ty + y) * imgW + tx) * ch;
for (let i = 0; i < tw * ch; i++, p += sb)
out[d + i] = sb === 2 ? r16(b, p) : r32(b, p);
}
return;
}
if (hdr & 0x8F) bad();
if ((hdr & NOXFORM) && (hdr & SUBGREEN)) bad();
if (ch < 3 && (hdr & (NOXFORM | SUBGREEN))) bad();
const xf = ch < 3 ? 2 : (hdr & NOXFORM) ? 2 : (hdr & SUBGREEN) ? 1 : 0;
const tsel = ch < 3 ? 0 : xf;
if (end - p < 2) bad();
const cmask = r16(b, p);
p += 2;
if (cmask >> np) bad();
let emask = 0;
if (hdr & ENTROPY) {
if (end - p < 2 || !book) bad();
emask = r16(b, p);
p += 2;
if ((emask >> np) || (emask & cmask)) bad();
}
const RL = TILE + 1, PL = TILE * RL;
const planes = new Uint8Array(np * PL), z = new Uint8Array(TILE_PX + GROUP);
for (let pl = 0; pl < np; pl++) {
if (!(cmask & (1 << pl))) continue;
if (p >= end) bad();
planes.fill(b[p++], pl * PL, (pl + 1) * PL);
}
for (let pl = 0; pl < np; pl++) {
if (cmask & (1 << pl)) continue;
z.fill(0);
const r = (emask & (1 << pl)) ? huffPlane(b, p, end, z, tw, th, book, tsel * 16 + pl)
: unpackPlane(b, p, end, z, tw, th);
p = r.p;
const base = pl * PL;
for (let y = 0; y < th; y++) {
const f = r.filt[y >> 3], k = upK(f);
unfilterRow(f, planes, base + y * RL,
y ? planes : ZERO_ROW, y ? base + (y - 1) * RL : 0,
y >= k ? planes : ZERO_ROW, y >= k ? base + (y - k) * RL : 0,
z, y * tw, tw);
}
}
if (p !== end) bad();
const v = [0, 0, 0, 0];
for (let y = 0; y < th; y++) {
const d0 = ((ty + y) * imgW + tx) * ch, r0 = y * RL;
for (let x = 0; x < tw; x++) {
for (let c = 0; c < ch; c++) {
let a = 0;
for (let k = 0; k < sb; k++) a = a * 256 + planes[(c * sb + k) * PL + r0 + x];
v[c] = a;
}
if (xf === 0) {
const o = mod(v[1] - half), q = mod(v[2] - half), t = mod(v[0] - sar1(q));
const g = mod(q + t), bb = mod(t - sar1(o));
v[0] = mod(bb + o); v[1] = g; v[2] = bb;
} else if (xf === 1) {
const g = v[0];
v[0] = mod(v[1] + g - half); v[2] = mod(v[2] + g - half); v[1] = g;
}
for (let c = 0; c < ch; c++) {
let s = v[c];
if (isf) s = (s & half) ? mod(s ^ half) : mod(~s);
out[d0 + x * ch + c] = s;
}
}
}
}
/* GPU block tile: raw, or packed lane by lane with the book. */
function decodeBlockTile(b, p, end, packed, out, o0, rowb, tbw, tbh, S, book) {
const n = tbw * tbh;
if (!packed) {
if (end - p !== n * S) bad();
for (let r = 0; r < tbh; r++) out.set(b.subarray(p + r * tbw * S, p + (r + 1) * tbw * S), o0 + r * rowb);
return;
}
if (!book) bad();
let buf = 0, bits = 0, pos = p, used = 0;
for (let k = 0; k < S; k++) {
const lut = book[k];
if (!lut) bad();
for (let i = 0; i < n; i++) {
while (bits <= 24) { buf |= (pos < end ? b[pos] : 0) << bits; pos++; bits += 8; }
const e = lut[buf & (HUFF_SIZE - 1)];
if (!e) bad();
const l = e >> 8;
buf >>>= l; bits -= l; used += l;
out[o0 + Math.floor(i / tbw) * rowb + (i % tbw) * S + k] = e & 255;
}
}
if (Math.ceil(used / 8) !== end - p) bad();
}
const BLOCK_FORMATS = {
0x40: [4, 4, 8], 0x41: [4, 4, 16], 0x42: [4, 4, 16], 0x43: [4, 4, 8], 0x44: [4, 4, 8],
0x45: [4, 4, 16], 0x46: [4, 4, 16], 0x47: [4, 4, 16], 0x48: [4, 4, 16], 0x49: [4, 4, 16],
0x50: [4, 4, 8], 0x51: [4, 4, 8], 0x52: [4, 4, 16], 0x53: [4, 4, 8], 0x54: [4, 4, 8],
0x55: [4, 4, 16], 0x56: [4, 4, 16]
};
[[4, 4], [5, 4], [5, 5], [6, 5], [6, 6], [8, 5], [8, 6], [8, 8], [10, 5], [10, 6], [10, 8],
[10, 10], [12, 10], [12, 12]].forEach(([w, h], i) => { BLOCK_FORMATS[0x60 + i] = [w, h, 16]; });
function mipSize(t, m) {
return [Math.max(1, t.width >>> m), Math.max(1, t.height >>> m),
t.shape === 2 ? Math.max(1, t.depth >>> m) : 1];
}
function surfaceBytes(t, m) {
const [w, h, d] = mipSize(t, m);
if (t.payload === 1) {
const [bw, bh, S] = BLOCK_FORMATS[t.format];
return Math.ceil(w / bw) * Math.ceil(h / bh) * d * S;
}
return w * h * d * t.channels * (t.sample === 0 ? 1 : t.sample === 3 ? 4 : 2);
}
/* Decodes one surface (v2) into a typed array. */
function decodeSurface(b, t, s, verify) {
const m = s % t.mipLevels, [w, h, d] = mipSize(t, m);
const off = t._off[s], size = t._size[s], blocks = t.payload === 1;
let tcx, tcy, nbx = 0, nby = 0, S = 0;
if (blocks) {
const f = BLOCK_FORMATS[t.format];
nbx = Math.ceil(w / f[0]); nby = Math.ceil(h / f[1]); S = f[2];
tcx = Math.ceil(nbx / BLOCK_TILE); tcy = Math.ceil(nby / BLOCK_TILE);
} else {
tcx = Math.ceil(w / TILE); tcy = Math.ceil(h / TILE);
}
const tiles = tcx * tcy * d;
if (size < 4) bad();
const bl = r32(b, off);
if (bl > size - 4 || Math.floor((size - 4 - bl) / 8) < tiles) bad();
const pre = 4 + bl;
if (verify && checksum(b, off, pre + tiles * 8) !== t._tcheck[s]) fail('checksum', 'damaged file: a checksum does not match');
let book = null;
if (bl) {
if (!(t.needs & NEED.ENTROPY)) bad();
book = readBook(b, off + 4, bl);
}
const table = off + pre, end = off + size;
let pos = table + tiles * 8;
const sizes = new Array(tiles), packed = new Array(tiles), starts = new Array(tiles);
for (let i = 0; i < tiles; i++) {
let sz = r32(b, table + i * 8);
packed[i] = sz >>> 31;
sz &= 0x7FFFFFFF;
if (packed[i] && (!blocks || !book)) bad();
if (sz > end - pos) bad();
starts[i] = pos; sizes[i] = sz; pos += sz;
}
if (pos !== end) bad();
const bytes = surfaceBytes(t, m);
let out, store;
if (blocks || t.sample === 0) out = store = new Uint8Array(bytes);
else if (t.sample === 3) { store = new Uint32Array(bytes / 4); out = new Float32Array(store.buffer); }
else out = store = new Uint16Array(bytes / 2);
for (let i = 0; i < tiles; i++) {
const p = starts[i], e = p + sizes[i];
if (verify && checksum(b, p, sizes[i]) !== r32(b, table + i * 8 + 4))
fail('checksum', 'damaged file: a checksum does not match');
const row = Math.floor(i / tcx), z = Math.floor(row / tcy), ty = row % tcy, tx = i % tcx;
if (blocks) {
const bx0 = tx * BLOCK_TILE, by0 = ty * BLOCK_TILE, rowb = nbx * S;
decodeBlockTile(b, p, e, packed[i], store, z * nby * rowb + by0 * rowb + bx0 * S, rowb,
Math.min(BLOCK_TILE, nbx - bx0), Math.min(BLOCK_TILE, nby - by0), S, book);
} else {
const x0 = tx * TILE, y0 = ty * TILE, tw = Math.min(TILE, w - x0), th = Math.min(TILE, h - y0);
const slice = w * h * t.channels;
const view = store.subarray(z * slice, (z + 1) * slice);
if (t.sample === 0) decodeTile(b, p, e, view, w, x0, y0, tw, th, t.channels, true, book);
else decodeTileWide(b, p, e, view, w, x0, y0, tw, th, t.channels, t.sample, book);
}
}
return out;
}
/*
* decodeTexture(bytes, { verify = true }) — any YAPF file (v1 or v2).
* Returns { version, width, height, depth, layers, frames, faces, shape,
* mipLevels, payload, format, sample, channels, flags, channelMap,
* linearMask, normal, primaries, transfer, mipFilter, alphaRef,
* loopCount, frameUs, notes: [{ tag, data }], surfaces: [...] }, where
* surfaces[s] is a Uint8Array (8-bit pixels, GPU blocks), Uint16Array
* (16-bit, half bits) or Float32Array, numbered
* s = ((frame · layers + layer) · faces + face) · mipLevels + mip.
* Errors are YapfError with .code 'not-yapf', 'version', 'unsupported',
* 'corrupt', 'checksum' or 'truncated'.
*/
function decodeTexture(input, opts) {
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
const verify = !(opts && opts.verify === false);
if (b.length < 5 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
fail('not-yapf', 'not a YAPF file');
if (b[4] === 1) {
const img = decodeV1(b);
return { version: 1, width: img.width, height: img.height, depth: 1, layers: 1, frames: 1,
faces: 1, shape: 0, mipLevels: img.mipLevels, payload: 0, format: img.gpuFormat,
sample: 0, channels: img.channels, flags: img.flags, channelMap: [0, 0, 0, 0],
linearMask: 0, normal: 0, primaries: 0, transfer: 0, mipFilter: 0, alphaRef: 0,
loopCount: 0, frameUs: [], notes: [], surfaces: img.mips };
}
if (b[4] !== 2) fail('version', 'format version ' + b[4] + ' is newer than this decoder');
if (b.length < 64) fail('truncated', 'the file is truncated');
if (verify && checksum(b, 0, 0x3C) !== r32(b, 0x3C)) fail('checksum', 'damaged file: a checksum does not match');
const t = {
version: 2, channels: b[5], format: b[6], mipLevels: b[7], flags: b[8], payload: b[9],
sample: b[10], shape: b[11], width: r32(b, 0x0C), height: r32(b, 0x10), depth: r32(b, 0x14),
layers: r16(b, 0x18), frames: r16(b, 0x1A), needs: r32(b, 0x1C),
channelMap: Array.from(b.subarray(0x20, 0x24)), linearMask: b[0x24], normal: b[0x25],
primaries: b[0x26], transfer: b[0x27], mipFilter: b[0x28], alphaRef: b[0x29],
loopCount: r16(b, 0x2A), frameUs: [], notes: []
};
const notesSize = r32(b, 0x2C), fileSize = r64(b, 0x30);
if (t.needs & ~NEED.ALL) fail('unsupported', 'the file needs a feature this decoder does not have');
t.faces = t.shape === 1 ? 6 : 1;
if (t.channels < 1 || t.channels > 4 || t.sample > 3 || t.payload > 1 || t.shape > 2 ||
t.width < 1 || t.width > MAX_DIM || t.height < 1 || t.height > MAX_DIM ||
!t.layers || !t.frames || t.mipLevels < 1 || t.mipLevels > MAX_MIPS) bad();
if (t.shape === 2 ? (t.depth < 1 || t.depth > MAX_DIM || t.layers !== 1) : t.depth > 1) bad();
if (t.shape !== 2) t.depth = 1;
if (t.shape === 1 && t.width !== t.height) bad();
if (t.payload === 1 && !BLOCK_FORMATS[t.format]) bad();
if ((t.payload === 0 && t.sample !== 0 && !(t.needs & NEED.WIDE)) ||
(t.payload === 1 && !(t.needs & NEED.BLOCKS)) ||
((t.shape !== 0 || t.layers > 1) && !(t.needs & NEED.LAYOUT)) ||
(t.frames > 1 && !(t.needs & NEED.FRAMES))) bad();
const S = t.frames * t.layers * t.faces * t.mipLevels;
if (S > (1 << 24)) bad();
const front = 64 + 16 * S + (t.frames > 1 ? 4 * t.frames : 0) + notesSize;
if (front > fileSize) bad();
if (b.length < fileSize) fail('truncated', 'the file is truncated');
if (verify && checksum(b, 64, front - 64) !== r32(b, 0x38)) fail('checksum', 'damaged file: a checksum does not match');
let q = 64 + 16 * S;
for (let f = 0; t.frames > 1 && f < t.frames; f++, q += 4) t.frameUs.push(r32(b, q));
const qe = q + notesSize;
while (q < qe) {
if (qe - q < 8) bad();
const sz = r32(b, q + 4);
if (qe - q - 8 < sz) bad();
t.notes.push({ tag: String.fromCharCode(b[q], b[q + 1], b[q + 2], b[q + 3]), data: b.slice(q + 8, q + 8 + sz) });
q += 8 + sz;
}
t._off = []; t._size = []; t._tcheck = [];
for (let s = 0; s < S; s++) {
const e = 64 + 16 * s, off = r64(b, e), size = r32(b, e + 8);
if (off < front || off > fileSize || size > fileSize - off) bad();
t._off.push(off); t._size.push(size); t._tcheck.push(r32(b, e + 12));
}
t.surfaces = [];
for (let s = 0; s < S; s++) t.surfaces.push(decodeSurface(b, t, s, verify));
delete t._off; delete t._size; delete t._tcheck;
return t;
}
/* ── GPU block decoding (previews) ───────────────────────────────── */
const PART2 = [0xCCCC, 0x8888, 0xEEEE, 0xECC8, 0xC880, 0xFEEC, 0xFEC8, 0xEC80, 0xC800, 0xFFEC, 0xFE80, 0xE800,
0xFFE8, 0xFF00, 0xFFF0, 0xF000, 0xF710, 0x008E, 0x7100, 0x08CE, 0x008C, 0x7310, 0x3100, 0x8CCE, 0x088C, 0x3110,
0x6666, 0x366C, 0x17E8, 0x0FF0, 0x718E, 0x399C, 0xAAAA, 0xF0F0, 0x5A5A, 0x33CC, 0x3C3C, 0x55AA, 0x9696, 0xA55A,
0x73CE, 0x13C8, 0x324C, 0x3BDC, 0x6996, 0xC33C, 0x9966, 0x0660, 0x0272, 0x04E4, 0x4E40, 0x2720, 0xC936, 0x936C,
0x39C6, 0x639C, 0x9336, 0x9CC6, 0x817E, 0xE718, 0xCCF0, 0x0FCC, 0x7744, 0xEE22];
const PART3 = [0xAA685050, 0x6A5A5040, 0x5A5A4200, 0x5450A0A8, 0xA5A50000, 0xA0A05050, 0x5555A0A0, 0x5A5A5050,
0xAA550000, 0xAA555500, 0xAAAA5500, 0x90909090, 0x94949494, 0xA4A4A4A4, 0xA9A59450, 0x2A0A4250, 0xA5945040,
0x0A425054, 0xA5A5A500, 0x55A0A0A0, 0xA8A85454, 0x6A6A4040, 0xA4A45000, 0x1A1A0500, 0x0050A4A4, 0xAAA59090,
0x14696914, 0x69691400, 0xA08585A0, 0xAA821414, 0x50A4A450, 0x6A5A0200, 0xA9A58000, 0x5090A0A8, 0xA8A09050,
0x24242424, 0x00AA5500, 0x24924924, 0x24499224, 0x50A50A50, 0x500AA550, 0xAAAA4444, 0x66660000, 0xA5A0A5A0,
0x50A050A0, 0x69286928, 0x44AAAA44, 0x66666600, 0xAA444444, 0x54A854A8, 0x95809580, 0x96969600, 0xA85454A8,
0x80959580, 0xAA141414, 0x96960000, 0xAAAA1414, 0xA05050A0, 0xA0A5A5A0, 0x96000000, 0x40804080, 0xA9A8A9A8,
0xAAAAAA44, 0x2A4A5254];
const ANCHOR2 = [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, 2, 2,
8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15];
const ANCHOR3A = [3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, 8, 6, 8, 5, 15,
15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3];
const ANCHOR3B = [15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, 6, 10,
15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 3, 15, 15, 8];
const WEIGHTS = { 2: [0, 21, 43, 64], 3: [0, 9, 18, 27, 37, 46, 55, 64],
4: [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64] };
const interp = (a, b, w) => (a * (64 - w) + b * w + 32) >> 6;
function bits128(b, o) {
let pos = 0;
return (n) => {
let v = 0;
for (let i = 0; i < n; i++, pos++) v |= ((b[o + (pos >> 3)] >> (pos & 7)) & 1) << i;
return v >>> 0;
};
}
function rgb565(c) {
const r = (c >> 11) & 31, g = (c >> 5) & 63, bl = c & 31;
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (bl << 3) | (bl >> 2)];
}
function bc1Block(b, o, out, fourOnly) {
const c0 = r16(b, o), c1 = r16(b, o + 2), idx = r32(b, o + 4), four = c0 > c1 || fourOnly;
const p0 = rgb565(c0), p1 = rgb565(c1), pal = [p0.concat(255), p1.concat(255), [0, 0, 0, 255], [0, 0, 0, 255]];
for (let k = 0; k < 3; k++) {
if (four) { pal[2][k] = ((2 * p0[k] + p1[k] + 1) / 3) | 0; pal[3][k] = ((p0[k] + 2 * p1[k] + 1) / 3) | 0; }
else { pal[2][k] = (p0[k] + p1[k] + 1) >> 1; pal[3][k] = 0; }
}
if (!four) pal[3][3] = 0;
for (let i = 0; i < 16; i++) { const c = pal[(idx >>> (2 * i)) & 3]; for (let k = 0; k < 4; k++) out[i * 4 + k] = c[k]; }
}
function bc4Block(b, o, out, ch, snorm) {
const pal = new Array(8);
let r0 = b[o], r1 = b[o + 1];
if (snorm) { r0 = Math.max(-127, (r0 << 24) >> 24); r1 = Math.max(-127, (r1 << 24) >> 24); }
pal[0] = r0; pal[1] = r1;
const div = (v, d) => (v >= 0 ? Math.floor((v + (d >> 1)) / d) : -Math.floor((-v + (d >> 1)) / d));
if (r0 > r1) for (let i = 2; i < 8; i++) pal[i] = div((8 - i) * r0 + (i - 1) * r1, 7);
else { for (let i = 2; i < 6; i++) pal[i] = div((6 - i) * r0 + (i - 1) * r1, 5); pal[6] = snorm ? -127 : 0; pal[7] = snorm ? 127 : 255; }
let lo = r32(b, o + 2), hi = r16(b, o + 6);
for (let i = 0; i < 16; i++) {
const bit = 3 * i, v = bit < 30 ? (lo >>> bit) & 7 : bit === 30 ? ((lo >>> 30) | ((hi & 1) << 2)) : (hi >> (bit - 32)) & 7;
out[i * 4 + ch] = pal[v] + (snorm ? 128 : 0);
}
}
const BC7 = [[3, 4, 0, 0, 4, 0, 1, 0, 3, 0], [2, 6, 0, 0, 6, 0, 0, 1, 3, 0], [3, 6, 0, 0, 5, 0, 0, 0, 2, 0],
[2, 6, 0, 0, 7, 0, 1, 0, 2, 0], [1, 0, 2, 1, 5, 6, 0, 0, 2, 3], [1, 0, 2, 0, 7, 8, 0, 0, 2, 2],
[1, 0, 0, 0, 7, 7, 1, 0, 4, 0], [2, 6, 0, 0, 5, 5, 1, 0, 2, 0]];
const subsetOf = (ns, part, i) => ns === 2 ? (PART2[part] >> i) & 1 : ns === 3 ? (PART3[part] >>> (2 * i)) & 3 : 0;
const isAnchor = (ns, part, i) => i === 0 || (ns === 2 && i === ANCHOR2[part]) ||
(ns === 3 && (i === ANCHOR3A[part] || i === ANCHOR3B[part]));
function bc7Block(b, o, out) {
const get = bits128(b, o);
let mode = 0;
while (mode < 8 && !get(1)) mode++;
if (mode === 8) { out.fill(0, 0, 64); return; }
const [ns, pb, rb, isbB, cb, ab, epb, spb, ib, ib2] = BC7[mode];
const part = get(pb), rot = get(rb), isb = get(isbB), ne = ns * 2;
const ep = []; for (let e = 0; e < ne; e++) ep.push([0, 0, 0, 255]);
for (let c = 0; c < 3; c++) for (let e = 0; e < ne; e++) ep[e][c] = get(cb);
for (let e = 0; e < ne; e++) ep[e][3] = ab ? get(ab) : 255;
const pbit = epb | spb;
if (epb) for (let e = 0; e < ne; e++) { const p = get(1); for (let c = 0; c < 4; c++) if (c < 3 || ab) ep[e][c] = (ep[e][c] << 1) | p; }
else if (spb) for (let k = 0; k < ns; k++) { const p = get(1); for (let c = 0; c < 3; c++) { ep[2 * k][c] = (ep[2 * k][c] << 1) | p; ep[2 * k + 1][c] = (ep[2 * k + 1][c] << 1) | p; } }
for (let e = 0; e < ne; e++) for (let c = 0; c < 4; c++) {
if (c === 3 && !ab) continue;
const nb = (c < 3 ? cb : ab) + pbit, v = ep[e][c] << (8 - nb);
ep[e][c] = v | (v >> nb);
}
const idx = [], idx2 = [];
for (let i = 0; i < 16; i++) idx.push(get(ib - (isAnchor(ns, part, i) ? 1 : 0)));
for (let i = 0; i < 16; i++) idx2.push(ib2 ? get(ib2 - (i === 0 ? 1 : 0)) : 0);
const w1 = WEIGHTS[ib], w2 = WEIGHTS[ib2 || 2];
for (let i = 0; i < 16; i++) {
const k = subsetOf(ns, part, i), A = ep[2 * k], B = ep[2 * k + 1];
let cw, aw;
if (!ib2) cw = aw = w1[idx[i]];
else if (!isb) { cw = w1[idx[i]]; aw = w2[idx2[i]]; }
else { cw = w2[idx2[i]]; aw = w1[idx[i]]; }
const px = [interp(A[0], B[0], cw), interp(A[1], B[1], cw), interp(A[2], B[2], cw), interp(A[3], B[3], aw)];
if (rot) { const t = px[3]; px[3] = px[rot - 1]; px[rot - 1] = t; }
for (let c = 0; c < 4; c++) out[i * 4 + c] = px[c];
}
}
/* BC6H header layouts: [field, first bit, count]; fields 0–3 red w x y z,
* 4–7 green, 8–11 blue, 12 partition. */
const RW = 0, RX = 1, RY = 2, RZ = 3, GW = 4, GX = 5, GY = 6, GZ = 7, BW = 8, BX = 9, BY = 10, BZ = 11, PD = 12;
const BC6 = [
[0x00, 10, 5, 5, 5, [[GY,4,1],[BY,4,1],[BZ,4,1],[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
[0x01, 7, 6, 6, 6, [[GY,5,1],[GZ,4,1],[GZ,5,1],[RW,0,7],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,7],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,7],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
[0x02, 11, 5, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[RW,10,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
[0x06, 11, 4, 5, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[GZ,4,1],[GY,0,4],[GX,0,5],[GW,10,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,4],[BZ,0,1],[BZ,2,1],[RZ,0,4],[GY,4,1],[BZ,3,1],[PD,0,5]]],
[0x0A, 11, 4, 4, 5, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[BY,4,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,5],[BW,10,1],[BY,0,4],[RY,0,4],[BZ,1,1],[BZ,2,1],[RZ,0,4],[BZ,4,1],[BZ,3,1],[PD,0,5]]],
[0x0E, 9, 5, 5, 5, [[RW,0,9],[BY,4,1],[GW,0,9],[GY,4,1],[BW,0,9],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
[0x12, 8, 6, 5, 5, [[RW,0,8],[GZ,4,1],[BY,4,1],[GW,0,8],[BZ,2,1],[GY,4,1],[BW,0,8],[BZ,3,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
[0x16, 8, 5, 6, 5, [[RW,0,8],[BZ,0,1],[BY,4,1],[GW,0,8],[GY,5,1],[GY,4,1],[BW,0,8],[GZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
[0x1A, 8, 5, 5, 6, [[RW,0,8],[BZ,1,1],[BY,4,1],[GW,0,8],[BY,5,1],[GY,4,1],[BW,0,8],[BZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
[0x1E, 6, 6, 6, 6, [[RW,0,6],[GZ,4,1],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,6],[GY,5,1],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,6],[GZ,5,1],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
[0x03, 10, 10, 10, 10, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,10],[GX,0,10],[BX,0,10]]],
[0x07, 11, 9, 9, 9, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,9],[RW,10,1],[GX,0,9],[GW,10,1],[BX,0,9],[BW,10,1]]],
[0x0B, 12, 8, 8, 8, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,8],[RW,11,1],[RW,10,1],[GX,0,8],[GW,11,1],[GW,10,1],[BX,0,8],[BW,11,1],[BW,10,1]]],
[0x0F, 16, 4, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,15,1],[RW,14,1],[RW,13,1],[RW,12,1],[RW,11,1],[RW,10,1],[GX,0,4],[GW,15,1],[GW,14,1],[GW,13,1],[GW,12,1],[GW,11,1],[GW,10,1],[BX,0,4],[BW,15,1],[BW,14,1],[BW,13,1],[BW,12,1],[BW,11,1],[BW,10,1]]]
];
const sext = (v, n) => { const m = 1 << (n - 1); v &= (1 << n) - 1; return (v ^ m) - m; };
function bc6Unq(v, n, sgn) {
if (!sgn) {
if (n >= 15) return v;
if (v === 0) return 0;
if (v === (1 << n) - 1) return 0xFFFF;
return ((v << 16) + 0x8000) >> n;
}
if (n >= 16) return v;
const s = v < 0; if (s) v = -v;
const u = v === 0 ? 0 : v >= (1 << (n - 1)) - 1 ? 0x7FFF : ((v << 15) + 0x4000) >> (n - 1);
return s ? -u : u;
}
function bc6Finish(v, sgn) {
if (!sgn) return (v * 31) >> 6;
v = v < 0 ? -(((-v) * 31) >> 5) : (v * 31) >> 5;
return v < 0 ? 0x8000 | -v : v;
}
function bc6Block(b, o, out, sgn) {
const get = bits128(b, o);
let code = get(2);
if (code > 1) code |= get(3) << 2;
const m = BC6.findIndex((x) => x[0] === code);
if (m < 0) { out.fill(0, 0, 48); return; }
const [, W, dr, dg, db, seg] = BC6[m], f = new Array(13).fill(0);
for (const [fi, lo, n] of seg) f[fi] |= get(n) << lo;
const regions = m < 10 ? 2 : 1, ne = regions * 2, dbits = [dr, dg, db], ep = [[], [], [], []];
for (let c = 0; c < 3; c++) {
for (let e = 0; e < ne; e++) ep[e][c] = f[c * 4 + e];
if (sgn) ep[0][c] = sext(ep[0][c], W);
const delta = m !== 9 && m !== 10;
for (let e = 1; e < ne; e++) {
if (delta || sgn) ep[e][c] = sext(ep[e][c], dbits[c]);
if (delta) { ep[e][c] = (ep[e][c] + ep[0][c]) & ((1 << W) - 1); if (sgn) ep[e][c] = sext(ep[e][c], W); }
}
for (let e = 0; e < ne; e++) ep[e][c] = bc6Unq(ep[e][c], W, sgn);
}
const part = f[PD], ib = regions === 2 ? 3 : 4, wt = WEIGHTS[ib];
for (let i = 0; i < 16; i++) {
const k = regions === 2 ? (PART2[part] >> i) & 1 : 0;
const anchor = i === 0 || (regions === 2 && i === ANCHOR2[part]);
const ix = get(ib - (anchor ? 1 : 0));
for (let c = 0; c < 3; c++) out[i * 3 + c] = bc6Finish(interp(ep[2 * k][c], ep[2 * k + 1][c], wt[ix]), sgn);
}
}
/* decodeBlocks(format, blocks, w, h) → RGBA Uint8Array (LDR) or RGB
* half-bits Uint16Array (BC6H); null for ETC2 / EAC / ASTC. */
function decodeBlocks(format, blocks, w, h) {
if (format >= 0x50 || !BLOCK_FORMATS[format]) return null;
const S = BLOCK_FORMATS[format][2], nbx = Math.ceil(w / 4), nby = Math.ceil(h / 4);
const hdr = format === 0x47 || format === 0x48;
const out = hdr ? new Uint16Array(w * h * 3) : new Uint8Array(w * h * 4);
const px = new Int32Array(64), hx = new Int32Array(48);
let o = 0;
for (let by = 0; by < nby; by++)
for (let bx = 0; bx < nbx; bx++, o += S) {
switch (format) {
case 0x40: bc1Block(blocks, o, px, false); break;
case 0x41:
bc1Block(blocks, o + 8, px, true);
for (let i = 0; i < 16; i++) px[i * 4 + 3] = ((blocks[o + (i >> 1)] >> (4 * (i & 1))) & 15) * 17;
break;
case 0x42: bc1Block(blocks, o + 8, px, true); bc4Block(blocks, o, px, 3, false); break;
case 0x43: case 0x44:
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x44);
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
break;
case 0x45: case 0x46:
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x46); bc4Block(blocks, o + 8, px, 1, format === 0x46);
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
break;
case 0x49: bc7Block(blocks, o, px); break;
default: bc6Block(blocks, o, hx, format === 0x48);
}
for (let y = 0; y < 4 && by * 4 + y < h; y++)
for (let x = 0; x < 4 && bx * 4 + x < w; x++) {
const d = (by * 4 + y) * w + bx * 4 + x, i = y * 4 + x;
if (hdr) for (let c = 0; c < 3; c++) out[d * 3 + c] = hx[i * 3 + c];
else for (let c = 0; c < 4; c++) out[d * 4 + c] = px[i * 4 + c];
}
}
return out;
}
function halfToFloat(h) {
const s = h & 0x8000 ? -1 : 1, e = (h >> 10) & 31, m = h & 0x3FF;
if (e === 0) return s * m * Math.pow(2, -24);
if (e === 31) return m ? NaN : s * Infinity;
return s * (1 + m / 1024) * Math.pow(2, e - 15);
}
const linToSrgb = (v) => !(v > 0) ? 0 : v >= 1 ? 255
: Math.round(255 * (v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055));
/*
* textureToRGBA(tex, surface = 0, slice = 0) → { width, height,
* rgba: Uint8ClampedArray } for display: gray expanded, 16-bit rounded,
* half / float clamped and sRGB encoded, GPU blocks decoded. Throws for
* ETC2 / EAC / ASTC.
*/
function textureToRGBA(tex, surface, slice) {
surface = surface || 0; slice = slice || 0;
const m = surface % tex.mipLevels, [w, h, d] = mipSize(tex, m), n = w * h;
const data = tex.surfaces[surface], rgba = new Uint8ClampedArray(n * 4);
let src, ch = tex.channels, kind = tex.payload === 1 ? 'u8' : ['u8', 'u16', 'f16', 'f32'][tex.sample];
if (tex.payload === 1) {
const per = data.length / d, blk = decodeBlocks(tex.format, data.subarray(per * slice, per * (slice + 1)), w, h);
if (!blk) fail('unsupported', 'ETC2 / EAC / ASTC blocks cannot be shown');
if (blk instanceof Uint16Array) { src = blk; ch = 3; kind = 'f16'; }
else {
rgba.set(blk);
if (tex.channels === 3) for (let i = 0; i < n; i++) rgba[i * 4 + 3] = 255;
return { width: w, height: h, rgba };
}
} else {
src = data.subarray(n * ch * slice, n * ch * (slice + 1));
}
for (let i = 0; i < n; i++) {
const v = [0, 0, 0, 255];
for (let c = 0; c < ch; c++) {
const x = src[i * ch + c], alpha = (ch === 2 && c === 1) || (ch === 4 && c === 3);
let q;
if (kind === 'u8') q = x;
else if (kind === 'u16') q = Math.round(x / 257);
else {
const f = kind === 'f16' ? halfToFloat(x) : x;
q = alpha ? Math.max(0, Math.min(255, Math.round(f * 255))) : linToSrgb(f);
}
v[ch <= 2 ? (c === 0 ? 0 : 3) : c] = q;
}
if (ch <= 2) v[1] = v[2] = v[0];
rgba.set(v, i * 4);
}
return { width: w, height: h, rgba };
}
function surfaceIndex(tex, frame, layer, face, mip) {
return (((frame || 0) * tex.layers + (layer || 0)) * tex.faces + (face || 0)) * tex.mipLevels + (mip || 0);
}
/* ── Decoder ──────────────────────────────────────────────────────── */
function unpackPlane(b, p, end, z, tw, th) {
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
if (end - p < nb) corrupt();
const filt = b.subarray(p, p + nb);
p += nb;
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) corrupt();
const gw = new Uint8Array(ng + 64);
let data = 0, g = 0, hi = 0, w = 0;
while (g < ng) {
if (p >= end) corrupt();
const c = (b[p] >> (hi * 4)) & 15, n = NIB_GROUPS[c];
p += hi; hi ^= 1;
if (c < NIB_REPEAT) w = c;
gw.fill(w, g, g + n);
data += w * n; g += n;
}
if (g !== ng) corrupt();
if (hi) { if (b[p] >> 4) corrupt(); p++; }
if (end - p < data) corrupt();
for (g = 0; g < ng; g++) {
w = gw[g];
const o = g * GROUP, mask = (1 << w) - 1;
let acc = 0, bits = 0;
for (let i = 0; i < GROUP; i++) {
while (bits < w) { acc |= b[p++] << bits; bits += 8; }
z[o + i] = acc & mask;
acc >>>= w; bits -= w;
}
}
return { p, filt };
}
function unfilterRow(f, R, r0, U, u0, K, k0, Z, z0, tw) {
if (f === F_LEFT) {
let L = 0;
for (let x = 0; x < tw; x++) R[r0 + x] = L = (L + unzz(Z[z0 + x])) & 255;
} else if (f === F_GRAD) {
let L = (U[u0] + unzz(Z[z0])) & 255;
R[r0] = L;
for (let x = 1; x < tw; x++)
R[r0 + x] = L = (L + U[u0 + x] - U[u0 + x - 1] + unzz(Z[z0 + x])) & 255;
} else {
/* pred = (a + b) >> 1: Up k a = b = K; Up-right a = b = U + 1;
* Up-average a = U, b = U + 1. Rows carry one extra sample. */
let A, a0, B, b0;
if (f >= F_UP) { A = K; a0 = k0; B = K; b0 = k0; }
else if (f === F_UPAVG) { A = U; a0 = u0; B = U; b0 = u0 + 1; }
else { A = U; a0 = u0 + 1; B = U; b0 = u0 + 1; }
for (let x = 0; x < tw; x++)
R[r0 + x] = (((A[a0 + x] + B[b0 + x]) >> 1) + unzz(Z[z0 + x])) & 255;
}
R[r0 + tw] = R[r0 + tw - 1];
}
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch, v2, book) {
if (end <= p) corrupt();
let hdr = b[p++];
if (hdr === STORED) {
const rowb = tw * ch;
if (end - p !== rowb * th) corrupt();
for (let y = 0; y < th; y++)
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return;
}
if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
if ((hdr & 0x40) && (hdr & SUBGREEN)) corrupt();
/* xf: 0 YCoCg-R, 1 subtract-green, 2 none (v2) */
const xf = (hdr & 0x40) ? 2 : (hdr & SUBGREEN) ? 1 : 0, ent = hdr & 0x20;
hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
let emask = 0;
if (ent) { /* v2: entropy planes */
if (p >= end || !book) corrupt();
emask = b[p++];
if ((emask >> ch) || (emask & hdr)) corrupt();
}
const rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen);
const z = [], filt = [null, null, null, null];
for (let c = 0; c < ch; c++) {
if (!(hdr & (1 << c))) continue;
if (p >= end) corrupt();
rows.fill(b[p++], c * planeLen, (c + 1) * planeLen);
}
for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue;
z[c] = new Uint8Array(TILE_PX + GROUP);
const r = (emask & (1 << c)) ? huffPlane(b, p, end, z[c], tw, th, book, xf * 16 + c)
: unpackPlane(b, p, end, z[c], tw, th);
p = r.p; filt[c] = r.filt;
}
if (p !== end) corrupt();
for (let y = 0; y < th; y++) {
const ri = (y % RING) * rowLen;
for (let c = 0; c < ch; c++) {
if (!filt[c]) continue;
const f = filt[c][y >> 3], k = upK(f), base = c * planeLen;
unfilterRow(f, rows, base + ri,
y ? rows : ZERO_ROW, y ? base + ((y - 1) % RING) * rowLen : 0,
y >= k ? rows : ZERO_ROW, y >= k ? base + ((y - k) % RING) * rowLen : 0,
z[c], y * tw, tw);
}
let d = ((ty + y) * imgW + tx) * ch;
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) {
if (xf === 2) {
out[d] = rows[P0 + x];
out[d + 1] = rows[P1 + x];
out[d + 2] = rows[P2 + x];
} else if (xf) {
const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g;
out[d + 2] = rows[P2 + x] + g - 128;
} else {
const co = (rows[P1 + x] - 128) & 255, cg = (rows[P2 + x] - 128) & 255;
const t = (rows[P0 + x] - (s8(cg) >> 1)) & 255;
const bb = (t - (s8(co) >> 1)) & 255;
out[d] = bb + co;
out[d + 1] = cg + t;
out[d + 2] = bb;
}
if (ch === 4) out[d + 3] = rows[P3 + x];
} else {
out[d] = rows[P0 + x];
if (ch === 2) out[d + 1] = rows[P1 + x];
}
}
}
}
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
/* decode(bytes) → 8-bit image. Reads version 1, and version 2 files
* that hold 8-bit pixels (2D; cube face 0 / layer 0 / frame 0). Other
* textures: see decodeTexture. */
function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
if (b.length >= 5 && b[0] === 0x59 && b[1] === 0x41 && b[2] === 0x50 && b[3] === 0x46 && b[4] === 2) {
const t = decodeTexture(b);
if (t.payload !== 0 || t.sample !== 0 || t.shape === 2)
throw new YapfError('unsupported', 'this is a YAPF 2 texture (GPU blocks, 16-bit / float or a ' +
'volume); use YAPF.decodeTexture and YAPF.textureToRGBA');
const mips = t.surfaces.slice(0, t.mipLevels);
return { width: t.width, height: t.height, channels: t.channels, gpuFormat: t.format,
flags: t.flags, mipLevels: t.mipLevels, pixels: mips[0], mips };
}
return decodeV1(b);
}
function decodeV1(b) {
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
throw new YapfError('not-yapf', 'not a YAPF file');
if (b[4] !== 1) throw new YapfError('version', 'unsupported format version ' + b[4]);
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
const idxEnd = 20 + 8 * mipLevels;
if (b.length < idxEnd) corrupt();
const mips = [];
for (let m = 0; m < mipLevels; m++) {
const off = r32(b, 20 + 8 * m), len = r32(b, 24 + 8 * m);
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
if (off < idxEnd || off > b.length || len > b.length - off) corrupt();
const out = new Uint8Array(mw * mh * ch);
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
if (len / 4 < tcount) corrupt();
let pos = off + tcount * 4;
for (let t = 0; t < tcount; t++) {
const sz = r32(b, off + t * 4);
if (sz > off + len - pos) corrupt();
const tx = (t % tcx) * TILE, ty = Math.floor(t / tcx) * TILE;
decodeTile(b, pos, pos + sz, out, mw, tx, ty,
Math.min(TILE, mw - tx), Math.min(TILE, mh - ty), ch);
pos += sz;
}
mips.push(out);
}
return { width: W, height: H, channels: ch, gpuFormat: b[6], flags: b[8],
mipLevels, pixels: mips[0], mips };
}
/* ── Encoder ──────────────────────────────────────────────────────── */
class Out {
constructor(n) { this.buf = new Uint8Array(Math.max(n, 1024)); this.len = 0; }
reserve(n) {
if (this.len + n <= this.buf.length) return;
let c = this.buf.length * 2;
while (c < this.len + n) c *= 2;
const nb = new Uint8Array(c); nb.set(this.buf.subarray(0, this.len)); this.buf = nb;
}
push(v) { this.reserve(1); this.buf[this.len++] = v; }
}
function predictRow(f, P, r0, u0, k0, pred, tw, U, K) {
switch (f) {
case F_LEFT: pred[0] = 0; for (let x = 1; x < tw; x++) pred[x] = P[r0 + x - 1]; break;
case F_GRAD:
pred[0] = U[u0];
for (let x = 1; x < tw; x++) pred[x] = (P[r0 + x - 1] + U[u0 + x] - U[u0 + x - 1]) & 255;
break;
case F_UPRIGHT:
for (let x = 0; x + 1 < tw; x++) pred[x] = U[u0 + x + 1];
pred[tw - 1] = U[u0 + tw - 1];
break;
case F_UPAVG:
for (let x = 0; x + 1 < tw; x++) pred[x] = (U[u0 + x] + U[u0 + x + 1]) >> 1;
pred[tw - 1] = U[u0 + tw - 1];
break;
default: for (let x = 0; x < tw; x++) pred[x] = K[k0 + x];
}
}
/* Picks a filter per band; fills z and filt; returns estimated cost. */
function chooseFilters(P, tw, th, z, filt) {
const total = tw * th, nb = (th + BAND - 1) >> 3;
const tmp = new Uint8Array(BAND * TILE + GROUP), pred = new Uint8Array(TILE);
let sum = 0, prevFi = 0;
for (let b = 0; b < nb; b++) {
const y0 = b * BAND, y1 = Math.min(y0 + BAND, th), n = (y1 - y0) * tw;
let bestCost = Infinity, bestFi = 0;
for (let t = 0; t < FILTERS.length; t++) {
const fi = t === 0 ? prevFi : (t <= prevFi ? t - 1 : t);
const f = FILTERS[fi], k = upK(f), serial = f === F_LEFT || f === F_GRAD;
let cost = 0, done = 0, lost = false;
for (let y = y0; y < y1 && !lost; y++) {
const r0 = y * tw;
const U = y ? P : ZERO_ROW, u0 = y ? r0 - tw : 0;
const K = y >= k ? P : ZERO_ROW, k0 = y >= k ? r0 - k * tw : 0;
predictRow(f, P, r0, u0, k0, pred, tw, U, K);
const T = (y - y0) * tw;
for (let x = 0; x < tw; x++) tmp[T + x] = zz((P[r0 + x] - pred[x]) & 255);
const last = y + 1 === y1, end = last ? n : (y + 1 - y0) * tw;
if (last) tmp.fill(0, n, n + GROUP);
for (; done + GROUP <= end || (last && done < end); done += GROUP) {
let m = 0;
for (let i = 0; i < GROUP; i++) m |= tmp[done + i];
const w = width(m);
cost += w ? 16 * w + 8 : 2;
}
const c = serial ? cost + Math.floor(cost / 25) : cost;
if (c > bestCost || (c === bestCost && fi > bestFi)) lost = true;
}
if (lost) continue;
if (serial) cost += Math.floor(cost / 25);
if (cost < bestCost || (cost === bestCost && fi < bestFi)) {
bestCost = cost; bestFi = fi;
z.set(tmp.subarray(0, n), y0 * tw);
}
}
prevFi = bestFi;
filt[b] = FILTERS[bestFi];
sum += bestCost;
}
z.fill(0, total, total + GROUP);
return sum;
}
function emitPlane(o, z, filt, tw, th) {
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
o.reserve(nb + ng + ng * 8);
for (let i = 0; i < nb; i++) o.buf[o.len++] = filt[i];
const need = new Uint8Array(ng);
for (let g = 0; g < ng; g++) {
let m = 0;
for (let i = 0; i < GROUP; i++) m |= z[g * GROUP + i];
need[g] = width(m);
}
/* span[j][g] = widest need in [g, g + 2^j) */
const span = [need.slice()];
for (let j = 1; j < 7; j++) {
const prev = span[j - 1], cur = new Uint8Array(ng), h = 1 << (j - 1);
for (let g = 0; g + (1 << j) <= ng; g++) cur[g] = Math.max(prev[g], prev[g + h]);
span.push(cur);
}
const cost = new Uint32Array((ng + 1) * 9), pick = new Uint8Array(ng * 9);
const mx = new Uint8Array(7);
for (let g = ng - 1; g >= 0; g--) {
let lc = Infinity, lk = 0;
for (let w = need[g]; w <= 8; w++) {
const c = 1 + 2 * w + cost[(g + 1) * 9 + w];
if (c < lc) { lc = c; lk = w; }
}
let nl = 0;
for (; nl < 7 && g + RUN_LENS[nl] <= ng; nl++) {
const L = RUN_LENS[nl];
mx[nl] = L === 3 ? Math.max(span[1][g], need[g + 2]) : span[31 - Math.clz32(L)][g];
}
for (let pw = 0; pw <= 8; pw++) {
let bc = lc, bk = lk;
for (let li = 0; li < nl && mx[li] <= pw; li++) {
const L = RUN_LENS[li], c = 1 + 2 * pw * L + cost[(g + L) * 9 + pw];
if (c < bc) { bc = c; bk = NIB_REPEAT + li; }
}
cost[g * 9 + pw] = bc;
pick[g * 9 + pw] = bk;
}
}
const gw = new Uint8Array(ng), nib = [];
let pw = 0;
for (let g = 0; g < ng;) {
const k = pick[g * 9 + pw];
nib.push(k);
if (k < NIB_REPEAT) { pw = k; gw[g++] = k; }
else for (let n = 0; n < NIB_GROUPS[k]; n++) gw[g++] = pw;
}
for (let k = 0; k < nib.length; k += 2)
o.buf[o.len++] = nib[k] | (k + 1 < nib.length ? nib[k + 1] << 4 : 0);
for (let g = 0; g < ng; g++) {
const w = gw[g];
let acc = 0, bits = 0;
for (let i = 0; i < GROUP; i++) {
acc |= z[g * GROUP + i] << bits; bits += w;
while (bits >= 8) { o.buf[o.len++] = acc & 255; acc >>>= 8; bits -= 8; }
}
}
}
function encodeTile(o, sc, px, imgW, tx, ty, tw, th, ch) {
const total = tw * th, start = o.len, raw = total * ch, tries = ch >= 3 ? 2 : 1;
const mask = [0, 0], est = [0, 0];
for (let t = 0; t < tries; t++) {
const plane = sc.plane[t];
for (let y = 0; y < th; y++) {
let s = ((ty + y) * imgW + tx) * ch;
for (let x = 0; x < tw; x++, s += ch) {
const i = y * tw + x;
if (ch < 3) { for (let c = 0; c < ch; c++) plane[c][i] = px[s + c]; continue; }
const r = px[s], g = px[s + 1], b = px[s + 2];
if (t === 0) {
const oo = (r - b) & 255, tt = (b + (s8(oo) >> 1)) & 255, q = (g - tt) & 255;
plane[0][i] = (tt + (s8(q) >> 1)) & 255;
plane[1][i] = (oo + 128) & 255;
plane[2][i] = (q + 128) & 255;
} else {
plane[0][i] = g;
plane[1][i] = (r - g + 128) & 255;
plane[2][i] = (b - g + 128) & 255;
}
if (ch === 4) plane[3][i] = px[s + 3];
}
}
for (let c = 0; c < ch; c++) {
const P = plane[c];
let i = 1;
while (i < total && P[i] === P[0]) i++;
if (i === total) { mask[t] |= 1 << c; est[t] += 16; continue; }
est[t] += chooseFilters(P, tw, th, sc.z[t][c], sc.filt[t][c]);
}
}
const t = tries === 2 && est[1] < est[0] ? 1 : 0;
o.reserve(5);
o.buf[o.len++] = mask[t] | (t ? SUBGREEN : 0);
for (let c = 0; c < ch; c++) if (mask[t] & (1 << c)) o.buf[o.len++] = sc.plane[t][c][0];
for (let c = 0; c < ch; c++)
if (!(mask[t] & (1 << c))) emitPlane(o, sc.z[t][c], sc.filt[t][c], tw, th);
if (o.len - start > raw + 1) {
o.len = start;
o.reserve(raw + 1);
o.buf[o.len++] = STORED;
for (let y = 0; y < th; y++) {
const s = ((ty + y) * imgW + tx) * ch;
o.buf.set(px.subarray(s, s + tw * ch), o.len);
o.len += tw * ch;
}
}
}
function encodeLevel(px, mw, mh, ch) {
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
const o = new Out(tcount * 4 + (mw * mh * ch >> 1));
o.reserve(tcount * 4);
o.len = tcount * 4;
const mk = (n) => [0, 1, 2, 3].map(() => new Uint8Array(n));
const sc = {
plane: [mk(TILE_PX), mk(TILE_PX)],
z: [mk(TILE_PX + GROUP), mk(TILE_PX + GROUP)],
filt: [mk(TILE / BAND), mk(TILE / BAND)],
};
let t = 0;
for (let ty = 0; ty < tcy; ty++)
for (let tx = 0; tx < tcx; tx++, t++) {
const x0 = tx * TILE, y0 = ty * TILE, start = o.len;
encodeTile(o, sc, px, mw, x0, y0, Math.min(TILE, mw - x0), Math.min(TILE, mh - y0), ch);
const n = o.len - start;
o.buf[t * 4] = n & 255; o.buf[t * 4 + 1] = (n >>> 8) & 255;
o.buf[t * 4 + 2] = (n >>> 16) & 255; o.buf[t * 4 + 3] = (n >>> 24) & 255;
}
return o.buf.subarray(0, o.len);
}
function encode(img) {
const { width: W, height: H, channels: ch } = img;
const mips = img.mips && img.mips.length ? img.mips : [img.pixels];
const mipLevels = img.mipLevels || mips.length;
if (!(W >= 1 && W <= MAX_DIM && H >= 1 && H <= MAX_DIM && ch >= 1 && ch <= 4 &&
mipLevels >= 1 && mipLevels <= MAX_MIPS && mips.length >= mipLevels))
throw new Error('YAPF: invalid image');
const levels = [];
for (let m = 0; m < mipLevels; m++) {
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
const px = m === 0 ? img.pixels : mips[m];
if (!px || px.length < mw * mh * ch) throw new Error('YAPF: mip ' + m + ' has too few pixels');
levels.push(encodeLevel(px, mw, mh, ch));
}
const idxEnd = 20 + 8 * mipLevels;
let total = idxEnd;
for (const l of levels) total += l.length;
const out = new Uint8Array(total);
out.set([0x59, 0x41, 0x50, 0x46, 1, ch, img.gpuFormat || 0, mipLevels, img.flags || 0]);
const w32 = (p, v) => { out[p] = v & 255; out[p + 1] = (v >>> 8) & 255;
out[p + 2] = (v >>> 16) & 255; out[p + 3] = (v >>> 24) & 255; };
w32(12, W); w32(16, H);
let off = idxEnd;
levels.forEach((l, m) => { w32(20 + 8 * m, off); w32(24 + 8 * m, l.length); out.set(l, off); off += l.length; });
return out;
}
/* Any channel count → RGBA (for canvas ImageData). */
function toRGBA(img, level) {
const m = level || 0, px = img.mips ? img.mips[m] : img.pixels;
const w = Math.max(1, img.width >>> m), h = Math.max(1, img.height >>> m), ch = img.channels;
if (ch === 4) return new Uint8ClampedArray(px.buffer, px.byteOffset, w * h * 4);
const out = new Uint8ClampedArray(w * h * 4);
for (let i = 0, s = 0; i < w * h; i++, s += ch) {
const o = i * 4;
if (ch >= 3) { out[o] = px[s]; out[o + 1] = px[s + 1]; out[o + 2] = px[s + 2]; out[o + 3] = 255; }
else { out[o] = out[o + 1] = out[o + 2] = px[s]; out[o + 3] = ch === 2 ? px[s + 1] : 255; }
}
return out;
}
return { decode, encode, toRGBA, decodeTexture, textureToRGBA, decodeBlocks, surfaceIndex,
checksum: (b) => checksum(b, 0, b.length), YapfError, GPU, FLAG, NEED,
VERSION: 1 /* written by encode */, READS: [1, 2] };
}));