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,27 @@
# YAPF Image Preview for VS Code
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.
- Reloads automatically when the file changes on disk.
Decoding runs in plain JavaScript inside the preview; no native code.
## Install
- From a release: download `yapf-preview.vsix`, then in VS Code run
**Extensions: Install from VSIX…** (or `code --install-extension yapf-preview.vsix`).
- From source (needs Node.js):
```sh
cd extensions/vscode
npm run package # → yapf-preview-1.0.0.vsix
code --install-extension yapf-preview-1.0.0.vsix
```
Works in VS Code, VSCodium and other VS Code–based editors (1.75+).

View file

@ -0,0 +1,65 @@
// YAPF image preview for VS Code — a read-only custom editor for *.yapf.
// Copyright 2026 Nexoniarz — Apache License 2.0.
const vscode = require('vscode');
class YapfPreviewProvider {
constructor(context) { this.context = context; }
openCustomDocument(uri) { return { uri, dispose() {} }; }
async resolveCustomEditor(document, panel) {
const media = vscode.Uri.joinPath(this.context.extensionUri, 'media');
panel.webview.options = { enableScripts: true, localResourceRoots: [media] };
panel.webview.html = this.html(panel.webview, media);
const send = async () => {
try {
const bytes = await vscode.workspace.fs.readFile(document.uri);
panel.webview.postMessage({ type: 'file', name: document.uri.path.split('/').pop(), bytes });
} catch (e) {
panel.webview.postMessage({ type: 'error', message: String(e.message || e) });
}
};
panel.webview.onDidReceiveMessage((m) => { if (m.type === 'ready') send(); });
// Reload when the file changes on disk.
const folder = vscode.Uri.joinPath(document.uri, '..');
const name = document.uri.path.split('/').pop();
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, name));
watcher.onDidChange(send);
panel.onDidDispose(() => watcher.dispose());
}
html(webview, media) {
const uri = (f) => webview.asWebviewUri(vscode.Uri.joinPath(media, f));
const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} data: blob:; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${uri('preview.css')}">
<title>YAPF Preview</title>
</head>
<body>
<div id="bar"><span id="info">Loading…</span><span id="controls"></span></div>
<div id="stage"><canvas id="canvas"></canvas></div>
<div id="error" hidden></div>
<script nonce="${nonce}" src="${uri('yapf.js')}"></script>
<script nonce="${nonce}" src="${uri('preview.js')}"></script>
</body>
</html>`;
}
}
function activate(context) {
context.subscriptions.push(vscode.window.registerCustomEditorProvider(
'yapf.preview', new YapfPreviewProvider(context),
{ webviewOptions: { retainContextWhenHidden: true }, supportsMultipleEditorsPerDocument: true }));
}
function deactivate() {}
module.exports = { activate, deactivate };

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,26 @@
html, body { height: 100%; margin: 0; }
body {
display: flex; flex-direction: column;
background: var(--vscode-editor-background, #1e1e1e);
color: var(--vscode-foreground, #ccc);
font: 12px var(--vscode-font-family, system-ui, sans-serif);
}
#bar {
display: flex; justify-content: space-between; align-items: center; gap: 12px;
padding: 6px 12px; border-bottom: 1px solid var(--vscode-panel-border, #333);
}
#bar select {
background: var(--vscode-dropdown-background, #3c3c3c);
color: var(--vscode-dropdown-foreground, #ccc);
border: 1px solid var(--vscode-dropdown-border, #555);
}
#stage { flex: 1; overflow: auto; display: grid; place-items: center; padding: 16px; cursor: zoom-in; }
body:not(.fit) #stage { cursor: zoom-out; }
canvas {
background-color: #fff;
background-image: conic-gradient(#ccc 25%, #fff 0 50%, #ccc 0 75%, #fff 0);
background-size: 16px 16px;
box-shadow: 0 0 0 1px var(--vscode-panel-border, #333);
}
canvas.pixelated { image-rendering: pixelated; }
#error { padding: 16px; color: var(--vscode-errorForeground, #f48771); }

View file

@ -0,0 +1,91 @@
// Webview side of the YAPF preview: decode, draw, zoom, mip picker, 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 CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
let image = null, level = 0, fit = true, zoom = 1;
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
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);
layout();
}
function layout() {
if (!image) 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;
canvas.style.width = Math.max(1, Math.round(w * scale)) + 'px';
canvas.style.height = Math.max(1, Math.round(h * scale)) + 'px';
canvas.classList.toggle('pixelated', scale >= 2);
document.body.classList.toggle('fit', fit);
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
}
function show(name, bytes) {
errorBox.hidden = true;
const t = performance.now();
try {
image = YAPF.decode(bytes);
} catch (e) {
image = null;
errorBox.hidden = false;
errorBox.textContent = name + ': ' + e.message;
info.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);
}
draw();
}
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
document.getElementById('stage').addEventListener('wheel', (e) => {
if (!e.ctrlKey || !image) return;
e.preventDefault();
fit = false;
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
layout();
}, { passive: false });
window.addEventListener('resize', layout);
window.addEventListener('message', (e) => {
const m = e.data;
if (m.type === 'file') show(m.name, m.bytes instanceof Uint8Array ? m.bytes : new Uint8Array(m.bytes));
if (m.type === 'error') { errorBox.hidden = false; errorBox.textContent = m.message; }
});
if (vscode) vscode.postMessage({ type: 'ready' });
window.yapfPreviewShow = show; // used by the browser test page
})();

View file

@ -0,0 +1,29 @@
{
"name": "yapf-preview",
"displayName": "YAPF Image Preview",
"description": "View YAPF (.yapf) lossless images inside VS Code",
"version": "1.0.0",
"publisher": "Nexoniarz",
"license": "Apache-2.0",
"icon": "media/icon.png",
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git" },
"engines": { "vscode": "^1.75.0" },
"categories": ["Visualization", "Other"],
"keywords": ["yapf", "image", "texture", "preview", "viewer"],
"main": "./extension.js",
"activationEvents": [],
"contributes": {
"customEditors": [
{
"viewType": "yapf.preview",
"displayName": "YAPF Image Preview",
"selector": [{ "filenamePattern": "*.yapf" }],
"priority": "default"
}
]
},
"scripts": {
"vscode:prepublish": "node scripts/copy-codec.js",
"package": "node scripts/copy-codec.js && npx --yes @vscode/vsce package --allow-missing-repository"
}
}

View file

@ -0,0 +1,6 @@
// Copies the shared JavaScript codec into media/ so the extension is self-contained.
const fs = require('fs');
const path = require('path');
const src = path.join(__dirname, '..', '..', '..', 'bindings', 'js', 'yapf.js');
fs.copyFileSync(src, path.join(__dirname, '..', 'media', 'yapf.js'));
console.log('copied', path.relative(process.cwd(), src), '-> media/yapf.js');