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

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# build outputs
/build/
/dist/
*.o
*.a
*.so
*.dylib
*.dll
*.exe
*.vsix
test_yapf_tmp.yapf
# generated: copied from bindings/js/yapf.js when packaging
/extensions/vscode/media/yapf.js
node_modules/
__pycache__/
*.pyc

65
Makefile Normal file
View file

@ -0,0 +1,65 @@
# YAPF — simple build for Linux, macOS and other Unix systems.
#
# make library (static + shared) and the `yapf` tool, in build/
# make test run the C test suite (and the JS one if node is present)
# sudo make install install header, libraries and tool under $(PREFIX)
# make NO_SIMD=1 plain C only
#
# Windows: use build.bat (LLVM) or any MinGW / MSVC compiler on yapf.c.
CC ?= cc
CFLAGS ?= -O2
PREFIX ?= /usr/local
BUILD ?= build
YAPF_CFLAGS = -std=c99 -Wall -Wextra $(CFLAGS) $(if $(NO_SIMD),-DYAPF_NO_SIMD)
LDLIBS = -lm -pthread
ifeq ($(shell uname -s),Darwin)
SHARED = libyapf.dylib
SHARED_FLAGS = -dynamiclib -install_name $(PREFIX)/lib/libyapf.dylib
else
SHARED = libyapf.so
SHARED_FLAGS = -shared
endif
all: $(BUILD)/libyapf.a $(BUILD)/$(SHARED) $(BUILD)/yapf
$(BUILD):
mkdir -p $(BUILD)
$(BUILD)/yapf.o: yapf.c yapf.h | $(BUILD)
$(CC) $(YAPF_CFLAGS) -fPIC -c yapf.c -o $@
$(BUILD)/libyapf.a: $(BUILD)/yapf.o
$(AR) rcs $@ $^
$(BUILD)/$(SHARED): $(BUILD)/yapf.o
$(CC) $(SHARED_FLAGS) $^ -o $@ -pthread
$(BUILD)/yapf: tools/yapf_cli.c yapf.c yapf.h | $(BUILD)
$(CC) $(YAPF_CFLAGS) tools/yapf_cli.c yapf.c -o $@ $(LDLIBS)
$(BUILD)/test_yapf: tests/test_yapf.c yapf.c yapf.h | $(BUILD)
$(CC) $(YAPF_CFLAGS) tests/test_yapf.c yapf.c -o $@ $(LDLIBS)
test: $(BUILD)/test_yapf
$(BUILD)/test_yapf other/YAPF.YAPF
@if command -v node >/dev/null 2>&1; then node bindings/js/test.js; \
else echo "(node not found: skipped the JavaScript tests)"; fi
install: all
install -d $(DESTDIR)$(PREFIX)/include $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/bin
install -m 644 yapf.h $(DESTDIR)$(PREFIX)/include/yapf.h
install -m 644 $(BUILD)/libyapf.a $(DESTDIR)$(PREFIX)/lib/libyapf.a
install -m 755 $(BUILD)/$(SHARED) $(DESTDIR)$(PREFIX)/lib/$(SHARED)
install -m 755 $(BUILD)/yapf $(DESTDIR)$(PREFIX)/bin/yapf
uninstall:
rm -f $(DESTDIR)$(PREFIX)/include/yapf.h $(DESTDIR)$(PREFIX)/lib/libyapf.a \
$(DESTDIR)$(PREFIX)/lib/$(SHARED) $(DESTDIR)$(PREFIX)/bin/yapf
clean:
rm -rf $(BUILD)
.PHONY: all test install uninstall clean

368
README.md
View file

@ -1,90 +1,127 @@
# YAPF — Yet Another Picture Format # YAPF — Yet Another Picture Format
**Fast. Small. Easy.** **Lossless images that decode several times faster than PNG — on one core.**
YAPF is a lossless image codec built around a single principle: decoding should YAPF is a lossless image format for photos, UI and game assets, built for
be as close to a memory copy as possible. Instead of running a general-purpose decode speed first. There is no entropy coder, no bit reader and no context
compressor like DEFLATE over pixel data, YAPF encodes images with five cheap model: every structure is byte aligned, and the decoder runs short loops that
operations — a pixel cache lookup, two flavours of delta encoding, run-length modern CPUs execute at memory speed — with SSE / NEON where available. The
encoding, and verbatim pixel storage — producing files that are smaller than encoder does the hard work, so the decoder only follows instructions.
uncompressed formats while decoding nearly as fast as `memcpy`.
- **6–10× faster decoding than PNG** (stb_image), single-threaded; a 4K image
decodes in about 6 ms (5.6 GB/s). Multithreaded decoding is optional.
- **Smaller than PNG** on UI, icons and screenshots; 20–45 % larger on photos
and natural textures — the price of having no entropy coder.
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
premultiplied flags, and random access to 64×64 tiles.
- **Tiny and portable**: two C99 files, no dependencies. Plus a CLI, Python,
JavaScript and plugins for the tools you already use.
This is **YAPF version 1**, the first stable format.
--- ---
## Why YAPF? ## Use it in…
Standard lossless formats like PNG compress well, but decompressing DEFLATE | Where | What you get | Guide | Status |
streams is CPU-intensive. On large images (4K and above) or in | :---- | :----------- | :---- | :----- |
latency-sensitive applications (game engines, real-time pipelines, embedded | **C / C++** | `yapf.h` + `yapf.c`, drop into any project | [below](#c--c) | ✅ tested on Linux x64, Windows x64, Linux ARM64 |
systems) this is a meaningful bottleneck. | **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux, Windows |
| **GIMP 3** | open, export (with mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 |
| **Blender 4.2+** | import, export, drag & drop, image as plane | [extensions/blender](extensions/blender) | ✅ tested in Blender 5.1 |
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ✅ packaged, preview tested in Chromium |
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ✅ plugin UI tested in Chromium |
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ not yet tested inside Photoshop |
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnailer tested, not in a file manager |
| **Python** | `yapf.load()` / `yapf.save()` | [bindings/python](bindings/python) | ✅ tested |
| **JavaScript** | browsers, Node.js, workers | [bindings/js](bindings/js) | ✅ tested, byte-identical to C |
YAPF trades a modest compression ratio advantage for dramatically lower decode Pre-built downloads (command-line tool and libraries for Windows, macOS and
complexity. There are no Huffman tables to build, no sliding windows to Linux, plus the plugin packages) are published on the
maintain, and no bit-level unpacking. The decoder is a tight byte-dispatch [Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can
loop that any modern CPU handles at memory bandwidth. also be built from source as described below.
### Performance
Benchmark: loading a 5K professional photograph (5120 × 2880 px).
| Format | Load time |
| :----- | ----------: |
| **YAPF** | **0.111 s** |
| PNG | 0.194 s |
YAPF decodes approximately **1.7× faster** than PNG on the same hardware
without SIMD or threading.
### When YAPF excels
- **Pixel art, sprites, UI textures** — large flat regions of identical colour
compress to single RUN bytes; palette-like images benefit heavily from the
cache.
- **Screenshots and diagrams** — solid backgrounds and repeated gradients hit
DIFF/LUMA and the cache constantly.
- **Game asset pipelines** — load textures at startup without paying a
decompression tax every frame.
- **Embedded / resource-constrained targets** — the entire codec fits in ~200
lines of portable C with zero external dependencies.
--- ---
## Integration ## Command line
The core library is two files. Drop them into your project and compile: ```sh
yapf photo.png photo.yapf # encode (formats by file extension)
yapf texture.png texture.yapf --mips # with a full mip chain
yapf photo.yapf photo.png # decode to PNG / JPG / BMP / TGA
yapf texture.yapf mip2.png --mip 2 # export one mip level
yapf info photo.yapf # size, channels, mips, flags, ratio
yapf bench photo.yapf # decode speed, 1 thread and all cores
yapf thumbnail photo.yapf thumb.png 256
```
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
`--premult` (alpha is premultiplied).
---
## Benchmark
Single core, AMD Ryzen 7 5700G, `gcc -O2`. PNG is decoded with `stb_image`;
the PNG files are the ones shipped with the respective projects. "Plain C"
is the library built with `-DYAPF_NO_SIMD`; the default build uses SSE / NEON.
| Image | Raw | PNG | **YAPF** | vs PNG | PNG decode | YAPF, plain C | **YAPF (default)** |
| :--------------------- | ------: | -----: | -------: | -----: | ---------: | ------------: | -----------------: |
| Wallpaper 3840×2160 | 32400 K | 1812 K | 2267 K | 125 % | 44.7 ms¹ | 10.3 ms | **5.9 ms** (5.6 GB/s) |
| UI screenshot 261×430 | 438 K | 176 K | 143 K | 81 % | 1.6 ms | 0.31 ms | **0.17 ms** |
| UI screenshot 305×529 | 630 K | 201 K | 185 K | 92 % | 2.1 ms | 0.45 ms | **0.24 ms** |
| App icon 128×128 | 64 K | 24 K | 23 K | 93 % | 0.26 ms | 0.06 ms | **0.03 ms** |
| Rock texture 512×512 | 768 K | 182 K | 221 K | 121 % | 1.4 ms | 0.35 ms | **0.16 ms** |
| Wood texture 512×512 | 768 K | 185 K | 234 K | 126 % | 1.4 ms | 0.46 ms | **0.16 ms** |
| Canvas texture 512×512 | 768 K | 171 K | 245 K | 143 % | 1.0 ms | 0.35 ms | **0.15 ms** |
| Pixel art 512×512 | 1024 K | — | 66 K | — | — | 0.74 ms | **0.32 ms** |
| Random noise 512×512 | 1024 K | — | 1024 K | — | — | 0.1 ms | 0.1 ms |
¹ PNG decode of the wallpaper measured between 44.7 and 66 ms depending on
the harness; the table uses the fastest. `-O2` and `-O3` builds of YAPF
perform the same.
---
## C / C++
The library is two files. Add them to your project:
``` ```
yapf.h — public API and format documentation yapf.h — public API and the complete format specification
yapf.c — encoder / decoder implementation yapf.c — encoder and decoder
``` ```
```c ```c
#include "yapf.h" #include "yapf.h"
int main(void) { yapf_image_t *img = yapf_load("texture.yapf"); /* or yapf_load_memory(buf, len) */
/* Load */ if (img) {
yapf_image_t *img = yapf_load("photo.yapf"); /* img->pixels: width × height × channels bytes, rows top to bottom */
if (!img) { /* file not found or not a valid YAPF stream */ } upload_texture(img->width, img->height, img->channels, img->pixels);
printf("%u x %u, %u channel(s)\n",
img->width, img->height, img->channels);
/* Save */
int rc = yapf_save("output.yapf", img);
if (rc != YAPF_OK) { /* write error */ }
yapf_free(img); yapf_free(img);
return 0;
} }
yapf_image_t out = { .width = w, .height = h, .channels = 4,
.mip_levels = 1, .pixels = rgba, .flags = YAPF_FLAG_SRGB };
yapf_save("out.yapf", &out);
``` ```
Compile with any C99-capable compiler — no flags required for the baseline: Compile with any C99 compiler (`-pthread` on Unix for the `_mt` loaders):
```sh ```sh
gcc -O2 -std=c99 main.c yapf.c -o myapp cc -O2 -std=c99 main.c yapf.c -o app -pthread
``` ```
--- SIMD is on by default: SSSE3 on x86 / x64 (chosen at run time with CPUID,
so binaries still run on older CPUs) and NEON on ARM64, with GCC, Clang,
MSVC and MinGW.
| Define | Effect |
| :------------------ | :----- |
| `-DYAPF_NO_SIMD` | plain C only |
| `-DYAPF_NO_THREADS` | no threads; the `_mt` loaders decode on the calling thread, and only the C standard library is needed |
## API Reference ## API Reference
@ -95,12 +132,17 @@ typedef struct {
uint32_t width; /* pixels per row */ uint32_t width; /* pixels per row */
uint32_t height; /* rows */ uint32_t height; /* rows */
uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */ uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */
uint8_t *pixels; /* raw pixel buffer, malloc-owned */ uint8_t gpu_format; /* YAPF_GPU_* hint */
uint8_t flags; /* YAPF_FLAG_* (premultiplied alpha, sRGB) */
uint8_t mip_levels; /* 1 = base only, up to 16 */
uint8_t *pixels; /* base level, malloc-owned */
uint8_t **mips; /* mips[0] == pixels, mips[1..] smaller levels */
} yapf_image_t; } yapf_image_t;
``` ```
`pixels` is a tightly packed, row-major byte array of `width × height × channels` `pixels` is a tightly packed, row-major byte array of `width × height × channels`
bytes. The layout per pixel matches the `channels` value: bytes. Mip level *m* is `max(1, width >> m) × max(1, height >> m)`. The
layout per pixel matches the `channels` value:
| `channels` | Layout per pixel | | `channels` | Layout per pixel |
| :--------- | :--------------- | | :--------- | :--------------- |
@ -113,29 +155,40 @@ bytes. The layout per pixel matches the `channels` value:
```c ```c
yapf_image_t *yapf_load(const char *filename); yapf_image_t *yapf_load(const char *filename);
yapf_image_t *yapf_load_memory(const void *buffer, size_t size);
``` ```
Decodes a YAPF file from disk. Returns a heap-allocated `yapf_image_t` on Decode a YAPF file from disk or from memory (e.g. a game archive). Return a
success, `NULL` on any failure (file not found, invalid magic, unsupported heap-allocated image on success, `NULL` on any failure (file not found,
version, out of memory). Free with `yapf_free()`. invalid or corrupt data, out of memory). Corrupt input is rejected, never
read out of bounds.
```c
yapf_image_t *yapf_load_mt(const char *filename, int threads);
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
```
Same, decoding tiles on up to `threads` threads (`0` = one per CPU core).
The output is identical to the single-threaded loaders.
```c ```c
int yapf_save(const char *filename, const yapf_image_t *img); int yapf_save(const char *filename, const yapf_image_t *img);
``` ```
Encodes an image to disk. Returns `YAPF_OK` (0) on success or a negative Encodes an image to disk. Writes `img->mip_levels`
`YAPF_ERR_*` code on failure. levels; `img->mips` may be `NULL` when `mip_levels == 1`. Returns `YAPF_OK`
(0) on success or a negative `YAPF_ERR_*` code on failure.
```c ```c
void yapf_free(yapf_image_t *img); void yapf_free(yapf_image_t *img);
``` ```
Releases memory returned by `yapf_load()`. Passing `NULL` is safe. Releases memory returned by the loaders. Passing `NULL` is safe.
### Return codes ### Return codes
| Constant | Value | Meaning | | Constant | Value | Meaning |
| :---------------- | ----: | :----------------------------------- | | :----------------- | ----: | :----------------------------------- |
| `YAPF_OK` | 0 | Success | | `YAPF_OK` | 0 | Success |
| `YAPF_ERR_INVALID`| –1 | NULL or logically invalid argument | | `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
| `YAPF_ERR_IO` | –2 | File could not be opened or written | | `YAPF_ERR_IO` | –2 | File could not be opened or written |
| `YAPF_ERR_OOM` | –3 | Out of memory |
### Channel constants ### Channel constants
@ -148,115 +201,102 @@ YAPF_CHANNELS_RGBA 4
--- ---
## Building the libraries ## Building
The build scripts uses the [LLVM](https://releases.llvm.org/) compiler | System | Command | Output |
| :----- | :------ | :----- |
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |
| All platforms at once | `tools/build_release.sh` (needs [zig](https://ziglang.org)) | `dist/release/`: every download on the Releases page |
The command-line tool is `tools/yapf_cli.c` + `yapf.c`; it uses the public
domain [stb](https://github.com/nothings/stb) headers in `third_party/`.
---
## How it works
1. **Two colour transforms** — per 64×64 tile, YCoCg-R or subtract-green
(`G, R−G, B−G`), whichever codes smaller. Both run mod 256, so every
plane stays one byte per sample.
2. **Twelve row filters** — per band of 8 rows: Left, Gradient (`L + A − D`),
Up-right, Up-average, or the row 1–8 rows above (catches repeating
textures and dither patterns). None depends on the previous sample by
more than one add, and the vertical ones decode as plain vector code.
3. **Groups of 8** — residuals are packed in groups of 8 at the group's bit
width: 8 values × *w* bits = exactly *w* bytes, so every group is byte
aligned and unpacks with one 64-bit load (or one SIMD shuffle).
4. **4-bit control codes** — one per group, or one per run of up to 64 groups
repeating the previous width; the encoder picks the cheapest sequence with
a small dynamic program.
5. **Validate first, then run** — all sizes in a tile are checked before the
hot loops start, so the loops need no bounds checks. Corrupt files are
rejected, never read out of bounds.
6. **Constant and stored tiles** — a plane that is constant in a tile costs
one byte; a tile that would not shrink is stored raw (noise never grows).
7. **Independent tiles** — any tile can be decoded on its own, in any order,
on any thread.
The complete specification is the comment at the top of [`yapf.h`](yapf.h):
```
HEADER (20 bytes) "YAPF", version 1, channels, GPU hint, mip count,
flags, 3 reserved, width u32, height u32
MIP INDEX per level: offset u32, size u32
LEVEL DATA tile_size u32 × tile_count, then the tiles
TILE (64×64) header byte: stored raw, or constant-plane mask + colour transform
per plane: filter byte per 8 rows, control nibbles,
w bytes per group of 8 values
```
---
## Testing
```sh ```sh
# Unix (Linux / macOS) make test # C suite: 784 checks, plus JavaScript if node is installed
chmod +x build.sh node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
./build.sh
# Windows
build.bat
``` ```
Output goes to `dist/` with one subdirectory per platform: What has been verified for this release:
``` - exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
dist/ single- and multi-threaded decoding;
├── windows-x64/ yapf.dll yapf.lib - the same tests pass on **Linux x64**, **Windows x64** (MinGW, run under
├── windows-arm64/ yapf.dll yapf.lib Wine), **Linux ARM64** (NEON, run under qemu), with and without SIMD, and
├── macos-x64/ libyapf.dylib under AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer;
├── macos-arm64/ libyapf.dylib - thousands of corrupted and truncated files are rejected without crashing;
├── linux-x64/ libyapf.so libyapf.a - the JavaScript and Python versions produce byte-identical files to C;
└── linux-arm64/ libyapf.so libyapf.a - the GIMP plugin and the Blender extension in the real applications.
```
### Optional build flags The macOS builds are cross-compiled and not yet run on a Mac.
| Flag | Effect |
| :---------------- | :-------------------------------------------------- |
| `-DYAPF_USE_SIMD` | Reserved for future SIMD acceleration paths |
The interactive build script will prompt for these options.
--- ---
## Technical Specification ## Project layout
### File header — 14 bytes
| Offset | Size | Type | Description |
| :----- | ---: | :--------- | :-------------------------------------- |
| 0x00 | 4 | `char[4]` | Magic: `"YAPF"` |
| 0x04 | 1 | `uint8_t` | Format version (currently `1`) |
| 0x05 | 4 | `uint32_t` | Width in pixels, little-endian |
| 0x09 | 4 | `uint32_t` | Height in pixels, little-endian |
| 0x0D | 1 | `uint8_t` | Channel count: 1 / 2 / 3 / 4 |
All multi-byte integers are always stored little-endian, regardless of host
architecture.
### Byte-stream opcodes
The payload begins immediately after the header and is decoded left-to-right.
A 64-slot pixel cache (zero-initialised) and a running "previous pixel" state
(`r=0, g=0, b=0, a=255`) are maintained throughout.
| Byte range | Opcode | Size | Description |
| :---------- | :----- | ------: | :-------------------------------------------- |
| 0x00–0x3F | INDEX | 1 byte | Restore pixel from cache slot `byte & 0x3F` |
| 0x40–0x7F | DIFF | 1 byte | Per-channel deltas: dr = bits[5:4]–2, dg = bits[3:2]–2, db = bits[1:0]–2. Range –2..+1. Alpha unchanged. |
| 0x80–0xBF | LUMA | 2 bytes | Green delta dg = (byte & 0x3F)–32. Second byte: dr = ((b1>>4)&0xF)–8+dg, db = (b1&0xF)–8+dg. Alpha unchanged. |
| 0xC0–0xFD | RUN | 1 byte | Repeat previous pixel (byte&0x3F)+1 times (1–62). Cache not updated. |
| 0xFE | RGB | 4 bytes | Next 3 bytes = R, G, B. Alpha unchanged. |
| 0xFF | RGBA | 5 bytes | Next 4 bytes = R, G, B, A. |
After every non-RUN opcode, the cache slot `HASH(r,g,b,a)` is updated and the
pixel becomes the new previous pixel.
``` ```
HASH(r,g,b,a) = (r×3 + g×5 + b×7 + a×11) mod 64 yapf.h, yapf.c the library (format spec in yapf.h)
tools/yapf_cli.c the `yapf` command-line tool
tools/build_release.sh builds every download for all platforms
tests/test_yapf.c C test suite
bindings/js/ JavaScript encoder / decoder
bindings/python/ Python module
extensions/gimp/ GIMP 3 plugin
extensions/blender/ Blender extension
extensions/vscode/ VS Code preview
extensions/figma/ Figma plugin
extensions/photoshop/ Photoshop scripts
extensions/linux/ MIME type and thumbnailer
third_party/ stb_image / stb_image_write (public domain, CLI only)
other/YAPF.YAPF sample image (the YAPF logo)
``` ```
The stream ends when exactly `width × height` pixels have been decoded; no
end-of-stream marker is required.
### Pixel buffer layout
Within the decoded buffer, each pixel occupies `channels` consecutive bytes in
the order shown in the API table above. Grayscale images store a single
luminance byte per pixel; the encoder internally promotes gray values to
`r = g = b = Y` for uniform opcode selection.
---
## GIMP Plugin
A plugin for GIMP is located in `extensions/gimp/`. See the
[build instructions](extensions/gimp/howtocompile.txt) for details.
Supported colour modes: Gray, Gray+Alpha, RGB, RGBA.
File extension: `.yapf`. MIME type: `image/x-yapf`.
---
## Platforms
| Platform | Architectures | Library format |
| :------------- | :------------ | :------------------ |
| Windows | x64, ARM64 | `.dll` + `.lib` |
| macOS | ARM64 | `.dylib` |
| Linux (glibc) | x64, ARM64 | `.so` |
| Linux (musl) | x64, ARM64 | `.a` (static) |
The core library (`yapf.c` / `yapf.h`) uses only the C99 standard library
(`<stdio.h>`, `<stdlib.h>`, `<string.h>`, `<stdint.h>`) and compiles cleanly
on any conforming C99 or C++ toolchain with no configuration required.
--- ---
## License ## License
Apache License 2.0 — see [LICENSE](LICENSE). Apache License 2.0 — see [LICENSE](LICENSE). `third_party/` contains
stb_image and stb_image_write by Sean Barrett (public domain / MIT).

22
bindings/js/README.md Normal file
View file

@ -0,0 +1,22 @@
# yapf.js — YAPF in JavaScript
Encoder and decoder for YAPF v1 in one dependency-free file. Works in
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
produces exactly the same bytes as the C library.
```js
const YAPF = require('./yapf.js'); // or <script src="yapf.js"> → window.YAPF
const img = YAPF.decode(bytes); // Uint8Array → image
// img = { width, height, channels, pixels, gpuFormat, flags, mipLevels, mips }
const file = YAPF.encode({ width, height, channels: 4, pixels }); // → Uint8Array
// draw on a canvas
ctx.putImageData(new ImageData(YAPF.toRGBA(img), img.width, img.height), 0, 0);
```
`decode` throws an `Error` for corrupt or unsupported data. Speed: a 4K
image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
Tests: `node test.js`.

11
bindings/js/package.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "yapf-image",
"version": "1.0.0",
"description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies",
"main": "yapf.js",
"files": ["yapf.js", "README.md"],
"scripts": { "test": "node test.js" },
"keywords": ["image", "codec", "lossless", "yapf", "texture"],
"license": "Apache-2.0",
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git", "directory": "bindings/js" }
}

57
bindings/js/test.js Normal file
View file

@ -0,0 +1,57 @@
/* node bindings/js/test.js — round-trips, the sample file, and corrupt input. */
'use strict';
const fs = require('fs');
const path = require('path');
const YAPF = require('./yapf.js');
let tests = 0, fails = 0;
const check = (ok, msg) => { tests++; if (!ok) { fails++; console.log('FAIL:', msg); } };
let seed = 12345;
const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) >>> 0) >>> 8;
function fill(w, h, ch, kind) {
const p = new Uint8Array(w * h * ch);
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) for (let c = 0; c < ch; c++) {
const i = (y * w + x) * ch + c;
p[i] = kind === 0 ? rnd() : kind === 1 ? x * 3 + y * 5 + c * 40
: kind === 2 ? (((x / 7 | 0) + (y / 5 | 0) + c) & 1 ? 200 : 30)
: kind === 3 ? ((x * x + y * y) / 50 | 0) + rnd() % 5
: c === ch - 1 ? 255 : x ^ y;
}
return p;
}
for (const [w, h] of [[1, 1], [7, 1], [1, 9], [63, 65], [64, 64], [97, 61], [200, 130]])
for (let ch = 1; ch <= 4; ch++) for (let kind = 0; kind < 5; kind++) {
const pixels = fill(w, h, ch, kind);
const back = YAPF.decode(YAPF.encode({ width: w, height: h, channels: ch, pixels }));
check(back.width === w && back.height === h && back.channels === ch &&
Buffer.compare(Buffer.from(back.pixels), Buffer.from(pixels)) === 0,
`roundtrip ${w}x${h} ch${ch} kind${kind}`);
}
/* The bundled sample: decode to the reference pixels, re-encode to the same bytes. */
const samplePath = path.join(__dirname, '..', '..', 'other', 'YAPF.YAPF');
if (fs.existsSync(samplePath)) {
const file = fs.readFileSync(samplePath);
const img = YAPF.decode(file);
let h = 2166136261;
for (const v of img.pixels) h = Math.imul(h ^ v, 16777619) >>> 0;
check(h === 0x8891c2b4, 'sample decodes to the reference pixels');
check(Buffer.compare(Buffer.from(YAPF.encode(img)), file) === 0,
'sample re-encodes to byte-identical output (matches the C encoder)');
}
/* Corrupt input must throw, never hang or return garbage silently. */
const good = YAPF.encode({ width: 130, height: 70, channels: 4, pixels: fill(130, 70, 4, 3) });
let threwOnTruncation = true;
for (let i = 0; i < 2000; i++) {
const bad = good.slice(0, i % 3 === 1 ? rnd() % good.length : good.length);
if (i % 3 !== 1) bad[rnd() % bad.length] ^= 1 << (rnd() % 8);
try { YAPF.decode(bad); if (i % 3 === 1) threwOnTruncation = false; } catch (e) { /* expected */ }
}
check(threwOnTruncation, 'truncated files are rejected');
console.log(`${tests} tests, ${fails} failures`);
process.exit(fails ? 1 : 0);

463
bindings/js/yapf.js Normal file
View file

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

23
bindings/python/README.md Normal file
View file

@ -0,0 +1,23 @@
# yapf.py — YAPF in Python
```python
import yapf
img = yapf.load("texture.yapf") # or yapf.decode(data)
print(img.width, img.height, img.channels, img.mip_levels)
img.pixels # bytes, rows top to bottom
yapf.save("copy.yapf", img) # or yapf.encode(img) -> bytes
yapf.save("new.yapf", yapf.Image(w, h, 4, rgba_bytes))
```
It uses the C library through `ctypes` when it finds one:
1. the file named by the `YAPF_LIBRARY` environment variable,
2. `libyapf.so` / `libyapf.dylib` / `yapf.dll` next to `yapf.py`, or in
`lib/<platform>/` beside it (e.g. `lib/windows-x64/yapf.dll`),
3. the system library path (`sudo make install` installs one).
Without the library it falls back to a pure-Python decoder that needs
`numpy` (decoding only; about 1.4 s for a 4K image). `yapf.backend()`
tells you which one is in use.

312
bindings/python/yapf.py Normal file
View file

@ -0,0 +1,312 @@
"""
yapf.py — Python bindings for YAPF v1 images.
Copyright 2026 Nexoniarz — Apache License 2.0.
import yapf
img = yapf.load("texture.yapf") # or yapf.decode(data)
img.width, img.height, img.channels # pixels: bytes, row-major
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
Uses the C library through ctypes when one can be found (fast, can
encode); otherwise falls back to a pure-Python decoder that needs numpy
(decode only). The library is looked up in this order:
1. the path in the YAPF_LIBRARY environment variable
2. next to this file: libyapf.so / libyapf.dylib / yapf.dll
(also in a platform subfolder such as lib/linux-x64/)
3. the system library path
Build the library with: cc -O2 -shared -fPIC yapf.c -o libyapf.so -pthread
"""
import ctypes
import os
import platform
import struct
import sys
__all__ = ["Image", "load", "save", "decode", "encode", "backend",
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
FLAG_PREMULT_ALPHA, FLAG_SRGB = 1, 2
class YapfError(Exception):
pass
class Image:
"""A decoded image. `pixels` holds width*height*channels bytes, rows top
to bottom; `mips` holds every mip level (mips[0] is pixels)."""
def __init__(self, width, height, channels, pixels,
gpu_format=None, flags=FLAG_SRGB, mips=None):
self.width, self.height, self.channels = int(width), int(height), int(channels)
self.pixels = bytes(pixels)
if gpu_format is None:
gpu_format = {1: GPU_R8, 2: GPU_RG8, 3: GPU_SRGB8, 4: GPU_SRGB8_A8}[self.channels]
self.gpu_format, self.flags = gpu_format, flags
self.mips = [self.pixels] + [bytes(m) for m in (mips or [])[1:]]
@property
def mip_levels(self):
return len(self.mips)
def __repr__(self):
return "<yapf.Image %dx%d, %d channel(s), %d mip level(s)>" % (
self.width, self.height, self.channels, self.mip_levels)
# ── ctypes backend ────────────────────────────────────────────────────
class _CImage(ctypes.Structure):
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32),
("channels", ctypes.c_uint8), ("gpu_format", ctypes.c_uint8),
("flags", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
("pixels", ctypes.POINTER(ctypes.c_uint8)),
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
def _platform_dir():
machine = platform.machine().lower()
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
system = {"win32": "windows", "darwin": "macos"}.get(sys.platform, "linux")
return "%s-%s" % (system, arch)
def _find_library():
names = {"win32": ["yapf.dll", "libyapf.dll"],
"darwin": ["libyapf.dylib"]}.get(sys.platform, ["libyapf.so"])
here = os.path.dirname(os.path.abspath(__file__))
candidates = []
if os.environ.get("YAPF_LIBRARY"):
candidates.append(os.environ["YAPF_LIBRARY"])
for folder in (here, os.path.join(here, "lib", _platform_dir())):
candidates += [os.path.join(folder, n) for n in names]
candidates += names
for path in candidates:
try:
lib = ctypes.CDLL(path)
lib.yapf_load_memory_mt.restype = ctypes.POINTER(_CImage)
lib.yapf_load_memory_mt.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int]
lib.yapf_free.argtypes = [ctypes.POINTER(_CImage)]
lib.yapf_save.argtypes = [ctypes.c_char_p, ctypes.POINTER(_CImage)]
return lib
except (OSError, AttributeError):
continue
return None
_lib = _find_library()
def backend():
"""'c' when the C library is used, 'python' for the fallback decoder."""
return "c" if _lib else "python"
def _c_decode(data):
data = bytes(data)
p = _lib.yapf_load_memory_mt(data, len(data), 0)
if not p:
raise YapfError("not a valid YAPF v1 file")
try:
c = p.contents
mips = []
for m in range(c.mip_levels):
w, h = max(1, c.width >> m), max(1, c.height >> m)
mips.append(ctypes.string_at(c.mips[m], w * h * c.channels))
return Image(c.width, c.height, c.channels, mips[0], c.gpu_format, c.flags, mips)
finally:
_lib.yapf_free(p)
def _c_save(path, img):
bufs = [ctypes.create_string_buffer(m, len(m)) for m in img.mips]
ptrs = (ctypes.POINTER(ctypes.c_uint8) * len(bufs))(
*[ctypes.cast(b, ctypes.POINTER(ctypes.c_uint8)) for b in bufs])
c = _CImage(img.width, img.height, img.channels, img.gpu_format, img.flags,
len(bufs), ptrs[0], ptrs)
rc = _lib.yapf_save(os.fsencode(path), ctypes.byref(c))
if rc != 0:
raise YapfError("yapf_save failed with code %d" % rc)
# ── pure-Python fallback decoder (numpy) ──────────────────────────────
_NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64]
def _py_decode(data):
try:
import numpy as np
except ImportError:
raise YapfError("the YAPF C library was not found and numpy is not "
"installed; build the library (see yapf.py) or install numpy")
b = np.frombuffer(bytes(data), dtype=np.uint8)
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
raise YapfError("not a YAPF file")
if b[4] != 1:
raise YapfError("unsupported YAPF version %d" % b[4])
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
W, H = struct.unpack_from("<II", bytes(b[12:20]))
if not (1 <= ch <= 4 and 1 <= mip_levels <= 16 and 1 <= W <= 65535 and 1 <= H <= 65535):
raise YapfError("corrupt header")
def bad():
raise YapfError("corrupt YAPF data")
mips = []
for m in range(mip_levels):
off, ln = struct.unpack_from("<II", bytes(b[20 + 8 * m: 28 + 8 * m]))
mw, mh = max(1, W >> m), max(1, H >> m)
if off > len(b) or ln > len(b) - off:
bad()
out = np.zeros((mh, mw, ch), dtype=np.uint8)
tcx, tcy = (mw + 63) // 64, (mh + 63) // 64
sizes = np.frombuffer(bytes(b[off: off + 4 * tcx * tcy]), dtype="<u4")
pos = off + 4 * tcx * tcy
for t in range(tcx * tcy):
tx, ty = (t % tcx) * 64, (t // tcx) * 64
tw, th = min(64, mw - tx), min(64, mh - ty)
end = pos + int(sizes[t])
if end > off + ln:
bad()
try:
out[ty:ty + th, tx:tx + tw] = _py_tile(np, b, pos, end, tw, th, ch, bad)
except (IndexError, ValueError):
bad()
pos = end
mips.append(out.tobytes())
return Image(W, H, ch, mips[0], gpu, flags, mips)
def _py_tile(np, b, p, end, tw, th, ch, bad):
hdr = int(b[p]); p += 1
if hdr == 0x80:
if end - p != tw * th * ch:
bad()
return b[p:end].reshape(th, tw, ch)
if hdr & 0xE0:
bad()
xf, mask = hdr & 0x10, hdr & 0x0F
planes = np.zeros((ch, th, tw), dtype=np.uint8)
for c in range(ch):
if mask >> c & 1:
planes[c, :, :] = b[p]; p += 1
for c in range(ch):
if mask >> c & 1:
continue
nb, ng = (th + 7) // 8, (tw * th + 7) // 8
filt = b[p:p + nb]; p += nb
widths, g, hi, w = [], 0, 0, 0
while g < ng:
code = (int(b[p]) >> (4 * hi)) & 15
p += hi; hi ^= 1
if code < 9:
w = code
n = _NIB_GROUPS[code]
widths += [w] * n; g += n
if g != ng:
bad()
if hi:
p += 1
z = np.zeros(ng * 8, dtype=np.uint8)
widths = np.array(widths, dtype=np.int64)
starts = p + np.concatenate(([0], np.cumsum(widths)[:-1]))
p += int(widths.sum())
if p > end:
bad()
for wv in range(1, 9):
sel = np.nonzero(widths == wv)[0]
if not len(sel):
continue
raw = b[starts[sel][:, None] + np.arange(wv)].astype(np.uint64)
word = (raw << (8 * np.arange(wv, dtype=np.uint64))).sum(axis=1, dtype=np.uint64)
shifts = np.arange(8, dtype=np.uint64) * np.uint64(wv)
vals = (word[:, None] >> shifts) & np.uint64((1 << wv) - 1)
z[(sel[:, None] * 8 + np.arange(8)).ravel()] = vals.ravel().astype(np.uint8)
r = ((z[:tw * th] >> 1) ^ (0 - (z[:tw * th] & 1)).astype(np.uint8)).reshape(th, tw).astype(np.int64)
P = planes[c].astype(np.int64)
for y in range(th):
f = int(filt[y // 8])
U = P[y - 1] if y else np.zeros(tw, np.int64)
if f == 0:
P[y] = np.cumsum(r[y]) & 255
elif f == 1:
inc = r[y] + U - np.concatenate(([0], U[:-1]))
P[y] = np.cumsum(inc) & 255
elif f in (2, 3):
AR = np.concatenate((U[1:], U[-1:]))
pred = AR if f == 2 else (U + AR) >> 1
P[y] = (pred + r[y]) & 255
elif 16 <= f < 24:
k = f - 15
K = P[y - k] if y >= k else np.zeros(tw, np.int64)
P[y] = (K + r[y]) & 255
else:
bad()
planes[c] = P.astype(np.uint8)
if p != end:
bad()
if ch < 3:
return planes.transpose(1, 2, 0)
P0, P1, P2 = (planes[i].astype(np.int64) for i in range(3))
if xf:
R, G, B = P1 + P0 - 128, P0, P2 + P0 - 128
else:
co, cg = (P1 - 128) & 255, (P2 - 128) & 255
s8 = lambda v: np.where(v > 127, v - 256, v)
t = (P0 - (s8(cg) >> 1)) & 255
B = (t - (s8(co) >> 1)) & 255
R, G = B + co, cg + t
out = [R & 255, G & 255, B & 255] + ([planes[3].astype(np.int64)] if ch == 4 else [])
return np.stack(out, axis=-1).astype(np.uint8)
# ── public API ────────────────────────────────────────────────────────
def decode(data):
"""Decode YAPF bytes into an Image."""
return _c_decode(data) if _lib else _py_decode(data)
def load(path):
"""Read and decode a .yapf file."""
with open(path, "rb") as f:
return decode(f.read())
def save(path, img):
"""Encode an Image (or anything with width/height/channels/pixels) to a
.yapf file. Needs the C library."""
if not _lib:
raise YapfError("saving .yapf needs the YAPF native library, which was not found "
"for this platform; see https://github.com/Nexoniarz/YAPF#python")
if not isinstance(img, Image):
img = Image(img.width, img.height, img.channels, img.pixels)
expected = img.width * img.height * img.channels
if len(img.pixels) != expected:
raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels)))
_c_save(path, img)
def encode(img):
"""Encode an Image to YAPF bytes. Needs the C library."""
import tempfile
fd, tmp = tempfile.mkstemp(suffix=".yapf")
os.close(fd)
try:
save(tmp, img)
with open(tmp, "rb") as f:
return f.read()
finally:
os.remove(tmp)
if __name__ == "__main__":
for path in sys.argv[1:]:
print(path, load(path), "backend:", backend())

View file

@ -10,7 +10,7 @@ setlocal enabledelayedexpansion
:: ::
:: Options: :: Options:
:: --debug Debug build (no optimisation, full symbols) :: --debug Debug build (no optimisation, full symbols)
:: --simd Enable SIMD paths (-DYAPF_USE_SIMD) :: --no-simd Plain C only (SIMD is on by default, picked at run time)
:: --static-only Build only static libraries :: --static-only Build only static libraries
:: --shared-only Build only shared libraries (DLL) :: --shared-only Build only shared libraries (DLL)
:: --help Show this message :: --help Show this message
@ -45,13 +45,13 @@ if !ANSI_OK!==1 (
:: ── Option parsing ─────────────────────────────────────────────────── :: ── Option parsing ───────────────────────────────────────────────────
set OPT_DEBUG=0 set OPT_DEBUG=0
set OPT_SIMD=0 set OPT_NOSIMD=0
set OPT_STATIC_ONLY=0 set OPT_STATIC_ONLY=0
set OPT_SHARED_ONLY=0 set OPT_SHARED_ONLY=0
for %%A in (%*) do ( for %%A in (%*) do (
if /i "%%A"=="--debug" set OPT_DEBUG=1 if /i "%%A"=="--debug" set OPT_DEBUG=1
if /i "%%A"=="--simd" set OPT_SIMD=1 if /i "%%A"=="--no-simd" set OPT_NOSIMD=1
if /i "%%A"=="--static-only" set OPT_STATIC_ONLY=1 if /i "%%A"=="--static-only" set OPT_STATIC_ONLY=1
if /i "%%A"=="--shared-only" set OPT_SHARED_ONLY=1 if /i "%%A"=="--shared-only" set OPT_SHARED_ONLY=1
if /i "%%A"=="--help" goto :show_help if /i "%%A"=="--help" goto :show_help
@ -115,9 +115,11 @@ if !OPT_DEBUG!==1 (
echo !C_CYN![info]!C_RST! Mode : release echo !C_CYN![info]!C_RST! Mode : release
) )
if !OPT_SIMD!==1 ( if !OPT_NOSIMD!==1 (
set CFLAGS=!CFLAGS! -DYAPF_USE_SIMD set CFLAGS=!CFLAGS! -DYAPF_NO_SIMD
echo !C_CYN![info]!C_RST! SIMD : enabled echo !C_CYN![info]!C_RST! SIMD : disabled
) else (
echo !C_CYN![info]!C_RST! SIMD : SSSE3 / NEON, chosen at run time
) )
set BUILD_SHARED=1 set BUILD_SHARED=1
@ -139,6 +141,9 @@ echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h
echo LIBRARY yapf echo LIBRARY yapf
echo EXPORTS echo EXPORTS
echo yapf_load echo yapf_load
echo yapf_load_memory
echo yapf_load_mt
echo yapf_load_memory_mt
echo yapf_save echo yapf_save
echo yapf_free echo yapf_free
) > dist\_yapf.def ) > dist\_yapf.def
@ -183,6 +188,15 @@ if !BUILD_STATIC!==1 (
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\libyapf.a (static) echo !C_GRN![ ok ]!C_RST! dist\windows-x64\libyapf.a (static)
) )
:: Command-line tool
clang --target=x86_64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
tools\yapf_cli.c yapf.c -o dist\windows-x64\yapf.exe
if errorlevel 1 (
echo !C_YLW![warn]!C_RST! yapf.exe build failed (non-fatal)
) else (
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\yapf.exe (command-line tool)
)
:: ==================================================================== :: ====================================================================
:: Build: Windows ARM64 :: Build: Windows ARM64
:: ==================================================================== :: ====================================================================
@ -227,6 +241,10 @@ if !BUILD_STATIC!==1 (
) )
) )
clang --target=aarch64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
tools\yapf_cli.c yapf.c -o dist\windows-arm64\yapf.exe
if not errorlevel 1 echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\yapf.exe (command-line tool)
:skip_arm64 :skip_arm64
:: ── Cleanup temp files ─────────────────────────────────────────────── :: ── Cleanup temp files ───────────────────────────────────────────────

21
build.sh Normal file → Executable file
View file

@ -8,7 +8,7 @@
# #
# Options: # Options:
# --debug Debug build: no optimisation, full symbols # --debug Debug build: no optimisation, full symbols
# --simd Enable SIMD paths (-DYAPF_USE_SIMD) # --no-simd Plain C only (SIMD is on by default, picked at run time)
# --static-only Build only static libraries # --static-only Build only static libraries
# --shared-only Build only shared libraries # --shared-only Build only shared libraries
# --help Show this message # --help Show this message
@ -40,14 +40,15 @@ step() { echo; echo "${BLD}──── $* ────${RST}"; }
# ── Option parsing ─────────────────────────────────────────────────── # ── Option parsing ───────────────────────────────────────────────────
OPT_DEBUG=0 OPT_DEBUG=0
OPT_SIMD=0 OPT_NOSIMD=0
OPT_STATIC_ONLY=0 OPT_STATIC_ONLY=0
OPT_SHARED_ONLY=0 OPT_SHARED_ONLY=0
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--debug) OPT_DEBUG=1 ;; --debug) OPT_DEBUG=1 ;;
--simd) OPT_SIMD=1 ;; --no-simd) OPT_NOSIMD=1 ;;
--simd) ;; # SIMD is the default now; kept for old scripts
--static-only) OPT_STATIC_ONLY=1 ;; --static-only) OPT_STATIC_ONLY=1 ;;
--shared-only) OPT_SHARED_ONLY=1 ;; --shared-only) OPT_SHARED_ONLY=1 ;;
--help) --help)
@ -89,7 +90,7 @@ fi
[[ -n "$STRIP" ]] && info "Strip : $STRIP" [[ -n "$STRIP" ]] && info "Strip : $STRIP"
# ── Compiler flags ─────────────────────────────────────────────────── # ── Compiler flags ───────────────────────────────────────────────────
CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -fPIC" CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -fPIC -pthread"
LFLAGS="" LFLAGS=""
if [[ $OPT_DEBUG -eq 1 ]]; then if [[ $OPT_DEBUG -eq 1 ]]; then
@ -101,7 +102,8 @@ else
info "Mode : release" info "Mode : release"
fi fi
[[ $OPT_SIMD -eq 1 ]] && { CFLAGS="$CFLAGS -DYAPF_USE_SIMD"; info "SIMD : enabled"; } if [[ $OPT_NOSIMD -eq 1 ]]; then CFLAGS="$CFLAGS -DYAPF_NO_SIMD"; info "SIMD : disabled"
else info "SIMD : SSSE3 / NEON, chosen at run time"; fi
# ── Detect OS ──────────────────────────────────────────────────────── # ── Detect OS ────────────────────────────────────────────────────────
OS=$(uname -s) OS=$(uname -s)
@ -176,6 +178,13 @@ if [[ "$OS" == "Darwin" ]]; then
ok "dist/macos/libyapf.a (universal)" ok "dist/macos/libyapf.a (universal)"
fi fi
step "Building the yapf command-line tool"
"$CLANG" $CFLAGS --target=arm64-apple-macos11 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_arm64 -lm
"$CLANG" $CFLAGS --target=x86_64-apple-macos10.15 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_x64 -lm
"$LIPO" -create /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64 -output dist/macos/yapf
rm -f /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64
ok "dist/macos/yapf (universal)"
# ── Linux build ────────────────────────────────────────────────────── # ── Linux build ──────────────────────────────────────────────────────
elif [[ "$OS" == "Linux" ]]; then elif [[ "$OS" == "Linux" ]]; then
@ -205,6 +214,8 @@ elif [[ "$OS" == "Linux" ]]; then
compile_static "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.a" compile_static "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.a"
ok "dist/$NATIVE_DIR/libyapf.a" ok "dist/$NATIVE_DIR/libyapf.a"
fi fi
"$CLANG" $CFLAGS --target="$NATIVE_TRIPLE" tools/yapf_cli.c yapf.c -o "dist/$NATIVE_DIR/yapf" -lm
ok "dist/$NATIVE_DIR/yapf (command-line tool)"
# ── Cross-compile to the other common arch ──────────────────────── # ── Cross-compile to the other common arch ────────────────────────
if [[ "$HOST_ARCH" == "x86_64" ]]; then if [[ "$HOST_ARCH" == "x86_64" ]]; then

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 * gimptool --install yapf-gimp.c
* *
* yapf.c is embedded via #include below because gimptool compiles a single * yapf.c (two folders up) is embedded via #include below because gimptool
* source file. Do NOT link separately — the include IS the build. * compiles a single source file. Do NOT link it separately.
*/ */
#include <libgimp/gimp.h> #include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
/* Pull in the entire encoder/decoder as a single translation unit. */ /* Pull in the entire encoder/decoder as a single translation unit. */
#include "yapf.c" #include "../../yapf.c"
/* ── GIMP procedure names ────────────────────────────────────────────── */ /* ── GIMP procedure names ────────────────────────────────────────────── */
#define LOAD_PROC "file-yapf-load" #define LOAD_PROC "file-yapf-load"
#define SAVE_PROC "file-yapf-save" #define SAVE_PROC "file-yapf-save"
#define PLUG_IN_BINARY "yapf-gimp"
/* ── Plugin type boilerplate ─────────────────────────────────────────── */ /* ── Plugin type boilerplate ─────────────────────────────────────────── */
@ -123,6 +125,14 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
"image/x-yapf"); "image/x-yapf");
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure), gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
"yapf"); "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; return procedure;
@ -142,7 +152,7 @@ static GimpValueArray *yapf_load_run(
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data; (void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
gchar *path = g_file_get_path(file); 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); g_free(path);
if (!img) { if (!img) {
@ -164,12 +174,12 @@ static GimpValueArray *yapf_load_run(
case YAPF_CHANNELS_GRAY: case YAPF_CHANNELS_GRAY:
base_type = GIMP_GRAY; base_type = GIMP_GRAY;
layer_type = GIMP_GRAY_IMAGE; layer_type = GIMP_GRAY_IMAGE;
babl_fmt = "Y' u8"; babl_fmt = is_srgb ? "Y' u8" : "Y u8";
break; break;
case YAPF_CHANNELS_GRAY_ALPHA: case YAPF_CHANNELS_GRAY_ALPHA:
base_type = GIMP_GRAY; base_type = GIMP_GRAY;
layer_type = GIMP_GRAYA_IMAGE; layer_type = GIMP_GRAYA_IMAGE;
babl_fmt = "Y'A u8"; babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
break; break;
case YAPF_CHANNELS_RGB: case YAPF_CHANNELS_RGB:
base_type = GIMP_RGB; base_type = GIMP_RGB;
@ -204,6 +214,44 @@ static GimpValueArray *yapf_load_run(
/* ── Save handler ────────────────────────────────────────────────────── */ /* ── 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( static GimpValueArray *yapf_save_run(
GimpProcedure *procedure, GimpProcedure *procedure,
GimpRunMode run_mode, GimpRunMode run_mode,
@ -214,93 +262,88 @@ static GimpValueArray *yapf_save_run(
GimpProcedureConfig *config, GimpProcedureConfig *config,
gpointer run_data) 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. */ if (run_mode == GIMP_RUN_INTERACTIVE) {
GimpLayer **layers = gimp_image_get_layers(image); gimp_ui_init(PLUG_IN_BINARY);
gint n_layers = 0; GtkWidget *dialog = gimp_export_procedure_dialog_new(
if (layers) { while (layers[n_layers]) n_layers++; } GIMP_EXPORT_PROCEDURE(procedure), config, image);
gimp_procedure_dialog_fill(GIMP_PROCEDURE_DIALOG(dialog), NULL);
if (!layers || n_layers == 0) { gboolean ok = gimp_procedure_dialog_run(GIMP_PROCEDURE_DIALOG(dialog));
g_free(layers); gtk_widget_destroy(dialog);
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, if (!ok)
"No layers to export."); return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL);
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
} }
GimpDrawable *drawable = GIMP_DRAWABLE(layers[0]); gboolean mipmaps = FALSE;
g_free(layers); g_object_get(config, "mipmaps", &mipmaps, NULL);
/* 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);
/* Determine channel layout and GPU format hint. */
gboolean has_alpha = gimp_drawable_has_alpha(drawable); gboolean has_alpha = gimp_drawable_has_alpha(drawable);
gboolean is_gray = gimp_drawable_is_gray(drawable); gboolean is_gray = gimp_drawable_is_gray(drawable);
const gchar *babl_fmt; const gchar *babl_fmt;
uint8_t ch; uint8_t ch, gpu_fmt;
uint8_t gpu_fmt;
uint8_t flags = YAPF_FLAG_SRGB; /* GIMP's native u8 formats are sRGB */
/* GIMP's 8-bit buffers are gamma encoded, so pixels are written as sRGB. */
if (is_gray) { if (is_gray) {
if (has_alpha) { ch = has_alpha ? YAPF_CHANNELS_GRAY_ALPHA : YAPF_CHANNELS_GRAY;
ch = YAPF_CHANNELS_GRAY_ALPHA; babl_fmt = "Y'A u8"; babl_fmt = has_alpha ? "Y'A u8" : "Y' u8";
gpu_fmt = YAPF_GPU_RG8; gpu_fmt = has_alpha ? YAPF_GPU_RG8 : YAPF_GPU_R8;
flags = 0; /* grayscale has no sRGB GPU hint */
} else { } else {
ch = YAPF_CHANNELS_GRAY; babl_fmt = "Y' u8"; ch = has_alpha ? YAPF_CHANNELS_RGBA : YAPF_CHANNELS_RGB;
gpu_fmt = YAPF_GPU_R8; babl_fmt = has_alpha ? "R'G'B'A u8" : "R'G'B' u8";
flags = 0; gpu_fmt = has_alpha ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_SRGB8;
}
} 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;
}
} }
gint w = gimp_drawable_get_width(drawable); gint w = gimp_drawable_get_width(drawable);
gint h = gimp_drawable_get_height(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); uint8_t *pixels = (uint8_t *)malloc((size_t)w * (size_t)h * ch);
if (!pixels) { if (!pixels) {
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Out of memory."); if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
return gimp_procedure_new_return_values(procedure, return yapf_error(procedure, "Out of memory.");
GIMP_PDB_EXECUTION_ERROR, err);
} }
GeglBuffer *buffer = gimp_drawable_get_buffer(drawable); GeglBuffer *buffer = gimp_drawable_get_buffer(drawable);
gegl_buffer_get(buffer, gegl_buffer_get(buffer, GEGL_RECTANGLE(0, 0, w, h), 1.0,
GEGL_RECTANGLE(0, 0, w, h), babl_format(babl_fmt), pixels,
1.0, babl_format(babl_fmt), GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
pixels, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref(buffer); 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; yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = (uint32_t)w; img.width = (uint32_t)w;
img.height = (uint32_t)h; img.height = (uint32_t)h;
img.channels = ch; img.channels = ch;
img.gpu_format = gpu_fmt; img.gpu_format = gpu_fmt;
img.flags = flags; img.flags = YAPF_FLAG_SRGB;
img.mip_levels = 1; img.mip_levels = 1;
img.pixels = pixels; img.pixels = pixels;
img.mips = NULL;
int result = YAPF_ERR_OOM;
if (!mipmaps || yapf_build_mips(&img)) {
gchar *path = g_file_get_path(file); gchar *path = g_file_get_path(file);
int result = yapf_save(path, &img); result = yapf_save(path, &img);
g_free(path); g_free(path);
}
if (img.mips) {
for (int m = 1; m < img.mip_levels; m++) free(img.mips[m]);
free(img.mips);
}
free(pixels); free(pixels);
if (result != YAPF_OK) { if (result != YAPF_OK)
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, return yapf_error(procedure, "Failed to export YAPF file.");
"Failed to export YAPF file.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
}
return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL); 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');

Binary file not shown.

180
tests/test_yapf.c Normal file
View file

@ -0,0 +1,180 @@
/*
* test_yapf.c — self-contained test suite for yapf.c
*
* cc -O2 -std=c99 tests/test_yapf.c yapf.c -o test_yapf -pthread
* ./test_yapf [path/to/other/YAPF.YAPF]
*
* Round-trips many sizes, channel counts and image kinds, checks mip
* chains, single- vs multi-threaded decode, the bundled sample file, and
* that corrupted or truncated files are rejected without crashing.
* Exit code 0 means every test passed.
*/
#include "../yapf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
#define SAMPLE_HASH 0x8891c2b4u
static int tests, fails;
static uint32_t rng = 12345;
static uint32_t rnd(void) { rng = rng * 1103515245u + 12345u; return rng >> 8; }
#define CHECK(cond, ...) do { tests++; if (!(cond)) { fails++; \
printf("FAIL: "); printf(__VA_ARGS__); printf("\n"); } } while (0)
/* Five kinds of content: noise, smooth gradient, hard-edged blocks,
* noisy curves, and an image with an opaque alpha channel. */
static void fill(uint8_t *p, int w, int h, int ch, int kind) {
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
for (int c = 0; c < ch; c++) {
uint8_t *d = p + ((size_t)y * w + x) * ch + c;
switch (kind) {
case 0: *d = (uint8_t)rnd(); break;
case 1: *d = (uint8_t)(x * 3 + y * 5 + c * 40); break;
case 2: *d = ((x / 7 + y / 5 + c) & 1) ? 200 : 30; break;
case 3: *d = (uint8_t)((x * x + y * y) / 50 + rnd() % 5); break;
default: *d = (c == ch - 1) ? 255 : (uint8_t)(x ^ y); break;
}
}
}
static uint8_t *encode_mem(const yapf_image_t *img, size_t *n) {
const char *tmp = "test_yapf_tmp.yapf";
if (yapf_save(tmp, img) != YAPF_OK) return NULL;
FILE *f = fopen(tmp, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*n = (size_t)ftell(f);
rewind(f);
uint8_t *b = (uint8_t *)malloc(*n);
if (b && fread(b, 1, *n, f) != *n) { free(b); b = NULL; }
fclose(f);
remove(tmp);
return b;
}
static void roundtrip(int w, int h, int ch, int kind) {
size_t n = (size_t)w * h * ch, len = 0;
uint8_t *p = (uint8_t *)malloc(n);
fill(p, w, h, ch, kind);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = (uint32_t)w; img.height = (uint32_t)h;
img.channels = (uint8_t)ch; img.mip_levels = 1; img.pixels = p;
uint8_t *file = encode_mem(&img, &len);
CHECK(file != NULL, "encode %dx%d ch%d kind%d", w, h, ch, kind);
if (file) {
yapf_image_t *a = yapf_load_memory(file, len);
yapf_image_t *b = yapf_load_memory_mt(file, len, 0);
CHECK(a && memcmp(a->pixels, p, n) == 0,
"roundtrip %dx%d ch%d kind%d", w, h, ch, kind);
CHECK(b && memcmp(b->pixels, p, n) == 0,
"roundtrip (threads) %dx%d ch%d kind%d", w, h, ch, kind);
yapf_free(a);
yapf_free(b);
free(file);
}
free(p);
}
static void mips(void) {
const int w = 300, h = 200, levels = 9;
uint8_t *mp[9];
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = w; img.height = h; img.channels = 4; img.mip_levels = levels;
for (int l = 0; l < levels; l++) {
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
mp[l] = (uint8_t *)malloc((size_t)mw * mh * 4);
fill(mp[l], mw, mh, 4, l % 5);
}
img.pixels = mp[0];
img.mips = mp;
size_t len = 0;
uint8_t *file = encode_mem(&img, &len);
yapf_image_t *d = file ? yapf_load_memory_mt(file, len, 3) : NULL;
int ok = d && d->mip_levels == levels;
for (int l = 0; ok && l < levels; l++) {
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
ok = memcmp(d->mips[l], mp[l], (size_t)mw * mh * 4) == 0;
}
CHECK(ok, "mip chain of %d levels", levels);
yapf_free(d);
free(file);
for (int l = 0; l < levels; l++) free(mp[l]);
}
/* Flip bits, truncate, scribble: the decoder must never crash, and a
* truncated file must be rejected. */
static void corruption(void) {
const int w = 130, h = 70, ch = 4;
uint8_t *p = (uint8_t *)malloc((size_t)w * h * ch);
fill(p, w, h, ch, 3);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = w; img.height = h; img.channels = ch; img.mip_levels = 1; img.pixels = p;
size_t len = 0;
uint8_t *file = encode_mem(&img, &len);
uint8_t *bad = (uint8_t *)malloc(len);
int truncated_ok = 1;
for (int it = 0; it < 3000 && file; it++) {
memcpy(bad, file, len);
size_t n = len;
switch (it % 3) {
case 0: for (int k = 0; k < 1 + it % 7; k++) bad[rnd() % len] ^= (uint8_t)(1u << (rnd() % 8)); break;
case 1: n = rnd() % len; break;
default: bad[rnd() % len] = (uint8_t)rnd(); break;
}
yapf_image_t *d = (it & 1) ? yapf_load_memory_mt(bad, n, 2) : yapf_load_memory(bad, n);
if (it % 3 == 1 && d) truncated_ok = 0;
yapf_free(d);
}
CHECK(file != NULL, "corruption test setup");
CHECK(truncated_ok, "truncated files are rejected");
free(bad);
free(file);
free(p);
}
static uint32_t fnv(const uint8_t *p, size_t n) {
uint32_t h = 2166136261u;
for (size_t i = 0; i < n; i++) h = (h ^ p[i]) * 16777619u;
return h;
}
int main(int argc, char **argv) {
static const int sizes[][2] = {
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
{1, 129}, {97, 61}, {200, 130}, {256, 256}, {513, 77},
{1000, 700} /* 176 tiles: big enough for the threaded decoder */
};
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++)
for (int ch = 1; ch <= 4; ch++)
for (int kind = 0; kind < 5; kind++)
roundtrip(sizes[s][0], sizes[s][1], ch, kind);
mips();
corruption();
const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF";
yapf_image_t *img = yapf_load(sample);
if (img) {
uint32_t h = fnv(img->pixels, (size_t)img->width * img->height * img->channels);
CHECK(img->width == 512 && img->height == 512 && img->channels == 4 &&
h == SAMPLE_HASH, "sample %s decodes to the reference pixels (hash %08x)",
sample, (unsigned)h);
yapf_free(img);
} else {
printf("note: sample %s not found, skipped\n", sample);
}
printf("%d tests, %d failures\n", tests, fails);
return fails != 0;
}

7988
third_party/stb_image.h vendored Normal file

File diff suppressed because it is too large Load diff

1724
third_party/stb_image_write.h vendored Normal file

File diff suppressed because it is too large Load diff

58
tools/build_release.sh Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Builds every release download into dist/release/ — for maintainers.
#
# tools/build_release.sh
#
# Needs: zig (cross-compiles all six platforms from one machine), python3,
# and node + npx for the VS Code package. Outputs:
# yapf-<platform>.zip command-line tool + shared/static library + header
# yapf_image-blender.zip Blender extension with libraries for all platforms
# yapf-preview.vsix VS Code extension
# yapf-figma.zip Figma plugin (import via manifest.json)
# yapf-photoshop.zip Photoshop scripts
# yapf-gimp-source.zip GIMP plugin source (build with gimptool)
set -euo pipefail
cd "$(dirname "$0")/.."
OUT=dist/release
LIBS=dist/libs
rm -rf "$OUT" "$LIBS"
mkdir -p "$OUT" "$LIBS"
command -v zig >/dev/null || { echo "zig not found: https://ziglang.org/download/" >&2; exit 1; }
export ZIG_GLOBAL_CACHE_DIR=${ZIG_GLOBAL_CACHE_DIR:-$PWD/dist/.zig-cache}
# zig target folder library tool
targets="x86_64-linux-gnu.2.17 linux-x64 libyapf.so yapf
aarch64-linux-gnu.2.17 linux-arm64 libyapf.so yapf
x86_64-windows-gnu windows-x64 yapf.dll yapf.exe
aarch64-windows-gnu windows-arm64 yapf.dll yapf.exe
x86_64-macos macos-x64 libyapf.dylib yapf
aarch64-macos macos-arm64 libyapf.dylib yapf"
echo "$targets" | while read -r tgt dir lib tool; do
pkg=dist/pkg/yapf-$dir
rm -rf "$pkg" && mkdir -p "$pkg" "$LIBS/$dir"
zig cc -target "$tgt" -O2 -std=c99 -shared -fPIC -s yapf.c -o "$LIBS/$dir/$lib"
zig cc -target "$tgt" -O2 -std=c99 -s tools/yapf_cli.c yapf.c -o "$pkg/$tool" -lm
zig cc -target "$tgt" -O2 -std=c99 -c yapf.c -o "$pkg/yapf.o"
zig ar rcs "$pkg/libyapf.a" "$pkg/yapf.o" && rm "$pkg/yapf.o"
cp "$LIBS/$dir/$lib" yapf.h LICENSE "$pkg/"
rm -f "$pkg"/*.lib "$pkg"/*.pdb "$LIBS/$dir"/*.lib "$LIBS/$dir"/*.pdb
(cd dist/pkg && python3 -m zipfile -c "../../$OUT/yapf-$dir.zip" "yapf-$dir")
echo "built yapf-$dir.zip"
done
python3 extensions/blender/build.py "$LIBS"
mv dist/yapf_image-blender.zip "$OUT/"
if command -v npx >/dev/null; then
(cd extensions/vscode && npm run --silent package && mv yapf-preview-*.vsix "../../$OUT/yapf-preview.vsix")
else
echo "node/npx not found: skipped the VS Code package" >&2
fi
(cd extensions/figma && node build.js 2>/dev/null || true)
python3 -m zipfile -c "$OUT/yapf-figma.zip" extensions/figma/manifest.json extensions/figma/code.js extensions/figma/ui.html
python3 -m zipfile -c "$OUT/yapf-photoshop.zip" extensions/photoshop
python3 -m zipfile -c "$OUT/yapf-gimp-source.zip" extensions/gimp yapf.c yapf.h
ls -la "$OUT"

345
tools/yapf_cli.c Normal file
View file

@ -0,0 +1,345 @@
/*
* yapf_cli.c — the `yapf` command-line tool.
*
* Copyright 2026 Nexoniarz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* -------------------------------------------------------------------------
*
* yapf <input> <output> [options] convert; formats chosen by extension
* yapf info <file.yapf> print header information
* yapf bench <file.yapf> time decoding
* yapf thumbnail <in.yapf> <out.png> [size] small preview (default 256)
*
* Reads PNG, JPEG, BMP, TGA, GIF (first frame), PSD (composite), PNM, HDR
* (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG.
*
* Options for writing YAPF:
* --mips also store a full mip chain (2×2 box filter)
* --linear mark pixels as linear instead of sRGB
* --premult mark alpha as premultiplied
* Options for reading YAPF:
* --mip N export mip level N instead of the base image
*
* Build: cc -O2 -std=c99 -Ithird_party tools/yapf_cli.c yapf.c -o yapf -lm -pthread
*/
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
# define _POSIX_C_SOURCE 200809L
#endif
#include "../yapf.h"
/* Third-party, public domain; keep their warnings out of our build. */
#if defined(__GNUC__) || defined(__clang__)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wunused-function"
# pragma GCC diagnostic ignored "-Wunused-but-set-variable"
# pragma GCC diagnostic ignored "-Wsign-compare"
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STBI_FAILURE_USERMSG
#include "../third_party/stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../third_party/stb_image_write.h"
#if defined(__GNUC__) || defined(__clang__)
# pragma GCC diagnostic pop
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
static int ends_with(const char *s, const char *ext) {
size_t n = strlen(s), m = strlen(ext);
if (n < m) return 0;
for (size_t i = 0; i < m; i++)
if (tolower((unsigned char)s[n - m + i]) != ext[i]) return 0;
return 1;
}
static int is_yapf(const char *path) { return ends_with(path, ".yapf"); }
static double now_s(void) {
#if defined(_WIN32)
return (double)clock() / CLOCKS_PER_SEC;
#else
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (double)t.tv_sec + (double)t.tv_nsec * 1e-9;
#endif
}
static uint8_t gpu_hint(int ch, int srgb) {
switch (ch) {
case 1: return YAPF_GPU_R8;
case 2: return YAPF_GPU_RG8;
case 3: return srgb ? YAPF_GPU_SRGB8 : YAPF_GPU_RGB8;
default: return srgb ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_RGBA8;
}
}
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes). */
static int 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 -1;
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 = img->width >> (m - 1), ph = img->height >> (m - 1);
uint32_t w = img->width >> m, h = img->height >> m;
if (pw < 1) pw = 1;
if (ph < 1) ph = 1;
if (w < 1) w = 1;
if (h < 1) h = 1;
uint8_t *src = img->mips[m - 1];
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
if (!dst) return -1;
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 = 2 * x < pw ? 2 * x : pw - 1, x1 = 2 * x + 1 < pw ? 2 * x + 1 : pw - 1;
uint32_t y0 = 2 * y < ph ? 2 * y : ph - 1, y1 = 2 * y + 1 < ph ? 2 * y + 1 : ph - 1;
unsigned s = 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)((s + 2) / 4);
}
img->mips[m] = dst;
}
return 0;
}
static int write_image(const char *path, const uint8_t *px, int w, int h, int ch) {
if (ends_with(path, ".png")) return stbi_write_png(path, w, h, ch, px, w * ch);
if (ends_with(path, ".bmp")) return stbi_write_bmp(path, w, h, ch, px);
if (ends_with(path, ".tga")) return stbi_write_tga(path, w, h, ch, px);
if (ends_with(path, ".jpg") || ends_with(path, ".jpeg"))
return stbi_write_jpg(path, w, h, ch, px, 95);
fprintf(stderr, "yapf: unsupported output format: %s (use .yapf .png .bmp .tga .jpg)\n", path);
return 0;
}
static const char *gpu_name(int g) {
static const char *n[] = { "RGBA8", "RGB8", "RG8", "R8", "SRGB8_A8", "SRGB8" };
return g >= 0 && g < 6 ? n[g] : "unknown";
}
static int cmd_info(const char *path) {
FILE *f = fopen(path, "rb");
uint8_t hdr[20];
if (!f || fread(hdr, 1, 20, f) != 20 || memcmp(hdr, "YAPF", 4) != 0) {
fprintf(stderr, "yapf: %s is not a YAPF file\n", path);
if (f) fclose(f);
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fclose(f);
unsigned w = hdr[12] | hdr[13] << 8 | (unsigned)hdr[14] << 16 | (unsigned)hdr[15] << 24;
unsigned h = hdr[16] | hdr[17] << 8 | (unsigned)hdr[18] << 16 | (unsigned)hdr[19] << 24;
printf("%s\n", path);
printf(" format version : %u\n", hdr[4]);
printf(" size : %u x %u, %u channel(s)\n", w, h, hdr[5]);
printf(" mip levels : %u\n", hdr[7]);
printf(" gpu format : %s\n", gpu_name(hdr[6]));
printf(" flags : %s%s\n", hdr[8] & YAPF_FLAG_SRGB ? "sRGB " : "linear ",
hdr[8] & YAPF_FLAG_PREMULT_ALPHA ? "premultiplied-alpha" : "");
printf(" file size : %ld bytes (%.1f%% of raw)\n", size,
100.0 * (double)size / ((double)w * h * (hdr[5] ? hdr[5] : 1)));
yapf_image_t *img = yapf_load(path);
printf(" decodes : %s\n", img ? "ok" : "FAILED (corrupt or unsupported)");
yapf_free(img);
return img ? 0 : 1;
}
static int cmd_bench(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); return 1; }
fseek(f, 0, SEEK_END);
long n = ftell(f);
rewind(f);
uint8_t *buf = (uint8_t *)malloc((size_t)n);
if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return 1; }
fclose(f);
yapf_image_t *img = yapf_load_memory(buf, (size_t)n);
if (!img) { fprintf(stderr, "yapf: %s does not decode\n", path); free(buf); return 1; }
double raw = (double)img->width * img->height * img->channels;
yapf_free(img);
for (int mt = 0; mt < 2; mt++) {
/* Best of 15 rounds; each round repeats enough decodes to last
* ~20 ms, so short images are measured warm and not timer-bound. */
double one = now_s();
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0) : yapf_load_memory(buf, (size_t)n));
one = now_s() - one;
int reps = (int)(0.02 / (one > 1e-6 ? one : 1e-6)) + 1;
double best = 1e9;
for (int r = 0; r < 15; r++) {
double t = now_s();
for (int k = 0; k < reps; k++)
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0)
: yapf_load_memory(buf, (size_t)n));
t = (now_s() - t) / reps;
if (t < best) best = t;
}
printf(" %-12s %8.3f ms %6.2f GB/s\n", mt ? "all cores" : "1 thread", best * 1e3, raw / best / 1e9);
}
free(buf);
return 0;
}
/* Preview no larger than size×size: start from the smallest mip that is
* still big enough, then average boxes of source pixels. */
static int cmd_thumbnail(const char *in, const char *out, int size) {
yapf_image_t *img = yapf_load_mt(in, 0);
if (!img) { fprintf(stderr, "yapf: cannot read %s\n", in); return 1; }
if (size < 1) size = 256;
int m = 0;
while (m + 1 < img->mip_levels &&
(int)(img->width >> (m + 1)) >= size && (int)(img->height >> (m + 1)) >= size)
m++;
uint32_t sw = img->width >> m ? img->width >> m : 1, sh = img->height >> m ? img->height >> m : 1;
const uint8_t *src = img->mips[m];
double k = (double)size / (double)(sw > sh ? sw : sh);
if (k > 1.0) k = 1.0;
uint32_t dw = (uint32_t)(sw * k + 0.5), dh = (uint32_t)(sh * k + 0.5);
if (dw < 1) dw = 1;
if (dh < 1) dh = 1;
int ch = img->channels;
uint8_t *dst = (uint8_t *)malloc((size_t)dw * dh * ch);
if (!dst) { yapf_free(img); return 1; }
for (uint32_t y = 0; y < dh; y++) {
uint32_t y0 = (uint32_t)((uint64_t)y * sh / dh), y1 = (uint32_t)((uint64_t)(y + 1) * sh / dh);
if (y1 <= y0) y1 = y0 + 1;
for (uint32_t x = 0; x < dw; x++) {
uint32_t x0 = (uint32_t)((uint64_t)x * sw / dw), x1 = (uint32_t)((uint64_t)(x + 1) * sw / dw);
if (x1 <= x0) x1 = x0 + 1;
for (int c = 0; c < ch; c++) {
uint64_t sum = 0;
for (uint32_t yy = y0; yy < y1; yy++)
for (uint32_t xx = x0; xx < x1; xx++)
sum += src[((size_t)yy * sw + xx) * ch + c];
uint64_t n = (uint64_t)(y1 - y0) * (x1 - x0);
dst[((size_t)y * dw + x) * ch + c] = (uint8_t)((sum + n / 2) / n);
}
}
}
int ok = stbi_write_png(out, (int)dw, (int)dh, ch, dst, (int)dw * ch);
free(dst);
yapf_free(img);
if (!ok) fprintf(stderr, "yapf: cannot write %s\n", out);
return ok ? 0 : 1;
}
static void usage(void) {
fprintf(stderr,
"usage: yapf <input> <output> [options] convert (formats by extension)\n"
" yapf info <file.yapf> show header information\n"
" yapf bench <file.yapf> time decoding\n"
" yapf thumbnail <in.yapf> <out.png> [size] preview, default 256 px\n"
"\n"
"reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n"
"writes: .yapf .png .jpg .bmp .tga\n"
"\n"
"options when writing .yapf:\n"
" --mips store a full mip chain\n"
" --linear pixels are linear, not sRGB\n"
" --premult alpha is premultiplied\n"
"options when reading .yapf:\n"
" --mip N export mip level N\n");
}
int main(int argc, char **argv) {
if (argc == 3 && strcmp(argv[1], "info") == 0) return cmd_info(argv[2]);
if (argc == 3 && strcmp(argv[1], "bench") == 0) return cmd_bench(argv[2]);
if ((argc == 4 || argc == 5) && strcmp(argv[1], "thumbnail") == 0)
return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256);
if (argc < 3) { usage(); return 2; }
const char *in = argv[1], *out = argv[2];
int mips = 0, srgb = 1, premult = 0, mip_out = 0;
for (int i = 3; i < argc; i++) {
if (strcmp(argv[i], "--mips") == 0) mips = 1;
else if (strcmp(argv[i], "--linear") == 0) srgb = 0;
else if (strcmp(argv[i], "--premult") == 0) premult = 1;
else if (strcmp(argv[i], "--mip") == 0 && i + 1 < argc) mip_out = atoi(argv[++i]);
else { usage(); return 2; }
}
/* Read */
yapf_image_t src;
yapf_image_t *loaded = NULL;
uint8_t *stb_px = NULL;
memset(&src, 0, sizeof(src));
if (is_yapf(in)) {
loaded = yapf_load_mt(in, 0);
if (!loaded) { fprintf(stderr, "yapf: cannot read %s (missing, corrupt or not YAPF v1)\n", in); return 1; }
src = *loaded;
} else {
int w, h, ch;
if (stbi_is_16_bit(in))
fprintf(stderr, "yapf: note: %s has 16-bit channels; YAPF stores 8 bits per channel\n", in);
stb_px = stbi_load(in, &w, &h, &ch, 0);
if (!stb_px) { fprintf(stderr, "yapf: cannot read %s: %s\n", in, stbi_failure_reason()); return 1; }
src.width = (uint32_t)w; src.height = (uint32_t)h; src.channels = (uint8_t)ch;
src.pixels = stb_px; src.mip_levels = 1;
src.flags = srgb ? YAPF_FLAG_SRGB : 0;
src.gpu_format = gpu_hint(ch, srgb);
}
/* Write */
int rc = 0;
if (is_yapf(out)) {
yapf_image_t dst = src;
if (!loaded) {
if (premult) dst.flags |= YAPF_FLAG_PREMULT_ALPHA;
}
dst.mips = NULL;
dst.mip_levels = 1;
if (mips && build_mips(&dst) != 0) { fprintf(stderr, "yapf: out of memory\n"); rc = 1; }
if (!rc && yapf_save(out, &dst) != YAPF_OK) { fprintf(stderr, "yapf: cannot write %s\n", out); rc = 1; }
if (dst.mips) {
for (int m = 1; m < dst.mip_levels; m++) free(dst.mips[m]);
free(dst.mips);
}
} else {
const uint8_t *px = src.pixels;
uint32_t w = src.width, h = src.height;
if (mip_out > 0) {
if (!loaded || mip_out >= src.mip_levels) {
fprintf(stderr, "yapf: %s has no mip level %d\n", in, mip_out);
rc = 1;
} else {
px = src.mips[mip_out];
w = src.width >> mip_out ? src.width >> mip_out : 1;
h = src.height >> mip_out ? src.height >> mip_out : 1;
}
}
if (!rc && !write_image(out, px, (int)w, (int)h, src.channels)) {
fprintf(stderr, "yapf: cannot write %s\n", out);
rc = 1;
}
}
yapf_free(loaded);
stbi_image_free(stb_px);
return rc;
}

1644
yapf.c

File diff suppressed because it is too large Load diff

131
yapf.h
View file

@ -20,21 +20,30 @@
* *
* YAPF — lossless, GPU-ready image format. * YAPF — lossless, GPU-ready image format.
* *
* Space savings come from two cheap mathematical steps — no entropy coder, * Designed for single-core decode speed. Everything is byte aligned:
* no lookup tables, no opcode dispatch. Decode is straight arithmetic: * no bit reader, no entropy coder, no context model. Per row the decoder
* dispatches one filter; per 8 samples it reads a 4-bit control code and
* exactly w bytes of payload, unpacked with one 64-bit load and shifts.
* The encoder does the hard work (filter search, colour transform choice,
* optimal control codes); the decoder only follows instructions.
* *
* 1. YCoCg-R color transform (RGB↔Y/Co/Cg, fully reversible integers) * 1. Per tile: YCoCg-R or subtract-green colour transform, 8-bit modular
* 2. 2-D neighbor prediction + per-tile adaptive bit-packing * 2. Per band of 8 rows: one of 12 row filters
* 3. Residuals in groups of 8, each group at its bit width (0–8);
* 4-bit control codes, with codes that repeat the previous width
* 4. Independent 64×64 tiles with a size table → random access and
* optional parallel decode
* *
* The file carries GPU metadata (format hint, mip levels, flags) so a * The file carries GPU metadata (format hint, mip levels, flags) so a
* loader can upload the decoded buffer straight to a texture without any * loader can upload the decoded buffer straight to a texture without any
* additional transcoding step. * additional transcoding step.
* *
* ========================================================================= * =========================================================================
* FILE FORMAT (version 2) * FILE FORMAT (version 1)
* ========================================================================= * =========================================================================
* *
* All multi-byte integers are little-endian. * All multi-byte integers are little-endian. All arithmetic on samples
* is mod 256.
* *
* ┌─────────────────────────────────────────────────────────────────────┐ * ┌─────────────────────────────────────────────────────────────────────┐
* │ HEADER — 20 bytes │ * │ HEADER — 20 bytes │
@ -42,7 +51,7 @@
* │ Offset │ Size │ Field │ * │ Offset │ Size │ Field │
* ├────────┼──────┼────────────────────────────────────────────────────┤ * ├────────┼──────┼────────────────────────────────────────────────────┤
* │ 0x00 │ 4 │ Magic: "YAPF" │ * │ 0x00 │ 4 │ Magic: "YAPF" │
* │ 0x04 │ 1 │ Version: 2 │ * │ 0x04 │ 1 │ Version: 1 │
* │ 0x05 │ 1 │ Channels: 1 gray / 2 gray+A / 3 RGB / 4 RGBA │ * │ 0x05 │ 1 │ Channels: 1 gray / 2 gray+A / 3 RGB / 4 RGBA │
* │ 0x06 │ 1 │ GPU format hint (YAPF_GPU_* constant) │ * │ 0x06 │ 1 │ GPU format hint (YAPF_GPU_* constant) │
* │ 0x07 │ 1 │ Mip levels (1 = base only, max 16) │ * │ 0x07 │ 1 │ Mip levels (1 = base only, max 16) │
@ -58,53 +67,64 @@
* │ +0 │ 4 │ Byte offset of mip data from start of file (uint32) │ * │ +0 │ 4 │ Byte offset of mip data from start of file (uint32) │
* │ +4 │ 4 │ Byte size of mip data (uint32) │ * │ +4 │ 4 │ Byte size of mip data (uint32) │
* └────────┴──────┴────────────────────────────────────────────────────┘ * └────────┴──────┴────────────────────────────────────────────────────┘
* Mip m has size max(1, width >> m) × max(1, height >> m).
* *
* ┌─────────────────────────────────────────────────────────────────────┐ * ┌─────────────────────────────────────────────────────────────────────┐
* │ PIXEL DATA — per mip level, referenced by the mip index │ * │ MIP LEVEL DATA │
* ├─────────────────────────────────────────────────────────────────────┤ * ├─────────────────────────────────────────────────────────────────────┤
* │ Pixels are divided into 32×32 tiles, stored in row-major order. │ * │ Pixels are divided into 64×64 tiles in row-major order; the last │
* │ The last column / row of tiles may be smaller (no padding). │ * │ column / row of tiles may be smaller (no padding). │
* │ │ * │ │
* │ Encoding pipeline (encoder): │ * │ uint32 tile_size[tile_count] byte size of each tile │
* │ a) RGB→YCoCg-R (skipped for gray/gray-alpha channels) │ * │ tile data, back to back │
* │ b) 2-D prediction residuals (see below) │
* │ c) Determine per-channel bit depth from max |residual| in tile │
* │ d) Write tile header + packed residuals │
* │ │ * │ │
* │ Tile layout: │ * │ TILE │
* │ [bit_depth per channel] 1 byte each, channels bytes total │ * │ 1 byte header │
* │ [first pixel, raw] 1 byte per channel │ * │ 0x80 stored: tile_w × tile_h × channels raw bytes │
* │ [packed residuals] bit_depth[c] bits per sample, │ * │ follow, interleaved, in the original channel order │
* │ stored MSB-first, zero-padded to the │ * │ otherwise coded: │
* │ next byte boundary at the end of tile │ * │ bits 0–3 bit c set = plane c is constant (only bits below │
* │ the channel count may be set) │
* │ bit 4 colour transform: 0 = YCoCg-R, 1 = subtract-green │
* │ (RGB / RGBA only; must be 0 for gray) │
* │ bits 5–7 must be 0 │
* │ coded tiles then hold one value byte per constant plane, and a │
* │ plane payload per other plane, both in plane order. The tile │
* │ must end exactly where its last payload ends. │
* │ │ * │ │
* │ Prediction (lossless 2-D): │ * │ PLANES │
* │ pixel (0,0) in tile → stored raw, no residual │ * │ gray / gray+A: the channels as stored. │
* │ pixel (x,0) → predict = left │ * │ RGB(A), YCoCg-R (>> is an arithmetic shift of the value as int8):│
* │ pixel (0,y) → predict = above │ * │ planes Y, Co, Cg (+ A) │
* │ pixel (x,y) → predict = left + above − diag │ * │ forward: o = R − B; t = B + (o >> 1); q = G − t; │
* │ residual = (actual − predict) wrapped mod 256, stored signed │ * │ Y = t + (q >> 1); Co = o + 128; Cg = q + 128 │
* │ inverse: o = Co − 128; q = Cg − 128; t = Y − (q >> 1); │
* │ G = q + t; B = t − (o >> 1); R = B + o │
* │ RGB(A), subtract-green: │
* │ planes G, R − G + 128, B − G + 128 (+ A) │
* │ │ * │ │
* │ Bit packing: │ * │ PLANE PAYLOAD │
* │ bits_needed = bit_depth[c] (0 means all residuals are 0) │ * │ filter[ceil(tile_h / 8)] one byte per band of 8 rows. │
* │ Each residual is stored as a (bits_needed)-bit two's-complement │ * │ L / A / D / AR = left, above, above-left, above-right sample; │
* │ signed integer, MSB first, packed across byte boundaries. │ * │ samples outside the tile read as 0, except that AR in the │
* │ bit_depth 1 → only the sign bit (value is −1 or 0) │ * │ last column reads as A. │
* │ bit_depth 0 → no bits written; all samples in channel = 0 │ * │ 0 Left pred = L │
* │ │ * │ 1 Gradient pred = L + A − D │
* │ YCoCg-R forward transform (applied before prediction): │ * │ 2 Up-right pred = AR │
* │ Co = R − B │ * │ 3 Up-average pred = (A + AR) >> 1 (not mod 256) │
* │ t = B + (Co >> 1) │ * │ 16 + k−1 Up k pred = sample k rows above, k = 1..8 │
* │ Cg = G − t │ * │ control nibbles, low nibble of each byte first, covering │
* │ Y = t + (Cg >> 1) │ * │ exactly ceil(tile_w × tile_h / 8) groups of 8 consecutive │
* │ Stored channel order: Y, Co, Cg (replaces R, G, B) │ * │ residuals in raster order: │
* │ Alpha is left unchanged. │ * │ 0–8 one group of width w │
* │ │ * │ 9–15 2, 3, 4, 8, 16, 32 or 64 groups at the width of │
* │ YCoCg-R inverse transform (applied after prediction): │ * │ the last 0–8 code in this plane (0 if none yet) │
* │ t = Y − (Cg >> 1) │ * │ an odd nibble count is padded with a zero high nibble │
* │ G = Cg + t │ * │ group data: w bytes per group, the 8 values packed little- │
* │ B = t − (Co >> 1) │ * │ endian, value i in bits [i·w, i·w + w) │
* │ R = B + Co │ * │ Residual r = (sample − pred) mod 256 is stored folded: │
* │ v = (r << 1) ^ (r >> 7) (0, −1, 1, −2, … → 0, 1, 2, 3, …) │
* │ The last group is padded with zero values. │
* └─────────────────────────────────────────────────────────────────────┘ * └─────────────────────────────────────────────────────────────────────┘
* *
* GPU FORMAT HINTS (YAPF_GPU_* values for the gpu_format field) * GPU FORMAT HINTS (YAPF_GPU_* values for the gpu_format field)
@ -161,7 +181,7 @@
#define YAPF_MAX_DIM 65535u #define YAPF_MAX_DIM 65535u
#define YAPF_MAX_MIPS 16u #define YAPF_MAX_MIPS 16u
#define YAPF_TILE_SIZE 32u #define YAPF_TILE_SIZE 64u
/* ── Image descriptor ────────────────────────────────────────────────── */ /* ── Image descriptor ────────────────────────────────────────────────── */
@ -189,6 +209,21 @@ extern "C" {
*/ */
yapf_image_t *yapf_load(const char *filename); yapf_image_t *yapf_load(const char *filename);
/*
* yapf_load_memory — decode a .yapf file already in memory (e.g. from a
* game archive). Same ownership and failure rules as yapf_load().
*/
yapf_image_t *yapf_load_memory(const void *buffer, size_t size);
/*
* yapf_load_mt / yapf_load_memory_mt — same as above, but decode tiles on
* up to `threads` threads (0 = one per CPU core). Output is identical.
* Without thread support (YAPF_NO_THREADS, or an unknown platform) these
* decode on the calling thread.
*/
yapf_image_t *yapf_load_mt(const char *filename, int threads);
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
/* /*
* yapf_save — encode an image to disk. * yapf_save — encode an image to disk.
* img->mip_levels controls how many levels are written; set to 1 to write * img->mip_levels controls how many levels are written; set to 1 to write