mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
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>
This commit is contained in:
parent
b9b8c93a22
commit
2ca726e3af
48 changed files with 14905 additions and 710 deletions
713
extensions/figma/ui.html
Normal file
713
extensions/figma/ui.html
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
<!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>
|
||||
Loading…
Reference in a new issue