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);
```
The language bindings and the JavaScript port speak version 1; the C
library's v1 functions also read 8-bit version 2 files (2D, cube face 0,
array layer 0), so plugins built on them open those too.
Every extension opens version 2 files: GIMP (at full 16-bit / float
precision, faces and frames as layers), Blender (HDR as float images),
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 |
| :----- | :------ | :----- |
| 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 |
| 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 | |

View file

@ -1,6 +1,6 @@
# 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
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`.
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');
/* 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`);
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.
*
@ -10,9 +10,14 @@
* const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
*
* const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
* const view = YAPF.textureToRGBA(tex, surface); // { width, height, rgba }
*
* An image is { width, height, channels (1–4), pixels: Uint8Array,
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
* 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) {
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 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 ──────────────────────────────────────────────────────── */
@ -102,7 +776,7 @@
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();
let hdr = b[p++];
if (hdr === STORED) {
@ -112,10 +786,18 @@
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return;
}
if (hdr & 0xE0) corrupt(); /* reserved bits set */
const xf = hdr & SUBGREEN;
if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
if ((hdr & 0x40) && (hdr & SUBGREEN)) corrupt();
/* xf: 0 YCoCg-R, 1 subtract-green, 2 none (v2) */
const xf = (hdr & 0x40) ? 2 : (hdr & SUBGREEN) ? 1 : 0, ent = hdr & 0x20;
hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
let emask = 0;
if (ent) { /* v2: entropy planes */
if (p >= end || !book) corrupt();
emask = b[p++];
if ((emask >> ch) || (emask & hdr)) corrupt();
}
const rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen);
@ -128,7 +810,8 @@
for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue;
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;
}
if (p !== end) corrupt();
@ -147,7 +830,11 @@
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) {
if (xf) {
if (xf === 2) {
out[d] = rows[P0 + x];
out[d + 1] = rows[P1 + x];
out[d + 2] = rows[P2 + x];
} else if (xf) {
const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g;
@ -171,11 +858,27 @@
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
/* decode(bytes) → 8-bit image. Reads version 1, and version 2 files
* that hold 8-bit pixels (2D; cube face 0 / layer 0 / frame 0). Other
* textures: see decodeTexture. */
function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
if (b.length >= 5 && b[0] === 0x59 && b[1] === 0x41 && b[2] === 0x50 && b[3] === 0x46 && b[4] === 2) {
const t = decodeTexture(b);
if (t.payload !== 0 || t.sample !== 0 || t.shape === 2)
throw new YapfError('unsupported', 'this is a YAPF 2 texture (GPU blocks, 16-bit / float or a ' +
'volume); use YAPF.decodeTexture and YAPF.textureToRGBA');
const mips = t.surfaces.slice(0, t.mipLevels);
return { width: t.width, height: t.height, channels: t.channels, gpuFormat: t.format,
flags: t.flags, mipLevels: t.mipLevels, pixels: mips[0], mips };
}
return decodeV1(b);
}
function decodeV1(b) {
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
throw new Error('YAPF: not a YAPF file');
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
throw new YapfError('not-yapf', 'not a YAPF file');
if (b[4] !== 1) throw new YapfError('version', 'unsupported format version ' + b[4]);
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
@ -459,5 +1162,7 @@
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.
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.
@ -8,6 +8,11 @@ Copyright 2026 Nexoniarz — Apache License 2.0.
img.width, img.height, img.channels # pixels: bytes, row-major
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
encode); otherwise falls back to a pure-Python decoder that needs numpy
(decode only). The library is looked up in this order:
@ -26,12 +31,22 @@ import platform
import struct
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",
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
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):
@ -70,6 +85,26 @@ class _CImage(ctypes.Structure):
("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():
machine = platform.machine().lower()
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),
ctypes.POINTER(ctypes.c_size_t)]
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
except (OSError, AttributeError):
continue
@ -115,7 +164,7 @@ def _c_decode(data):
data = bytes(data)
p = _lib.yapf_load_memory_mt(data, len(data), 0)
if not p:
raise YapfError("not a valid YAPF v1 file")
raise YapfError(_why(data))
try:
c = p.contents
mips = []
@ -157,6 +206,9 @@ def _py_decode(data):
b = np.frombuffer(bytes(data), dtype=np.uint8)
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
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:
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])
@ -275,6 +327,127 @@ def _py_tile(np, b, p, end, tw, th, ch, bad):
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 ────────────────────────────────────────────────────────
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
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
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++
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
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

View file

@ -12,6 +12,14 @@ Blender 5.1).
Blender cannot read `.yapf` on its own, so imported images are **packed into
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
1. Get `yapf_image-blender.zip` from the

View file

@ -33,30 +33,61 @@ from . import yapf
# ── conversion helpers ────────────────────────────────────────────────
def load_image(filepath):
"""Read a .yapf file into a packed bpy.types.Image."""
img = yapf.load(filepath)
def _set_colorspace(bimg, names):
for n in names: # names differ between versions
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
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)
if ch >= 3:
rgba[..., :3] = src[..., :3]
else:
rgba[..., :3] = src[..., :1]
rgba[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
if ch in (2, 4):
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)
bimg = bpy.data.images.new(name, w, h, alpha=ch in (2, 4))
bimg.colorspace_settings.name = "sRGB" if img.flags & yapf.FLAG_SRGB else "Non-Color"
if img.flags & yapf.FLAG_PREMULT_ALPHA:
bimg = bpy.data.images.new(name, w, h, alpha=alpha, float_buffer=wide)
if tex is not None and tex.is_hdr:
_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.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.use_fake_user = True # …even before anything uses it
bimg["yapf_source"] = filepath
bimg["yapf_alpha"] = ch in (2, 4)
bimg["yapf_alpha"] = alpha
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
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)
1. Use the **Figma desktop app**.

View file

@ -64,14 +64,26 @@ $('tab-import').onclick = () => tab('import');
$('tab-export').onclick = () => tab('export');
/* 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) {
const images = [];
for (const f of files) {
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);
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) {
status(f.name + ': ' + e.message);
}

View file

@ -47,7 +47,7 @@
<div id="status"></div>
<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.
*
@ -58,9 +58,14 @@
* const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
*
* const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
* const view = YAPF.textureToRGBA(tex, surface); // { width, height, rgba }
*
* An image is { width, height, channels (1–4), pixels: Uint8Array,
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
* 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) {
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 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 ──────────────────────────────────────────────────────── */
@ -150,7 +824,7 @@
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();
let hdr = b[p++];
if (hdr === STORED) {
@ -160,10 +834,18 @@
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return;
}
if (hdr & 0xE0) corrupt(); /* reserved bits set */
const xf = hdr & SUBGREEN;
if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
if ((hdr & 0x40) && (hdr & SUBGREEN)) corrupt();
/* xf: 0 YCoCg-R, 1 subtract-green, 2 none (v2) */
const xf = (hdr & 0x40) ? 2 : (hdr & SUBGREEN) ? 1 : 0, ent = hdr & 0x20;
hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
let emask = 0;
if (ent) { /* v2: entropy planes */
if (p >= end || !book) corrupt();
emask = b[p++];
if ((emask >> ch) || (emask & hdr)) corrupt();
}
const rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen);
@ -176,7 +858,8 @@
for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue;
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;
}
if (p !== end) corrupt();
@ -195,7 +878,11 @@
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) {
if (xf) {
if (xf === 2) {
out[d] = rows[P0 + x];
out[d + 1] = rows[P1 + x];
out[d + 2] = rows[P2 + x];
} else if (xf) {
const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g;
@ -219,11 +906,27 @@
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
/* decode(bytes) → 8-bit image. Reads version 1, and version 2 files
* that hold 8-bit pixels (2D; cube face 0 / layer 0 / frame 0). Other
* textures: see decodeTexture. */
function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
if (b.length >= 5 && b[0] === 0x59 && b[1] === 0x41 && b[2] === 0x50 && b[3] === 0x46 && b[4] === 2) {
const t = decodeTexture(b);
if (t.payload !== 0 || t.sample !== 0 || t.shape === 2)
throw new YapfError('unsupported', 'this is a YAPF 2 texture (GPU blocks, 16-bit / float or a ' +
'volume); use YAPF.decodeTexture and YAPF.textureToRGBA');
const mips = t.surfaces.slice(0, t.mipLevels);
return { width: t.width, height: t.height, channels: t.channels, gpuFormat: t.format,
flags: t.flags, mipLevels: t.mipLevels, pixels: mips[0], mips };
}
return decodeV1(b);
}
function decodeV1(b) {
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
throw new Error('YAPF: not a YAPF file');
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
throw new YapfError('not-yapf', 'not a YAPF file');
if (b[4] !== 1) throw new YapfError('version', 'unsupported format version ' + b[4]);
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
@ -507,7 +1210,9 @@
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>/*
@ -653,14 +1358,26 @@ $('tab-import').onclick = () => tab('import');
$('tab-export').onclick = () => tab('export');
/* 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) {
const images = [];
for (const f of files) {
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);
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) {
status(f.name + ': ' + e.message);
}

View file

@ -2,7 +2,11 @@
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
flattened into a copy; your document is untouched), with an optional
**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
gamma encoded. Files marked linear load as linear.
- 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
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;
}
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg);
/* ── 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(
GimpProcedure *procedure,
GimpRunMode run_mode,
@ -151,60 +187,94 @@ static GimpValueArray *yapf_load_run(
{
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
gchar *path = g_file_get_path(file);
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */
gchar *path = g_file_get_path(file);
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);
if (!img) {
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
"Failed to load YAPF file.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
/* What the pixels look like after decoding: GPU blocks decode to
* RGBA 8-bit, BC6H to RGB half float. */
gboolean blocks = t->payload == YAPF_PAYLOAD_BLOCKS;
gboolean bc6 = blocks && (t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF);
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.
* Use gamma-corrected babl formats (R'G'B') when the file carries the
* sRGB flag; fall back to linear (RGB) otherwise. */
gboolean is_srgb = (img->flags & YAPF_FLAG_SRGB) != 0;
GimpImageBaseType base_type = GIMP_RGB;
GimpImageType layer_type = GIMP_RGBA_IMAGE;
const gchar *babl_fmt = is_srgb ? "R'G'B'A u8" : "RGBA u8";
static const gchar *chn[2][4] = { { "Y", "YA", "RGB", "RGBA" }, { "Y'", "Y'A", "R'G'B'", "R'G'B'A" } };
static const gchar *typ[4] = { "u8", "u16", "half", "float" };
gchar *fmt_name = g_strdup_printf("%s %s", chn[gamma ? 1 : 0][ch - 1], typ[sample]);
const Babl *fmt = babl_format(fmt_name);
g_free(fmt_name);
switch (img->channels) {
case YAPF_CHANNELS_GRAY:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAY_IMAGE;
babl_fmt = is_srgb ? "Y' u8" : "Y u8";
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;
GimpPrecision prec;
switch (sample) {
case YAPF_SAMPLE_U16: prec = gamma ? GIMP_PRECISION_U16_NON_LINEAR : GIMP_PRECISION_U16_LINEAR; break;
case YAPF_SAMPLE_F16: prec = GIMP_PRECISION_HALF_LINEAR; break;
case YAPF_SAMPLE_F32: prec = GIMP_PRECISION_FLOAT_LINEAR; break;
default: prec = gamma ? GIMP_PRECISION_U8_NON_LINEAR : GIMP_PRECISION_U8_LINEAR; 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);
GimpLayer *layer = gimp_layer_new(image, "Background",
img->width, img->height, layer_type,
100.0,
gimp_image_get_default_new_layer_mode(image));
gimp_image_insert_layer(image, layer, NULL, 0);
/* Mip 0 of every frame, layer, face and slice becomes one GIMP layer
* (the first at the bottom, as GIMP animations expect). */
uint32_t w, h, d, faces = yapf_tex_faces(t);
yapf_tex_mip_size(t, 0, &w, &h, &d);
GimpImage *image = gimp_image_new_with_precision(w, h, base, prec);
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));
gegl_buffer_set(buffer,
GEGL_RECTANGLE(0, 0, (gint)img->width, (gint)img->height),
0, babl_format(babl_fmt),
img->pixels, GEGL_AUTO_ROWSTRIDE);
g_object_unref(buffer);
for (uint32_t fr = 0; fr < t->frames; fr++)
for (uint32_t ly = 0; ly < t->layers; ly++)
for (uint32_t fc = 0; fc < faces; fc++)
for (uint32_t z = 0; z < d; z++) {
uint32_t s = yapf_tex_surface(t, fr, ly, fc, 0);
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,
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
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
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 <KPluginFactory>
#include <QFile>
#include <QImage>
#include <cstring>
extern "C" {
#include "yapf.h"
}
@ -20,52 +23,50 @@ public:
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
{
const QByteArray path = request.url().toLocalFile().toLocal8Bit();
yapf_image_t *img = yapf_load(path.constData());
if (!img) {
QFile file(request.url().toLocalFile());
if (!file.open(QIODevice::ReadOnly)) {
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();
}
// Start from the smallest stored mip level that still covers the
// requested size; Qt scales the rest.
// The smallest stored mip that still covers the requested size;
// Qt scales the rest.
const QSize want = request.targetSize();
int m = 0;
while (m + 1 < img->mip_levels &&
int(img->width >> (m + 1)) >= want.width() &&
int(img->height >> (m + 1)) >= want.height()) {
uint32_t m = 0, w = 0, h = 0;
while (m + 1 < tex->mip_levels) {
yapf_tex_mip_size(tex, m + 1, &w, &h, nullptr);
if (int(w) < want.width() || int(h) < want.height()) {
break;
}
++m;
}
const int w = qMax(1, int(img->width >> m));
const int h = qMax(1, int(img->height >> m));
const uchar *px = img->mips[m];
yapf_tex_mip_size(tex, m, &w, &h, nullptr);
const uint32_t s = yapf_tex_surface(tex, 0, 0, 0, m);
QImage image;
switch (img->channels) {
case 1:
image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy();
break;
case 2: { // gray + alpha → RGBA
image = QImage(w, h, QImage::Format_RGBA8888);
for (int y = 0; y < h; ++y) {
const uchar *s = px + size_t(y) * w * 2;
uchar *d = image.scanLine(y);
for (int x = 0; x < w; ++x, s += 2, d += 4) {
d[0] = d[1] = d[2] = s[0];
d[3] = s[1];
}
QImage image(int(w), int(h), (tex->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
: QImage::Format_RGBA8888);
bool ok = !image.isNull() &&
yapf_tex_decode_surface(tex, data.constData(), size_t(data.size()), s, 0) == YAPF_OK;
if (ok) {
// RGBA 8-bit rows (blocks decoded, 16-bit / float converted)
QByteArray rgba(int(size_t(w) * h * 4), Qt::Uninitialized);
ok = yapf_tex_to_rgba8(tex, s, 0, reinterpret_cast<uint8_t *>(rgba.data())) == YAPF_OK;
for (int y = 0; ok && y < int(h); ++y) {
memcpy(image.scanLine(y), rgba.constData() + size_t(y) * w * 4, size_t(w) * 4);
}
break;
}
case 3:
image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy();
break;
default:
image = QImage(px, w, h, w * 4,
(img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
: QImage::Format_RGBA8888).copy();
break;
yapf_tex_free(tex);
if (!ok) {
return KIO::ThumbnailResult::fail();
}
yapf_free(img);
if (image.width() > want.width() || image.height() > want.height()) {
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),
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
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];
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)) {
alert("Could not open " + files[i].name + ".\n\n" + YAPF_TOOL_HELP);
alert("Could not open " + files[i].name + ".\n\n" + yapfFailText());
return;
}
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`.
## 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
The scripts are written for Photoshop CC's ExtendScript and are not yet

View file

@ -22,8 +22,10 @@
try {
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
copy.changeMode(ChangeMode.RGB);
if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT)
copy.bitsPerChannel = BitsPerChannelType.EIGHT;
// 8 and 16 bits per channel are kept (16-bit becomes a 16-bit
// YAPF texture); 32-bit documents are saved as 16-bit.
if (copy.bitsPerChannel === BitsPerChannelType.THIRTYTWO)
copy.bitsPerChannel = BitsPerChannelType.SIXTEEN;
var opts = new PNGSaveOptions();
opts.interlaced = false;
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
@ -35,6 +37,6 @@
if (mips) args.push("--mips");
var ok = yapfRun(args, out);
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)");
})();

View file

@ -20,16 +20,52 @@ function yapfTool() {
return names[0]; // rely on the PATH
}
// Runs the tool; returns true when `expectOut` was produced.
function yapfRun(args, expectOut) {
// Runs the tool with its output (stdout) going to `stdoutFile` when given.
// Returns the tool's error text ("" when it printed none).
function yapfExec(args, stdoutFile) {
var errFile = yapfTempFile(".txt");
var cmd = yapfQuote(yapfTool());
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 (expectOut.exists) expectOut.remove();
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;
}
// 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) {
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
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.
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
to zoom (pixels stay sharp when enlarged).
- Picker for every **mip level** stored in the file.
- Info bar: size, channels, sRGB / linear, GPU format hint, file size and
compression ratio, decode time.
- Opens every YAPF file, version 1 and 2: GPU blocks (BC1–BC7, BC6H)
decoded, 16-bit and float textures shown in 8-bit (floats sRGB encoded).
- 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.
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
engine (headless Chromium) with real files: correct pixels, mip picker,
error message for corrupt files. **It has not yet been run inside VS Code
itself.**
error message for corrupt files; the YAPF 2 preview was tested the same
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 () {
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
const canvas = document.getElementById('canvas');
const info = document.getElementById('info');
const controls = document.getElementById('controls');
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 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 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() {
const w = Math.max(1, image.width >>> level), h = Math.max(1, image.height >>> level);
canvas.width = w; canvas.height = h;
canvas.getContext('2d').putImageData(new ImageData(YAPF.toRGBA(image, level), w, h), 0, 0);
const s = YAPF.surfaceIndex(tex, sel.frame, sel.layer, sel.face, sel.mip);
sel.slice = Math.min(sel.slice, mipDims(sel.mip)[2] - 1);
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();
}
function layout() {
if (!image) return;
if (!tex) return;
const w = canvas.width, h = canvas.height;
const stage = document.getElementById('stage');
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';
}
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) {
errorBox.hidden = true;
const t = performance.now();
try {
image = YAPF.decode(bytes);
tex = YAPF.decodeTexture(bytes);
} catch (e) {
image = null;
tex = null;
errorBox.hidden = false;
errorBox.textContent = name + ': ' + e.message;
errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
info.textContent = '';
controls.textContent = '';
return;
}
const ms = performance.now() - t;
const raw = image.width * image.height * image.channels;
level = Math.min(level, image.mipLevels - 1);
info.textContent = [
image.width + ' × ' + image.height,
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);
let raw = 0;
for (let m = 0; m < tex.mipLevels; m++) {
const [w, h, d] = mipDims(m);
raw += w * h * d * (tex.payload ? 4 : tex.channels * [1, 2, 2, 4][tex.sample]);
}
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();
}
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
document.getElementById('stage').addEventListener('wheel', (e) => {
if (!e.ctrlKey || !image) return;
if (!e.ctrlKey || !tex) return;
e.preventDefault();
fit = false;
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);
}
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) {
int ok = 1;
for (uint32_t h = 0; h < 0x10000 && ok; h++) {
@ -697,6 +726,7 @@ int main(int argc, char **argv) {
v2_gpu();
v2_mips();
v2_misc();
v2_preview();
printf("%d tests, %d failures\n", tests, fails);
return fails != 0;

View file

@ -167,6 +167,8 @@ static int parse_map(const char *s, uint8_t *map) {
return 1;
}
static const char *err_name(int e);
/* ── Reading any input into a texture ────────────────────────────────── */
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)) {
int err = 0;
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;
return t;
}
@ -399,6 +401,58 @@ static uint8_t *to8(const float *f, size_t n) {
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) {
if (ends_with(path, ".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");
return 1;
}
if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr"))
fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit\n");
int ok = write_image(out, px, (int)w, (int)h, ch, hdr);
int ok;
if (t->payload == YAPF_PAYLOAD_PIXELS && t->sample == YAPF_SAMPLE_U16 && ends_with(out, ".png")) {
/* 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);
if (!ok) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; }
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;
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,
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. */
uint32_t yapf_checksum(const void *data, size_t size);