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,34 @@
# YAPF for Blender
Import and export `.yapf` images in **Blender 4.2 or later** (tested with
Blender 5.1).
- **File → Import → YAPF Image (.yapf)** — optionally as a **plane** with a
material that shows the image.
- **Image Editor → Image → Open YAPF… / Save as YAPF…** (with mipmaps option).
- **Drag and drop** `.yapf` files into the 3D Viewport (creates a plane), the
Image Editor or a node editor.
Blender cannot read `.yapf` on its own, so imported images are **packed into
the .blend** and survive saving and reopening.
## Install
1. Get `yapf_image-blender.zip` from the
[Releases](https://github.com/Nexoniarz/YAPF/releases), or build it (below).
2. Blender → **Edit → Preferences → Get Extensions → ⌄ → Install from Disk…**
and pick the zip.
The zip contains the native YAPF library for Windows, macOS and Linux (x64
and ARM64). If no library matches your system, the extension still
**imports** using a pure-Python decoder (numpy, bundled with Blender), but
cannot export.
## Build the zip
```sh
python3 extensions/blender/build.py # library for this machine only
tools/build_release.sh # all platforms (needs zig)
```
Both write `dist/yapf_image-blender.zip`.

View file

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Package the Blender extension: python3 extensions/blender/build.py [libs_dir]
Writes dist/yapf_image-blender.zip, ready for Blender's
Edit > Preferences > Get Extensions > Install from Disk.
libs_dir (optional) holds prebuilt libraries as <platform>/<file>, e.g.
linux-x64/libyapf.so, windows-x64/yapf.dll, macos-arm64/libyapf.dylib
(tools/build_libs.sh makes them). Without libraries the extension still
imports .yapf files using numpy, but cannot export. If no libs_dir is
given, a library for this machine is built with `cc` when available.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
SRC = os.path.join(ROOT, "extensions", "blender", "yapf_image")
sys.path.insert(0, os.path.join(ROOT, "bindings", "python"))
import yapf # noqa: E402 (for the platform folder name)
def host_lib(tmp):
name = {"win32": "yapf.dll", "darwin": "libyapf.dylib"}.get(sys.platform, "libyapf.so")
out = os.path.join(tmp, yapf._platform_dir(), name)
os.makedirs(os.path.dirname(out), exist_ok=True)
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
if not cc:
return None
cmd = [cc, "-O2", "-std=c99", "-shared", "-fPIC", os.path.join(ROOT, "yapf.c"), "-o", out, "-pthread"]
return tmp if subprocess.call(cmd) == 0 else None
def main():
tmp = tempfile.mkdtemp()
libs = sys.argv[1] if len(sys.argv) > 1 else host_lib(tmp)
os.makedirs(os.path.join(ROOT, "dist"), exist_ok=True)
out = os.path.join(ROOT, "dist", "yapf_image-blender.zip")
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for f in ("__init__.py", "blender_manifest.toml"):
z.write(os.path.join(SRC, f), f)
z.write(os.path.join(ROOT, "bindings", "python", "yapf.py"), "yapf.py")
z.write(os.path.join(ROOT, "LICENSE"), "LICENSE")
if libs:
for plat in sorted(os.listdir(libs)):
for f in sorted(os.listdir(os.path.join(libs, plat))):
if f.endswith((".so", ".dll", ".dylib")):
z.write(os.path.join(libs, plat, f), "lib/%s/%s" % (plat, f))
print(" bundled lib/%s/%s" % (plat, f))
shutil.rmtree(tmp, ignore_errors=True)
print("wrote", out)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,251 @@
# YAPF image support for Blender.
#
# Copyright 2026 Nexoniarz — Apache License 2.0.
#
# File > Import > YAPF Image (.yapf) open an image, optionally as a plane
# Image Editor > Image > Open YAPF… open into the Image Editor
# Image Editor > Image > Save as YAPF… export the current image
# Drag a .yapf file into the 3D Viewport, Image Editor or a node editor.
#
# Blender cannot read .yapf files by itself, so imported images are packed
# into the .blend file; they survive saving and reopening.
bl_info = {
"name": "YAPF Image Format",
"author": "Nexoniarz",
"version": (1, 0, 0),
"blender": (4, 2, 0),
"location": "File > Import > YAPF Image, Image Editor > Image menu",
"description": "Import and export YAPF (.yapf) lossless images",
"doc_url": "https://github.com/Nexoniarz/YAPF",
"category": "Import-Export",
}
import os
import bpy
import numpy as np
from bpy.props import BoolProperty, CollectionProperty, StringProperty
from bpy_extras.io_utils import ExportHelper, ImportHelper
from . import yapf
# ── conversion helpers ────────────────────────────────────────────────
def load_image(filepath):
"""Read a .yapf file into a packed bpy.types.Image."""
img = yapf.load(filepath)
w, h, ch = img.width, img.height, img.channels
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
rgba = np.ones((h, w, 4), dtype=np.float32)
if ch >= 3:
rgba[..., :3] = src[..., :3]
else:
rgba[..., :3] = src[..., :1]
if ch in (2, 4):
rgba[..., 3] = src[..., ch - 1]
name = os.path.basename(filepath)
bimg = bpy.data.images.new(name, w, h, alpha=ch in (2, 4))
bimg.colorspace_settings.name = "sRGB" if img.flags & yapf.FLAG_SRGB else "Non-Color"
if img.flags & yapf.FLAG_PREMULT_ALPHA:
bimg.alpha_mode = "PREMUL"
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
bimg.file_format = "PNG"
bimg.pack() # keep it inside the .blend
bimg.use_fake_user = True # …even before anything uses it
bimg["yapf_source"] = filepath
bimg["yapf_alpha"] = ch in (2, 4)
return bimg
def save_image(bimg, filepath, mips=False):
"""Write a bpy.types.Image to a .yapf file."""
w, h = bimg.size
if w == 0 or h == 0:
raise yapf.YapfError("image '%s' has no pixels" % bimg.name)
buf = np.empty(w * h * 4, dtype=np.float32)
bimg.pixels.foreach_get(buf)
px = (np.clip(buf.reshape(h, w, 4)[::-1], 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
has_alpha = bool((px[..., 3] != 255).any())
ch = 4 if has_alpha else 3
px = np.ascontiguousarray(px[..., :ch])
srgb = bimg.colorspace_settings.name not in ("Non-Color", "Linear", "Linear Rec.709", "Raw")
flags = (yapf.FLAG_SRGB if srgb else 0) | (yapf.FLAG_PREMULT_ALPHA if bimg.alpha_mode == "PREMUL" else 0)
levels = [px]
if mips:
cur = px.astype(np.uint32)
while cur.shape[0] > 1 or cur.shape[1] > 1:
hh, ww = max(1, cur.shape[0] // 2), max(1, cur.shape[1] // 2)
p = np.pad(cur, ((0, cur.shape[0] % 2), (0, cur.shape[1] % 2), (0, 0)), mode="edge")
cur = (p[0::2, 0::2] + p[1::2, 0::2] + p[0::2, 1::2] + p[1::2, 1::2] + 2) // 4
cur = cur[:hh, :ww]
levels.append(cur.astype(np.uint8))
if len(levels) == 16:
break
out = yapf.Image(w, h, ch, levels[0].tobytes(), flags=flags,
mips=[l.tobytes() for l in levels])
if srgb:
out.gpu_format = yapf.GPU_SRGB8_A8 if ch == 4 else yapf.GPU_SRGB8
else:
out.gpu_format = yapf.GPU_RGBA8 if ch == 4 else yapf.GPU_RGB8
yapf.save(filepath, out)
def add_image_plane(context, bimg):
"""A plane with the image's aspect ratio and a material that shows it."""
w, h = bimg.size
bpy.ops.mesh.primitive_plane_add(size=1.0, location=context.scene.cursor.location)
obj = context.active_object
obj.name = os.path.splitext(bimg.name)[0]
obj.scale = (w / max(w, h), h / max(w, h), 1.0)
mat = bpy.data.materials.new(obj.name)
mat.use_nodes = True
nodes, links = mat.node_tree.nodes, mat.node_tree.links
bsdf = next(n for n in nodes if n.type == "BSDF_PRINCIPLED")
tex = nodes.new("ShaderNodeTexImage")
tex.image = bimg
tex.location = (bsdf.location.x - 350, bsdf.location.y)
links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
if bimg.get("yapf_alpha"):
links.new(tex.outputs["Alpha"], bsdf.inputs["Alpha"])
obj.data.materials.append(mat)
return obj
def show_in_image_editor(context, bimg):
for area in context.screen.areas if context.screen else []:
if area.type == "IMAGE_EDITOR":
area.spaces.active.image = bimg
return True
return False
# ── operators ─────────────────────────────────────────────────────────
class IMPORT_IMAGE_OT_yapf(bpy.types.Operator, ImportHelper):
"""Import YAPF (.yapf) images"""
bl_idname = "import_image.yapf"
bl_label = "Import YAPF Image"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".yapf"
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
files: CollectionProperty(type=bpy.types.OperatorFileListElement,
options={"HIDDEN", "SKIP_SAVE"})
directory: StringProperty(subtype="DIR_PATH", options={"HIDDEN", "SKIP_SAVE"})
as_plane: BoolProperty(name="Create Plane",
description="Add a plane showing the image to the scene",
default=False)
def paths(self):
if self.files and self.directory:
return [os.path.join(self.directory, f.name) for f in self.files if f.name]
return [self.filepath]
def execute(self, context):
done = 0
for path in self.paths():
try:
bimg = load_image(path)
except (OSError, yapf.YapfError) as e:
self.report({"ERROR"}, "%s: %s" % (os.path.basename(path), e))
continue
done += 1
if self.as_plane and context.mode == "OBJECT":
add_image_plane(context, bimg)
else:
show_in_image_editor(context, bimg)
if done:
self.report({"INFO"}, "Imported %d YAPF image(s)" % done)
return {"FINISHED"} if done else {"CANCELLED"}
def invoke(self, context, event):
# Dropped files arrive with filepath/directory already set.
if self.directory and self.files:
if context.area and context.area.type == "VIEW_3D":
self.as_plane = True
return self.execute(context)
return ImportHelper.invoke(self, context, event)
class EXPORT_IMAGE_OT_yapf(bpy.types.Operator, ExportHelper):
"""Save the current image as a YAPF (.yapf) file"""
bl_idname = "export_image.yapf"
bl_label = "Save as YAPF"
filename_ext = ".yapf"
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
image_name: StringProperty(name="Image", description="Image to save (default: the one in the Image Editor)")
mips: BoolProperty(name="Store Mipmaps", description="Also store a full mip chain", default=False)
def target(self, context):
if self.image_name:
return bpy.data.images.get(self.image_name)
space = context.space_data
return getattr(space, "image", None)
def invoke(self, context, event):
img = self.target(context)
if img is None:
self.report({"ERROR"}, "Open an image in the Image Editor first")
return {"CANCELLED"}
self.image_name = img.name
self.filepath = os.path.splitext(img.name)[0] + ".yapf"
return ExportHelper.invoke(self, context, event)
def execute(self, context):
img = self.target(context)
if img is None:
self.report({"ERROR"}, "No image to save")
return {"CANCELLED"}
try:
save_image(img, self.filepath, self.mips)
except (OSError, yapf.YapfError) as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
self.report({"INFO"}, "Saved %s" % os.path.basename(self.filepath))
return {"FINISHED"}
class IMAGE_FH_yapf(bpy.types.FileHandler):
"""Drag and drop .yapf files into Blender"""
bl_idname = "IMAGE_FH_yapf"
bl_label = "YAPF Image"
bl_import_operator = "import_image.yapf"
bl_file_extensions = ".yapf"
@classmethod
def poll_drop(cls, context):
return context.area is not None and context.area.type in {
"VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR"}
def menu_import(self, context):
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="YAPF Image (.yapf)")
def menu_image(self, context):
self.layout.separator()
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="Open YAPF…")
self.layout.operator(EXPORT_IMAGE_OT_yapf.bl_idname, text="Save as YAPF…")
classes = (IMPORT_IMAGE_OT_yapf, EXPORT_IMAGE_OT_yapf, IMAGE_FH_yapf)
def register():
for c in classes:
bpy.utils.register_class(c)
bpy.types.TOPBAR_MT_file_import.append(menu_import)
bpy.types.IMAGE_MT_image.append(menu_image)
def unregister():
bpy.types.IMAGE_MT_image.remove(menu_image)
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
for c in reversed(classes):
bpy.utils.unregister_class(c)

View file

@ -0,0 +1,16 @@
schema_version = "1.0.0"
id = "yapf_image"
version = "1.0.0"
name = "YAPF Image Format"
tagline = "Import and export YAPF lossless images"
maintainer = "Nexoniarz"
type = "add-on"
website = "https://github.com/Nexoniarz/YAPF"
tags = ["Import-Export"]
blender_version_min = "4.2.0"
license = ["SPDX:Apache-2.0"]
[permissions]
files = "Read and write .yapf image files"

View file

@ -0,0 +1,32 @@
# YAPF for Figma
- **Plugins → YAPF Image Format → Import YAPF image…** — choose or drop
`.yapf` files; each becomes a rectangle filled with the image.
- **Plugins → YAPF Image Format → Export selection as YAPF…** — every
selected layer is exported at 1×, 2× or 4× and downloaded as `.yapf`.
Both directions are pixel-exact: the plugin carries its own small PNG
encoder/decoder instead of going through a canvas, which would alter
semi-transparent pixels.
## Install (development plugin)
1. Use the **Figma desktop app**.
2. **Plugins → Development → Import plugin from manifest…** and choose
`extensions/figma/manifest.json` (or the one inside `yapf-figma.zip`
from the [Releases](https://github.com/Nexoniarz/YAPF/releases)).
3. Run it from **Plugins → Development → YAPF Image Format**.
To publish it to the Figma Community, use **Publish** in the same menu;
Figma then assigns the plugin its real `id`.
## Build
`ui.html` is generated: it inlines `bindings/js/yapf.js` and `src/png.js`
into `src/ui.src.html`, because Figma loads the UI as a single file.
```sh
node build.js
```
Figma limits images to 4096 × 4096; larger `.yapf` files report an error.

View file

@ -0,0 +1,9 @@
// node build.js — inlines the YAPF and PNG codecs into ui.html (Figma needs one file).
const fs = require('fs');
const path = require('path');
const read = (p) => fs.readFileSync(path.join(__dirname, p), 'utf8');
const html = read('src/ui.src.html')
.replace('/*__YAPF_JS__*/', () => read('../../bindings/js/yapf.js'))
.replace('/*__PNG_JS__*/', () => read('src/png.js'));
fs.writeFileSync(path.join(__dirname, 'ui.html'), html);
console.log('wrote ui.html (' + html.length + ' bytes)');

54
extensions/figma/code.js Normal file
View file

@ -0,0 +1,54 @@
// YAPF for Figma — plugin main thread (has the document, no DOM).
// Copyright 2026 Nexoniarz — Apache License 2.0.
// Decoding/encoding happens in the UI (ui.html), which has a DOM.
figma.showUI(__html__, { width: 380, height: 330, themeColors: true });
figma.ui.postMessage({ type: 'mode', mode: figma.command || 'import' });
function sendSelection() {
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
figma.ui.postMessage({ type: 'selection', names: nodes.map((n) => n.name) });
}
figma.on('selectionchange', sendSelection);
sendSelection();
figma.ui.onmessage = async (msg) => {
if (msg.type === 'import') {
// msg.images: [{ name, width, height, png: Uint8Array }]
const created = [];
const center = figma.viewport.center;
let x = center.x;
for (const im of msg.images) {
try {
const image = figma.createImage(im.png);
const rect = figma.createRectangle();
rect.name = im.name;
rect.resize(im.width, im.height);
rect.fills = [{ type: 'IMAGE', imageHash: image.hash, scaleMode: 'FILL' }];
rect.x = Math.round(x - (created.length ? 0 : im.width / 2));
rect.y = Math.round(center.y - im.height / 2);
x = rect.x + im.width + 40;
figma.currentPage.appendChild(rect);
created.push(rect);
} catch (e) {
figma.notify(im.name + ': ' + (e.message || e) + ' (Figma accepts images up to 4096×4096)', { error: true });
}
}
if (created.length) {
figma.currentPage.selection = created;
figma.viewport.scrollAndZoomIntoView(created);
figma.notify('Imported ' + created.length + ' YAPF image' + (created.length > 1 ? 's' : ''));
}
}
if (msg.type === 'export') {
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
if (!nodes.length) { figma.notify('Select one or more layers to export', { error: true }); return; }
for (const node of nodes) {
const png = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: msg.scale || 1 } });
figma.ui.postMessage({ type: 'exported', name: node.name, png });
}
}
if (msg.type === 'close') figma.closePlugin();
};

View file

@ -0,0 +1,14 @@
{
"name": "YAPF Image Format",
"id": "yapf-image-format",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": ["figma", "figjam"],
"documentAccess": "dynamic-page",
"networkAccess": { "allowedDomains": ["none"] },
"menu": [
{ "name": "Import YAPF image…", "command": "import" },
{ "name": "Export selection as YAPF…", "command": "export" }
]
}

126
extensions/figma/src/png.js Normal file
View file

@ -0,0 +1,126 @@
/*
* 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 };
}));

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>

713
extensions/figma/ui.html Normal file
View 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>

48
extensions/gimp/README.md Normal file
View file

@ -0,0 +1,48 @@
# YAPF for GIMP
Open and export `.yapf` images in **GIMP 3.0 or later**.
- **File → Open** reads `.yapf` (Gray, Gray + Alpha, RGB, RGBA).
- **File → Export As… → `name.yapf`** writes the visible image (layers are
flattened into a copy; your document is untouched), with an optional
**Store mipmaps** checkbox for game engines.
## Install
You need GIMP 3 with its development files (`gimptool`).
| System | Development files |
| :-------------- | :-------------------------------------------------- |
| Arch / Manjaro | `sudo pacman -S gimp` (includes them) |
| Debian / Ubuntu | `sudo apt install libgimp-3.0-dev` (GIMP 3 packages) |
| Fedora | `sudo dnf install gimp-devel` |
| Windows | MSYS2: `pacman -S mingw-w64-ucrt-x86_64-gimp` |
| macOS | build GIMP 3 from source or use MacPorts/Homebrew |
Then, from this folder:
```sh
gimptool --install yapf-gimp.c
```
That compiles the plugin (it pulls in `../../yapf.c` itself) and installs it
into your personal GIMP plug-ins folder. Restart GIMP.
### Manual build
```sh
gcc -O2 $(pkg-config --cflags gimpui-3.0) yapf-gimp.c $(pkg-config --libs gimpui-3.0) -o yapf-gimp
```
Copy `yapf-gimp` to `<GIMP config>/plug-ins/yapf-gimp/yapf-gimp`, where the
GIMP config folder is `~/.config/GIMP/3.0` on Linux,
`%APPDATA%\GIMP\3.0` on Windows and
`~/Library/Application Support/GIMP/3.0` on macOS.
## Notes
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
gamma encoded. Files marked linear load as linear.
- When a file has mipmaps, GIMP opens the full-size image.
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
pixel for pixel, and export equals GIMP's own flattened PNG of the image.

View file

@ -1,40 +0,0 @@
YAPF GIMP Plugin — Build Instructions
======================================
Requirements
------------
- GIMP 3.0 or later (with development headers)
- gimptool (included with GIMP 3 dev packages)
- yapf.c and yapf.h must be in the SAME directory as yapf-gimp.c
(the plugin embeds the codec via a direct #include)
On Arch/Manjaro: sudo pacman -S gimp
On Debian/Ubuntu: sudo apt install gimp libgmp3-dev
On Fedora: sudo dnf install gimp gimp-devel
Installation (automatic)
-------------------------
Open a terminal in this directory and run:
$ gimptool --install yapf-gimp.c
This compiles the plugin and installs it into your GIMP plug-ins folder
automatically. Restart GIMP and .yapf files will appear in the "Open" and
"Export As" dialogs under "YAPF image (*.yapf)".
Manual compilation
------------------
$ gcc $(gimptool --cflags) -O2 yapf-gimp.c \
$(gimptool --libs) -o yapf-gimp
$ install -Dm755 yapf-gimp \
~/.config/GIMP/3.(your version here)/plug-ins/yapf-gimp/yapf-gimp
Notes
-----
- The plugin registers two procedures: file-yapf-load and file-yapf-save.
- Supported colour modes: Gray, Gray+Alpha, RGB, RGBA.
- The MIME type is image/x-yapf and the file extension is .yapf.
- RGB and RGBA images are saved with the sRGB GPU hint (YAPF_FLAG_SRGB)
because GIMP's native 8-bit buffers are always gamma-corrected sRGB.
- Mip levels are NOT generated on export — GIMP does not expose them.
Use the YAPF C API directly if you need a mip chain.

View file

@ -21,22 +21,24 @@
*
* -------------------------------------------------------------------------
*
* Compilation:
* Compilation (from this folder):
* gimptool --install yapf-gimp.c
*
* yapf.c is embedded via #include below because gimptool compiles a single
* source file. Do NOT link separately — the include IS the build.
* yapf.c (two folders up) is embedded via #include below because gimptool
* compiles a single source file. Do NOT link it separately.
*/
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
/* Pull in the entire encoder/decoder as a single translation unit. */
#include "yapf.c"
#include "../../yapf.c"
/* ── GIMP procedure names ────────────────────────────────────────────── */
#define LOAD_PROC "file-yapf-load"
#define SAVE_PROC "file-yapf-save"
#define PLUG_IN_BINARY "yapf-gimp"
/* ── Plugin type boilerplate ─────────────────────────────────────────── */
@ -123,6 +125,14 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
"image/x-yapf");
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
"yapf");
/* GIMP flattens / converts a copy of the image for us as needed. */
gimp_export_procedure_set_capabilities(GIMP_EXPORT_PROCEDURE(procedure),
GIMP_EXPORT_CAN_HANDLE_RGB | GIMP_EXPORT_CAN_HANDLE_GRAY |
GIMP_EXPORT_CAN_HANDLE_ALPHA, NULL, NULL, NULL);
gimp_procedure_add_boolean_argument(procedure, "mipmaps",
"Store _mipmaps",
"Also store a full mip chain (for game engines and GPUs)",
FALSE, G_PARAM_READWRITE);
}
return procedure;
@ -142,7 +152,7 @@ static GimpValueArray *yapf_load_run(
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
gchar *path = g_file_get_path(file);
yapf_image_t *img = yapf_load(path);
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */
g_free(path);
if (!img) {
@ -164,12 +174,12 @@ static GimpValueArray *yapf_load_run(
case YAPF_CHANNELS_GRAY:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAY_IMAGE;
babl_fmt = "Y' u8";
babl_fmt = is_srgb ? "Y' u8" : "Y u8";
break;
case YAPF_CHANNELS_GRAY_ALPHA:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAYA_IMAGE;
babl_fmt = "Y'A u8";
babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
break;
case YAPF_CHANNELS_RGB:
base_type = GIMP_RGB;
@ -204,6 +214,44 @@ static GimpValueArray *yapf_load_run(
/* ── Save handler ────────────────────────────────────────────────────── */
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes).
* Fills img->mips / img->mip_levels; mips[0] aliases img->pixels. */
static gboolean yapf_build_mips(yapf_image_t *img) {
int levels = 1;
while (levels < (int)YAPF_MAX_MIPS &&
((img->width >> levels) > 0 || (img->height >> levels) > 0))
levels++;
img->mips = (uint8_t **)calloc((size_t)levels, sizeof(uint8_t *));
if (!img->mips) return FALSE;
img->mips[0] = img->pixels;
img->mip_levels = (uint8_t)levels;
int ch = img->channels;
for (int m = 1; m < levels; m++) {
uint32_t pw = MAX(1u, img->width >> (m - 1)), ph = MAX(1u, img->height >> (m - 1));
uint32_t w = MAX(1u, img->width >> m), h = MAX(1u, img->height >> m);
const uint8_t *src = img->mips[m - 1];
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
if (!dst) return FALSE;
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++)
for (int c = 0; c < ch; c++) {
uint32_t x0 = MIN(2 * x, pw - 1), x1 = MIN(2 * x + 1, pw - 1);
uint32_t y0 = MIN(2 * y, ph - 1), y1 = MIN(2 * y + 1, ph - 1);
unsigned sum = src[((size_t)y0 * pw + x0) * ch + c] + src[((size_t)y0 * pw + x1) * ch + c]
+ src[((size_t)y1 * pw + x0) * ch + c] + src[((size_t)y1 * pw + x1) * ch + c];
dst[((size_t)y * w + x) * ch + c] = (uint8_t)((sum + 2) / 4);
}
img->mips[m] = dst;
}
return TRUE;
}
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg) {
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR,
g_error_new_literal(GIMP_PLUG_IN_ERROR, 0, msg));
}
static GimpValueArray *yapf_save_run(
GimpProcedure *procedure,
GimpRunMode run_mode,
@ -214,93 +262,88 @@ static GimpValueArray *yapf_save_run(
GimpProcedureConfig *config,
gpointer run_data)
{
(void)run_mode; (void)options; (void)metadata; (void)config; (void)run_data;
(void)metadata; (void)run_data;
/* Retrieve the top-most layer. */
GimpLayer **layers = gimp_image_get_layers(image);
gint n_layers = 0;
if (layers) { while (layers[n_layers]) n_layers++; }
if (!layers || n_layers == 0) {
g_free(layers);
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
"No layers to export.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
if (run_mode == GIMP_RUN_INTERACTIVE) {
gimp_ui_init(PLUG_IN_BINARY);
GtkWidget *dialog = gimp_export_procedure_dialog_new(
GIMP_EXPORT_PROCEDURE(procedure), config, image);
gimp_procedure_dialog_fill(GIMP_PROCEDURE_DIALOG(dialog), NULL);
gboolean ok = gimp_procedure_dialog_run(GIMP_PROCEDURE_DIALOG(dialog));
gtk_widget_destroy(dialog);
if (!ok)
return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL);
}
GimpDrawable *drawable = GIMP_DRAWABLE(layers[0]);
g_free(layers);
gboolean mipmaps = FALSE;
g_object_get(config, "mipmaps", &mipmaps, NULL);
/* Determine channel layout and GPU format hint. */
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
gboolean is_gray = gimp_drawable_is_gray(drawable);
/* A flattened copy when the image has several layers; the visible
* result is what gets exported. */
GimpExportReturn export = gimp_export_options_get_image(options, &image);
GList *layers = gimp_image_list_layers(image);
if (!layers) {
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
return yapf_error(procedure, "No layers to export.");
}
GimpDrawable *drawable = GIMP_DRAWABLE(layers->data);
g_list_free(layers);
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
gboolean is_gray = gimp_drawable_is_gray(drawable);
const gchar *babl_fmt;
uint8_t ch;
uint8_t gpu_fmt;
uint8_t flags = YAPF_FLAG_SRGB; /* GIMP's native u8 formats are sRGB */
uint8_t ch, gpu_fmt;
/* GIMP's 8-bit buffers are gamma encoded, so pixels are written as sRGB. */
if (is_gray) {
if (has_alpha) {
ch = YAPF_CHANNELS_GRAY_ALPHA; babl_fmt = "Y'A u8";
gpu_fmt = YAPF_GPU_RG8;
flags = 0; /* grayscale has no sRGB GPU hint */
} else {
ch = YAPF_CHANNELS_GRAY; babl_fmt = "Y' u8";
gpu_fmt = YAPF_GPU_R8;
flags = 0;
}
ch = has_alpha ? YAPF_CHANNELS_GRAY_ALPHA : YAPF_CHANNELS_GRAY;
babl_fmt = has_alpha ? "Y'A u8" : "Y' u8";
gpu_fmt = has_alpha ? YAPF_GPU_RG8 : YAPF_GPU_R8;
} else {
if (has_alpha) {
ch = YAPF_CHANNELS_RGBA; babl_fmt = "R'G'B'A u8";
gpu_fmt = YAPF_GPU_SRGB8_A8;
} else {
ch = YAPF_CHANNELS_RGB; babl_fmt = "R'G'B' u8";
gpu_fmt = YAPF_GPU_SRGB8;
}
ch = has_alpha ? YAPF_CHANNELS_RGBA : YAPF_CHANNELS_RGB;
babl_fmt = has_alpha ? "R'G'B'A u8" : "R'G'B' u8";
gpu_fmt = has_alpha ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_SRGB8;
}
gint w = gimp_drawable_get_width(drawable);
gint h = gimp_drawable_get_height(drawable);
/* Read pixel data from GIMP's buffer. */
uint8_t *pixels = (uint8_t *)malloc((size_t)w * (size_t)h * ch);
if (!pixels) {
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Out of memory.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
return yapf_error(procedure, "Out of memory.");
}
GeglBuffer *buffer = gimp_drawable_get_buffer(drawable);
gegl_buffer_get(buffer,
GEGL_RECTANGLE(0, 0, w, h),
1.0, babl_format(babl_fmt),
pixels, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
gegl_buffer_get(buffer, GEGL_RECTANGLE(0, 0, w, h), 1.0,
babl_format(babl_fmt), pixels,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref(buffer);
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
/* Build yapf_image_t — base level only, no mip chain from GIMP. */
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = (uint32_t)w;
img.height = (uint32_t)h;
img.channels = ch;
img.gpu_format = gpu_fmt;
img.flags = flags;
img.flags = YAPF_FLAG_SRGB;
img.mip_levels = 1;
img.pixels = pixels;
img.mips = NULL;
gchar *path = g_file_get_path(file);
int result = yapf_save(path, &img);
g_free(path);
int result = YAPF_ERR_OOM;
if (!mipmaps || yapf_build_mips(&img)) {
gchar *path = g_file_get_path(file);
result = yapf_save(path, &img);
g_free(path);
}
if (img.mips) {
for (int m = 1; m < img.mip_levels; m++) free(img.mips[m]);
free(img.mips);
}
free(pixels);
if (result != YAPF_OK) {
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
"Failed to export YAPF file.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
}
if (result != YAPF_OK)
return yapf_error(procedure, "Failed to export YAPF file.");
return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL);
}

View file

@ -0,0 +1,15 @@
# Linux desktop integration
Makes file managers recognise `.yapf` files (MIME type `image/x-yapf`) and
show **thumbnails** for them.
```sh
sh extensions/linux/install.sh # for your user
sudo sh extensions/linux/install.sh --system
```
Thumbnails are made by `yapf thumbnail` (from the `yapf` tool, which must be
on your `PATH` — `sudo make install` puts it in `/usr/local/bin`). They work
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
system and is not covered yet.

18
extensions/linux/install.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
# Registers the YAPF MIME type and thumbnailer for the current user, so file
# managers that use freedesktop thumbnailers (GNOME Files, Nemo, Caja,
# Thunar with tumbler) show .yapf previews. Needs `yapf` on your PATH.
# sh extensions/linux/install.sh (current user)
# sudo sh extensions/linux/install.sh --system
set -e
here=$(cd "$(dirname "$0")" && pwd)
if [ "$1" = "--system" ]; then data=/usr/share; else data=${XDG_DATA_HOME:-$HOME/.local/share}; fi
command -v yapf >/dev/null 2>&1 || echo "warning: 'yapf' is not on your PATH; thumbnails need it" >&2
mkdir -p "$data/mime/packages" "$data/thumbnailers"
cp "$here/yapf-mime.xml" "$data/mime/packages/yapf.xml"
cp "$here/yapf.thumbnailer" "$data/thumbnailers/yapf.thumbnailer"
update-mime-database "$data/mime" >/dev/null 2>&1 || true
echo "Installed the image/x-yapf MIME type and thumbnailer into $data"
echo "Restart your file manager (e.g. 'nautilus -q') and clear ~/.cache/thumbnails/fail if needed."

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Registers .yapf files as image/x-yapf (shared-mime-info). -->
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="image/x-yapf">
<comment>YAPF image</comment>
<generic-icon name="image-x-generic"/>
<magic priority="60">
<match type="string" value="YAPF" offset="0"/>
</magic>
<glob pattern="*.yapf"/>
</mime-type>
</mime-info>

View file

@ -0,0 +1,4 @@
[Thumbnailer Entry]
TryExec=yapf
Exec=yapf thumbnail %i %o %s
MimeType=image/x-yapf;

View file

@ -0,0 +1,28 @@
// Open YAPF — opens .yapf images in Photoshop.
// Copyright 2026 Nexoniarz — Apache License 2.0.
// Install: copy this folder's .jsx / .jsxinc files (and yapf / yapf.exe)
// into Photoshop's Presets/Scripts folder; then File > Scripts > Open YAPF.
#target photoshop
#include "yapf_common.jsxinc"
(function () {
var filter = YAPF_IS_WINDOWS ? "YAPF images:*.yapf,All files:*.*"
: function (f) { return f instanceof Folder || /\.yapf$/i.test(f.name); };
var files = File.openDialog("Open YAPF image", filter, true);
if (!files) return;
if (!(files instanceof Array)) files = [files];
for (var i = 0; i < files.length; i++) {
var tmp = yapfTempFile(".png");
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
alert("Could not open " + files[i].name + ".\n\n" + YAPF_TOOL_HELP);
return;
}
var doc = app.open(tmp);
// The temporary PNG is no longer needed once the document is open;
// use File > Scripts > Save as YAPF to write a .yapf again.
doc.info.caption = files[i].fsName;
tmp.remove();
}
})();

View file

@ -0,0 +1,33 @@
# YAPF for Photoshop
Two scripts that appear under **File → Scripts**:
- **Open YAPF** — opens one or more `.yapf` files as documents.
- **Save as YAPF** — saves the visible image of the active document (a
flattened 8-bit copy; your document is not changed), keeping
transparency, with an optional mip chain.
They convert through the `yapf` command-line tool, so Photoshop needs no
native plugin.
## Install
1. Download `yapf-<your platform>.zip` from the
[Releases](https://github.com/Nexoniarz/YAPF/releases) (or build the tool:
`make` → `build/yapf`).
2. Copy `Open YAPF.jsx`, `Save as YAPF.jsx`, `yapf_common.jsxinc` **and**
`yapf` / `yapf.exe` into Photoshop's scripts folder:
- Windows: `C:\Program Files\Adobe\Adobe Photoshop <version>\Presets\Scripts`
- macOS: `/Applications/Adobe Photoshop <version>/Presets/Scripts`
3. Restart Photoshop. The scripts are under **File → Scripts**; you can give
them keyboard shortcuts in **Edit → Keyboard Shortcuts** or record them
in Actions.
Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
## Status
The scripts are written for Photoshop CC's ExtendScript and are not yet
tested inside Photoshop (the converter itself is fully tested). A native
format plugin (`.8bi` / `.plugin`, so `.yapf` shows up directly in the Open
and Save dialogs) needs Adobe's Photoshop SDK and is planned.

View file

@ -0,0 +1,40 @@
// Save as YAPF — writes the active document as a .yapf image.
// Copyright 2026 Nexoniarz — Apache License 2.0.
// The visible result is saved: a flattened 8-bit RGB (or grayscale) copy,
// transparency kept. Your document is not modified.
#target photoshop
#include "yapf_common.jsxinc"
(function () {
if (!app.documents.length) { alert("Open a document first."); return; }
var doc = app.activeDocument;
var base = doc.name.replace(/\.[^.]+$/, "");
var start = new File((doc.saved && doc.path ? doc.path.fsName : Folder.desktop.fsName) + "/" + base + ".yapf");
var out = start.saveDlg("Save as YAPF", YAPF_IS_WINDOWS ? "YAPF images:*.yapf" : undefined);
if (!out) return;
if (!/\.yapf$/i.test(out.name)) out = new File(out.fsName + ".yapf");
var mips = confirm("Also store mipmaps (for game engines / GPUs)?", true, "YAPF");
var tmp = yapfTempFile(".png");
var copy = doc.duplicate(base + " (YAPF export)", true); // merged copy
try {
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
copy.changeMode(ChangeMode.RGB);
if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT)
copy.bitsPerChannel = BitsPerChannelType.EIGHT;
var opts = new PNGSaveOptions();
opts.interlaced = false;
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
} finally {
copy.close(SaveOptions.DONOTSAVECHANGES);
}
var args = [tmp.fsName, out.fsName];
if (mips) args.push("--mips");
var ok = yapfRun(args, out);
tmp.remove();
if (!ok) { alert("Could not write " + out.name + ".\n\n" + YAPF_TOOL_HELP); return; }
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
})();

View file

@ -0,0 +1,41 @@
// Shared helpers for the YAPF Photoshop scripts.
// Copyright 2026 Nexoniarz — Apache License 2.0.
//
// The scripts convert through the `yapf` command-line tool. It is looked
// up next to these scripts first (yapf.exe / yapf), then on the PATH.
var YAPF_IS_WINDOWS = $.os.indexOf("Windows") !== -1;
function yapfQuote(s) {
return '"' + String(s).replace(/"/g, '\\"') + '"';
}
function yapfTool() {
var here = new File($.fileName).parent;
var names = YAPF_IS_WINDOWS ? ["yapf.exe"] : ["yapf"];
for (var i = 0; i < names.length; i++) {
var f = new File(here.fsName + "/" + names[i]);
if (f.exists) return f.fsName;
}
return names[0]; // rely on the PATH
}
// Runs the tool; returns true when `expectOut` was produced.
function yapfRun(args, expectOut) {
var cmd = yapfQuote(yapfTool());
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
if (expectOut.exists) expectOut.remove();
app.system(cmd);
return expectOut.exists;
}
function yapfTempFile(ext) {
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
Math.floor(Math.random() * 1e6) + ext);
}
var YAPF_TOOL_HELP =
"The 'yapf' command-line tool was not found or failed.\n\n" +
"Put yapf" + (YAPF_IS_WINDOWS ? ".exe" : "") + " in the same folder as this script, " +
"or add it to your PATH.\nDownload or build it: https://github.com/Nexoniarz/YAPF";

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');