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
27
extensions/vscode/README.md
Normal file
27
extensions/vscode/README.md
Normal 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+).
|
||||
65
extensions/vscode/extension.js
Normal file
65
extensions/vscode/extension.js
Normal 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 };
|
||||
BIN
extensions/vscode/media/icon.png
Normal file
BIN
extensions/vscode/media/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
26
extensions/vscode/media/preview.css
Normal file
26
extensions/vscode/media/preview.css
Normal 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); }
|
||||
91
extensions/vscode/media/preview.js
Normal file
91
extensions/vscode/media/preview.js
Normal 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
|
||||
})();
|
||||
29
extensions/vscode/package.json
Normal file
29
extensions/vscode/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
extensions/vscode/scripts/copy-codec.js
Normal file
6
extensions/vscode/scripts/copy-codec.js
Normal 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');
|
||||
Loading…
Reference in a new issue