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:
Nexoniarz 2026-10-03 14:17:59 +02:00
commit 2ca726e3af
48 changed files with 14905 additions and 710 deletions

View file

@ -0,0 +1,124 @@
<!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__*/</script>
<script>/*__PNG_JS__*/</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>