YAPF/extensions/figma/ui.html
Nexoniarz 2ca726e3af YAPF v1: fast byte-aligned format, SIMD, tools and plugins
Format (version 1, replaces the earlier experimental versions):
- per-tile YCoCg-R or subtract-green colour transform, 8-bit modular
- 12 row filters per 8-row band (Left, Gradient, Up-right, Up-average,
  Up 1-8 rows), none with more than a one-add serial dependency
- residuals in groups of 8 at the group's width (w bits x 8 = w bytes),
  4-bit control codes with repeat-previous-width runs chosen by DP
- constant and stored tiles, independent 64x64 tiles, mips, GPU hints
- all sizes validated before decoding; corrupt input is rejected

Library:
- SIMD on by default: SSSE3 (runtime CPUID check; GCC, Clang, MSVC,
  MinGW) and NEON on ARM64; -DYAPF_NO_SIMD for plain C
- yapf_load_memory, yapf_load_mt, yapf_load_memory_mt
- 6-10x faster decoding than PNG (stb_image) on one core; -O2 and -O3
  builds perform the same
- fixes for macOS (sysconf) and the Windows import library exports

Tools and integrations:
- `yapf` CLI: convert PNG/JPG/TGA/BMP/PSD/GIF <-> YAPF, info, bench,
  thumbnail, mips
- JavaScript and Python bindings (byte-identical to the C encoder)
- GIMP 3 plugin: exports the flattened image, optional mipmaps
- Blender extension: import (also as plane), export, drag and drop
- VS Code preview, Figma plugin, Photoshop scripts, Linux MIME type and
  thumbnailer
- Makefile, updated build.sh / build.bat, tools/build_release.sh
- tests/test_yapf.c (784 checks) and bindings/js/test.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:17:59 +02:00

713 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 12px Inter, system-ui, sans-serif; margin: 0; padding: 14px;
color: var(--figma-color-text, #333); background: var(--figma-color-bg, #fff); }
h2 { font-size: 13px; margin: 0 0 10px; }
.tabs { display: flex; gap: 6px; margin-bottom: 12px; }
.tabs button { flex: 1; }
button { font: inherit; padding: 7px 10px; border-radius: 6px; cursor: pointer;
border: 1px solid var(--figma-color-border, #ccc);
background: var(--figma-color-bg-secondary, #f5f5f5); color: inherit; }
button.primary, .tabs button.on { background: var(--figma-color-bg-brand, #0d99ff);
color: var(--figma-color-text-onbrand, #fff); border-color: transparent; }
#drop { border: 2px dashed var(--figma-color-border, #ccc); border-radius: 8px; padding: 28px 10px;
text-align: center; cursor: pointer; color: var(--figma-color-text-secondary, #777); }
#drop.hover { border-color: var(--figma-color-border-brand, #0d99ff); }
#status { margin-top: 10px; min-height: 32px; color: var(--figma-color-text-secondary, #777);
white-space: pre-line; }
.row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
select { font: inherit; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<div class="tabs">
<button id="tab-import">Import</button>
<button id="tab-export">Export</button>
</div>
<section id="import">
<h2>Import YAPF images</h2>
<div id="drop">Drop .yapf files here<br>or click to choose</div>
<input id="file" type="file" accept=".yapf" multiple hidden>
</section>
<section id="export" hidden>
<h2>Export selection as YAPF</h2>
<div id="sel">Nothing selected</div>
<div class="row">Scale
<select id="scale"><option>1</option><option>2</option><option>4</option></select>
</div>
<button id="go" class="primary">Export</button>
</section>
<div id="status"></div>
<script>/*
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript.
*
* Copyright 2026 Nexoniarz — Apache License 2.0.
*
* Works in browsers, Node.js, Figma plugins and VS Code webviews; no
* dependencies. Produces exactly the same bytes as the C library.
*
* const img = YAPF.decode(bytes); // Uint8Array → image
* const file = YAPF.encode(img); // image → Uint8Array
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
*
* 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.
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.YAPF = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const TILE = 64, TILE_PX = TILE * TILE, GROUP = 8, BAND = 8, RING = 16;
const MAX_GROUPS = TILE_PX / GROUP, MAX_DIM = 65535, MAX_MIPS = 16;
const STORED = 0x80, SUBGREEN = 0x10, NIB_REPEAT = 9;
const F_LEFT = 0, F_GRAD = 1, F_UPRIGHT = 2, F_UPAVG = 3, F_UP = 16;
const NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64];
const RUN_LENS = [2, 3, 4, 8, 16, 32, 64];
/* Encoder try order: cheapest to decode first, ties go to the earlier. */
const FILTERS = [F_UP, F_UP + 1, F_UP + 2, F_UP + 3, F_UP + 4, F_UP + 5,
F_UP + 6, F_UP + 7, F_UPRIGHT, F_UPAVG, F_LEFT, F_GRAD];
const GPU = { RGBA8: 0, RGB8: 1, RG8: 2, R8: 3, SRGB8_A8: 4, SRGB8: 5 };
const FLAG = { PREMULT_ALPHA: 1, SRGB: 2 };
const ZERO_ROW = new Uint8Array(TILE + 1);
const zz = (r) => ((r << 1) ^ ((r << 24) >> 31)) & 255; /* r is 0..255 */
const unzz = (z) => ((z >>> 1) ^ -(z & 1)) & 255;
const width = (z) => (z === 0 ? 0 : 32 - Math.clz32(z));
const s8 = (v) => (v << 24) >> 24;
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); }
/* ── Decoder ──────────────────────────────────────────────────────── */
function unpackPlane(b, p, end, z, tw, th) {
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
if (end - p < nb) corrupt();
const filt = b.subarray(p, p + nb);
p += nb;
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) corrupt();
const gw = new Uint8Array(ng + 64);
let data = 0, g = 0, hi = 0, w = 0;
while (g < ng) {
if (p >= end) corrupt();
const c = (b[p] >> (hi * 4)) & 15, n = NIB_GROUPS[c];
p += hi; hi ^= 1;
if (c < NIB_REPEAT) w = c;
gw.fill(w, g, g + n);
data += w * n; g += n;
}
if (g !== ng) corrupt();
if (hi) { if (b[p] >> 4) corrupt(); p++; }
if (end - p < data) corrupt();
for (g = 0; g < ng; g++) {
w = gw[g];
const o = g * GROUP, mask = (1 << w) - 1;
let acc = 0, bits = 0;
for (let i = 0; i < GROUP; i++) {
while (bits < w) { acc |= b[p++] << bits; bits += 8; }
z[o + i] = acc & mask;
acc >>>= w; bits -= w;
}
}
return { p, filt };
}
function unfilterRow(f, R, r0, U, u0, K, k0, Z, z0, tw) {
if (f === F_LEFT) {
let L = 0;
for (let x = 0; x < tw; x++) R[r0 + x] = L = (L + unzz(Z[z0 + x])) & 255;
} else if (f === F_GRAD) {
let L = (U[u0] + unzz(Z[z0])) & 255;
R[r0] = L;
for (let x = 1; x < tw; x++)
R[r0 + x] = L = (L + U[u0 + x] - U[u0 + x - 1] + unzz(Z[z0 + x])) & 255;
} else {
/* pred = (a + b) >> 1: Up k a = b = K; Up-right a = b = U + 1;
* Up-average a = U, b = U + 1. Rows carry one extra sample. */
let A, a0, B, b0;
if (f >= F_UP) { A = K; a0 = k0; B = K; b0 = k0; }
else if (f === F_UPAVG) { A = U; a0 = u0; B = U; b0 = u0 + 1; }
else { A = U; a0 = u0 + 1; B = U; b0 = u0 + 1; }
for (let x = 0; x < tw; x++)
R[r0 + x] = (((A[a0 + x] + B[b0 + x]) >> 1) + unzz(Z[z0 + x])) & 255;
}
R[r0 + tw] = R[r0 + tw - 1];
}
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) {
if (end <= p) corrupt();
let hdr = b[p++];
if (hdr === STORED) {
const rowb = tw * ch;
if (end - p !== rowb * th) corrupt();
for (let y = 0; y < th; y++)
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
return;
}
if (hdr & 0xE0) corrupt(); /* reserved bits set */
const xf = hdr & SUBGREEN;
hdr &= 0x0F;
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
const rowLen = TILE + 1, planeLen = RING * rowLen;
const rows = new Uint8Array(4 * planeLen);
const z = [], filt = [null, null, null, null];
for (let c = 0; c < ch; c++) {
if (!(hdr & (1 << c))) continue;
if (p >= end) corrupt();
rows.fill(b[p++], c * planeLen, (c + 1) * planeLen);
}
for (let c = 0; c < ch; c++) {
if (hdr & (1 << c)) continue;
z[c] = new Uint8Array(TILE_PX + GROUP);
const r = unpackPlane(b, p, end, z[c], tw, th);
p = r.p; filt[c] = r.filt;
}
if (p !== end) corrupt();
for (let y = 0; y < th; y++) {
const ri = (y % RING) * rowLen;
for (let c = 0; c < ch; c++) {
if (!filt[c]) continue;
const f = filt[c][y >> 3], k = upK(f), base = c * planeLen;
unfilterRow(f, rows, base + ri,
y ? rows : ZERO_ROW, y ? base + ((y - 1) % RING) * rowLen : 0,
y >= k ? rows : ZERO_ROW, y >= k ? base + ((y - k) % RING) * rowLen : 0,
z[c], y * tw, tw);
}
let d = ((ty + y) * imgW + tx) * ch;
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
for (let x = 0; x < tw; x++, d += ch) {
if (ch >= 3) {
if (xf) {
const g = rows[P0 + x];
out[d] = rows[P1 + x] + g - 128;
out[d + 1] = g;
out[d + 2] = rows[P2 + x] + g - 128;
} else {
const co = (rows[P1 + x] - 128) & 255, cg = (rows[P2 + x] - 128) & 255;
const t = (rows[P0 + x] - (s8(cg) >> 1)) & 255;
const bb = (t - (s8(co) >> 1)) & 255;
out[d] = bb + co;
out[d + 1] = cg + t;
out[d + 2] = bb;
}
if (ch === 4) out[d + 3] = rows[P3 + x];
} else {
out[d] = rows[P0 + x];
if (ch === 2) out[d + 1] = rows[P1 + x];
}
}
}
}
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
function decode(input) {
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
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]);
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
const idxEnd = 20 + 8 * mipLevels;
if (b.length < idxEnd) corrupt();
const mips = [];
for (let m = 0; m < mipLevels; m++) {
const off = r32(b, 20 + 8 * m), len = r32(b, 24 + 8 * m);
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
if (off < idxEnd || off > b.length || len > b.length - off) corrupt();
const out = new Uint8Array(mw * mh * ch);
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
if (len / 4 < tcount) corrupt();
let pos = off + tcount * 4;
for (let t = 0; t < tcount; t++) {
const sz = r32(b, off + t * 4);
if (sz > off + len - pos) corrupt();
const tx = (t % tcx) * TILE, ty = Math.floor(t / tcx) * TILE;
decodeTile(b, pos, pos + sz, out, mw, tx, ty,
Math.min(TILE, mw - tx), Math.min(TILE, mh - ty), ch);
pos += sz;
}
mips.push(out);
}
return { width: W, height: H, channels: ch, gpuFormat: b[6], flags: b[8],
mipLevels, pixels: mips[0], mips };
}
/* ── Encoder ──────────────────────────────────────────────────────── */
class Out {
constructor(n) { this.buf = new Uint8Array(Math.max(n, 1024)); this.len = 0; }
reserve(n) {
if (this.len + n <= this.buf.length) return;
let c = this.buf.length * 2;
while (c < this.len + n) c *= 2;
const nb = new Uint8Array(c); nb.set(this.buf.subarray(0, this.len)); this.buf = nb;
}
push(v) { this.reserve(1); this.buf[this.len++] = v; }
}
function predictRow(f, P, r0, u0, k0, pred, tw, U, K) {
switch (f) {
case F_LEFT: pred[0] = 0; for (let x = 1; x < tw; x++) pred[x] = P[r0 + x - 1]; break;
case F_GRAD:
pred[0] = U[u0];
for (let x = 1; x < tw; x++) pred[x] = (P[r0 + x - 1] + U[u0 + x] - U[u0 + x - 1]) & 255;
break;
case F_UPRIGHT:
for (let x = 0; x + 1 < tw; x++) pred[x] = U[u0 + x + 1];
pred[tw - 1] = U[u0 + tw - 1];
break;
case F_UPAVG:
for (let x = 0; x + 1 < tw; x++) pred[x] = (U[u0 + x] + U[u0 + x + 1]) >> 1;
pred[tw - 1] = U[u0 + tw - 1];
break;
default: for (let x = 0; x < tw; x++) pred[x] = K[k0 + x];
}
}
/* Picks a filter per band; fills z and filt; returns estimated cost. */
function chooseFilters(P, tw, th, z, filt) {
const total = tw * th, nb = (th + BAND - 1) >> 3;
const tmp = new Uint8Array(BAND * TILE + GROUP), pred = new Uint8Array(TILE);
let sum = 0, prevFi = 0;
for (let b = 0; b < nb; b++) {
const y0 = b * BAND, y1 = Math.min(y0 + BAND, th), n = (y1 - y0) * tw;
let bestCost = Infinity, bestFi = 0;
for (let t = 0; t < FILTERS.length; t++) {
const fi = t === 0 ? prevFi : (t <= prevFi ? t - 1 : t);
const f = FILTERS[fi], k = upK(f), serial = f === F_LEFT || f === F_GRAD;
let cost = 0, done = 0, lost = false;
for (let y = y0; y < y1 && !lost; y++) {
const r0 = y * tw;
const U = y ? P : ZERO_ROW, u0 = y ? r0 - tw : 0;
const K = y >= k ? P : ZERO_ROW, k0 = y >= k ? r0 - k * tw : 0;
predictRow(f, P, r0, u0, k0, pred, tw, U, K);
const T = (y - y0) * tw;
for (let x = 0; x < tw; x++) tmp[T + x] = zz((P[r0 + x] - pred[x]) & 255);
const last = y + 1 === y1, end = last ? n : (y + 1 - y0) * tw;
if (last) tmp.fill(0, n, n + GROUP);
for (; done + GROUP <= end || (last && done < end); done += GROUP) {
let m = 0;
for (let i = 0; i < GROUP; i++) m |= tmp[done + i];
const w = width(m);
cost += w ? 16 * w + 8 : 2;
}
const c = serial ? cost + Math.floor(cost / 25) : cost;
if (c > bestCost || (c === bestCost && fi > bestFi)) lost = true;
}
if (lost) continue;
if (serial) cost += Math.floor(cost / 25);
if (cost < bestCost || (cost === bestCost && fi < bestFi)) {
bestCost = cost; bestFi = fi;
z.set(tmp.subarray(0, n), y0 * tw);
}
}
prevFi = bestFi;
filt[b] = FILTERS[bestFi];
sum += bestCost;
}
z.fill(0, total, total + GROUP);
return sum;
}
function emitPlane(o, z, filt, tw, th) {
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
o.reserve(nb + ng + ng * 8);
for (let i = 0; i < nb; i++) o.buf[o.len++] = filt[i];
const need = new Uint8Array(ng);
for (let g = 0; g < ng; g++) {
let m = 0;
for (let i = 0; i < GROUP; i++) m |= z[g * GROUP + i];
need[g] = width(m);
}
/* span[j][g] = widest need in [g, g + 2^j) */
const span = [need.slice()];
for (let j = 1; j < 7; j++) {
const prev = span[j - 1], cur = new Uint8Array(ng), h = 1 << (j - 1);
for (let g = 0; g + (1 << j) <= ng; g++) cur[g] = Math.max(prev[g], prev[g + h]);
span.push(cur);
}
const cost = new Uint32Array((ng + 1) * 9), pick = new Uint8Array(ng * 9);
const mx = new Uint8Array(7);
for (let g = ng - 1; g >= 0; g--) {
let lc = Infinity, lk = 0;
for (let w = need[g]; w <= 8; w++) {
const c = 1 + 2 * w + cost[(g + 1) * 9 + w];
if (c < lc) { lc = c; lk = w; }
}
let nl = 0;
for (; nl < 7 && g + RUN_LENS[nl] <= ng; nl++) {
const L = RUN_LENS[nl];
mx[nl] = L === 3 ? Math.max(span[1][g], need[g + 2]) : span[31 - Math.clz32(L)][g];
}
for (let pw = 0; pw <= 8; pw++) {
let bc = lc, bk = lk;
for (let li = 0; li < nl && mx[li] <= pw; li++) {
const L = RUN_LENS[li], c = 1 + 2 * pw * L + cost[(g + L) * 9 + pw];
if (c < bc) { bc = c; bk = NIB_REPEAT + li; }
}
cost[g * 9 + pw] = bc;
pick[g * 9 + pw] = bk;
}
}
const gw = new Uint8Array(ng), nib = [];
let pw = 0;
for (let g = 0; g < ng;) {
const k = pick[g * 9 + pw];
nib.push(k);
if (k < NIB_REPEAT) { pw = k; gw[g++] = k; }
else for (let n = 0; n < NIB_GROUPS[k]; n++) gw[g++] = pw;
}
for (let k = 0; k < nib.length; k += 2)
o.buf[o.len++] = nib[k] | (k + 1 < nib.length ? nib[k + 1] << 4 : 0);
for (let g = 0; g < ng; g++) {
const w = gw[g];
let acc = 0, bits = 0;
for (let i = 0; i < GROUP; i++) {
acc |= z[g * GROUP + i] << bits; bits += w;
while (bits >= 8) { o.buf[o.len++] = acc & 255; acc >>>= 8; bits -= 8; }
}
}
}
function encodeTile(o, sc, px, imgW, tx, ty, tw, th, ch) {
const total = tw * th, start = o.len, raw = total * ch, tries = ch >= 3 ? 2 : 1;
const mask = [0, 0], est = [0, 0];
for (let t = 0; t < tries; t++) {
const plane = sc.plane[t];
for (let y = 0; y < th; y++) {
let s = ((ty + y) * imgW + tx) * ch;
for (let x = 0; x < tw; x++, s += ch) {
const i = y * tw + x;
if (ch < 3) { for (let c = 0; c < ch; c++) plane[c][i] = px[s + c]; continue; }
const r = px[s], g = px[s + 1], b = px[s + 2];
if (t === 0) {
const oo = (r - b) & 255, tt = (b + (s8(oo) >> 1)) & 255, q = (g - tt) & 255;
plane[0][i] = (tt + (s8(q) >> 1)) & 255;
plane[1][i] = (oo + 128) & 255;
plane[2][i] = (q + 128) & 255;
} else {
plane[0][i] = g;
plane[1][i] = (r - g + 128) & 255;
plane[2][i] = (b - g + 128) & 255;
}
if (ch === 4) plane[3][i] = px[s + 3];
}
}
for (let c = 0; c < ch; c++) {
const P = plane[c];
let i = 1;
while (i < total && P[i] === P[0]) i++;
if (i === total) { mask[t] |= 1 << c; est[t] += 16; continue; }
est[t] += chooseFilters(P, tw, th, sc.z[t][c], sc.filt[t][c]);
}
}
const t = tries === 2 && est[1] < est[0] ? 1 : 0;
o.reserve(5);
o.buf[o.len++] = mask[t] | (t ? SUBGREEN : 0);
for (let c = 0; c < ch; c++) if (mask[t] & (1 << c)) o.buf[o.len++] = sc.plane[t][c][0];
for (let c = 0; c < ch; c++)
if (!(mask[t] & (1 << c))) emitPlane(o, sc.z[t][c], sc.filt[t][c], tw, th);
if (o.len - start > raw + 1) {
o.len = start;
o.reserve(raw + 1);
o.buf[o.len++] = STORED;
for (let y = 0; y < th; y++) {
const s = ((ty + y) * imgW + tx) * ch;
o.buf.set(px.subarray(s, s + tw * ch), o.len);
o.len += tw * ch;
}
}
}
function encodeLevel(px, mw, mh, ch) {
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
const o = new Out(tcount * 4 + (mw * mh * ch >> 1));
o.reserve(tcount * 4);
o.len = tcount * 4;
const mk = (n) => [0, 1, 2, 3].map(() => new Uint8Array(n));
const sc = {
plane: [mk(TILE_PX), mk(TILE_PX)],
z: [mk(TILE_PX + GROUP), mk(TILE_PX + GROUP)],
filt: [mk(TILE / BAND), mk(TILE / BAND)],
};
let t = 0;
for (let ty = 0; ty < tcy; ty++)
for (let tx = 0; tx < tcx; tx++, t++) {
const x0 = tx * TILE, y0 = ty * TILE, start = o.len;
encodeTile(o, sc, px, mw, x0, y0, Math.min(TILE, mw - x0), Math.min(TILE, mh - y0), ch);
const n = o.len - start;
o.buf[t * 4] = n & 255; o.buf[t * 4 + 1] = (n >>> 8) & 255;
o.buf[t * 4 + 2] = (n >>> 16) & 255; o.buf[t * 4 + 3] = (n >>> 24) & 255;
}
return o.buf.subarray(0, o.len);
}
function encode(img) {
const { width: W, height: H, channels: ch } = img;
const mips = img.mips && img.mips.length ? img.mips : [img.pixels];
const mipLevels = img.mipLevels || mips.length;
if (!(W >= 1 && W <= MAX_DIM && H >= 1 && H <= MAX_DIM && ch >= 1 && ch <= 4 &&
mipLevels >= 1 && mipLevels <= MAX_MIPS && mips.length >= mipLevels))
throw new Error('YAPF: invalid image');
const levels = [];
for (let m = 0; m < mipLevels; m++) {
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
const px = m === 0 ? img.pixels : mips[m];
if (!px || px.length < mw * mh * ch) throw new Error('YAPF: mip ' + m + ' has too few pixels');
levels.push(encodeLevel(px, mw, mh, ch));
}
const idxEnd = 20 + 8 * mipLevels;
let total = idxEnd;
for (const l of levels) total += l.length;
const out = new Uint8Array(total);
out.set([0x59, 0x41, 0x50, 0x46, 1, ch, img.gpuFormat || 0, mipLevels, img.flags || 0]);
const w32 = (p, v) => { out[p] = v & 255; out[p + 1] = (v >>> 8) & 255;
out[p + 2] = (v >>> 16) & 255; out[p + 3] = (v >>> 24) & 255; };
w32(12, W); w32(16, H);
let off = idxEnd;
levels.forEach((l, m) => { w32(20 + 8 * m, off); w32(24 + 8 * m, l.length); out.set(l, off); off += l.length; });
return out;
}
/* Any channel count → RGBA (for canvas ImageData). */
function toRGBA(img, level) {
const m = level || 0, px = img.mips ? img.mips[m] : img.pixels;
const w = Math.max(1, img.width >>> m), h = Math.max(1, img.height >>> m), ch = img.channels;
if (ch === 4) return new Uint8ClampedArray(px.buffer, px.byteOffset, w * h * 4);
const out = new Uint8ClampedArray(w * h * 4);
for (let i = 0, s = 0; i < w * h; i++, s += ch) {
const o = i * 4;
if (ch >= 3) { out[o] = px[s]; out[o + 1] = px[s + 1]; out[o + 2] = px[s + 2]; out[o + 3] = 255; }
else { out[o] = out[o + 1] = out[o + 2] = px[s]; out[o + 3] = ch === 2 ? px[s + 1] : 255; }
}
return out;
}
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 };
}));
</script>
<script>/*
* png.js — exact 8-bit PNG encode / decode for the browser and Node 18+.
* Uses the platform's CompressionStream / DecompressionStream for zlib.
* Canvas round-trips premultiply alpha and are not lossless; this is.
* Copyright 2026 Nexoniarz — Apache License 2.0.
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.PNG = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const CRC = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
CRC[n] = c >>> 0;
}
function crc32(bytes, start, end) {
let c = 0xffffffff;
for (let i = start; i < end; i++) c = CRC[(c ^ bytes[i]) & 255] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
async function zlib(data, mode) {
const stream = new Blob([data]).stream().pipeThrough(
mode === 'deflate' ? new CompressionStream('deflate') : new DecompressionStream('deflate'));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
const COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 }; // channels → PNG colour type
const CHANNELS = { 0: 1, 4: 2, 2: 3, 6: 4 };
function paeth(a, b, c) {
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
}
/* pixels: Uint8Array of w*h*ch bytes, rows top to bottom. */
async function encode(pixels, w, h, ch) {
const stride = w * ch, raw = new Uint8Array((stride + 1) * h), row = new Uint8Array(stride);
for (let y = 0; y < h; y++) {
const cur = pixels.subarray(y * stride, (y + 1) * stride);
const prev = y ? pixels.subarray((y - 1) * stride, y * stride) : null;
let best = 0, bestSum = Infinity;
for (let f = 0; f < 5; f++) { // pick the filter with the smallest sum
let sum = 0;
for (let i = 0; i < stride; i++) {
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
const v = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
sum += v < 128 ? v : 256 - v;
}
if (sum < bestSum) { bestSum = sum; best = f; }
}
const o = y * (stride + 1);
raw[o] = best;
for (let i = 0; i < stride; i++) {
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
row[i] = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][best]) & 255;
}
raw.set(row, o + 1);
}
const idat = await zlib(raw, 'deflate');
const chunks = [];
const chunk = (type, data) => {
const c = new Uint8Array(12 + data.length), dv = new DataView(c.buffer);
dv.setUint32(0, data.length);
for (let i = 0; i < 4; i++) c[4 + i] = type.charCodeAt(i);
c.set(data, 8);
dv.setUint32(8 + data.length, crc32(c, 4, 8 + data.length));
chunks.push(c);
};
const ihdr = new Uint8Array(13), dv = new DataView(ihdr.buffer);
dv.setUint32(0, w); dv.setUint32(4, h);
ihdr.set([8, COLOR_TYPE[ch], 0, 0, 0], 8);
chunk('IHDR', ihdr); chunk('IDAT', idat); chunk('IEND', new Uint8Array(0));
const total = 8 + chunks.reduce((n, c) => n + c.length, 0), out = new Uint8Array(total);
out.set([137, 80, 78, 71, 13, 10, 26, 10]);
let p = 8;
for (const c of chunks) { out.set(c, p); p += c.length; }
return out;
}
/* Returns { width, height, channels, pixels } or throws for PNGs it does
* not handle (16-bit, interlaced); callers can fall back to a canvas. */
async function decode(bytes) {
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
if (bytes.length < 8 || sig.some((v, i) => bytes[i] !== v)) throw new Error('not a PNG');
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let w = 0, h = 0, depth = 0, type = 0, interlace = 0, palette = null, trns = null;
const idat = [];
for (let p = 8; p + 8 <= bytes.length;) {
const len = dv.getUint32(p), name = String.fromCharCode(...bytes.subarray(p + 4, p + 8));
const data = bytes.subarray(p + 8, p + 8 + len);
if (name === 'IHDR') { w = dv.getUint32(p + 8); h = dv.getUint32(p + 12); depth = data[8]; type = data[9]; interlace = data[12]; }
else if (name === 'PLTE') palette = data;
else if (name === 'tRNS') trns = data;
else if (name === 'IDAT') idat.push(data);
else if (name === 'IEND') break;
p += 12 + len;
}
if (depth !== 8 || interlace !== 0 || !(type in CHANNELS || type === 3))
throw new Error('unsupported PNG (needs 8-bit, non-interlaced)');
const inCh = type === 3 ? 1 : CHANNELS[type], stride = w * inCh;
const raw = await zlib(new Uint8Array(await new Blob(idat).arrayBuffer()), 'inflate');
const px = new Uint8Array(stride * h);
for (let y = 0; y < h; y++) {
const f = raw[y * (stride + 1)], src = y * (stride + 1) + 1, o = y * stride;
for (let i = 0; i < stride; i++) {
const a = i >= inCh ? px[o + i - inCh] : 0, b = y ? px[o - stride + i] : 0;
const c = y && i >= inCh ? px[o - stride + i - inCh] : 0;
px[o + i] = (raw[src + i] + [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
}
}
if (type !== 3) return { width: w, height: h, channels: inCh, pixels: px };
const ch = trns ? 4 : 3, out = new Uint8Array(w * h * ch); // expand a palette
for (let i = 0; i < w * h; i++) {
const k = px[i];
out.set(palette.subarray(k * 3, k * 3 + 3), i * ch);
if (trns) out[i * ch + 3] = k < trns.length ? trns[k] : 255;
}
return { width: w, height: h, channels: ch, pixels: out };
}
return { encode, decode };
}));
</script>
<script>
const $ = (id) => document.getElementById(id);
const post = (m) => parent.postMessage({ pluginMessage: m }, '*');
const status = (t) => { $('status').textContent = t; };
function tab(name) {
$('import').hidden = name !== 'import';
$('export').hidden = name !== 'export';
$('tab-import').classList.toggle('on', name === 'import');
$('tab-export').classList.toggle('on', name === 'export');
status('');
}
$('tab-import').onclick = () => tab('import');
$('tab-export').onclick = () => tab('export');
/* Import: .yapf → pixels → exact PNG → Figma image */
async function importFiles(files) {
const images = [];
for (const f of files) {
try {
const img = YAPF.decode(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 + ')');
} catch (e) {
status(f.name + ': ' + e.message);
}
}
if (images.length) post({ type: 'import', images });
}
$('drop').onclick = () => $('file').click();
$('file').onchange = (e) => importFiles([...e.target.files]);
$('drop').ondragover = (e) => { e.preventDefault(); $('drop').classList.add('hover'); };
$('drop').ondragleave = () => $('drop').classList.remove('hover');
$('drop').ondrop = (e) => { e.preventDefault(); $('drop').classList.remove('hover'); importFiles([...e.dataTransfer.files]); };
/* Export: Figma PNG → exact pixels → .yapf download */
$('go').onclick = () => { status('Exporting…'); post({ type: 'export', scale: +$('scale').value }); };
async function exported(name, pngBytes) {
let img = await PNG.decode(pngBytes);
let { pixels, channels } = img;
if (channels === 4) { // drop alpha when fully opaque
let opaque = true;
for (let i = 3; i < pixels.length && opaque; i += 4) opaque = pixels[i] === 255;
if (opaque) {
const rgb = new Uint8Array(img.width * img.height * 3);
for (let i = 0, j = 0; i < pixels.length; i += 4, j += 3) rgb.set(pixels.subarray(i, i + 3), j);
pixels = rgb; channels = 3;
}
}
const file = YAPF.encode({ width: img.width, height: img.height, channels, pixels,
flags: YAPF.FLAG.SRGB,
gpuFormat: channels === 4 ? YAPF.GPU.SRGB8_A8 : YAPF.GPU.SRGB8 });
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([file], { type: 'image/x-yapf' }));
a.download = name.replace(/[\\/:*?"<>|]/g, '_') + '.yapf';
document.body.appendChild(a); a.click(); a.remove();
status('Saved ' + a.download + '\n' + img.width + '×' + img.height + ', ' +
(file.length / 1024).toFixed(1) + ' KB (' +
(100 * file.length / (img.width * img.height * channels)).toFixed(1) + '% of raw)');
}
onmessage = (e) => {
const m = e.data.pluginMessage;
if (!m) return;
if (m.type === 'mode') tab(m.mode === 'export' ? 'export' : 'import');
if (m.type === 'selection')
$('sel').textContent = m.names.length ? 'Selected: ' + m.names.join(', ') : 'Nothing selected';
if (m.type === 'exported') exported(m.name, m.png).catch((err) => status(m.name + ': ' + err.message));
};
</script>
</body>
</html>