Merge pull request #4

Extensions and bindings read YAPF 2; Nix package
This commit is contained in:
Nexoniarz 2026-10-11 12:04:16 +02:00 • committed by GitHub
commit 06a4e7db9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2468 additions and 186 deletions

View file

@ -129,9 +129,13 @@ for (uint32_t m = 0; m < t->mip_levels; m++) {
yapf_tex_free(t); yapf_tex_free(t);
``` ```
The language bindings and the JavaScript port speak version 1; the C Every extension opens version 2 files: GIMP (at full 16-bit / float
library's v1 functions also read 8-bit version 2 files (2D, cube face 0, precision, faces and frames as layers), Blender (HDR as float images),
array layer 0), so plugins built on them open those too. KDE and Linux thumbnails, VS Code (with face / layer / frame / slice
pickers), Figma and Photoshop. The Python binding (`yapf.load_texture`)
and the JavaScript port (`YAPF.decodeTexture`) read every texture; the
other bindings wrap the v1 functions, which also read 8-bit version 2
files (2D, cube face 0, array layer 0).
--- ---
@ -332,6 +336,7 @@ YAPF_CHANNELS_RGBA 4
| System | Command | Output | | System | Command | Output |
| :----- | :------ | :----- | | :----- | :------ | :----- |
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` | | Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
| NixOS / Nix | `nix-build` (or `nix-env -if .`) — see [extensions/linux](extensions/linux#nixos) | `result/`: `bin/yapf`, `lib/`, `include/`, MIME type, thumbnailer |
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries | | Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` | | Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | | | Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |

View file

@ -1,6 +1,6 @@
# yapf.js — YAPF in JavaScript # yapf.js — YAPF in JavaScript
Encoder and decoder for YAPF v1 in one dependency-free file. Works in Encoder (version 1) and decoder (versions 1 and 2) in one dependency-free file. Works in
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
produces exactly the same bytes as the C library. produces exactly the same bytes as the C library.
@ -22,3 +22,22 @@ image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
TypeScript types are in `yapf.d.ts`. TypeScript types are in `yapf.d.ts`.
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`. Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.
## Textures (YAPF 2)
```js
const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
// tex: { width, height, depth, layers, frames, faces, shape, mipLevels,
// payload, format, sample, channels, flags, channelMap, normal,
// primaries, transfer, frameUs, notes, surfaces: [...] }
const s = YAPF.surfaceIndex(tex, frame, layer, face, mip);
const view = YAPF.textureToRGBA(tex, s); // { width, height, rgba } for a canvas
```
`surfaces[s]` is a `Uint8Array` (8-bit pixels or GPU blocks, ready for
WebGL / WebGPU compressed textures), `Uint16Array` (16-bit, half) or
`Float32Array`. Checksums are verified (`{ verify: false }` skips them);
errors are `YAPF.YapfError` with a `code` (`corrupt`, `checksum`,
`truncated`, `not-yapf`, `version`, `unsupported`). `decode()` also reads
8-bit version 2 files. The decoder is checked against the C library on the
files in `test-data/`.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,137 @@
{
"v2-rgba8.yapf": {
"surfaces": [
480599072
],
"preview0": 480599072
},
"v2-float-gray-entropy.yapf": {
"surfaces": [
1656156314
],
"preview0": 2474033295
},
"v2-half-gray-alpha-entropy.yapf": {
"surfaces": [
2599040047
],
"preview0": 297007736
},
"v2-rgba8-entropy.yapf": {
"surfaces": [
554695116
],
"preview0": 554695116
},
"v2-u16-rgba-entropy.yapf": {
"surfaces": [
3416063949
],
"preview0": 1518500476
},
"v2-cube-mips.yapf": {
"surfaces": [
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753,
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753,
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753,
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753,
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753,
3098789294,
2090642189,
3178169037,
3641419477,
333399741,
2333110333,
1703812753
],
"preview0": 3098789294
},
"v2-u16-volume.yapf": {
"surfaces": [
1504297975,
2857113671,
3272367306,
4248642449
],
"preview0": 3611091147
},
"v2-animation-notes.yapf": {
"surfaces": [
2496952478,
2084186354,
2971951804,
4204487618
],
"preview0": 2496952478
},
"v2-bc6h-random-blocks.yapf": {
"surfaces": [
2051803746,
562394057,
2093502260
],
"preview0": 2386146506
},
"v2-bc7-random-blocks.yapf": {
"surfaces": [
3714498223,
242396130,
2835116443
],
"preview0": 4042788133
},
"v2-bc3-packed.yapf": {
"surfaces": [
1798067479
],
"preview0": 3027945807
},
"v2-bc5-packed.yapf": {
"surfaces": [
1681957115
],
"preview0": 1079658926
},
"v2-bc7-encoded.yapf": {
"surfaces": [
3415378546
],
"preview0": 851871445
},
"v2-bc6h-encoded.yapf": {
"surfaces": [
3372163902
],
"preview0": 89762387
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -53,5 +53,39 @@ for (let i = 0; i < 2000; i++) {
} }
check(threwOnTruncation, 'truncated files are rejected'); check(threwOnTruncation, 'truncated files are rejected');
/* Version 2 fixtures written by the C library: every surface and the
* preview must match what C decodes (FNV-1a hashes in v2-expected.json). */
const fnv = (u8) => { let h = 2166136261; for (const v of u8) h = Math.imul(h ^ v, 16777619) >>> 0; return h; };
const dataDir = path.join(__dirname, 'test-data');
const expected = JSON.parse(fs.readFileSync(path.join(dataDir, 'v2-expected.json'), 'utf8'));
for (const [name, want] of Object.entries(expected)) {
const bytes = fs.readFileSync(path.join(dataDir, name));
let tex = null;
try { tex = YAPF.decodeTexture(bytes); } catch (e) { check(false, `${name}: ${e.message}`); continue; }
const got = tex.surfaces.map((s) => fnv(new Uint8Array(s.buffer, s.byteOffset, s.byteLength)));
check(JSON.stringify(got) === JSON.stringify(want.surfaces), `${name}: surfaces match the C decoder`);
if (want.preview0 !== null) {
const view = YAPF.textureToRGBA(tex, 0, 0);
/* float previews may differ by 1 in rounding; compare only exact kinds */
if (tex.payload === 1 ? tex.format !== 0x47 && tex.format !== 0x48 : tex.sample < 2)
check(fnv(view.rgba) === want.preview0, `${name}: preview matches the C library`);
}
/* damage must be caught by the checksums */
const broken = Uint8Array.from(bytes);
broken[broken.length - 3] ^= 0x10;
let code = null;
try { YAPF.decodeTexture(broken); } catch (e) { code = e.code; }
check(code === 'checksum', `${name}: a damaged byte is reported as a checksum error`);
}
{ /* v1 decode() reads 8-bit v2 files and names what it cannot read */
const cube = YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-cube-mips.yapf')));
check(cube.mipLevels === 7 && cube.width === 64, 'decode() reads face 0 of a v2 cube');
let msg = '';
try { YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-bc7-encoded.yapf'))); } catch (e) { msg = e.message; }
check(/decodeTexture/.test(msg), 'decode() points GPU-block files to decodeTexture');
const yapfSum = (t) => YAPF.checksum(new TextEncoder().encode(t));
check(yapfSum('') === 0xd7550ed0 && yapfSum('YAPF') === 0xc8cc41d4, 'yapf-sum test vectors');
}
console.log(`${tests} tests, ${fails} failures`); console.log(`${tests} tests, ${fails} failures`);
process.exit(fails ? 1 : 0); process.exit(fails ? 1 : 0);

View file

@ -1,5 +1,5 @@
/* /*
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript. * yapf.js — YAPF encoder (v1) and decoder (v1 and v2) in plain JavaScript.
* *
* Copyright 2026 Nexoniarz — Apache License 2.0. * Copyright 2026 Nexoniarz — Apache License 2.0.
* *
@ -10,9 +10,14 @@
* const file = YAPF.encode(img); // image → Uint8Array * const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels * 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, * An image is { width, height, channels (1–4), pixels: Uint8Array,
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }. * gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
* The specification is the comment at the top of yapf.h. * 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) { (function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory(); if (typeof module === 'object' && module.exports) module.exports = factory();
@ -42,7 +47,676 @@
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8); const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1); const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); } 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 ──────────────────────────────────────────────────────── */ /* ── Decoder ──────────────────────────────────────────────────────── */
@ -102,7 +776,7 @@
R[r0 + tw] = R[r0 + tw - 1]; R[r0 + tw] = R[r0 + tw - 1];
} }
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) { function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch, v2, book) {
if (end <= p) corrupt(); if (end <= p) corrupt();
let hdr = b[p++]; let hdr = b[p++];
if (hdr === STORED) { if (hdr === STORED) {
@ -112,10 +786,18 @@
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch); out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return; return;
} }
if (hdr & 0xE0) corrupt(); /* reserved bits set */ if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
const xf = hdr & SUBGREEN; 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; hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt(); 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 rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen); const rows = new Uint8Array(4 * planeLen);
@ -128,7 +810,8 @@
for (let c = 0; c < ch; c++) { for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue; if (hdr & (1 << c)) continue;
z[c] = new Uint8Array(TILE_PX + GROUP); z[c] = new Uint8Array(TILE_PX + GROUP);
const r = unpackPlane(b, p, end, z[c], tw, th); 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; p = r.p; filt[c] = r.filt;
} }
if (p !== end) corrupt(); if (p !== end) corrupt();
@ -147,7 +830,11 @@
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri; const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) { for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) { if (ch >= 3) {
if (xf) { 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]; const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128; out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g; out[d + 1] = g;
@ -171,11 +858,27 @@
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0; 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) { function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(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) if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
throw new Error('YAPF: not a YAPF file'); throw new YapfError('not-yapf', 'not a YAPF file');
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]); 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); 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 || if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt(); W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
@ -459,5 +1162,7 @@
return out; return out;
} }
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 }; 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] };
})); }));

View file

@ -23,3 +23,17 @@ Without the library it falls back to a pure-Python decoder that needs
tells you which one is in use. tells you which one is in use.
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF` Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`
## Textures (YAPF 2)
```python
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file, needs the C library
tex.payload, tex.format, tex.sample, tex.channels, tex.mip_levels
tex.shape, tex.layers, tex.frames, tex.channel_map, tex.normal, tex.notes
raw = tex.surface(frame=0, layer=0, face=0, mip=0) # bytes as stored (GPU blocks / samples)
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
```
`yapf.load` / `yapf.decode` still return 8-bit images; on a texture they
cannot represent they raise `YapfError` saying so. Errors name the
reason (damaged, truncated, not YAPF, newer version).

Binary file not shown.

View file

@ -1,5 +1,5 @@
""" """
yapf.py — Python bindings for YAPF v1 images. yapf.py — Python bindings for YAPF images (v1) and textures (v2).
Copyright 2026 Nexoniarz — Apache License 2.0. Copyright 2026 Nexoniarz — Apache License 2.0.
@ -8,6 +8,11 @@ Copyright 2026 Nexoniarz — Apache License 2.0.
img.width, img.height, img.channels # pixels: bytes, row-major img.width, img.height, img.channels # pixels: bytes, row-major
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file (C library)
tex.payload, tex.format, tex.sample # GPU blocks, 16-bit, float, ...
raw = tex.surface(mip=0) # bytes as stored (blocks or samples)
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
Uses the C library through ctypes when one can be found (fast, can Uses the C library through ctypes when one can be found (fast, can
encode); otherwise falls back to a pure-Python decoder that needs numpy encode); otherwise falls back to a pure-Python decoder that needs numpy
(decode only). The library is looked up in this order: (decode only). The library is looked up in this order:
@ -26,12 +31,22 @@ import platform
import struct import struct
import sys import sys
__all__ = ["Image", "load", "save", "decode", "encode", "backend", __all__ = ["Image", "Texture", "load", "save", "decode", "encode", "backend",
"load_texture", "decode_texture",
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8", "GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"] "FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6) GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
FLAG_PREMULT_ALPHA, FLAG_SRGB = 1, 2 GPU_BC1, GPU_BC2, GPU_BC3, GPU_BC4, GPU_BC4_SNORM, GPU_BC5, GPU_BC5_SNORM = range(0x40, 0x47)
GPU_BC6H, GPU_BC6H_SF, GPU_BC7 = 0x47, 0x48, 0x49
FLAG_PREMULT_ALPHA, FLAG_SRGB, FLAG_COVERAGE = 1, 2, 4
PAYLOAD_PIXELS, PAYLOAD_BLOCKS = 0, 1
SAMPLE_U8, SAMPLE_U16, SAMPLE_F16, SAMPLE_F32 = 0, 1, 2, 3
SHAPE_2D, SHAPE_CUBE, SHAPE_VOLUME = 0, 1, 2
_ERRORS = {-1: "invalid argument", -2: "cannot read the file", -3: "out of memory",
-4: "corrupt or not a YAPF file", -5: "needs a feature this library does not have",
-6: "damaged: a checksum does not match", -7: "the file is truncated"}
class YapfError(Exception): class YapfError(Exception):
@ -70,6 +85,26 @@ class _CImage(ctypes.Structure):
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))] ("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
class _CNote(ctypes.Structure):
_fields_ = [("tag", ctypes.c_char * 4), ("size", ctypes.c_uint32),
("data", ctypes.POINTER(ctypes.c_uint8))]
class _CTexture(ctypes.Structure):
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32), ("depth", ctypes.c_uint32),
("layers", ctypes.c_uint16), ("frames", ctypes.c_uint16),
("shape", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
("payload", ctypes.c_uint8), ("format", ctypes.c_uint8),
("sample", ctypes.c_uint8), ("channels", ctypes.c_uint8), ("flags", ctypes.c_uint8),
("channel_map", ctypes.c_uint8 * 4), ("linear_mask", ctypes.c_uint8),
("normal", ctypes.c_uint8), ("primaries", ctypes.c_uint8), ("transfer", ctypes.c_uint8),
("mip_filter", ctypes.c_uint8), ("alpha_ref", ctypes.c_uint8),
("loop_count", ctypes.c_uint16),
("frame_us", ctypes.POINTER(ctypes.c_uint32)),
("note_count", ctypes.c_uint32), ("notes", ctypes.POINTER(_CNote)),
("data", ctypes.POINTER(ctypes.c_void_p)), ("internal_", ctypes.c_void_p)]
def _platform_dir(): def _platform_dir():
machine = platform.machine().lower() machine = platform.machine().lower()
arch = "arm64" if machine in ("arm64", "aarch64") else "x64" arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
@ -97,6 +132,20 @@ def _find_library():
lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p), lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_size_t)] ctypes.POINTER(ctypes.c_size_t)]
lib.yapf_free_buffer.argtypes = [ctypes.c_void_p] lib.yapf_free_buffer.argtypes = [ctypes.c_void_p]
try: # version 2 (texture) API
T = ctypes.POINTER(_CTexture)
lib.yapf_tex_load_memory.restype = T
lib.yapf_tex_load_memory.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int,
ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
lib.yapf_tex_free.argtypes = [T]
lib.yapf_tex_surface_bytes.restype = ctypes.c_size_t
lib.yapf_tex_surface_bytes.argtypes = [T, ctypes.c_uint32]
lib.yapf_tex_to_rgba8.argtypes = [T, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p]
lib.yapf_blocks_decode.argtypes = [ctypes.c_uint8, ctypes.c_void_p, ctypes.c_uint32,
ctypes.c_uint32, ctypes.c_void_p]
lib.has_textures = True
except AttributeError: # an older library: v1 only
lib.has_textures = False
return lib return lib
except (OSError, AttributeError): except (OSError, AttributeError):
continue continue
@ -115,7 +164,7 @@ def _c_decode(data):
data = bytes(data) data = bytes(data)
p = _lib.yapf_load_memory_mt(data, len(data), 0) p = _lib.yapf_load_memory_mt(data, len(data), 0)
if not p: if not p:
raise YapfError("not a valid YAPF v1 file") raise YapfError(_why(data))
try: try:
c = p.contents c = p.contents
mips = [] mips = []
@ -157,6 +206,9 @@ def _py_decode(data):
b = np.frombuffer(bytes(data), dtype=np.uint8) b = np.frombuffer(bytes(data), dtype=np.uint8)
if len(b) < 20 or bytes(b[:4]) != b"YAPF": if len(b) < 20 or bytes(b[:4]) != b"YAPF":
raise YapfError("not a YAPF file") raise YapfError("not a YAPF file")
if b[4] == 2:
raise YapfError("this is a YAPF 2 file; reading it needs the YAPF native library, "
"which was not found (the pure-Python decoder reads version 1 only)")
if b[4] != 1: if b[4] != 1:
raise YapfError("unsupported YAPF version %d" % b[4]) raise YapfError("unsupported YAPF version %d" % b[4])
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8]) ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
@ -275,6 +327,127 @@ def _py_tile(np, b, p, end, tw, th, ch, bad):
return np.stack(out, axis=-1).astype(np.uint8) return np.stack(out, axis=-1).astype(np.uint8)
# ── textures (version 2, C library) ──────────────────────────────────
def _why(data):
"""Why `decode` could not read data, in words."""
if len(data) < 5 or data[:4] != b"YAPF":
return "not a YAPF file"
if data[4] not in (1, 2):
return "YAPF format version %d is newer than this library" % data[4]
if _lib and getattr(_lib, "has_textures", False):
err = ctypes.c_int(0)
p = _lib.yapf_tex_load_memory(data, len(data), 0, 1, ctypes.byref(err))
if p:
_lib.yapf_tex_free(p)
return ("this is a YAPF 2 texture (GPU blocks, 16-bit / float or a volume) "
"that yapf.decode cannot return as 8-bit pixels; use yapf.load_texture")
return _ERRORS.get(err.value, "error %d" % err.value)
if data[4] == 2:
return "YAPF 2 files need the YAPF native library (not found)"
return "corrupt YAPF file"
class Texture:
"""A decoded YAPF texture (version 1 or 2). Description fields mirror
yapf_texture_t; `surfaces[s]` holds the bytes of surface s as stored
(GPU blocks, or samples in native byte order), numbered
s = ((frame * layers + layer) * faces + face) * mip_levels + mip."""
def __init__(self, c, lib):
for f, _ in _CTexture._fields_:
if f not in ("frame_us", "notes", "data", "internal_", "channel_map", "note_count"):
setattr(self, f, getattr(c, f))
self.channel_map = list(c.channel_map)
self.faces = 6 if c.shape == SHAPE_CUBE else 1
self.frame_us = [c.frame_us[i] for i in range(c.frames)] if c.frame_us else []
self.notes = [(c.notes[i].tag.decode("latin-1"), ctypes.string_at(c.notes[i].data, c.notes[i].size))
for i in range(c.note_count)]
count = c.frames * c.layers * self.faces * c.mip_levels
self.surfaces = [ctypes.string_at(c.data[s], lib.yapf_tex_surface_bytes(ctypes.byref(c), s % c.mip_levels))
for s in range(count)]
def surface_index(self, frame=0, layer=0, face=0, mip=0):
return ((frame * self.layers + layer) * self.faces + face) * self.mip_levels + mip
def surface(self, frame=0, layer=0, face=0, mip=0):
return self.surfaces[self.surface_index(frame, layer, face, mip)]
def mip_size(self, mip=0):
d = max(1, self.depth >> mip) if self.shape == SHAPE_VOLUME else 1
return max(1, self.width >> mip), max(1, self.height >> mip), d
@property
def is_hdr(self):
"""Float / half samples or BC6H blocks: values may go above 1."""
if self.payload == PAYLOAD_BLOCKS:
return self.format in (GPU_BC6H, GPU_BC6H_SF)
return self.sample in (SAMPLE_F16, SAMPLE_F32)
def rgba_float(self, frame=0, layer=0, face=0, mip=0, slice=0):
"""One image as numpy float32 (height, width, 4), rows top to bottom:
8 / 16-bit scaled to 0..1 (still in their stored colour space),
floats as stored, GPU blocks decoded. Needs numpy."""
import numpy as np
w, h, d = self.mip_size(mip)
raw = self.surface(frame, layer, face, mip)
raw = raw[len(raw) // d * slice:len(raw) // d * (slice + 1)]
out = np.ones((h, w, 4), dtype=np.float32)
if self.payload == PAYLOAD_BLOCKS:
hdr = self.format in (GPU_BC6H, GPU_BC6H_SF)
buf = ctypes.create_string_buffer(w * h * (6 if hdr else 4))
rc = _lib.yapf_blocks_decode(self.format, raw, w, h, buf)
if rc != 0:
raise YapfError("GPU block format 0x%02x cannot be decoded here" % self.format)
if hdr:
out[..., :3] = np.frombuffer(buf.raw, dtype=np.float16).reshape(h, w, 3)
else:
out[...] = np.frombuffer(buf.raw, dtype=np.uint8).reshape(h, w, 4) / 255.0
if self.channels == 3:
out[..., 3] = 1.0
return out
dt = {SAMPLE_U8: np.uint8, SAMPLE_U16: np.uint16, SAMPLE_F16: np.float16, SAMPLE_F32: np.float32}[self.sample]
ch = self.channels
src = np.frombuffer(raw, dtype=dt).reshape(h, w, ch).astype(np.float32)
if self.sample == SAMPLE_U8:
src /= 255.0
elif self.sample == SAMPLE_U16:
src /= 65535.0
out[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
if ch in (2, 4):
out[..., 3] = src[..., ch - 1]
return out
def __repr__(self):
w, h, d = self.mip_size(0)
return "<yapf.Texture %dx%d%s, %d mip(s), %s>" % (
w, h, "x%d" % d if d > 1 else "", self.mip_levels,
"blocks 0x%02x" % self.format if self.payload == PAYLOAD_BLOCKS
else "%d ch %s" % (self.channels, ["u8", "u16", "f16", "f32"][self.sample]))
def decode_texture(data, verify=True):
"""Decode any YAPF file (v1 or v2) into a Texture. Needs the C library."""
if not (_lib and getattr(_lib, "has_textures", False)):
raise YapfError("YAPF textures need the YAPF native library (version 2), which was not found")
data = bytes(data)
err = ctypes.c_int(0)
p = _lib.yapf_tex_load_memory(data, len(data), 0 if verify else 1, 0, ctypes.byref(err))
if not p:
raise YapfError(_why(data) if len(data) < 5 or data[:4] != b"YAPF"
else _ERRORS.get(err.value, "error %d" % err.value))
try:
return Texture(p.contents, _lib)
finally:
_lib.yapf_tex_free(p)
def load_texture(path, verify=True):
"""Read and decode any .yapf file into a Texture."""
with open(path, "rb") as f:
return decode_texture(f.read(), verify)
# ── public API ──────────────────────────────────────────────────────── # ── public API ────────────────────────────────────────────────────────
def decode(data): def decode(data):

39
default.nix Normal file
View file

@ -0,0 +1,39 @@
# YAPF for Nix / NixOS: the `yapf` tool, libyapf (static + shared) and
# yapf.h, plus the image/x-yapf MIME type and a freedesktop thumbnailer.
#
# nix-build build ./result
# nix-env -if . install for your user
# NixOS: environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
{ pkgs ? import <nixpkgs> { }, lib ? pkgs.lib, stdenv ? pkgs.stdenv }:
stdenv.mkDerivation {
pname = "yapf";
version = "2.0";
src = lib.cleanSourceWith {
src = ./.;
filter = path: type:
let base = baseNameOf path; in
!(lib.elem base [ "build" "dist" "result" ".git" "node_modules" ]);
};
makeFlags = [ "PREFIX=$(out)" ];
doCheck = true;
checkTarget = "test";
postInstall = ''
install -Dm644 extensions/linux/yapf-mime.xml $out/share/mime/packages/yapf.xml
install -Dm644 extensions/linux/yapf.thumbnailer $out/share/thumbnailers/yapf.thumbnailer
substituteInPlace $out/share/thumbnailers/yapf.thumbnailer \
--replace-fail "TryExec=yapf" "TryExec=$out/bin/yapf" \
--replace-fail "Exec=yapf " "Exec=$out/bin/yapf "
'';
meta = {
description = "YAPF: fast lossless images and GPU-ready textures";
homepage = "https://github.com/Nexoniarz/YAPF";
license = lib.licenses.asl20;
platforms = lib.platforms.unix;
mainProgram = "yapf";
};
}

View file

@ -22,13 +22,14 @@ the same native code, so they are all about equally fast; the JavaScript
port needs no native library at all. Every example also re-encodes the port needs no native library at all. Every example also re-encodes the
image and checks that the result is byte-identical to the original file. image and checks that the result is byte-identical to the original file.
**YAPF 2 textures.** The bindings wrap the version 1 API (`yapf_load`, **YAPF 2 textures.** Most bindings wrap the version 1 API (`yapf_load`,
`yapf_save`, …). Through it they also read 8-bit version 2 files (2D, or `yapf_save`, …). Through it they also read 8-bit version 2 files (2D, or
face 0 / layer 0 of cubes and arrays). GPU blocks, 16-bit / float pixels, face 0 / layer 0 of cubes and arrays). GPU blocks, 16-bit / float pixels,
volumes and the other version 2 features are available from C and C++ volumes and the other version 2 features are available from C and C++
through the `yapf_tex_*` functions in [`yapf.h`](../yapf.h) (see through the `yapf_tex_*` functions in [`yapf.h`](../yapf.h) (see
[FORMAT.md](FORMAT.md)); any language with a C FFI can call them the same [FORMAT.md](FORMAT.md)); any language with a C FFI can call them the same
way. The JavaScript port reads and writes version 1 only. way. Python (`yapf.load_texture`) and JavaScript (`YAPF.decodeTexture`)
read every version 2 texture; the JavaScript port writes version 1.
## Getting the native library ## Getting the native library

View file

@ -12,6 +12,14 @@ Blender 5.1).
Blender cannot read `.yapf` on its own, so imported images are **packed into Blender cannot read `.yapf` on its own, so imported images are **packed into
the .blend** and survive saving and reopening. the .blend** and survive saving and reopening.
Every YAPF file opens, version 1 and 2: 8-bit images; GPU blocks (BC1–BC7,
BC6H) decoded; 16-bit, half and float textures as **float images** (HDR
values above 1 are kept, packed as EXR); for cubes, arrays and animations
the first image. Normal maps follow Blender's OpenGL convention: DirectX
normals get their green flipped, and X/Y-only normals (such as BC5) get Z
rebuilt. Version 2 files need the bundled native library; the pure-Python
fallback reads version 1 only. Tested in Blender 4.4.3 (nixpkgs).
## Install ## Install
1. Get `yapf_image-blender.zip` from the 1. Get `yapf_image-blender.zip` from the

View file

@ -33,30 +33,61 @@ from . import yapf
# ── conversion helpers ──────────────────────────────────────────────── # ── conversion helpers ────────────────────────────────────────────────
def load_image(filepath): def _set_colorspace(bimg, names):
"""Read a .yapf file into a packed bpy.types.Image.""" for n in names: # names differ between versions
img = yapf.load(filepath) try:
bimg.colorspace_settings.name = n
return
except TypeError:
continue
def _read(filepath):
"""(rgba float32 h×w×4, has_alpha, flags, float_data, texture or None)."""
if yapf.backend() == "c" and getattr(yapf._lib, "has_textures", False):
tex = yapf.load_texture(filepath) # every v1 / v2 file
rgba = tex.rgba_float() # frame 0, layer 0, face +X, mip 0
if tex.normal & 3 == 2: # DirectX (-Y) → Blender's OpenGL (+Y)
rgba[..., 1] = 1.0 - rgba[..., 1]
if tex.normal & 4 or (tex.normal and tex.channels == 2): # XY: rebuild Z
x, y = rgba[..., 0] * 2.0 - 1.0, rgba[..., 1] * 2.0 - 1.0
rgba[..., 2] = (np.sqrt(np.clip(1.0 - x * x - y * y, 0.0, 1.0)) + 1.0) * 0.5
alpha = tex.channels in (2, 4) and not tex.normal
wide = tex.is_hdr or (tex.payload == yapf.PAYLOAD_PIXELS and tex.sample == yapf.SAMPLE_U16)
return rgba, alpha, tex.flags, wide, tex
img = yapf.load(filepath) # pure-Python decoder: v1 only
w, h, ch = img.width, img.height, img.channels w, h, ch = img.width, img.height, img.channels
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0 src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
rgba = np.ones((h, w, 4), dtype=np.float32) rgba = np.ones((h, w, 4), dtype=np.float32)
if ch >= 3: rgba[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
rgba[..., :3] = src[..., :3]
else:
rgba[..., :3] = src[..., :1]
if ch in (2, 4): if ch in (2, 4):
rgba[..., 3] = src[..., ch - 1] rgba[..., 3] = src[..., ch - 1]
return rgba, ch in (2, 4), img.flags, False, None
def load_image(filepath):
"""Read a .yapf file into a packed bpy.types.Image. 16-bit, half,
float and BC6H textures become float images (packed as EXR so nothing
is lost); GPU blocks are decoded."""
rgba, alpha, flags, wide, tex = _read(filepath)
h, w = rgba.shape[:2]
name = os.path.basename(filepath) name = os.path.basename(filepath)
bimg = bpy.data.images.new(name, w, h, alpha=ch in (2, 4)) bimg = bpy.data.images.new(name, w, h, alpha=alpha, float_buffer=wide)
bimg.colorspace_settings.name = "sRGB" if img.flags & yapf.FLAG_SRGB else "Non-Color" if tex is not None and tex.is_hdr:
if img.flags & yapf.FLAG_PREMULT_ALPHA: _set_colorspace(bimg, ("Linear Rec.709", "Linear"))
elif flags & yapf.FLAG_SRGB:
_set_colorspace(bimg, ("sRGB",))
else:
_set_colorspace(bimg, ("Non-Color",))
if flags & yapf.FLAG_PREMULT_ALPHA:
bimg.alpha_mode = "PREMUL" bimg.alpha_mode = "PREMUL"
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
bimg.file_format = "PNG" bimg.file_format = "OPEN_EXR" if wide else "PNG"
bimg.pack() # keep it inside the .blend bimg.pack() # keep it inside the .blend
bimg.use_fake_user = True # …even before anything uses it bimg.use_fake_user = True # …even before anything uses it
bimg["yapf_source"] = filepath bimg["yapf_source"] = filepath
bimg["yapf_alpha"] = ch in (2, 4) bimg["yapf_alpha"] = alpha
return bimg return bimg

View file

@ -9,6 +9,12 @@ Both directions are pixel-exact: the plugin carries its own small PNG
encoder/decoder instead of going through a canvas, which would alter encoder/decoder instead of going through a canvas, which would alter
semi-transparent pixels. semi-transparent pixels.
Import reads every YAPF file, version 1 and 2. 8-bit images keep their
exact pixels; GPU blocks (BC1–BC7, BC6H), 16-bit and float textures are
converted to 8-bit for display (Figma images are 8-bit); cubes, arrays,
volumes and animations bring their first image. Damaged files are
reported as such.
## Install (development plugin) ## Install (development plugin)
1. Use the **Figma desktop app**. 1. Use the **Figma desktop app**.

View file

@ -64,14 +64,26 @@ $('tab-import').onclick = () => tab('import');
$('tab-export').onclick = () => tab('export'); $('tab-export').onclick = () => tab('export');
/* Import: .yapf → pixels → exact PNG → Figma image */ /* Import: .yapf → pixels → exact PNG → Figma image */
/* Any YAPF file → 8-bit pixels. 8-bit images keep their exact pixels and
* channels; GPU blocks, 16-bit and float textures are converted for display
* (RGBA). Cubes, arrays, volumes and animations give their first image. */
function yapfToPixels(tex) {
const w = tex.width, h = tex.height;
if (tex.payload === 0 && tex.sample === 0)
return { width: w, height: h, channels: tex.channels,
pixels: tex.surfaces[0].subarray(0, w * h * tex.channels) };
const v = YAPF.textureToRGBA(tex, 0, 0);
return { width: w, height: h, channels: 4, pixels: new Uint8Array(v.rgba.buffer),
note: tex.payload === 1 ? 'GPU blocks decoded' : 'converted to 8-bit' };
}
async function importFiles(files) { async function importFiles(files) {
const images = []; const images = [];
for (const f of files) { for (const f of files) {
try { try {
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer())); const img = yapfToPixels(YAPF.decodeTexture(new Uint8Array(await f.arrayBuffer())));
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels); const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png }); images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')'); status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + (img.note ? ', ' + img.note : '') + ')');
} catch (e) { } catch (e) {
status(f.name + ': ' + e.message); status(f.name + ': ' + e.message);
} }

View file

@ -47,7 +47,7 @@
<div id="status"></div> <div id="status"></div>
<script>/* <script>/*
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript. * yapf.js — YAPF encoder (v1) and decoder (v1 and v2) in plain JavaScript.
* *
* Copyright 2026 Nexoniarz — Apache License 2.0. * Copyright 2026 Nexoniarz — Apache License 2.0.
* *
@ -58,9 +58,14 @@
* const file = YAPF.encode(img); // image → Uint8Array * const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels * 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, * An image is { width, height, channels (1–4), pixels: Uint8Array,
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }. * gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
* The specification is the comment at the top of yapf.h. * 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) { (function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory(); if (typeof module === 'object' && module.exports) module.exports = factory();
@ -90,7 +95,676 @@
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8); const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1); const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); } 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 ──────────────────────────────────────────────────────── */ /* ── Decoder ──────────────────────────────────────────────────────── */
@ -150,7 +824,7 @@
R[r0 + tw] = R[r0 + tw - 1]; R[r0 + tw] = R[r0 + tw - 1];
} }
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) { function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch, v2, book) {
if (end <= p) corrupt(); if (end <= p) corrupt();
let hdr = b[p++]; let hdr = b[p++];
if (hdr === STORED) { if (hdr === STORED) {
@ -160,10 +834,18 @@
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch); out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return; return;
} }
if (hdr & 0xE0) corrupt(); /* reserved bits set */ if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
const xf = hdr & SUBGREEN; 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; hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt(); 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 rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen); const rows = new Uint8Array(4 * planeLen);
@ -176,7 +858,8 @@
for (let c = 0; c < ch; c++) { for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue; if (hdr & (1 << c)) continue;
z[c] = new Uint8Array(TILE_PX + GROUP); z[c] = new Uint8Array(TILE_PX + GROUP);
const r = unpackPlane(b, p, end, z[c], tw, th); 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; p = r.p; filt[c] = r.filt;
} }
if (p !== end) corrupt(); if (p !== end) corrupt();
@ -195,7 +878,11 @@
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri; const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) { for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) { if (ch >= 3) {
if (xf) { 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]; const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128; out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g; out[d + 1] = g;
@ -219,11 +906,27 @@
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0; 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) { function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(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) if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
throw new Error('YAPF: not a YAPF file'); throw new YapfError('not-yapf', 'not a YAPF file');
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]); 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); 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 || if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt(); W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
@ -507,7 +1210,9 @@
return out; return out;
} }
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 }; 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] };
})); }));
</script> </script>
<script>/* <script>/*
@ -653,14 +1358,26 @@ $('tab-import').onclick = () => tab('import');
$('tab-export').onclick = () => tab('export'); $('tab-export').onclick = () => tab('export');
/* Import: .yapf → pixels → exact PNG → Figma image */ /* Import: .yapf → pixels → exact PNG → Figma image */
/* Any YAPF file → 8-bit pixels. 8-bit images keep their exact pixels and
* channels; GPU blocks, 16-bit and float textures are converted for display
* (RGBA). Cubes, arrays, volumes and animations give their first image. */
function yapfToPixels(tex) {
const w = tex.width, h = tex.height;
if (tex.payload === 0 && tex.sample === 0)
return { width: w, height: h, channels: tex.channels,
pixels: tex.surfaces[0].subarray(0, w * h * tex.channels) };
const v = YAPF.textureToRGBA(tex, 0, 0);
return { width: w, height: h, channels: 4, pixels: new Uint8Array(v.rgba.buffer),
note: tex.payload === 1 ? 'GPU blocks decoded' : 'converted to 8-bit' };
}
async function importFiles(files) { async function importFiles(files) {
const images = []; const images = [];
for (const f of files) { for (const f of files) {
try { try {
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer())); const img = yapfToPixels(YAPF.decodeTexture(new Uint8Array(await f.arrayBuffer())));
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels); const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png }); images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')'); status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + (img.note ? ', ' + img.note : '') + ')');
} catch (e) { } catch (e) {
status(f.name + ': ' + e.message); status(f.name + ': ' + e.message);
} }

View file

@ -2,7 +2,11 @@
Open and export `.yapf` images in **GIMP 3.0 or later**. Open and export `.yapf` images in **GIMP 3.0 or later**.
- **File → Open** reads `.yapf` (Gray, Gray + Alpha, RGB, RGBA). - **File → Open** reads every `.yapf` file, version 1 and 2: Gray, Gray +
Alpha, RGB, RGBA in 8-bit, 16-bit, half and float precision; GPU blocks
(BC1–BC7, BC6H HDR) decoded for viewing; cube faces, array layers, volume
slices and animation frames as separate layers. When a file cannot be
opened, the message says why (damaged, truncated, not YAPF, newer format).
- **File → Export As… → `name.yapf`** writes the visible image (layers are - **File → Export As… → `name.yapf`** writes the visible image (layers are
flattened into a copy; your document is untouched), with an optional flattened into a copy; your document is untouched), with an optional
**Store mipmaps** checkbox for game engines. **Store mipmaps** checkbox for game engines.
@ -50,5 +54,9 @@ GIMP config folder is `~/.config/GIMP/3.0` on Linux,
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are - Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
gamma encoded. Files marked linear load as linear. gamma encoded. Files marked linear load as linear.
- When a file has mipmaps, GIMP opens the full-size image. - When a file has mipmaps, GIMP opens the full-size image.
- ETC2 / EAC / ASTC blocks cannot be shown yet. Export writes 8-bit
version 1 files, as before.
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder - Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
pixel for pixel, and export equals GIMP's own flattened PNG of the image. pixel for pixel, and export equals GIMP's own flattened PNG of the image.
The version 2 loader was tested in GIMP 3.0.6 (nixpkgs 25.05, headless):
BC7, BC5, BC6H, half float, 16-bit, cube, animation and a damaged file.

View file

@ -138,8 +138,44 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
return procedure; return procedure;
} }
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg);
/* ── Load handler ────────────────────────────────────────────────────── */ /* ── Load handler ────────────────────────────────────────────────────── */
/* Why a file could not be opened, in words. */
static gchar *yapf_load_reason(const gchar *path, int err) {
FILE *f = fopen(path, "rb");
uint8_t h[8] = {0};
size_t n = f ? fread(h, 1, sizeof(h), f) : 0;
if (f) fclose(f);
if (!f || err == YAPF_ERR_IO)
return g_strdup("The file cannot be read.");
if (n < 5 || memcmp(h, "YAPF", 4) != 0)
return g_strdup("This is not a YAPF file (it does not start with \"YAPF\").");
if (h[4] != 1 && h[4] != 2)
return g_strdup_printf("This YAPF file is format version %u; this plug-in reads "
"versions 1 and 2. Update the plug-in.", h[4]);
switch (err) {
case YAPF_ERR_UNSUPPORTED:
return g_strdup("This YAPF file needs a feature this plug-in does not have yet. "
"Update the plug-in.");
case YAPF_ERR_CHECKSUM:
return g_strdup("This YAPF file is damaged: a checksum does not match.");
case YAPF_ERR_TRUNCATED:
return g_strdup("This YAPF file is incomplete (truncated).");
case YAPF_ERR_OOM:
return g_strdup("Out of memory.");
default:
return g_strdup_printf("This YAPF file (version %u) is corrupt, or was written by a "
"tool that does not follow the YAPF specification.", h[4]);
}
}
static const gchar *yapf_face_name(uint32_t f) {
static const gchar *n[6] = { "+X", "-X", "+Y", "-Y", "+Z", "-Z" };
return f < 6 ? n[f] : "?";
}
static GimpValueArray *yapf_load_run( static GimpValueArray *yapf_load_run(
GimpProcedure *procedure, GimpProcedure *procedure,
GimpRunMode run_mode, GimpRunMode run_mode,
@ -151,60 +187,94 @@ static GimpValueArray *yapf_load_run(
{ {
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data; (void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
gchar *path = g_file_get_path(file); gchar *path = g_file_get_path(file);
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */ int err = 0;
yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err); /* v1 and v2, all cores */
if (!t) {
gchar *why = yapf_load_reason(path, err);
g_free(path);
GError *e = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Failed to load YAPF file: %s", why);
g_free(why);
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR, e);
}
g_free(path); g_free(path);
if (!img) { /* What the pixels look like after decoding: GPU blocks decode to
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, * RGBA 8-bit, BC6H to RGB half float. */
"Failed to load YAPF file."); gboolean blocks = t->payload == YAPF_PAYLOAD_BLOCKS;
return gimp_procedure_new_return_values(procedure, gboolean bc6 = blocks && (t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF);
GIMP_PDB_EXECUTION_ERROR, err); if (blocks && t->format >= YAPF_GPU_ETC2_RGB8) {
yapf_tex_free(t);
return yapf_error(procedure, "Failed to load YAPF file: it holds ETC2 / EAC / ASTC "
"GPU blocks, which this plug-in cannot show yet.");
} }
int ch = blocks ? (bc6 ? 3 : 4) : t->channels;
int sample = blocks ? (bc6 ? YAPF_SAMPLE_F16 : YAPF_SAMPLE_U8) : t->sample;
/* sRGB data is gamma encoded; floats are always linear */
gboolean gamma = (t->flags & YAPF_FLAG_SRGB) &&
(sample == YAPF_SAMPLE_U8 || sample == YAPF_SAMPLE_U16);
/* Map YAPF channel count to GIMP image / layer types. static const gchar *chn[2][4] = { { "Y", "YA", "RGB", "RGBA" }, { "Y'", "Y'A", "R'G'B'", "R'G'B'A" } };
* Use gamma-corrected babl formats (R'G'B') when the file carries the static const gchar *typ[4] = { "u8", "u16", "half", "float" };
* sRGB flag; fall back to linear (RGB) otherwise. */ gchar *fmt_name = g_strdup_printf("%s %s", chn[gamma ? 1 : 0][ch - 1], typ[sample]);
gboolean is_srgb = (img->flags & YAPF_FLAG_SRGB) != 0; const Babl *fmt = babl_format(fmt_name);
GimpImageBaseType base_type = GIMP_RGB; g_free(fmt_name);
GimpImageType layer_type = GIMP_RGBA_IMAGE;
const gchar *babl_fmt = is_srgb ? "R'G'B'A u8" : "RGBA u8";
switch (img->channels) { GimpPrecision prec;
case YAPF_CHANNELS_GRAY: switch (sample) {
base_type = GIMP_GRAY; case YAPF_SAMPLE_U16: prec = gamma ? GIMP_PRECISION_U16_NON_LINEAR : GIMP_PRECISION_U16_LINEAR; break;
layer_type = GIMP_GRAY_IMAGE; case YAPF_SAMPLE_F16: prec = GIMP_PRECISION_HALF_LINEAR; break;
babl_fmt = is_srgb ? "Y' u8" : "Y u8"; case YAPF_SAMPLE_F32: prec = GIMP_PRECISION_FLOAT_LINEAR; break;
break; default: prec = gamma ? GIMP_PRECISION_U8_NON_LINEAR : GIMP_PRECISION_U8_LINEAR; break;
case YAPF_CHANNELS_GRAY_ALPHA:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAYA_IMAGE;
babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
break;
case YAPF_CHANNELS_RGB:
base_type = GIMP_RGB;
layer_type = GIMP_RGB_IMAGE;
babl_fmt = is_srgb ? "R'G'B' u8" : "RGB u8";
break;
default: /* RGBA */
break;
} }
GimpImageBaseType base = ch <= 2 ? GIMP_GRAY : GIMP_RGB;
GimpImageType ltype = ch == 1 ? GIMP_GRAY_IMAGE : ch == 2 ? GIMP_GRAYA_IMAGE
: ch == 3 ? GIMP_RGB_IMAGE : GIMP_RGBA_IMAGE;
GimpImage *image = gimp_image_new(img->width, img->height, base_type); /* Mip 0 of every frame, layer, face and slice becomes one GIMP layer
GimpLayer *layer = gimp_layer_new(image, "Background", * (the first at the bottom, as GIMP animations expect). */
img->width, img->height, layer_type, uint32_t w, h, d, faces = yapf_tex_faces(t);
100.0, yapf_tex_mip_size(t, 0, &w, &h, &d);
gimp_image_get_default_new_layer_mode(image)); GimpImage *image = gimp_image_new_with_precision(w, h, base, prec);
gimp_image_insert_layer(image, layer, NULL, 0); size_t texel = (size_t)ch * yapf_sample_bytes((uint8_t)sample);
void *tmp = blocks ? g_malloc((size_t)w * h * texel) : NULL;
gint count = (gint)(t->frames * t->layers * faces * d);
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer)); for (uint32_t fr = 0; fr < t->frames; fr++)
gegl_buffer_set(buffer, for (uint32_t ly = 0; ly < t->layers; ly++)
GEGL_RECTANGLE(0, 0, (gint)img->width, (gint)img->height), for (uint32_t fc = 0; fc < faces; fc++)
0, babl_format(babl_fmt), for (uint32_t z = 0; z < d; z++) {
img->pixels, GEGL_AUTO_ROWSTRIDE); uint32_t s = yapf_tex_surface(t, fr, ly, fc, 0);
g_object_unref(buffer); size_t slice = yapf_tex_surface_bytes(t, 0) / d;
const uint8_t *src = (const uint8_t *)t->data[s] + slice * z;
if (blocks) {
yapf_blocks_decode(t->format, src, w, h, tmp);
src = (const uint8_t *)tmp;
}
GString *name = g_string_new(NULL);
if (t->frames > 1) {
g_string_append_printf(name, "Frame %u", fr + 1);
if (t->frame_us)
g_string_append_printf(name, " (%ums)", (t->frame_us[fr] + 500) / 1000);
}
if (t->layers > 1) g_string_append_printf(name, "%sLayer %u", name->len ? ", " : "", ly);
if (faces > 1) g_string_append_printf(name, "%sFace %s", name->len ? ", " : "", yapf_face_name(fc));
if (d > 1) g_string_append_printf(name, "%sSlice %u", name->len ? ", " : "", z);
if (!name->len) g_string_append(name, "Background");
yapf_free(img); GimpLayer *layer = gimp_layer_new(image, name->str, (gint)w, (gint)h, ltype,
100.0, gimp_image_get_default_new_layer_mode(image));
g_string_free(name, TRUE);
gimp_image_insert_layer(image, layer, NULL, 0);
if (count > 1 && !(fr == 0 && ly == 0 && fc == 0 && z == 0))
gimp_item_set_visible(GIMP_ITEM(layer), FALSE);
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, (gint)w, (gint)h), 0, fmt,
src, GEGL_AUTO_ROWSTRIDE);
g_object_unref(buffer);
}
g_free(tmp);
yapf_tex_free(t);
GimpValueArray *ret = gimp_procedure_new_return_values(procedure, GimpValueArray *ret = gimp_procedure_new_return_values(procedure,
GIMP_PDB_SUCCESS, NULL); GIMP_PDB_SUCCESS, NULL);

View file

@ -48,3 +48,9 @@ Log out and back in, then enable **YAPF Images** in Dolphin's preview settings.
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images; plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
corrupt files are rejected). Not yet tried inside a running Dolphin. corrupt files are rejected). Not yet tried inside a running Dolphin.
Reads YAPF version 1 and 2: GPU blocks (BC1–BC7, BC6H), 16-bit and float
textures, cubes (face +X), arrays, volumes and animations (the first
image). Only the mip level the thumbnail needs is decoded. Tested with
KF6 from nixpkgs 25.05 on BC7, BC5, BC6H, half float, 16-bit, cube and
animation files. ETC2 / ASTC files get no thumbnail.

View file

@ -3,8 +3,11 @@
#include <KIO/ThumbnailCreator> #include <KIO/ThumbnailCreator>
#include <KPluginFactory> #include <KPluginFactory>
#include <QFile>
#include <QImage> #include <QImage>
#include <cstring>
extern "C" { extern "C" {
#include "yapf.h" #include "yapf.h"
} }
@ -20,52 +23,50 @@ public:
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
{ {
const QByteArray path = request.url().toLocalFile().toLocal8Bit(); QFile file(request.url().toLocalFile());
yapf_image_t *img = yapf_load(path.constData()); if (!file.open(QIODevice::ReadOnly)) {
if (!img) { return KIO::ThumbnailResult::fail();
}
const QByteArray data = file.readAll();
// Version 1 and 2: read the tables, then decode only the mip we
// need (frame 0, layer 0, face 0, slice 0).
int err = 0;
yapf_texture_t *tex = yapf_tex_open(data.constData(), size_t(data.size()), 0, &err);
if (!tex) {
return KIO::ThumbnailResult::fail(); return KIO::ThumbnailResult::fail();
} }
// Start from the smallest stored mip level that still covers the // The smallest stored mip that still covers the requested size;
// requested size; Qt scales the rest. // Qt scales the rest.
const QSize want = request.targetSize(); const QSize want = request.targetSize();
int m = 0; uint32_t m = 0, w = 0, h = 0;
while (m + 1 < img->mip_levels && while (m + 1 < tex->mip_levels) {
int(img->width >> (m + 1)) >= want.width() && yapf_tex_mip_size(tex, m + 1, &w, &h, nullptr);
int(img->height >> (m + 1)) >= want.height()) { if (int(w) < want.width() || int(h) < want.height()) {
break;
}
++m; ++m;
} }
const int w = qMax(1, int(img->width >> m)); yapf_tex_mip_size(tex, m, &w, &h, nullptr);
const int h = qMax(1, int(img->height >> m)); const uint32_t s = yapf_tex_surface(tex, 0, 0, 0, m);
const uchar *px = img->mips[m];
QImage image; QImage image(int(w), int(h), (tex->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
switch (img->channels) { : QImage::Format_RGBA8888);
case 1: bool ok = !image.isNull() &&
image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy(); yapf_tex_decode_surface(tex, data.constData(), size_t(data.size()), s, 0) == YAPF_OK;
break; if (ok) {
case 2: { // gray + alpha → RGBA // RGBA 8-bit rows (blocks decoded, 16-bit / float converted)
image = QImage(w, h, QImage::Format_RGBA8888); QByteArray rgba(int(size_t(w) * h * 4), Qt::Uninitialized);
for (int y = 0; y < h; ++y) { ok = yapf_tex_to_rgba8(tex, s, 0, reinterpret_cast<uint8_t *>(rgba.data())) == YAPF_OK;
const uchar *s = px + size_t(y) * w * 2; for (int y = 0; ok && y < int(h); ++y) {
uchar *d = image.scanLine(y); memcpy(image.scanLine(y), rgba.constData() + size_t(y) * w * 4, size_t(w) * 4);
for (int x = 0; x < w; ++x, s += 2, d += 4) {
d[0] = d[1] = d[2] = s[0];
d[3] = s[1];
}
} }
break;
} }
case 3: yapf_tex_free(tex);
image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy(); if (!ok) {
break; return KIO::ThumbnailResult::fail();
default:
image = QImage(px, w, h, w * 4,
(img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
: QImage::Format_RGBA8888).copy();
break;
} }
yapf_free(img);
if (image.width() > want.width() || image.height() > want.height()) { if (image.width() > want.width() || image.height() > want.height()) {
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation); image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);

View file

@ -13,3 +13,22 @@ on your `PATH` — `sudo make install` puts it in `/usr/local/bin`). They work
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus), in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
system and is not covered yet. system and is not covered yet.
Thumbnails cover every YAPF file the tool reads: version 1 and 2, GPU
blocks (BC1–BC7, BC6H), 16-bit and float textures, and the first image of
cubes, arrays and animations. KDE Dolphin: see [../kde](../kde).
### NixOS
`/usr/share` is not used on NixOS. Install the package from the
repository's `default.nix` instead — it carries the MIME type and a
thumbnailer that calls `yapf` by its full store path:
```nix
# configuration.nix
environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
environment.pathsToLink = [ "/share/thumbnailers" ]; # if your desktop does not link it already
```
For your user only: `nix-env -if /path/to/YAPF`, then
`sh extensions/linux/install.sh` (the per-user install).

View file

@ -14,9 +14,11 @@
if (!(files instanceof Array)) files = [files]; if (!(files instanceof Array)) files = [files];
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
var tmp = yapfTempFile(".png"); // 8 / 16-bit images come through PNG (16 bits are kept); half,
// float and BC6H HDR textures through Radiance .hdr (32-bit).
var tmp = yapfTempFile(yapfIsHdr(files[i]) ? ".hdr" : ".png");
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) { if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
alert("Could not open " + files[i].name + ".\n\n" + YAPF_TOOL_HELP); alert("Could not open " + files[i].name + ".\n\n" + yapfFailText());
return; return;
} }
var doc = app.open(tmp); var doc = app.open(tmp);

View file

@ -25,6 +25,16 @@ native plugin.
Instead of copying `yapf` next to the scripts you can put it on your `PATH`. Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
## What opens how
Every YAPF file the `yapf` tool reads (version 1 and 2): 8-bit images open
as 8-bit documents, 16-bit textures as 16-bit documents, half / float and
BC6H HDR textures as 32-bit documents (through Radiance `.hdr`), other GPU
blocks (BC1–BC7) decoded to 8-bit. Cubes, arrays and animations open their
first image. When a file cannot be opened, the alert shows the tool's
reason (for example a damaged file). Saving keeps 8 or 16 bits per
channel.
## Status ## Status
The scripts are written for Photoshop CC's ExtendScript and are not yet The scripts are written for Photoshop CC's ExtendScript and are not yet

View file

@ -22,8 +22,10 @@
try { try {
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE) if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
copy.changeMode(ChangeMode.RGB); copy.changeMode(ChangeMode.RGB);
if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT) // 8 and 16 bits per channel are kept (16-bit becomes a 16-bit
copy.bitsPerChannel = BitsPerChannelType.EIGHT; // YAPF texture); 32-bit documents are saved as 16-bit.
if (copy.bitsPerChannel === BitsPerChannelType.THIRTYTWO)
copy.bitsPerChannel = BitsPerChannelType.SIXTEEN;
var opts = new PNGSaveOptions(); var opts = new PNGSaveOptions();
opts.interlaced = false; opts.interlaced = false;
copy.saveAs(tmp, opts, true, Extension.LOWERCASE); copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
@ -35,6 +37,6 @@
if (mips) args.push("--mips"); if (mips) args.push("--mips");
var ok = yapfRun(args, out); var ok = yapfRun(args, out);
tmp.remove(); tmp.remove();
if (!ok) { alert("Could not write " + out.name + ".\n\n" + YAPF_TOOL_HELP); return; } if (!ok) { alert("Could not write " + out.name + ".\n\n" + yapfFailText()); return; }
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)"); alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
})(); })();

View file

@ -20,16 +20,52 @@ function yapfTool() {
return names[0]; // rely on the PATH return names[0]; // rely on the PATH
} }
// Runs the tool; returns true when `expectOut` was produced. // Runs the tool with its output (stdout) going to `stdoutFile` when given.
function yapfRun(args, expectOut) { // Returns the tool's error text ("" when it printed none).
function yapfExec(args, stdoutFile) {
var errFile = yapfTempFile(".txt");
var cmd = yapfQuote(yapfTool()); var cmd = yapfQuote(yapfTool());
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i])); for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
if (stdoutFile) cmd += " > " + yapfQuote(stdoutFile.fsName);
cmd += " 2> " + yapfQuote(errFile.fsName);
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
if (expectOut.exists) expectOut.remove();
app.system(cmd); app.system(cmd);
var text = yapfReadText(errFile);
errFile.remove();
return text;
}
function yapfReadText(f) {
if (!f.exists || !f.open("r")) return "";
var t = f.read();
f.close();
return t.replace(/\s+$/, "");
}
// Runs the tool; returns true when `expectOut` was produced. The tool's
// own message (why it failed) is left in yapfLastError.
var yapfLastError = "";
function yapfRun(args, expectOut) {
if (expectOut.exists) expectOut.remove();
yapfLastError = yapfExec(args, null);
return expectOut.exists; return expectOut.exists;
} }
// The tool's message, or the help text when the tool could not be run.
function yapfFailText() {
return /yapf: /.test(yapfLastError) ? yapfLastError : YAPF_TOOL_HELP;
}
// True when a .yapf file holds float / HDR data (half, float, BC6H), so it
// should open as a 32-bit document instead of a clamped 8-bit one.
function yapfIsHdr(file) {
var info = yapfTempFile(".txt");
yapfExec(["info", file.fsName], info);
var text = yapfReadText(info);
info.remove();
return /payload\s*:[^\n]*(half float|, float|BC6H)/.test(text);
}
function yapfTempFile(ext) { function yapfTempFile(ext) {
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" + return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
Math.floor(Math.random() * 1e6) + ext); Math.floor(Math.random() * 1e6) + ext);

View file

@ -5,9 +5,13 @@ Click any `.yapf` file in VS Code to see the image.
- Checkerboard behind transparent pixels. - Checkerboard behind transparent pixels.
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll** - **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
to zoom (pixels stay sharp when enlarged). to zoom (pixels stay sharp when enlarged).
- Picker for every **mip level** stored in the file. - Opens every YAPF file, version 1 and 2: GPU blocks (BC1–BC7, BC6H)
- Info bar: size, channels, sRGB / linear, GPU format hint, file size and decoded, 16-bit and float textures shown in 8-bit (floats sRGB encoded).
compression ratio, decode time. - Pickers for **mip level**, cube **face**, array **layer**, animation
**frame** and volume **slice**.
- Info bar: size, shape, storage (e.g. *GPU BC7*, *RGB half float*), sRGB /
linear, normal-map convention, format version, file size, decode time.
Damaged files say so (checksum mismatch).
- Reloads automatically when the file changes on disk. - Reloads automatically when the file changes on disk.
Decoding runs in plain JavaScript inside the preview; no native code. Decoding runs in plain JavaScript inside the preview; no native code.
@ -30,5 +34,6 @@ Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
The `.vsix` packages cleanly, and the preview code was tested in a browser The `.vsix` packages cleanly, and the preview code was tested in a browser
engine (headless Chromium) with real files: correct pixels, mip picker, engine (headless Chromium) with real files: correct pixels, mip picker,
error message for corrupt files. **It has not yet been run inside VS Code error message for corrupt files; the YAPF 2 preview was tested the same
itself.** way (BC7, BC5, BC6H, half float, 16-bit, cube, animation, damaged and
non-YAPF files). **It has not yet been run inside VS Code itself.**

View file

@ -1,26 +1,49 @@
// Webview side of the YAPF preview: decode, draw, zoom, mip picker, info. // Webview side of the YAPF preview: decode, draw, zoom, surface pickers, info.
(function () { (function () {
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null; const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
const info = document.getElementById('info'); const info = document.getElementById('info');
const controls = document.getElementById('controls'); const controls = document.getElementById('controls');
const errorBox = document.getElementById('error'); const errorBox = document.getElementById('error');
const GPU_NAMES = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8']; const PIXEL_HINTS = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8', 'RGBA16', 'RGB16', 'RG16', 'R16',
'RGBA16F', 'RGB16F', 'RG16F', 'R16F', 'RGBA32F', 'RGB32F', 'RG32F', 'R32F'];
const BLOCKS = { 0x40: 'BC1', 0x41: 'BC2', 0x42: 'BC3', 0x43: 'BC4', 0x44: 'BC4 snorm', 0x45: 'BC5',
0x46: 'BC5 snorm', 0x47: 'BC6H', 0x48: 'BC6H signed', 0x49: 'BC7', 0x50: 'ETC2 RGB',
0x51: 'ETC2 RGB A1', 0x52: 'ETC2 RGBA', 0x53: 'EAC R11', 0x54: 'EAC R11 snorm',
0x55: 'EAC RG11', 0x56: 'EAC RG11 snorm' };
['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((n, i) => { BLOCKS[0x60 + i] = 'ASTC ' + n; });
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA']; const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
const SAMPLES = ['8-bit', '16-bit', 'half float', 'float'];
const FACES = ['+X', '−X', '+Y', '−Y', '+Z', '−Z'];
let image = null, level = 0, fit = true, zoom = 1; let tex = null, fit = true, zoom = 1;
const sel = { mip: 0, face: 0, layer: 0, frame: 0, slice: 0 };
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; } function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
function mipDims(m) {
return [Math.max(1, tex.width >>> m), Math.max(1, tex.height >>> m),
tex.shape === 2 ? Math.max(1, tex.depth >>> m) : 1];
}
function draw() { function draw() {
const w = Math.max(1, image.width >>> level), h = Math.max(1, image.height >>> level); const s = YAPF.surfaceIndex(tex, sel.frame, sel.layer, sel.face, sel.mip);
canvas.width = w; canvas.height = h; sel.slice = Math.min(sel.slice, mipDims(sel.mip)[2] - 1);
canvas.getContext('2d').putImageData(new ImageData(YAPF.toRGBA(image, level), w, h), 0, 0); try {
const view = YAPF.textureToRGBA(tex, s, sel.slice);
canvas.width = view.width; canvas.height = view.height;
canvas.getContext('2d').putImageData(new ImageData(view.rgba, view.width, view.height), 0, 0);
errorBox.hidden = true;
} catch (e) {
errorBox.hidden = false;
errorBox.textContent = e.message;
}
layout(); layout();
} }
function layout() { function layout() {
if (!image) return; if (!tex) return;
const w = canvas.width, h = canvas.height; const w = canvas.width, h = canvas.height;
const stage = document.getElementById('stage'); const stage = document.getElementById('stage');
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom; const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
@ -31,49 +54,76 @@
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit'; stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
} }
function picker(key, count, label) {
if (count <= 1) return;
const el = document.createElement('select');
for (let i = 0; i < count; i++) {
const o = document.createElement('option');
o.value = i;
o.textContent = label(i);
el.appendChild(o);
}
sel[key] = Math.min(sel[key], count - 1);
el.value = sel[key];
el.onchange = () => { sel[key] = +el.value; if (key === 'mip') buildControls(); draw(); };
controls.appendChild(el);
}
function buildControls() {
controls.textContent = '';
picker('mip', tex.mipLevels, (m) => { const [w, h, d] = mipDims(m); return 'Mip ' + m + ' (' + w + '×' + h + (d > 1 ? '×' + d : '') + ')'; });
picker('face', tex.faces, (f) => 'Face ' + FACES[f]);
picker('layer', tex.layers, (l) => 'Layer ' + l);
picker('frame', tex.frames, (f) => 'Frame ' + (f + 1) + (tex.frameUs[f] ? ' (' + Math.round(tex.frameUs[f] / 1000) + ' ms)' : ''));
picker('slice', mipDims(sel.mip)[2], (z) => 'Slice ' + z);
}
function show(name, bytes) { function show(name, bytes) {
errorBox.hidden = true; errorBox.hidden = true;
const t = performance.now(); const t = performance.now();
try { try {
image = YAPF.decode(bytes); tex = YAPF.decodeTexture(bytes);
} catch (e) { } catch (e) {
image = null; tex = null;
errorBox.hidden = false; errorBox.hidden = false;
errorBox.textContent = name + ': ' + e.message; errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
info.textContent = ''; info.textContent = '';
controls.textContent = '';
return; return;
} }
const ms = performance.now() - t; const ms = performance.now() - t;
const raw = image.width * image.height * image.channels; let raw = 0;
level = Math.min(level, image.mipLevels - 1); for (let m = 0; m < tex.mipLevels; m++) {
info.textContent = [ const [w, h, d] = mipDims(m);
image.width + ' × ' + image.height, raw += w * h * d * (tex.payload ? 4 : tex.channels * [1, 2, 2, 4][tex.sample]);
CH_NAMES[image.channels],
(image.flags & 2 ? 'sRGB' : 'linear') + (image.flags & 1 ? ', premultiplied' : ''),
'GPU ' + (GPU_NAMES[image.gpuFormat] || '?'),
kb(bytes.length) + ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)',
'decoded in ' + ms.toFixed(1) + ' ms',
].join(' · ');
controls.textContent = '';
if (image.mipLevels > 1) {
const sel = document.createElement('select');
for (let m = 0; m < image.mipLevels; m++) {
const o = document.createElement('option');
o.value = m;
o.textContent = 'Mip ' + m + ' (' + Math.max(1, image.width >>> m) + '×' + Math.max(1, image.height >>> m) + ')';
sel.appendChild(o);
}
sel.value = level;
sel.onchange = () => { level = +sel.value; draw(); };
controls.appendChild(sel);
} }
raw *= tex.frames * tex.layers * tex.faces;
const [w0, h0, d0] = mipDims(0);
const shape = tex.shape === 1 ? (tex.layers > 1 ? 'cube array of ' + tex.layers : 'cubemap')
: tex.shape === 2 ? 'volume' : tex.layers > 1 ? 'array of ' + tex.layers : '';
const storage = tex.payload === 1 ? 'GPU ' + (BLOCKS[tex.format] || '?')
: CH_NAMES[tex.channels] + ' ' + SAMPLES[tex.sample] + ', GPU ' + (PIXEL_HINTS[tex.format] || '?');
const parts = [
w0 + ' × ' + h0 + (d0 > 1 ? ' × ' + d0 : ''),
shape,
tex.frames > 1 ? tex.frames + ' frames' : '',
storage,
(tex.flags & 2 ? 'sRGB' : 'linear') + (tex.flags & 1 ? ', premultiplied' : ''),
tex.normal ? 'normal map (' + ((tex.normal & 3) === 2 ? 'DirectX' : (tex.normal & 3) === 1 ? 'OpenGL' : '?') + ')' : '',
tex.version === 2 ? 'YAPF 2' : 'YAPF 1',
kb(bytes.length) + (tex.payload ? '' : ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)'),
'decoded in ' + ms.toFixed(1) + ' ms',
];
const author = tex.notes.find((n) => n.tag === 'AUTH');
if (author) parts.push('by ' + new TextDecoder().decode(author.data));
info.textContent = parts.filter(Boolean).join(' · ');
buildControls();
draw(); draw();
} }
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); }); document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
document.getElementById('stage').addEventListener('wheel', (e) => { document.getElementById('stage').addEventListener('wheel', (e) => {
if (!e.ctrlKey || !image) return; if (!e.ctrlKey || !tex) return;
e.preventDefault(); e.preventDefault();
fit = false; fit = false;
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8))); zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));

View file

@ -632,6 +632,35 @@ static void v2_mips(void) {
yapf_tex_free(t); yapf_tex_free(t);
} }
static void v2_preview(void) {
uint8_t out[4 * 4 * 4];
/* gray + alpha u16 */
yapf_texture_t d = desc(4, 4, 2, YAPF_SAMPLE_U16);
yapf_texture_t *t = yapf_tex_create(&d);
uint16_t *p = (uint16_t *)t->data[0];
for (int i = 0; i < 16; i++) { p[2 * i] = 65535; p[2 * i + 1] = 32896; }
int ok = yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK &&
out[0] == 255 && out[1] == 255 && out[2] == 255 && out[3] == 128;
yapf_tex_free(t);
/* linear half 0.2140 → sRGB 128, 2.0 clamps, alpha stays linear */
d = desc(4, 4, 4, YAPF_SAMPLE_F16);
t = yapf_tex_create(&d);
p = (uint16_t *)t->data[0];
for (int i = 0; i < 16; i++) {
p[4 * i] = yapf_float_to_half(0.2158605f); p[4 * i + 1] = yapf_float_to_half(2.0f);
p[4 * i + 2] = yapf_float_to_half(-1.0f); p[4 * i + 3] = yapf_float_to_half(0.5f);
}
ok = ok && yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK &&
out[0] == 128 && out[1] == 255 && out[2] == 0 && out[3] == 128;
/* BC7 and BC6H go through the block decoders */
yapf_texture_t *c = NULL;
ok = ok && yapf_tex_compress(t, YAPF_GPU_BC6H, &c) == YAPF_OK &&
yapf_tex_to_rgba8(c, 0, 0, out) == YAPF_OK && out[3] == 255 && out[1] == 255;
yapf_tex_free(c);
yapf_tex_free(t);
CHECK(ok, "yapf_tex_to_rgba8 converts every payload for display");
}
static void v2_misc(void) { static void v2_misc(void) {
int ok = 1; int ok = 1;
for (uint32_t h = 0; h < 0x10000 && ok; h++) { for (uint32_t h = 0; h < 0x10000 && ok; h++) {
@ -697,6 +726,7 @@ int main(int argc, char **argv) {
v2_gpu(); v2_gpu();
v2_mips(); v2_mips();
v2_misc(); v2_misc();
v2_preview();
printf("%d tests, %d failures\n", tests, fails); printf("%d tests, %d failures\n", tests, fails);
return fails != 0; return fails != 0;

View file

@ -167,6 +167,8 @@ static int parse_map(const char *s, uint8_t *map) {
return 1; return 1;
} }
static const char *err_name(int e);
/* ── Reading any input into a texture ────────────────────────────────── */ /* ── Reading any input into a texture ────────────────────────────────── */
static yapf_texture_t *new_tex(uint32_t w, uint32_t h, int ch, int sample) { static yapf_texture_t *new_tex(uint32_t w, uint32_t h, int ch, int sample) {
@ -182,7 +184,7 @@ static yapf_texture_t *read_input(const char *path, const opts_t *o, int *from_y
if (is_yapf(path)) { if (is_yapf(path)) {
int err = 0; int err = 0;
yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err); yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err);
if (!t) fprintf(stderr, "yapf: cannot read %s (error %d)\n", path, err); if (!t) fprintf(stderr, "yapf: cannot read %s: %s\n", path, err_name(err));
*from_yapf = 1; *from_yapf = 1;
return t; return t;
} }
@ -399,6 +401,58 @@ static uint8_t *to8(const float *f, size_t n) {
return o; return o;
} }
/* 16-bit PNG (stb_image_write only writes 8 bits): rows of big-endian
* samples, filter 0, compressed with stb's zlib. */
static uint32_t png_crc(const uint8_t *p, size_t n, uint32_t c) {
c = ~c;
for (size_t i = 0; i < n; i++) {
c ^= p[i];
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1)));
}
return ~c;
}
static void png_chunk(FILE *f, const char *type, const uint8_t *d, uint32_t n) {
uint8_t b[4] = { (uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n };
fwrite(b, 1, 4, f);
fwrite(type, 1, 4, f);
if (n) fwrite(d, 1, n, f);
uint32_t c = png_crc(d, n, png_crc((const uint8_t *)type, 4, 0));
uint8_t cb[4] = { (uint8_t)(c >> 24), (uint8_t)(c >> 16), (uint8_t)(c >> 8), (uint8_t)c };
fwrite(cb, 1, 4, f);
}
static int write_png16(const char *path, const uint16_t *px, int w, int h, int ch) {
size_t row = (size_t)w * ch * 2 + 1;
uint8_t *raw = (uint8_t *)malloc(row * h);
if (!raw) return 0;
for (int y = 0; y < h; y++) {
uint8_t *r = raw + row * y;
r[0] = 0;
for (int i = 0; i < w * ch; i++) {
uint16_t v = px[(size_t)y * w * ch + i];
r[1 + 2 * i] = (uint8_t)(v >> 8);
r[2 + 2 * i] = (uint8_t)v;
}
}
int zlen = 0;
uint8_t *z = stbi_zlib_compress(raw, (int)(row * h), &zlen, 8);
free(raw);
FILE *f = z ? fopen(path, "wb") : NULL;
if (!f) { STBIW_FREE(z); return 0; }
static const uint8_t sig[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 };
static const uint8_t ctype[5] = { 0, 0, 4, 2, 6 };
uint8_t ihdr[13] = { (uint8_t)(w >> 24), (uint8_t)(w >> 16), (uint8_t)(w >> 8), (uint8_t)w,
(uint8_t)(h >> 24), (uint8_t)(h >> 16), (uint8_t)(h >> 8), (uint8_t)h,
16, ctype[ch], 0, 0, 0 };
fwrite(sig, 1, 8, f);
png_chunk(f, "IHDR", ihdr, 13);
png_chunk(f, "IDAT", z, (uint32_t)zlen);
png_chunk(f, "IEND", NULL, 0);
STBIW_FREE(z);
return fclose(f) == 0;
}
static int write_image(const char *path, const void *px, int w, int h, int ch, int hdr) { static int write_image(const char *path, const void *px, int w, int h, int ch, int hdr) {
if (ends_with(path, ".hdr")) { if (ends_with(path, ".hdr")) {
if (!hdr) { if (!hdr) {
@ -445,9 +499,19 @@ static int export_image(const yapf_texture_t *t, const char *in, const char *out
: "no such slice, or out of memory"); : "no such slice, or out of memory");
return 1; return 1;
} }
if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr")) int ok;
fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit\n"); if (t->payload == YAPF_PAYLOAD_PIXELS && t->sample == YAPF_SAMPLE_U16 && ends_with(out, ".png")) {
int ok = write_image(out, px, (int)w, (int)h, ch, hdr); /* keep all 16 bits */
uint32_t m = s % t->mip_levels, d;
yapf_tex_mip_size(t, m, NULL, NULL, &d);
const uint16_t *src = (const uint16_t *)((const uint8_t *)t->data[s] +
yapf_tex_surface_bytes(t, m) / d * (uint32_t)o->slice);
ok = write_png16(out, src, (int)w, (int)h, t->channels);
} else {
if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr"))
fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit (use .png to keep them)\n");
ok = write_image(out, px, (int)w, (int)h, ch, hdr);
}
free(px); free(px);
if (!ok) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; } if (!ok) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; }
return 0; return 0;

68
yapf.c
View file

@ -4677,3 +4677,71 @@ int yapf_tex_build_mips(yapf_texture_t *tex, uint32_t levels, uint8_t alpha_ref)
else tex->flags &= (uint8_t)~YAPF_FLAG_COVERAGE; else tex->flags &= (uint8_t)~YAPF_FLAG_COVERAGE;
return YAPF_OK; return YAPF_OK;
} }
/* ========================================================================
* Previews
* ======================================================================== */
/* Linear 0..1 → sRGB code, exact to the 8-bit rounding (thresholds). */
static uint8_t yapf__lin_to_code(const yapf__srgb_tab *t, float v) {
if (!(v > 0.0f)) return 0; /* also NaN */
if (v >= 1.0f) return 255;
return (uint8_t)(yapf__lin_to_srgb(t, v) * 255.0f + 0.5f);
}
int yapf_tex_to_rgba8(const yapf_texture_t *tex, uint32_t surface, uint32_t slice, uint8_t *out) {
uint32_t S;
if (!out || yapf__tex_check(tex, &S) != 0 || surface >= S || !tex->data || !tex->data[surface])
return YAPF_ERR_INVALID;
uint32_t m = surface % tex->mip_levels, w, h, d;
yapf_tex_mip_size(tex, m, &w, &h, &d);
if (slice >= d) return YAPF_ERR_INVALID;
size_t n = (size_t)w * h;
const uint8_t *src = (const uint8_t *)tex->data[surface] + yapf_tex_surface_bytes(tex, m) / d * slice;
int ch = tex->channels, sample = tex->sample;
uint16_t *half = NULL;
if (tex->payload == YAPF_PAYLOAD_BLOCKS) {
if (tex->format != YAPF_GPU_BC6H && tex->format != YAPF_GPU_BC6H_SF) {
int rc = yapf_blocks_decode(tex->format, src, w, h, out);
if (rc == YAPF_OK && tex->channels == 3) /* BC1 / BC7 without alpha */
for (size_t i = 0; i < n; i++) out[i * 4 + 3] = 255;
return rc;
}
half = (uint16_t *)malloc(n * 3 * sizeof(uint16_t));
if (!half) return YAPF_ERR_OOM;
yapf_blocks_decode(tex->format, src, w, h, half);
src = (const uint8_t *)half;
ch = 3;
sample = YAPF_SAMPLE_F16;
}
yapf__srgb_tab *st = NULL;
if (sample == YAPF_SAMPLE_F16 || sample == YAPF_SAMPLE_F32) {
st = (yapf__srgb_tab *)malloc(sizeof(yapf__srgb_tab));
if (!st) { free(half); return YAPF_ERR_OOM; }
yapf__srgb_init(st);
}
size_t sb = yapf__sbytes(sample);
for (size_t i = 0; i < n; i++) {
uint8_t v[4] = { 0, 0, 0, 255 };
for (int c = 0; c < ch; c++) {
uint32_t x = yapf__get_sample(src + (i * ch + c) * sb, sample);
int alpha = (ch == 2 && c == 1) || (ch == 4 && c == 3);
uint8_t b;
if (sample == YAPF_SAMPLE_U8) b = (uint8_t)x;
else if (sample == YAPF_SAMPLE_U16) b = (uint8_t)((x + 128) / 257);
else {
float f;
if (sample == YAPF_SAMPLE_F16) f = yapf_half_to_float((uint16_t)x);
else memcpy(&f, &x, 4);
b = alpha ? (uint8_t)yapf__clampi(f * 255.0f, 0, 255) : yapf__lin_to_code(st, f);
}
v[ch <= 2 ? (c == 0 ? 0 : 3) : c] = b;
}
if (ch <= 2) v[1] = v[2] = v[0];
memcpy(out + i * 4, v, 4);
}
free(st);
free(half);
return YAPF_OK;
}

10
yapf.h
View file

@ -601,6 +601,16 @@ int yapf_tex_compress(const yapf_texture_t *src, uint8_t format,
int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h, int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h,
void *out); void *out);
/*
* yapf_tex_to_rgba8 — one slice of a decoded surface as 8-bit RGBA for
* display (thumbnails, previews): gray is expanded, 16-bit is rounded to
* 8 bits, float and half are clamped to 0..1 and sRGB encoded (they are
* linear), GPU blocks are decoded. out holds w × h × 4 bytes of that
* mip. Returns YAPF_ERR_UNSUPPORTED for ETC2 / EAC / ASTC blocks.
*/
int yapf_tex_to_rgba8(const yapf_texture_t *tex, uint32_t surface, uint32_t slice,
uint8_t *out);
/* yapf-sum, the checksum used by version 2 files. */ /* yapf-sum, the checksum used by version 2 files. */
uint32_t yapf_checksum(const void *data, size_t size); uint32_t yapf_checksum(const void *data, size_t size);