Merge YAPF v1: fast format, SIMD, tools, plugins and language bindings

YAPF v1: fast byte-aligned format, SIMD, tools and plugins
This commit is contained in:
Nexoniarz 2026-10-03 14:58:59 +02:00 • committed by GitHub
commit 5642b8c85d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 16727 additions and 712 deletions

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# 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
# Rust
/bindings/rust/target/
/bindings/rust/Cargo.lock
# C# / Go / Java build outputs
bin/
obj/
*.class

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

400
README.md
View file

@ -1,90 +1,137 @@
# 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`.
- **5–12× faster decoding than PNG** (stb_image), single-threaded; a 4K image
decodes in about 6–7 ms (around 5 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 (compiles as C or C++) | [below](#c--c) | ✅ tested — see [platforms](#platforms) |
systems) this is a meaningful bottleneck. | **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `compare`, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux and Windows |
| **Your language** | Python, C#, Rust, Go, Java, Zig, Lua, JavaScript / TypeScript, … | [docs/LANGUAGES.md](docs/LANGUAGES.md) | ✅ every binding tested |
| **GIMP 3** | open, export (flattened, optional 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 |
| **KDE Dolphin** | thumbnails for `.yapf` files | [extensions/kde](extensions/kde) | ⚠️ tested through KDE's plugin loader (KF6); not yet inside a running Dolphin |
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnail command tested; not yet in a file manager |
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ⚠️ packaged; preview code tested in a browser engine (Chromium); **not yet run inside VS Code** |
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ⚠️ plugin UI tested in a browser engine with a simulated Figma; **not yet run inside Figma** |
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ **syntax-checked only; not yet run in Photoshop** |
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 ### Platforms
Benchmark: loading a 5K professional photograph (5120 × 2880 px). | Platform | Status |
| :------- | :----- |
| Format | Load time | | Linux x64 | ✅ built and fully tested |
| :----- | ----------: | | Windows x64 | ✅ built and fully tested (MinGW and Zig builds, run under Wine) |
| **YAPF** | **0.111 s** | | Linux ARM64 | ✅ built and fully tested (NEON, run under qemu) |
| PNG | 0.194 s | | Windows ARM64 | ⚠️ built only, not run |
| macOS x64 / ARM64 | ⚠️ **cross-compiled only, not yet run on a Mac** |
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
yapf compare photo.png ui.png # size and decode speed vs the original format
```
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
`--premult` (alpha is premultiplied).
---
## Benchmark
`yapf compare` on a mixed set of images: one binary (`make`, gcc -O2) for
both formats, PNG decoded with stb_image, one core, AMD Ryzen 7 5700G.
Every result is checked to decode back to the exact original pixels.
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
Smaller than PNG on UI-style images, 20–50 % larger on photos and textures
(the price of having no entropy coder), and 5–12× faster to decode. More
in [examples/](examples) — and run `yapf compare` on your own images.
---
## 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
@ -92,15 +139,20 @@ gcc -O2 -std=c99 main.c yapf.c -o myapp
```c ```c
typedef struct { 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 +165,47 @@ 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
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
void yapf_free_buffer(void *data);
```
Encodes into memory: `*out_data` receives a complete `.yapf` file of
`*out_size` bytes, to be released with `yapf_free_buffer()`.
```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 +218,109 @@ 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: 786 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:
``` - exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
dist/ single- and multi-threaded decoding, in-memory and file encoding;
├── windows-x64/ yapf.dll yapf.lib - the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux
├── windows-arm64/ yapf.dll yapf.lib ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as
├── macos-x64/ libyapf.dylib C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
├── macos-arm64/ libyapf.dylib ThreadSanitizer;
├── linux-x64/ libyapf.so libyapf.a - thousands of corrupted and truncated files are rejected without crashing;
└── linux-arm64/ libyapf.so libyapf.a - every language binding runs its example; JavaScript, Python, C++, C#,
``` Rust, Go, Java and Lua re-encode the sample to byte-identical output;
- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1;
- the KDE thumbnailer through KDE's own plugin loader.
### Optional build flags **Not yet verified:** running on a Mac (macOS builds are cross-compiled
only), Windows on ARM, and running inside Photoshop, VS Code, Figma or a
| Flag | Effect | live Dolphin / GNOME file manager.
| :---------------- | :-------------------------------------------------- |
| `-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
examples/ speed and size comparison, C example
docs/LANGUAGES.md using YAPF from other languages
bindings/ cpp, python, csharp, rust, go, java, zig, lua, js
extensions/gimp/ GIMP 3 plugin
extensions/blender/ Blender extension
extensions/kde/ Dolphin thumbnail plugin (KF6)
extensions/linux/ MIME type and freedesktop thumbnailer
extensions/vscode/ VS Code preview
extensions/figma/ Figma plugin
extensions/photoshop/ Photoshop scripts
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).

18
bindings/cpp/README.md Normal file
View file

@ -0,0 +1,18 @@
# YAPF for C++
`yapf.hpp` is a header-only C++17 wrapper: RAII, exceptions, `std::vector`.
Add `../../yapf.c` to your project and include the header.
```cpp
#include "yapf.hpp"
yapf::Image img = yapf::Image::load("texture.yapf"); // or Image::decode(bytes)
upload(img.width(), img.height(), img.channels(), img.pixels());
std::vector<uint8_t> file = yapf::Image(256, 256, 4, rgba).encode();
```
`yapf.c` compiles as C or as C++, so you can simply add it to a C++ project
(Visual Studio, CMake, …).
Example: `g++ -O2 -std=c++17 example.cpp -x c++ ../../yapf.c -o example -pthread && ./example`

33
bindings/cpp/example.cpp Normal file
View file

@ -0,0 +1,33 @@
// g++ -O2 -std=c++17 example.cpp ../../yapf.c -o example -pthread && ./example ../../other/YAPF.YAPF
#include "yapf.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <fstream>
#include <iterator>
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1] : "../../other/YAPF.YAPF";
std::ifstream f(path, std::ios::binary);
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(f)), {});
yapf::Image img = yapf::Image::decode(bytes);
double ms = 1e9; // best of 10, as in a running program
for (int i = 0; i < 10; i++) {
auto t0 = std::chrono::steady_clock::now();
img = yapf::Image::decode(bytes);
ms = std::min(ms, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
}
std::printf("%s: %ux%u, %u channels, decoded in %.2f ms, file is %.1f%% of raw\n", path,
img.width(), img.height(), img.channels(), ms, 100.0 * bytes.size() / img.size());
std::vector<uint8_t> again = img.encode();
std::printf("re-encoded: %zu bytes, identical to the file: %s\n", again.size(),
again == bytes ? "yes" : "no");
std::vector<uint8_t> gray(64 * 64);
for (size_t i = 0; i < gray.size(); i++) gray[i] = uint8_t(i % 64 * 4);
yapf::Image(64, 64, 1, gray.data()).save("gray.yapf");
std::printf("wrote gray.yapf\n");
}

98
bindings/cpp/yapf.hpp Normal file
View file

@ -0,0 +1,98 @@
// yapf.hpp — header-only C++17 wrapper for YAPF. Copyright 2026 Nexoniarz — Apache 2.0.
//
// #include "yapf.hpp" // and compile/link ../../yapf.c
// yapf::Image img = yapf::Image::load("texture.yapf");
// img.width(); img.height(); img.channels(); img.pixels(); // span of bytes
// std::vector<uint8_t> file = img.encode();
// yapf::Image mine(256, 256, 4, my_rgba_bytes); mine.save("out.yapf");
#pragma once
extern "C" {
#include "../../yapf.h"
}
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace yapf {
struct Error : std::runtime_error {
using std::runtime_error::runtime_error;
};
class Image {
public:
/* Decode a file or a memory buffer; threads = 0 uses every core, 1 the calling thread. */
static Image load(const std::string &path, int threads = 1) {
yapf_image_t *p = yapf_load_mt(path.c_str(), threads);
if (!p) throw Error("YAPF: cannot load " + path);
return Image(p);
}
static Image decode(const void *data, size_t size, int threads = 1) {
yapf_image_t *p = yapf_load_memory_mt(data, size, threads);
if (!p) throw Error("YAPF: invalid or corrupt data");
return Image(p);
}
static Image decode(const std::vector<uint8_t> &bytes, int threads = 1) {
return decode(bytes.data(), bytes.size(), threads);
}
/* A new image from your pixels (copied). channels: 1 gray, 2 gray+A, 3 RGB, 4 RGBA. */
Image(uint32_t width, uint32_t height, uint8_t channels, const uint8_t *pixels,
uint8_t flags = YAPF_FLAG_SRGB)
: own_(pixels, pixels + size_t(width) * height * channels) {
std::memset(&view_, 0, sizeof(view_));
view_.width = width; view_.height = height; view_.channels = channels;
view_.flags = flags; view_.mip_levels = 1;
view_.gpu_format = channels == 4 ? YAPF_GPU_SRGB8_A8 : channels == 3 ? YAPF_GPU_SRGB8
: channels == 2 ? YAPF_GPU_RG8 : YAPF_GPU_R8;
view_.pixels = own_.data();
}
Image(Image &&o) noexcept : img_(std::exchange(o.img_, nullptr)), own_(std::move(o.own_)), view_(o.view_) {
if (!img_) view_.pixels = own_.data();
}
Image &operator=(Image &&o) noexcept {
if (this != &o) { yapf_free(img_); img_ = std::exchange(o.img_, nullptr); own_ = std::move(o.own_);
view_ = o.view_; if (!img_) view_.pixels = own_.data(); }
return *this;
}
Image(const Image &) = delete;
Image &operator=(const Image &) = delete;
~Image() { yapf_free(img_); }
uint32_t width() const { return raw().width; }
uint32_t height() const { return raw().height; }
uint8_t channels() const { return raw().channels; }
uint8_t flags() const { return raw().flags; }
uint8_t mipLevels() const { return raw().mip_levels; }
const uint8_t *pixels() const { return raw().pixels; }
size_t size() const { return size_t(width()) * height() * channels(); }
const uint8_t *mip(int level) const { return raw().mips ? raw().mips[level] : raw().pixels; }
std::vector<uint8_t> encode() const {
void *data = nullptr;
size_t n = 0;
if (yapf_encode(&raw(), &data, &n) != YAPF_OK) throw Error("YAPF: encoding failed");
std::vector<uint8_t> out(static_cast<uint8_t *>(data), static_cast<uint8_t *>(data) + n);
yapf_free_buffer(data);
return out;
}
void save(const std::string &path) const {
if (yapf_save(path.c_str(), &raw()) != YAPF_OK) throw Error("YAPF: cannot write " + path);
}
const yapf_image_t &raw() const { return img_ ? *img_ : view_; }
private:
explicit Image(yapf_image_t *p) : img_(p) { std::memset(&view_, 0, sizeof(view_)); }
yapf_image_t *img_ = nullptr; // decoded by the library
std::vector<uint8_t> own_; // or pixels we own
yapf_image_t view_{};
};
} // namespace yapf

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Include="../Yapf.cs" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,25 @@
// dotnet run -- ../../../other/YAPF.YAPF (with yapf.dll / libyapf.so on the library path)
using System.Diagnostics;
using Yapf;
string path = args.Length > 0 ? args[0] : "../../../other/YAPF.YAPF";
byte[] file = File.ReadAllBytes(path);
var img = YapfImage.Decode(file); // first call also loads the library / JIT
double ms = double.MaxValue; // best of 10, as in a running program
for (int i = 0; i < 10; i++)
{
var sw = Stopwatch.StartNew();
img = YapfImage.Decode(file);
ms = Math.Min(ms, sw.Elapsed.TotalMilliseconds);
}
Console.WriteLine($"{path}: {img.Width}x{img.Height}, {img.Channels} channels, decoded in {ms:F2} ms");
Console.WriteLine($"file is {file.Length} bytes, {100.0 * file.Length / img.Pixels.Length:F1}% of the raw pixels");
byte[] again = img.Encode();
Console.WriteLine($"re-encoded: {again.Length} bytes, identical: {again.AsSpan().SequenceEqual(file)}");
var rgba = new byte[64 * 64 * 4];
for (int i = 0; i < rgba.Length; i += 4) { rgba[i] = (byte)(i / 4 % 64 * 4); rgba[i + 1] = 100; rgba[i + 2] = 200; rgba[i + 3] = 255; }
new YapfImage(64, 64, 4, rgba).Save("csharp.yapf");
Console.WriteLine("wrote csharp.yapf");

19
bindings/csharp/README.md Normal file
View file

@ -0,0 +1,19 @@
# YAPF for C# / .NET
`Yapf.cs` is a single-file P/Invoke wrapper for .NET 6 or later (tested with
.NET 8). It should also work in Unity with the native library in
`Assets/Plugins`, but that is not tested yet. Add it to your project and put
the native library next to your program: `yapf.dll` (Windows),
`libyapf.so` (Linux) or `libyapf.dylib` (macOS) — from the
[Releases](https://github.com/Nexoniarz/YAPF/releases) or `make`.
```csharp
using Yapf;
var img = YapfImage.Load("texture.yapf"); // or YapfImage.Decode(bytes)
byte[] pixels = img.Pixels; // Width * Height * Channels
new YapfImage(256, 256, 4, rgba).Save("out.yapf"); // or .Encode() → byte[]
```
Example: `cd Example && dotnet run -- ../../../other/YAPF.YAPF`
(Linux: `LD_LIBRARY_PATH=../../../build dotnet run …` after `make`).

121
bindings/csharp/Yapf.cs Normal file
View file

@ -0,0 +1,121 @@
// Yapf.cs — C# bindings for YAPF (P/Invoke). Copyright 2026 Nexoniarz — Apache 2.0.
//
// Needs the native library next to your program or on the library path:
// yapf.dll (Windows), libyapf.so (Linux), libyapf.dylib (macOS).
//
// var img = YapfImage.Load("texture.yapf");
// byte[] pixels = img.Pixels; // Width * Height * Channels, rows top to bottom
// byte[] file = new YapfImage(256, 256, 4, myRgba).Encode();
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Yapf
{
public sealed class YapfException : Exception
{
public YapfException(string message) : base(message) { }
}
public sealed class YapfImage
{
public const byte FlagPremultipliedAlpha = 1, FlagSrgb = 2;
public int Width { get; }
public int Height { get; }
public int Channels { get; } // 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA
public byte Flags { get; }
public byte GpuFormat { get; }
public byte[] Pixels => Mips[0];
public byte[][] Mips { get; } // Mips[0] is the full-size image
public YapfImage(int width, int height, int channels, byte[] pixels,
byte flags = FlagSrgb, byte[][]? mips = null)
{
if (pixels.Length != width * height * channels)
throw new ArgumentException("pixels must hold width * height * channels bytes");
Width = width; Height = height; Channels = channels; Flags = flags;
GpuFormat = channels switch { 4 => (byte)4, 3 => (byte)5, 2 => (byte)2, _ => (byte)3 };
Mips = mips ?? new[] { pixels };
Mips[0] = pixels;
}
private YapfImage(Native n)
{
Width = (int)n.width; Height = (int)n.height; Channels = n.channels;
Flags = n.flags; GpuFormat = n.gpu_format;
Mips = new byte[n.mip_levels][];
for (int m = 0; m < n.mip_levels; m++)
{
int w = Math.Max(1, Width >> m), h = Math.Max(1, Height >> m);
Mips[m] = new byte[w * h * Channels];
Marshal.Copy(Marshal.ReadIntPtr(n.mips, m * IntPtr.Size), Mips[m], 0, Mips[m].Length);
}
}
/// <summary>Decode YAPF bytes. threads: 1 = calling thread, 0 = all cores.</summary>
public static YapfImage Decode(byte[] data, int threads = 1)
{
IntPtr p = NativeMethods.yapf_load_memory_mt(data, (UIntPtr)data.Length, threads);
if (p == IntPtr.Zero) throw new YapfException("not a valid YAPF file");
try { return new YapfImage(Marshal.PtrToStructure<Native>(p)); }
finally { NativeMethods.yapf_free(p); }
}
public static YapfImage Load(string path, int threads = 1) => Decode(File.ReadAllBytes(path), threads);
/// <summary>Encode to the bytes of a .yapf file.</summary>
public byte[] Encode()
{
var handles = new GCHandle[Mips.Length];
IntPtr table = Marshal.AllocHGlobal(IntPtr.Size * Mips.Length);
try
{
for (int m = 0; m < Mips.Length; m++)
{
handles[m] = GCHandle.Alloc(Mips[m], GCHandleType.Pinned);
Marshal.WriteIntPtr(table, m * IntPtr.Size, handles[m].AddrOfPinnedObject());
}
var n = new Native
{
width = (uint)Width, height = (uint)Height, channels = (byte)Channels,
gpu_format = GpuFormat, flags = Flags, mip_levels = (byte)Mips.Length,
pixels = handles[0].AddrOfPinnedObject(), mips = table,
};
int rc = NativeMethods.yapf_encode(ref n, out IntPtr data, out UIntPtr size);
if (rc != 0) throw new YapfException($"yapf_encode failed ({rc})");
try
{
var bytes = new byte[(int)size];
Marshal.Copy(data, bytes, 0, bytes.Length);
return bytes;
}
finally { NativeMethods.yapf_free_buffer(data); }
}
finally
{
foreach (var h in handles) if (h.IsAllocated) h.Free();
Marshal.FreeHGlobal(table);
}
}
public void Save(string path) => File.WriteAllBytes(path, Encode());
[StructLayout(LayoutKind.Sequential)]
private struct Native
{
public uint width, height;
public byte channels, gpu_format, flags, mip_levels;
public IntPtr pixels, mips;
}
private static class NativeMethods
{
private const string Lib = "yapf";
[DllImport(Lib)] public static extern IntPtr yapf_load_memory_mt(byte[] data, UIntPtr size, int threads);
[DllImport(Lib)] public static extern void yapf_free(IntPtr img);
[DllImport(Lib)] public static extern int yapf_encode(ref Native img, out IntPtr data, out UIntPtr size);
[DllImport(Lib)] public static extern void yapf_free_buffer(IntPtr data);
}
}
}

17
bindings/go/README.md Normal file
View file

@ -0,0 +1,17 @@
# YAPF for Go
A cgo package that builds the C library itself (needs a C compiler, as any
cgo package).
```sh
go get github.com/Nexoniarz/YAPF/bindings/go
```
```go
import yapf "github.com/Nexoniarz/YAPF/bindings/go"
img, err := yapf.Load("texture.yapf") // or yapf.Decode(bytes)
data, err := yapf.Encode(&yapf.Image{Width: 256, Height: 256, Channels: 4, Pixels: rgba})
```
Example: `go run ./example ../../other/YAPF.YAPF` · tests: `go test`.

View file

@ -0,0 +1,47 @@
// go run ./example ../../other/YAPF.YAPF
package main
import (
"bytes"
"fmt"
"os"
"time"
yapf "github.com/Nexoniarz/YAPF/bindings/go"
)
func main() {
path := "../../other/YAPF.YAPF"
if len(os.Args) > 1 {
path = os.Args[1]
}
file, err := os.ReadFile(path)
if err != nil {
panic(err)
}
img, err := yapf.Decode(file)
if err != nil {
panic(err)
}
best := time.Hour // best of 10, as in a running program
for i := 0; i < 10; i++ {
t := time.Now()
img, _ = yapf.Decode(file)
best = min(best, time.Since(t))
}
fmt.Printf("%s: %dx%d, %d channels, decoded in %.2f ms\n", path, img.Width, img.Height,
img.Channels, float64(best.Microseconds())/1000)
fmt.Printf("file is %d bytes, %.1f%% of the raw pixels\n", len(file), 100*float64(len(file))/float64(len(img.Pixels)))
again, _ := yapf.Encode(img)
fmt.Printf("re-encoded: %d bytes, identical: %v\n", len(again), bytes.Equal(again, file))
px := make([]byte, 128*128)
for i := range px {
px[i] = byte(i % 128 * 2)
}
if err := yapf.Save("go.yapf", &yapf.Image{Width: 128, Height: 128, Channels: 1, Flags: yapf.FlagSRGB, Pixels: px}); err != nil {
panic(err)
}
fmt.Println("wrote go.yapf")
}

3
bindings/go/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/Nexoniarz/YAPF/bindings/go
go 1.21

111
bindings/go/yapf.go Normal file
View file

@ -0,0 +1,111 @@
// Package yapf reads and writes YAPF lossless images (cgo bindings to the C library).
//
// img, err := yapf.Load("texture.yapf")
// data, err := yapf.Encode(&yapf.Image{Width: 256, Height: 256, Channels: 4, Pixels: rgba})
package yapf
/*
#cgo CFLAGS: -O2 -std=c99 -I${SRCDIR}/../..
#cgo !windows LDFLAGS: -lm -lpthread
#include <stdlib.h>
#include "yapf.h"
*/
import "C"
import (
"errors"
"os"
"unsafe"
)
const (
FlagPremultAlpha = 1
FlagSRGB = 2
)
var ErrInvalid = errors.New("yapf: not a valid YAPF file")
// Image holds Width*Height*Channels bytes in Pixels, rows top to bottom.
// Channels: 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA. Mips holds the smaller
// mip levels (1, 2, …), if any.
type Image struct {
Width, Height int
Channels int
Flags uint8
GPUFormat uint8
Pixels []byte
Mips [][]byte
}
// Decode decodes YAPF bytes on the calling thread; DecodeThreads(data, 0) uses every core.
func Decode(data []byte) (*Image, error) { return DecodeThreads(data, 1) }
func DecodeThreads(data []byte, threads int) (*Image, error) {
if len(data) == 0 {
return nil, ErrInvalid
}
p := C.yapf_load_memory_mt(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(threads))
if p == nil {
return nil, ErrInvalid
}
defer C.yapf_free(p)
img := &Image{Width: int(p.width), Height: int(p.height), Channels: int(p.channels),
Flags: uint8(p.flags), GPUFormat: uint8(p.gpu_format)}
mips := unsafe.Slice(p.mips, int(p.mip_levels))
for m := range mips {
w, h := max(1, img.Width>>m), max(1, img.Height>>m)
level := C.GoBytes(unsafe.Pointer(mips[m]), C.int(w*h*img.Channels))
if m == 0 {
img.Pixels = level
} else {
img.Mips = append(img.Mips, level)
}
}
return img, nil
}
func Load(path string) (*Image, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return Decode(data)
}
// Encode returns the bytes of a .yapf file.
func Encode(img *Image) ([]byte, error) {
if img.Channels < 1 || img.Channels > 4 || len(img.Pixels) != img.Width*img.Height*img.Channels {
return nil, errors.New("yapf: pixels must hold Width*Height*Channels bytes")
}
// Copy the levels into C memory: cgo forbids passing Go pointers inside C structs.
levels := append([][]byte{img.Pixels}, img.Mips...)
table := (**C.uint8_t)(C.malloc(C.size_t(len(levels)) * C.size_t(unsafe.Sizeof(uintptr(0)))))
defer C.free(unsafe.Pointer(table))
ptrs := unsafe.Slice(table, len(levels))
for i, l := range levels {
ptrs[i] = (*C.uint8_t)(C.CBytes(l))
defer C.free(unsafe.Pointer(ptrs[i]))
}
gpu := img.GPUFormat
if gpu == 0 {
gpu = map[int]uint8{4: 4, 3: 5, 2: 2, 1: 3}[img.Channels]
}
raw := C.yapf_image_t{width: C.uint32_t(img.Width), height: C.uint32_t(img.Height),
channels: C.uint8_t(img.Channels), gpu_format: C.uint8_t(gpu), flags: C.uint8_t(img.Flags),
mip_levels: C.uint8_t(len(levels)), pixels: ptrs[0], mips: table}
var data unsafe.Pointer
var size C.size_t
if rc := C.yapf_encode(&raw, &data, &size); rc != 0 {
return nil, errors.New("yapf: encoding failed")
}
defer C.yapf_free_buffer(data)
return C.GoBytes(data, C.int(size)), nil
}
func Save(path string, img *Image) error {
data, err := Encode(img)
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}

2
bindings/go/yapf_c.c Normal file
View file

@ -0,0 +1,2 @@
// Builds the C library as part of this Go package (cgo compiles .c files here).
#include "../../yapf.c"

46
bindings/go/yapf_test.go Normal file
View file

@ -0,0 +1,46 @@
package yapf
import (
"bytes"
"os"
"testing"
)
func TestRoundtrip(t *testing.T) {
for ch := 1; ch <= 4; ch++ {
w, h := 97, 61
px := make([]byte, w*h*ch)
for i := range px {
px[i] = byte(i*7 ^ i>>5)
}
data, err := Encode(&Image{Width: w, Height: h, Channels: ch, Flags: FlagSRGB, Pixels: px})
if err != nil {
t.Fatal(err)
}
back, err := Decode(data)
if err != nil || !bytes.Equal(back.Pixels, px) {
t.Fatalf("roundtrip failed for %d channels", ch)
}
}
}
func TestSampleReencodesIdentically(t *testing.T) {
file, err := os.ReadFile("../../other/YAPF.YAPF")
if err != nil {
t.Skip("sample not found")
}
img, err := Decode(file)
if err != nil {
t.Fatal(err)
}
again, _ := Encode(img)
if !bytes.Equal(again, file) {
t.Fatal("re-encoded sample differs from the C encoder's output")
}
}
func TestRejectsGarbage(t *testing.T) {
if _, err := Decode([]byte("not an image")); err == nil {
t.Fatal("garbage was accepted")
}
}

View file

@ -0,0 +1,29 @@
// java --enable-native-access=ALL-UNNAMED -Dyapf.library=../../build/libyapf.so Example.java ../../other/YAPF.YAPF
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
public class Example {
public static void main(String[] args) throws Exception {
Path path = Path.of(args.length > 0 ? args[0] : "../../other/YAPF.YAPF");
byte[] file = Files.readAllBytes(path);
Yapf.Image img = Yapf.decode(file); // first call also loads the library
double ms = Double.MAX_VALUE; // best of 10, as in a running program
for (int i = 0; i < 10; i++) {
long t = System.nanoTime();
img = Yapf.decode(file);
ms = Math.min(ms, (System.nanoTime() - t) / 1e6);
}
System.out.printf("%s: %dx%d, %d channels, decoded in %.2f ms%n", path, img.width(), img.height(), img.channels(), ms);
System.out.printf("file is %d bytes, %.1f%% of the raw pixels%n", file.length, 100.0 * file.length / img.pixels().length);
byte[] again = Yapf.encode(img);
System.out.printf("re-encoded: %d bytes, identical: %b%n", again.length, Arrays.equals(again, file));
byte[] rgb = new byte[64 * 64 * 3];
for (int i = 0; i < rgb.length; i++) rgb[i] = (byte) (i * 3);
Yapf.save(Path.of("java.yapf"), new Yapf.Image(64, 64, 3, rgb));
System.out.println("wrote java.yapf");
}
}

16
bindings/java/README.md Normal file
View file

@ -0,0 +1,16 @@
# YAPF for Java (and Kotlin)
`Yapf.java` uses the Foreign Function & Memory API of **Java 22+** — no JNI,
no native glue to build; just the YAPF shared library (`yapf.dll`,
`libyapf.so`, `libyapf.dylib`) on the library path or passed with
`-Dyapf.library=/path/to/lib`.
```java
Yapf.Image img = Yapf.load(Path.of("texture.yapf")); // or Yapf.decode(bytes)
byte[] file = Yapf.encode(new Yapf.Image(256, 256, 4, rgba));
```
Kotlin can call the same class directly. Run with
`--enable-native-access=ALL-UNNAMED` to silence the native-access warning.
Example: `java --enable-native-access=ALL-UNNAMED -Dyapf.library=../../build/libyapf.so Example.java`

121
bindings/java/Yapf.java Normal file
View file

@ -0,0 +1,121 @@
// Yapf.java — YAPF for Java 22+ via the Foreign Function & Memory API (no JNI).
// Copyright 2026 Nexoniarz — Apache License 2.0.
//
// Needs the native library: yapf.dll / libyapf.so / libyapf.dylib on the
// library path, or its full path in the system property -Dyapf.library=...
// Run with --enable-native-access=ALL-UNNAMED to avoid the warning.
//
// Yapf.Image img = Yapf.load(Path.of("texture.yapf"));
// byte[] file = Yapf.encode(new Yapf.Image(256, 256, 4, rgba));
import java.io.IOException;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.lang.foreign.ValueLayout.*;
public final class Yapf {
public static final int FLAG_PREMULT_ALPHA = 1, FLAG_SRGB = 2;
/** pixels: width*height*channels bytes, rows top to bottom. mips[0] == pixels. */
public record Image(int width, int height, int channels, int flags, int gpuFormat, byte[][] mips) {
public Image(int width, int height, int channels, byte[] pixels) {
this(width, height, channels, FLAG_SRGB, switch (channels) { case 4 -> 4; case 3 -> 5; case 2 -> 2; default -> 3; },
new byte[][] { pixels });
}
public byte[] pixels() { return mips[0]; }
}
// yapf_image_t: u32 width, u32 height, u8 channels, gpu_format, flags, mip_levels, ptr pixels, ptr mips
private static final StructLayout IMAGE = MemoryLayout.structLayout(
JAVA_INT.withName("width"), JAVA_INT.withName("height"),
JAVA_BYTE.withName("channels"), JAVA_BYTE.withName("gpu_format"),
JAVA_BYTE.withName("flags"), JAVA_BYTE.withName("mip_levels"),
MemoryLayout.paddingLayout(4), ADDRESS.withName("pixels"), ADDRESS.withName("mips"));
private static final MethodHandle LOAD, FREE, ENCODE, FREE_BUFFER;
static {
Linker linker = Linker.nativeLinker();
String custom = System.getProperty("yapf.library");
SymbolLookup lib = custom != null
? SymbolLookup.libraryLookup(Path.of(custom), Arena.global())
: SymbolLookup.libraryLookup(System.mapLibraryName("yapf"), Arena.global());
LOAD = linker.downcallHandle(lib.find("yapf_load_memory_mt").orElseThrow(),
FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG, JAVA_INT));
FREE = linker.downcallHandle(lib.find("yapf_free").orElseThrow(), FunctionDescriptor.ofVoid(ADDRESS));
ENCODE = linker.downcallHandle(lib.find("yapf_encode").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
FREE_BUFFER = linker.downcallHandle(lib.find("yapf_free_buffer").orElseThrow(), FunctionDescriptor.ofVoid(ADDRESS));
}
private Yapf() {}
/** Decode YAPF bytes; threads = 1 decodes on this thread, 0 uses every core. */
public static Image decode(byte[] data, int threads) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment in = arena.allocateFrom(JAVA_BYTE, data);
MemorySegment p = (MemorySegment) LOAD.invokeExact(in, (long) data.length, threads);
if (p.equals(MemorySegment.NULL)) throw new IllegalArgumentException("not a valid YAPF file");
try {
MemorySegment s = p.reinterpret(IMAGE.byteSize());
int w = s.get(JAVA_INT, 0), h = s.get(JAVA_INT, 4);
int ch = Byte.toUnsignedInt(s.get(JAVA_BYTE, 8)), levels = Byte.toUnsignedInt(s.get(JAVA_BYTE, 11));
MemorySegment table = s.get(ADDRESS, 24).reinterpret(ADDRESS.byteSize() * levels);
byte[][] mips = new byte[levels][];
for (int m = 0; m < levels; m++) {
long n = (long) Math.max(1, w >> m) * Math.max(1, h >> m) * ch;
mips[m] = table.getAtIndex(ADDRESS, m).reinterpret(n).toArray(JAVA_BYTE);
}
return new Image(w, h, ch, Byte.toUnsignedInt(s.get(JAVA_BYTE, 10)),
Byte.toUnsignedInt(s.get(JAVA_BYTE, 9)), mips);
} finally {
FREE.invokeExact(p);
}
} catch (RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
public static Image decode(byte[] data) { return decode(data, 1); }
public static Image load(Path path) throws IOException { return decode(Files.readAllBytes(path)); }
/** Encode to the bytes of a .yapf file. */
public static byte[] encode(Image img) {
if (img.pixels().length != img.width() * img.height() * img.channels())
throw new IllegalArgumentException("pixels must hold width*height*channels bytes");
try (Arena arena = Arena.ofConfined()) {
byte[][] mips = img.mips();
MemorySegment table = arena.allocate(ADDRESS, mips.length);
for (int m = 0; m < mips.length; m++) table.setAtIndex(ADDRESS, m, arena.allocateFrom(JAVA_BYTE, mips[m]));
MemorySegment s = arena.allocate(IMAGE);
s.set(JAVA_INT, 0, img.width());
s.set(JAVA_INT, 4, img.height());
s.set(JAVA_BYTE, 8, (byte) img.channels());
s.set(JAVA_BYTE, 9, (byte) img.gpuFormat());
s.set(JAVA_BYTE, 10, (byte) img.flags());
s.set(JAVA_BYTE, 11, (byte) mips.length);
s.set(ADDRESS, 16, table.getAtIndex(ADDRESS, 0));
s.set(ADDRESS, 24, table);
MemorySegment outData = arena.allocate(ADDRESS), outSize = arena.allocate(JAVA_LONG);
int rc = (int) ENCODE.invokeExact(s, outData, outSize);
if (rc != 0) throw new IllegalStateException("yapf_encode failed (" + rc + ")");
MemorySegment data = outData.get(ADDRESS, 0);
try {
return data.reinterpret(outSize.get(JAVA_LONG, 0)).toArray(JAVA_BYTE);
} finally {
FREE_BUFFER.invokeExact(data);
}
} catch (RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
public static void save(Path path, Image img) throws IOException { Files.write(path, encode(img)); }
}

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

@ -0,0 +1,24 @@
# 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).
TypeScript types are in `yapf.d.ts`.
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.

19
bindings/js/example.mjs Normal file
View file

@ -0,0 +1,19 @@
// node example.mjs ../../other/YAPF.YAPF
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
const YAPF = createRequire(import.meta.url)('./yapf.js');
const path = process.argv[2] ?? '../../other/YAPF.YAPF';
const file = readFileSync(path);
let img = YAPF.decode(file);
let best = Infinity; // best of 10 decodes
for (let i = 0; i < 10; i++) {
const t = performance.now();
img = YAPF.decode(file);
best = Math.min(best, performance.now() - t);
}
console.log(`${path}: ${img.width}x${img.height}, ${img.channels} channels, decoded in ${best.toFixed(2)} ms (pure JavaScript)`);
console.log(`file is ${file.length} bytes, ${(100 * file.length / img.pixels.length).toFixed(1)}% of the raw pixels`);
const again = YAPF.encode(img);
console.log(`re-encoded: ${again.length} bytes, identical: ${Buffer.compare(Buffer.from(again), file) === 0}`);

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

@ -0,0 +1,12 @@
{
"name": "yapf-image",
"version": "1.0.0",
"description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies",
"main": "yapf.js",
"types": "yapf.d.ts",
"files": ["yapf.js", "yapf.d.ts", "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);

25
bindings/js/yapf.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
// TypeScript declarations for yapf.js
export interface YapfImage {
width: number;
height: number;
/** 1 gray, 2 gray + alpha, 3 RGB, 4 RGBA */
channels: 1 | 2 | 3 | 4;
/** width * height * channels bytes, rows top to bottom */
pixels: Uint8Array;
gpuFormat?: number;
flags?: number;
mipLevels?: number;
/** mips[0] is the full-size image */
mips?: Uint8Array[];
}
/** Decode a .yapf file; throws on corrupt or unsupported data. */
export function decode(bytes: Uint8Array | ArrayBuffer): Required<YapfImage>;
/** Encode an image to the bytes of a .yapf file (identical to the C encoder). */
export function encode(image: YapfImage): Uint8Array;
/** Any channel count → RGBA, ready for `new ImageData(rgba, w, h)`. */
export function toRGBA(image: YapfImage, level?: number): Uint8ClampedArray;
export const GPU: { RGBA8: 0; RGB8: 1; RG8: 2; R8: 3; SRGB8_A8: 4; SRGB8: 5 };
export const FLAG: { PREMULT_ALPHA: 1; SRGB: 2 };
export const VERSION: 1;

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

13
bindings/lua/README.md Normal file
View file

@ -0,0 +1,13 @@
# YAPF for Lua (LuaJIT)
`yapf.lua` uses LuaJIT's FFI with the YAPF shared library (`libyapf.so`,
`yapf.dll`, `libyapf.dylib`, or the path in `$YAPF_LIBRARY`). Works in
LÖVE (love2d), which runs on LuaJIT.
```lua
local yapf = require("yapf")
local img = yapf.load("texture.yapf") -- width, height, channels, pixels (string)
yapf.save("out.yapf", img)
```
Example: `YAPF_LIBRARY=../../build/libyapf.so luajit example.lua`

17
bindings/lua/example.lua Normal file
View file

@ -0,0 +1,17 @@
-- YAPF_LIBRARY=../../build/libyapf.so luajit example.lua ../../other/YAPF.YAPF
package.path = "./?.lua;" .. package.path
local yapf = require("yapf")
local path = arg[1] or "../../other/YAPF.YAPF"
local f = assert(io.open(path, "rb")); local data = f:read("*a"); f:close()
local img = assert(yapf.decode(data))
local best = math.huge -- best of 10 decodes
for _ = 1, 10 do
local t = os.clock()
img = yapf.decode(data)
best = math.min(best, os.clock() - t)
end
print(string.format("%s: %dx%d, %d channels, decoded in %.2f ms", path, img.width, img.height, img.channels, best * 1000))
print(string.format("file is %d bytes, %.1f%% of the raw pixels", #data, 100 * #data / #img.pixels))
local again = yapf.encode(img)
print(string.format("re-encoded: %d bytes, identical: %s", #again, tostring(again == data)))

71
bindings/lua/yapf.lua Normal file
View file

@ -0,0 +1,71 @@
-- yapf.lua — YAPF for LuaJIT via its FFI. Copyright 2026 Nexoniarz — Apache 2.0.
-- local yapf = require("yapf")
-- local img = yapf.load("texture.yapf") -- img.width, img.height, img.channels, img.pixels (Lua string)
-- local bytes = yapf.encode(img) -- or yapf.save(path, img)
-- Loads libyapf.so / yapf.dll / libyapf.dylib, or the path in $YAPF_LIBRARY.
local ffi = require("ffi")
ffi.cdef [[
typedef struct {
uint32_t width, height;
uint8_t channels, gpu_format, flags, mip_levels;
uint8_t *pixels;
uint8_t **mips;
} yapf_image_t;
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
void yapf_free(yapf_image_t *img);
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
void yapf_free_buffer(void *data);
]]
local lib = ffi.load(os.getenv("YAPF_LIBRARY") or "yapf")
local M = { FLAG_PREMULT_ALPHA = 1, FLAG_SRGB = 2 }
-- Decode a Lua string holding a .yapf file. threads: 1 (default) or 0 for all cores.
function M.decode(data, threads)
local p = lib.yapf_load_memory_mt(data, #data, threads or 1)
if p == nil then return nil, "not a valid YAPF file" end
local img = {
width = p.width, height = p.height, channels = p.channels,
flags = p.flags, gpu_format = p.gpu_format,
pixels = ffi.string(p.pixels, p.width * p.height * p.channels),
}
lib.yapf_free(p)
return img
end
function M.load(path)
local f, err = io.open(path, "rb")
if not f then return nil, err end
local data = f:read("*a")
f:close()
return M.decode(data)
end
-- img: { width, height, channels, pixels = string } → string with the .yapf bytes
function M.encode(img)
local raw = ffi.new("yapf_image_t")
local px = ffi.new("uint8_t[?]", #img.pixels)
ffi.copy(px, img.pixels, #img.pixels)
local table = ffi.new("uint8_t*[1]", px)
raw.width, raw.height, raw.channels = img.width, img.height, img.channels
raw.flags = img.flags or M.FLAG_SRGB
raw.gpu_format = img.gpu_format or ({ 3, 2, 5, 4 })[img.channels]
raw.mip_levels, raw.pixels, raw.mips = 1, px, table
local out, size = ffi.new("void*[1]"), ffi.new("size_t[1]")
if lib.yapf_encode(raw, out, size) ~= 0 then return nil, "encoding failed" end
local bytes = ffi.string(out[0], size[0])
lib.yapf_free_buffer(out[0])
return bytes
end
function M.save(path, img)
local bytes, err = M.encode(img)
if not bytes then return nil, err end
local f = assert(io.open(path, "wb"))
f:write(bytes)
f:close()
return true
end
return M

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

@ -0,0 +1,25 @@
# 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.
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`

View file

@ -0,0 +1,33 @@
"""
example.py — YAPF from Python.
make # builds build/libyapf.so (or get it from Releases)
YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF
"""
import sys
import time
import yapf
path = sys.argv[1] if len(sys.argv) > 1 else "../../other/YAPF.YAPF"
data = open(path, "rb").read()
img = yapf.decode(data)
ms = float("inf") # best of 10, as in a running program
for _ in range(10):
t = time.perf_counter()
img = yapf.decode(data)
ms = min(ms, (time.perf_counter() - t) * 1000)
raw = img.width * img.height * img.channels
print("%s: %dx%d, %d channels, decoded in %.2f ms (%s backend)"
% (path, img.width, img.height, img.channels, ms, yapf.backend()))
print("file is %d bytes, %.1f%% of the raw pixels" % (len(data), 100.0 * len(data) / raw))
if yapf.backend() == "c":
again = yapf.encode(img)
print("re-encoded:", len(again), "bytes, identical:", again == data)
w = h = 128 # your own pixels → .yapf
rgb = bytes(v for y in range(h) for x in range(w) for v in (x * 2, y * 2, 128))
yapf.save("gradient.yapf", yapf.Image(w, h, 3, rgb))
print("wrote gradient.yapf")

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

@ -0,0 +1,314 @@
"""
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)]
lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_size_t)]
lib.yapf_free_buffer.argtypes = [ctypes.c_void_p]
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_encode(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)
data, size = ctypes.c_void_p(), ctypes.c_size_t()
rc = _lib.yapf_encode(ctypes.byref(c), ctypes.byref(data), ctypes.byref(size))
if rc != 0:
raise YapfError("yapf_encode failed with code %d" % rc)
try:
return ctypes.string_at(data, size.value)
finally:
_lib.yapf_free_buffer(data)
# ── 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 encode(img):
"""Encode an Image (or anything with width/height/channels/pixels) to
YAPF bytes. 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)))
return _c_encode(img)
def save(path, img):
"""Encode an Image to a .yapf file. Needs the C library."""
data = encode(img)
with open(path, "wb") as f:
f.write(data)
if __name__ == "__main__":
for path in sys.argv[1:]:
print(path, load(path), "backend:", backend())

12
bindings/rust/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "yapf"
version = "1.0.0"
edition = "2021"
description = "YAPF lossless images: fast byte-aligned decoder and encoder (bindings to the C library)"
license = "Apache-2.0"
repository = "https://github.com/Nexoniarz/YAPF"
links = "yapf"
build = "build.rs"
[build-dependencies]
cc = "1"

18
bindings/rust/README.md Normal file
View file

@ -0,0 +1,18 @@
# YAPF for Rust
A crate that compiles the C library with the `cc` crate, so `cargo build`
works on Windows, macOS and Linux with no extra steps.
```toml
[dependencies]
yapf = { git = "https://github.com/Nexoniarz/YAPF" }
```
```rust
let img = yapf::Image::load("texture.yapf")?; // or Image::decode(&bytes)
println!("{}x{}, {} channels", img.width, img.height, img.channels);
yapf::Image::new(256, 256, 4, rgba).save("out.yapf")?; // or .encode() → Vec<u8>
```
Example: `cargo run --release --example demo -- ../../other/YAPF.YAPF` ·
tests: `cargo test`.

12
bindings/rust/build.rs Normal file
View file

@ -0,0 +1,12 @@
// Compiles the C library that lives two folders up.
fn main() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
cc::Build::new()
.file(root.join("yapf.c"))
.include(&root)
.flag_if_supported("-std=c99")
.opt_level(2)
.compile("yapf");
println!("cargo:rerun-if-changed={}", root.join("yapf.c").display());
println!("cargo:rerun-if-changed={}", root.join("yapf.h").display());
}

View file

@ -0,0 +1,26 @@
// cargo run --release --example demo -- ../../other/YAPF.YAPF
use std::time::Instant;
fn main() -> Result<(), yapf::Error> {
let path = std::env::args().nth(1).unwrap_or_else(|| "../../other/YAPF.YAPF".into());
let file = std::fs::read(&path)?;
let mut img = yapf::Image::decode(&file)?;
let mut best = f64::MAX; // best of 10, as in a running program
for _ in 0..10 {
let t = Instant::now();
img = yapf::Image::decode(&file)?;
best = best.min(t.elapsed().as_secs_f64() * 1e3);
}
println!("{path}: {}x{}, {} channels, decoded in {best:.2} ms", img.width, img.height, img.channels);
println!("file is {} bytes, {:.1}% of the raw pixels",
file.len(), 100.0 * file.len() as f64 / img.pixels.len() as f64);
let again = img.encode()?;
println!("re-encoded: {} bytes, identical: {}", again.len(), again == file);
let px: Vec<u8> = (0..128 * 128).flat_map(|i| [(i % 128 * 2) as u8, (i / 128 * 2) as u8, 128]).collect();
yapf::Image::new(128, 128, 3, px).save("rust.yapf")?;
println!("wrote rust.yapf");
Ok(())
}

156
bindings/rust/src/lib.rs Normal file
View file

@ -0,0 +1,156 @@
//! YAPF lossless images for Rust.
//!
//! ```no_run
//! let img = yapf::Image::load("texture.yapf")?;
//! println!("{}x{} with {} channels", img.width, img.height, img.channels);
//! let bytes = img.encode()?;
//! # Ok::<(), yapf::Error>(())
//! ```
use std::ffi::{c_int, c_void};
use std::fmt;
use std::path::Path;
#[repr(C)]
struct RawImage {
width: u32,
height: u32,
channels: u8,
gpu_format: u8,
flags: u8,
mip_levels: u8,
pixels: *mut u8,
mips: *mut *mut u8,
}
extern "C" {
fn yapf_load_memory_mt(buffer: *const c_void, size: usize, threads: c_int) -> *mut RawImage;
fn yapf_free(img: *mut RawImage);
fn yapf_encode(img: *const RawImage, out_data: *mut *mut c_void, out_size: *mut usize) -> c_int;
fn yapf_free_buffer(data: *mut c_void);
}
pub const FLAG_PREMULT_ALPHA: u8 = 1;
pub const FLAG_SRGB: u8 = 2;
#[derive(Debug)]
pub enum Error {
/// Not a YAPF v1 file, or corrupt.
Invalid,
/// The encoder rejected the image (bad size or channel count) or ran out of memory.
Encode(i32),
Io(std::io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Invalid => write!(f, "not a valid YAPF file"),
Error::Encode(c) => write!(f, "YAPF encoding failed (code {c})"),
Error::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self { Error::Io(e) }
}
/// A decoded image: `pixels` holds `width * height * channels` bytes, rows top to bottom.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
pub width: u32,
pub height: u32,
/// 1 gray, 2 gray + alpha, 3 RGB, 4 RGBA
pub channels: u8,
pub flags: u8,
pub gpu_format: u8,
pub pixels: Vec<u8>,
/// Smaller mip levels (1, 2, …); empty when the file stores only the base image.
pub mips: Vec<Vec<u8>>,
}
impl Image {
pub fn new(width: u32, height: u32, channels: u8, pixels: Vec<u8>) -> Image {
let gpu_format = match channels { 4 => 4, 3 => 5, 2 => 2, _ => 3 };
Image { width, height, channels, flags: FLAG_SRGB, gpu_format, pixels, mips: Vec::new() }
}
/// Decode YAPF bytes; `threads` = 1 decodes on this thread, 0 uses every core.
pub fn decode_with_threads(data: &[u8], threads: i32) -> Result<Image, Error> {
unsafe {
let p = yapf_load_memory_mt(data.as_ptr().cast(), data.len(), threads);
if p.is_null() {
return Err(Error::Invalid);
}
let r = &*p;
let level = |m: usize| {
let w = (r.width >> m).max(1) as usize;
let h = (r.height >> m).max(1) as usize;
std::slice::from_raw_parts(*r.mips.add(m), w * h * r.channels as usize).to_vec()
};
let img = Image {
width: r.width, height: r.height, channels: r.channels, flags: r.flags,
gpu_format: r.gpu_format, pixels: level(0),
mips: (1..r.mip_levels as usize).map(level).collect(),
};
yapf_free(p);
Ok(img)
}
}
pub fn decode(data: &[u8]) -> Result<Image, Error> { Image::decode_with_threads(data, 1) }
pub fn load<P: AsRef<Path>>(path: P) -> Result<Image, Error> {
Image::decode(&std::fs::read(path)?)
}
/// Encode to the bytes of a `.yapf` file.
pub fn encode(&self) -> Result<Vec<u8>, Error> {
let mut levels: Vec<*mut u8> = std::iter::once(self.pixels.as_ptr() as *mut u8)
.chain(self.mips.iter().map(|m| m.as_ptr() as *mut u8))
.collect();
let raw = RawImage {
width: self.width, height: self.height, channels: self.channels,
gpu_format: self.gpu_format, flags: self.flags, mip_levels: levels.len() as u8,
pixels: levels[0], mips: levels.as_mut_ptr(),
};
if self.pixels.len() != self.width as usize * self.height as usize * self.channels as usize {
return Err(Error::Encode(-1));
}
let mut data: *mut c_void = std::ptr::null_mut();
let mut size = 0usize;
let rc = unsafe { yapf_encode(&raw, &mut data, &mut size) };
if rc != 0 {
return Err(Error::Encode(rc));
}
let out = unsafe { std::slice::from_raw_parts(data as *const u8, size).to_vec() };
unsafe { yapf_free_buffer(data) };
Ok(out)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
std::fs::write(path, self.encode()?)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
for ch in 1..=4u8 {
let (w, h) = (97u32, 61u32);
let px: Vec<u8> = (0..w * h * ch as u32).map(|i| (i * 7 ^ i >> 5) as u8).collect();
let img = Image::new(w, h, ch, px);
let back = Image::decode(&img.encode().unwrap()).unwrap();
assert_eq!(back, img);
}
}
#[test]
fn rejects_garbage() {
assert!(Image::decode(b"not an image").is_err());
}
}

13
bindings/zig/README.md Normal file
View file

@ -0,0 +1,13 @@
# YAPF for Zig
Zig imports `yapf.h` directly — no bindings needed:
```zig
const c = @cImport(@cInclude("yapf.h"));
const img = c.yapf_load("texture.yapf") orelse return error.CannotLoad;
defer c.yapf_free(img);
```
Add `yapf.c` to your build (`exe.addCSourceFile(.{ .file = b.path("yapf.c") })`,
`exe.linkLibC()`), or run the example:
`zig run example.zig ../../yapf.c -I../.. -lc -O ReleaseFast`.

33
bindings/zig/example.zig Normal file
View file

@ -0,0 +1,33 @@
// YAPF from Zig: the C header is imported directly.
// zig run example.zig ../../yapf.c -I../.. -lc -O ReleaseFast
// (Uses only the C API plus std.debug.print, so it works across Zig versions.)
const std = @import("std");
const c = @cImport({
@cInclude("yapf.h");
@cInclude("time.h");
});
pub fn main() !void {
const path: [:0]const u8 = "../../other/YAPF.YAPF";
var img: *c.yapf_image_t = c.yapf_load(path.ptr) orelse return error.CannotLoad;
var best: c.clock_t = std.math.maxInt(c.clock_t); // best of 10 decodes
for (0..10) |_| {
const t = c.clock();
const again = c.yapf_load(path.ptr) orelse return error.CannotLoad;
best = @min(best, c.clock() - t);
c.yapf_free(img);
img = again;
}
defer c.yapf_free(img);
std.debug.print("{s}: {d}x{d}, {d} channels, decoded in {d:.2} ms\n", .{
path, img.width, img.height, img.channels, @as(f64, @floatFromInt(best)) * 1000.0 / @as(f64, @floatFromInt(c.CLOCKS_PER_SEC)),
});
var data: ?*anyopaque = null;
var size: usize = 0;
if (c.yapf_encode(img, &data, &size) != c.YAPF_OK) return error.EncodeFailed;
defer c.yapf_free_buffer(data);
const raw = @as(f64, @floatFromInt(img.width * img.height * img.channels));
std.debug.print("encoded: {d} bytes, {d:.1}% of the raw pixels\n", .{ size, 100.0 * @as(f64, @floatFromInt(size)) / raw });
}

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,8 +141,13 @@ 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_encode
echo yapf_free echo yapf_free
echo yapf_free_buffer
) > dist\_yapf.def ) > dist\_yapf.def
:: ==================================================================== :: ====================================================================
@ -183,6 +190,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 +243,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

144
docs/LANGUAGES.md Normal file
View file

@ -0,0 +1,144 @@
# Using YAPF from your language
YAPF is a small C library with six functions, so every language that can
call C can use it. Ready-made bindings, each with a runnable example:
| Language | Folder | How it works | Tested | Decode¹ |
| :------- | :----- | :----------- | :----- | ------: |
| **C** | [`yapf.h`](../yapf.h), [`examples/c`](../examples/c) | the library itself | ✅ | 0.26 ms |
| **C++** | [`bindings/cpp`](../bindings/cpp) | header-only RAII wrapper (C++17) | ✅ g++ | 0.25 ms |
| **Python** | [`bindings/python`](../bindings/python) | ctypes; numpy fallback without the library | ✅ CPython 3.13 | 0.23 ms |
| **C# / .NET** | [`bindings/csharp`](../bindings/csharp) | P/Invoke, single file | ✅ .NET 8 | 0.30 ms |
| **Rust** | [`bindings/rust`](../bindings/rust) | crate, builds the C code with `cc` | ✅ Rust 1.95 | 0.28 ms |
| **Go** | [`bindings/go`](../bindings/go) | cgo package | ✅ Go 1.26 | 0.31 ms |
| **Java / Kotlin** | [`bindings/java`](../bindings/java) | Foreign Function & Memory API (Java 22+) | ✅ JDK 25 | 0.42 ms |
| **Zig** | [`bindings/zig`](../bindings/zig) | `@cImport` of `yapf.h` | ✅ Zig 0.16 | 0.28 ms |
| **Lua** | [`bindings/lua`](../bindings/lua) | LuaJIT FFI (also LÖVE) | ✅ LuaJIT 2.1 | 0.28 ms |
| **JavaScript / TypeScript** | [`bindings/js`](../bindings/js) | pure JS port, `.d.ts` types | ✅ Node 24, Chromium | 5.9 ms |
¹ Decoding the 512×512 RGBA YAPF logo (`other/YAPF.YAPF`), best of 10 after
a warm-up, on an AMD Ryzen 7 5700G. Every binding except JavaScript runs
the same native code, so they are all about equally fast; the JavaScript
port needs no native library at all. Every example also re-encodes the
image and checks that the result is byte-identical to the original file.
## Getting the native library
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
library at run time:
| System | File | Get it |
| :------ | :-------------- | :----- |
| Windows | `yapf.dll` | [Releases](https://github.com/Nexoniarz/YAPF/releases) or `build.bat` |
| Linux | `libyapf.so` | Releases, `make`, or `sudo make install` |
| macOS | `libyapf.dylib` | Releases or `make` |
Put it next to your program, on the library path (`PATH`,
`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`), or pass its path explicitly
(`YAPF_LIBRARY` for Python and Lua, `-Dyapf.library=` for Java).
## Quick look
**C**
```c
yapf_image_t *img = yapf_load("texture.yapf");
/* img->width, img->height, img->channels, img->pixels */
yapf_free(img);
```
**C++**
```cpp
auto img = yapf::Image::load("texture.yapf");
auto bytes = img.encode();
```
**Python**
```python
img = yapf.load("texture.yapf")
yapf.save("copy.yapf", img)
```
**C#**
```csharp
var img = YapfImage.Load("texture.yapf");
File.WriteAllBytes("copy.yapf", img.Encode());
```
**Rust**
```rust
let img = yapf::Image::load("texture.yapf")?;
img.save("copy.yapf")?;
```
**Go**
```go
img, err := yapf.Load("texture.yapf")
data, err := yapf.Encode(img)
```
**Java**
```java
Yapf.Image img = Yapf.load(Path.of("texture.yapf"));
Yapf.save(Path.of("copy.yapf"), img);
```
**Zig**
```zig
const img = c.yapf_load("texture.yapf") orelse return error.CannotLoad;
defer c.yapf_free(img);
```
**Lua**
```lua
local img = yapf.load("texture.yapf")
yapf.save("copy.yapf", img)
```
**JavaScript**
```js
const img = YAPF.decode(bytes);
const file = YAPF.encode(img);
```
## Any other language
Swift, Kotlin/Native, Dart, PHP, Ruby, Julia, Nim, D, Odin, Haskell, … —
anything with a C foreign-function interface can call the library
directly. These are the whole API:
```c
typedef struct {
uint32_t width, height;
uint8_t channels; /* 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA */
uint8_t gpu_format; /* hint, see yapf.h */
uint8_t flags; /* 1 premultiplied alpha, 2 sRGB */
uint8_t mip_levels;
uint8_t *pixels; /* width*height*channels bytes, rows top to bottom */
uint8_t **mips; /* mips[0] == pixels */
} yapf_image_t; /* 32 bytes on 64-bit systems: pixels at offset 16, mips at 24 */
yapf_image_t *yapf_load_memory_mt(const void *data, size_t size, int threads); /* threads: 1 or 0 = all cores */
void yapf_free(yapf_image_t *img);
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
void yapf_free_buffer(void *data);
yapf_image_t *yapf_load(const char *path);
int yapf_save(const char *path, const yapf_image_t *img);
```
Returned images and buffers must be released with `yapf_free` /
`yapf_free_buffer` from the same library. Ready-made bindings for these
languages are not included yet; the snippets below show the starting point
and are **not tested**.
**Swift** — add a module map for `yapf.h` (or a bridging header) and compile
`yapf.c` in the target; then `let img = yapf_load(path)` works as is.
**Dart** — `dart:ffi`: `DynamicLibrary.open('libyapf.so')` and
`lookupFunction<Pointer<YapfImage> Function(Pointer<Uint8>, IntPtr, Int32), …>('yapf_load_memory_mt')`.
**PHP 7.4+** — `$ffi = FFI::cdef(file_get_contents('yapf_api.h'), 'libyapf.so');`
with the declarations above in `yapf_api.h`.
**Ruby** — `Fiddle::Function.new(lib['yapf_load'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)`.
**Julia** — `ccall((:yapf_load, "libyapf"), Ptr{Cvoid}, (Cstring,), path)`.

62
examples/README.md Normal file
View file

@ -0,0 +1,62 @@
# Examples: how fast, how small
## Try it on your own images
```sh
make # builds build/yapf
build/yapf compare photo.png ui.png # any PNG / JPG / TGA / BMP
```
`yapf compare` encodes each image to YAPF in memory, checks that it decodes
back to exactly the same pixels, and times decoding the original format
against YAPF on one core and on all cores:
```
ui_reverb.png (261 x 430, RGBA)
raw 438.4 KB
PNG 176.6 KB 40.3% of raw decode 2.049 ms
YAPF 143.7 KB 32.8% of raw decode 0.170 ms 12.1x faster, 81% of the PNG size
YAPF on all cores: decode 0.176 ms (2.5 GB/s), encode 8 ms, lossless: yes
```
## Results on a mixed set of images
AMD Ryzen 7 5700G, `make` (gcc -O2), PNG decoded with stb_image. The test
images (a 4K wallpaper, UI screenshots, an app icon and seamless textures
from open-source projects) are not redistributed here; run `yapf compare`
on your own files to reproduce.
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
In short: **smaller than PNG on UI-style images, 20–50 % larger on photos
and textures, and 5–12× faster to decode** — always lossless. The decoder
reaches 5–6 GB/s on large images on one core, and more with
`yapf_load_mt`.
## Code examples
| Language | Example |
| :------- | :------ |
| C | [`examples/c/example.c`](c/example.c) |
| C++ | [`bindings/cpp/example.cpp`](../bindings/cpp/example.cpp) |
| Python | [`bindings/python/example.py`](../bindings/python/example.py) |
| C# | [`bindings/csharp/Example`](../bindings/csharp/Example) |
| Rust | [`bindings/rust/examples/demo.rs`](../bindings/rust/examples/demo.rs) |
| Go | [`bindings/go/example`](../bindings/go/example) |
| Java | [`bindings/java/Example.java`](../bindings/java/Example.java) |
| Zig | [`bindings/zig/example.zig`](../bindings/zig/example.zig) |
| Lua | [`bindings/lua/example.lua`](../bindings/lua/example.lua) |
| JavaScript | [`bindings/js/example.mjs`](../bindings/js/example.mjs) |
Each one loads a `.yapf` file, prints its size and decode time, re-encodes
it (byte-identical to the file) and writes a new image from its own pixels.
See [docs/LANGUAGES.md](../docs/LANGUAGES.md) for setup in each language.

61
examples/c/example.c Normal file
View file

@ -0,0 +1,61 @@
/*
* example.c — load, inspect, time and write YAPF images from C.
*
* cc -O2 -std=c99 -I../.. example.c ../../yapf.c -o example -pthread
* ./example ../../other/YAPF.YAPF
*/
#include "yapf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static double seconds(void) { return (double)clock() / CLOCKS_PER_SEC; }
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1] : "../../other/YAPF.YAPF";
/* 1. Load a file (yapf_load_memory works the same on a buffer). */
yapf_image_t *img = yapf_load(path);
if (!img) { fprintf(stderr, "cannot load %s\n", path); return 1; }
double t = 1e9; /* decode time: best of 10 */
for (int i = 0; i < 10; i++) {
double t0 = seconds();
yapf_free(yapf_load(path));
t0 = seconds() - t0;
if (t0 < t) t = t0;
}
printf("%s: %u x %u, %u channels, %u mip level(s), decoded in %.2f ms\n",
path, img->width, img->height, img->channels, img->mip_levels, t * 1e3);
/* 2. Pixels: width * height * channels bytes, rows top to bottom. */
const uint8_t *p = img->pixels;
printf("top-left pixel: %u %u %u %u\n", p[0], p[1], p[2], img->channels == 4 ? p[3] : 255);
/* 3. Encode to memory and to a file. */
void *data = NULL;
size_t size = 0;
if (yapf_encode(img, &data, &size) == YAPF_OK) {
printf("re-encoded: %zu bytes (%.1f%% of raw)\n", size,
100.0 * size / ((double)img->width * img->height * img->channels));
yapf_free_buffer(data);
}
/* 4. Create an image from your own pixels. */
enum { W = 256, H = 256 };
static uint8_t rgba[W * H * 4];
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++) {
uint8_t *q = rgba + (y * W + x) * 4;
q[0] = (uint8_t)x; q[1] = (uint8_t)y; q[2] = 128; q[3] = 255;
}
yapf_image_t mine;
memset(&mine, 0, sizeof(mine));
mine.width = W; mine.height = H; mine.channels = 4; mine.mip_levels = 1;
mine.pixels = rgba; mine.flags = YAPF_FLAG_SRGB; mine.gpu_format = YAPF_GPU_SRGB8_A8;
if (yapf_save("gradient.yapf", &mine) == YAPF_OK) printf("wrote gradient.yapf\n");
yapf_free(img);
return 0;
}

View file

@ -0,0 +1,36 @@
# 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). Tested in Blender 5.1 on Linux; the Windows library is tested
outside Blender, and the macOS libraries are cross-compiled and **not yet run
on a Mac**. 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,39 @@
# 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.
## Status
The plugin UI was tested in a browser engine (headless Chromium) with a
simulated Figma main thread: import hands Figma a pixel-exact PNG, and
export downloads a pixel-exact `.yapf`. `code.js` is syntax-checked.
**It has not yet been run inside Figma itself.**

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);
/* Determine channel layout and GPU format hint. */ /* A flattened copy when the image has several layers; the visible
gboolean has_alpha = gimp_drawable_has_alpha(drawable); * result is what gets exported. */
gboolean is_gray = gimp_drawable_is_gray(drawable); GimpExportReturn export = gimp_export_options_get_image(options, &image);
GList *layers = gimp_image_list_layers(image);
if (!layers) {
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
return yapf_error(procedure, "No layers to export.");
}
GimpDrawable *drawable = GIMP_DRAWABLE(layers->data);
g_list_free(layers);
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
gboolean is_gray = gimp_drawable_is_gray(drawable);
const gchar *babl_fmt; 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 {
ch = YAPF_CHANNELS_GRAY; babl_fmt = "Y' u8";
gpu_fmt = YAPF_GPU_R8;
flags = 0;
}
} else { } else {
if (has_alpha) { ch = has_alpha ? YAPF_CHANNELS_RGBA : YAPF_CHANNELS_RGB;
ch = YAPF_CHANNELS_RGBA; babl_fmt = "R'G'B'A u8"; babl_fmt = has_alpha ? "R'G'B'A u8" : "R'G'B' u8";
gpu_fmt = YAPF_GPU_SRGB8_A8; gpu_fmt = has_alpha ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_SRGB8;
} 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;
gchar *path = g_file_get_path(file); int result = YAPF_ERR_OOM;
int result = yapf_save(path, &img); if (!mipmaps || yapf_build_mips(&img)) {
g_free(path); gchar *path = g_file_get_path(file);
result = yapf_save(path, &img);
g_free(path);
}
if (img.mips) {
for (int m = 1; m < img.mip_levels; m++) free(img.mips[m]);
free(img.mips);
}
free(pixels); 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,26 @@
# YAPF thumbnails for KDE Dolphin (KF6 KIO thumbnail plugin).
# cmake -B build -DCMAKE_BUILD_TYPE=Release
# cmake --build build
# sudo cmake --install build (or see README.md for a per-user install)
cmake_minimum_required(VERSION 3.16)
project(yapfthumbnail LANGUAGES C CXX)
find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui)
find_package(KF6 6.0 REQUIRED COMPONENTS KIO)
find_package(Threads REQUIRED)
kcoreaddons_add_plugin(yapfthumbnail
SOURCES yapfthumbnail.cpp ../../yapf.c
INSTALL_NAMESPACE "kf6/thumbcreator")
set_source_files_properties(../../yapf.c PROPERTIES LANGUAGE C)
target_include_directories(yapfthumbnail PRIVATE ../..)
target_link_libraries(yapfthumbnail PRIVATE KF6::KIOGui Qt6::Gui Threads::Threads)
# Register the image/x-yapf file type so Dolphin recognises .yapf files.
install(FILES ../linux/yapf-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR} RENAME yapf.xml)

50
extensions/kde/README.md Normal file
View file

@ -0,0 +1,50 @@
# YAPF thumbnails for KDE (Dolphin)
Shows previews of `.yapf` files in **Dolphin**, the KDE file dialogs and
anything else that uses KIO thumbnails. KDE Frameworks 6 / Plasma 6.
Files with mipmaps are thumbnailed from the nearest mip level, so even huge
textures preview instantly.
## Build and install
You need CMake, a C/C++ compiler and the KDE development packages:
| System | Packages |
| :-------------- | :------- |
| Arch / Manjaro | `sudo pacman -S cmake extra-cmake-modules kio` |
| Debian / Ubuntu | `sudo apt install cmake extra-cmake-modules libkf6kio-dev qt6-base-dev` |
| Fedora | `sudo dnf install cmake extra-cmake-modules kf6-kio-devel qt6-qtbase-devel` |
| openSUSE | `sudo zypper install cmake kf6-extra-cmake-modules kf6-kio-devel qt6-base-devel` |
| NixOS | `nix-shell kde-shell.nix` (in this folder) |
```sh
cd extensions/kde
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
cmake --build build
sudo cmake --install build
update-mime-database /usr/share/mime
```
This installs the plugin (`kf6/thumbcreator/yapfthumbnail.so`) and the
`image/x-yapf` file type. Then in Dolphin: **Settings → Configure Dolphin →
Interface → Previews**, tick **YAPF Images**, and turn previews on.
### Without root (per user, also NixOS)
```sh
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/.local
cmake --build build && cmake --install build
update-mime-database ~/.local/share/mime
mkdir -p ~/.config/plasma-workspace/env
echo 'export QT_PLUGIN_PATH="$HOME/.local/lib/plugins:$QT_PLUGIN_PATH"' > ~/.config/plasma-workspace/env/yapf.sh
```
Log out and back in, then enable **YAPF Images** in Dolphin's preview settings.
(Some distributions use `lib64/plugins` or `lib/qt6/plugins`; `cmake
--install` prints the exact folder.)
## Status
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
corrupt files are rejected). Not yet tried inside a running Dolphin.

View file

@ -0,0 +1,6 @@
# Development shell for building the plugin on NixOS: nix-shell kde-shell.nix
with import <nixpkgs> {};
mkShell {
nativeBuildInputs = [ cmake kdePackages.extra-cmake-modules pkg-config ];
buildInputs = [ kdePackages.kio kdePackages.qtbase kdePackages.kcoreaddons ];
}

View file

@ -0,0 +1,79 @@
// YAPF thumbnails for KDE Dolphin and other KIO file dialogs.
// Copyright 2026 Nexoniarz — Apache License 2.0.
#include <KIO/ThumbnailCreator>
#include <KPluginFactory>
#include <QImage>
extern "C" {
#include "yapf.h"
}
class YapfThumbnailCreator : public KIO::ThumbnailCreator
{
Q_OBJECT
public:
YapfThumbnailCreator(QObject *parent, const QVariantList &args)
: KIO::ThumbnailCreator(parent, args)
{
}
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
{
const QByteArray path = request.url().toLocalFile().toLocal8Bit();
yapf_image_t *img = yapf_load(path.constData());
if (!img) {
return KIO::ThumbnailResult::fail();
}
// Start from the smallest stored mip level that still covers the
// requested size; Qt scales the rest.
const QSize want = request.targetSize();
int m = 0;
while (m + 1 < img->mip_levels &&
int(img->width >> (m + 1)) >= want.width() &&
int(img->height >> (m + 1)) >= want.height()) {
++m;
}
const int w = qMax(1, int(img->width >> m));
const int h = qMax(1, int(img->height >> m));
const uchar *px = img->mips[m];
QImage image;
switch (img->channels) {
case 1:
image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy();
break;
case 2: { // gray + alpha → RGBA
image = QImage(w, h, QImage::Format_RGBA8888);
for (int y = 0; y < h; ++y) {
const uchar *s = px + size_t(y) * w * 2;
uchar *d = image.scanLine(y);
for (int x = 0; x < w; ++x, s += 2, d += 4) {
d[0] = d[1] = d[2] = s[0];
d[3] = s[1];
}
}
break;
}
case 3:
image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy();
break;
default:
image = QImage(px, w, h, w * 4,
(img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
: QImage::Format_RGBA8888).copy();
break;
}
yapf_free(img);
if (image.width() > want.width() || image.height() > want.height()) {
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return KIO::ThumbnailResult::pass(image);
}
};
K_PLUGIN_CLASS_WITH_JSON(YapfThumbnailCreator, "yapfthumbnail.json")
#include "yapfthumbnail.moc"

View file

@ -0,0 +1,7 @@
{
"CacheThumbnail": true,
"KPlugin": {
"MimeTypes": ["image/x-yapf"],
"Name": "YAPF Images"
}
}

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,34 @@
# 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
```
Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
## Status
The `.vsix` packages cleanly, and the preview code was tested in a browser
engine (headless Chromium) with real files: correct pixels, mip picker,
error message for corrupt files. **It has not yet been run inside VS Code
itself.**

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.

202
tests/test_yapf.c Normal file
View file

@ -0,0 +1,202 @@
/*
* 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) {
void *data = NULL;
if (yapf_encode(img, &data, n) != YAPF_OK) return NULL;
uint8_t *copy = (uint8_t *)malloc(*n);
if (copy) memcpy(copy, data, *n);
yapf_free_buffer(data);
return copy;
}
/* yapf_save() writes exactly what yapf_encode() returns. */
static void save_matches_encode(void) {
const char *tmp = "test_yapf_tmp.yapf";
uint8_t px[37 * 23 * 3];
fill(px, 37, 23, 3, 3);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = 37; img.height = 23; img.channels = 3; img.mip_levels = 1; img.pixels = px;
size_t len = 0;
uint8_t *mem = encode_mem(&img, &len);
int ok = mem && yapf_save(tmp, &img) == YAPF_OK;
FILE *f = ok ? fopen(tmp, "rb") : NULL;
if (f) {
uint8_t *disk = (uint8_t *)malloc(len + 1);
ok = fread(disk, 1, len + 1, f) == len && memcmp(disk, mem, len) == 0;
free(disk);
fclose(f);
}
remove(tmp);
free(mem);
CHECK(ok, "yapf_save writes the same bytes as yapf_encode");
void *d = (void *)1;
size_t n = 1;
CHECK(yapf_encode(NULL, &d, &n) == YAPF_ERR_INVALID && d == NULL && n == 0,
"yapf_encode rejects a NULL image");
}
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();
save_matches_encode();
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

60
tools/build_release.sh Executable file
View file

@ -0,0 +1,60 @@
#!/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)
# yapf-kde-source.zip Dolphin thumbnail plugin source (build with CMake)
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
python3 -m zipfile -c "$OUT/yapf-kde-source.zip" extensions/kde extensions/linux/yapf-mime.xml yapf.c yapf.h
ls -la "$OUT"

454
tools/yapf_cli.c Normal file
View file

@ -0,0 +1,454 @@
/*
* 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)
* yapf compare <image.png> [more…] size and decode speed: original vs YAPF
*
* 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;
}
/* Best time over several rounds of a decode call, for fair small-image timing. */
#define YAPF_TIME_BEST(result, expr) \
do { \
double one_ = now_s(); { expr; } one_ = now_s() - one_; \
int reps_ = (int)(0.02 / (one_ > 1e-6 ? one_ : 1e-6)) + 1; \
double best_ = 1e9; \
for (int r_ = 0; r_ < 9; r_++) { \
double t_ = now_s(); \
for (int k_ = 0; k_ < reps_; k_++) { expr; } \
t_ = (now_s() - t_) / reps_; \
if (t_ < best_) best_ = t_; \
} \
(result) = best_; \
} while (0)
static const char *fmt_kb(double bytes, char *buf) {
if (bytes >= 1024.0 * 1024.0) sprintf(buf, "%.2f MB", bytes / (1024.0 * 1024.0));
else sprintf(buf, "%.1f KB", bytes / 1024.0);
return buf;
}
static const char *ext_name(const char *path) {
const char *dot = strrchr(path, '.');
static char name[8];
size_t i = 0;
if (!dot) return "file";
for (dot++; *dot && i < sizeof(name) - 1; dot++) name[i++] = (char)toupper((unsigned char)*dot);
name[i] = 0;
return name;
}
/* Original format vs YAPF: size, decode time (1 thread / all cores),
* and a lossless check. */
static int cmd_compare(int n, char **paths) {
int rc = 0;
double tot_src = 0, tot_yapf = 0, tot_src_t = 0, tot_yapf_t = 0;
for (int i = 0; i < n; i++) {
const char *path = paths[i];
if (is_yapf(path)) {
fprintf(stderr, "yapf: compare takes the original image (PNG, JPG, ...), not %s\n", path);
rc = 1;
continue;
}
FILE *f = fopen(path, "rb");
if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); rc = 1; continue; }
fseek(f, 0, SEEK_END);
long fsz = ftell(f);
rewind(f);
uint8_t *src = (uint8_t *)malloc((size_t)fsz);
if (!src || fread(src, 1, (size_t)fsz, f) != (size_t)fsz) { fclose(f); free(src); rc = 1; continue; }
fclose(f);
int w, h, ch;
uint8_t *px = stbi_load_from_memory(src, (int)fsz, &w, &h, &ch, 0);
if (!px) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); free(src); rc = 1; continue; }
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 = px; img.flags = YAPF_FLAG_SRGB;
img.gpu_format = gpu_hint(ch, 1);
void *enc = NULL;
size_t esz = 0;
double t_enc = now_s();
int erc = yapf_encode(&img, &enc, &esz);
t_enc = now_s() - t_enc;
if (erc != YAPF_OK) { fprintf(stderr, "yapf: cannot encode %s\n", path); stbi_image_free(px); free(src); rc = 1; continue; }
yapf_image_t *chk = yapf_load_memory(enc, esz);
int lossless = chk && memcmp(chk->pixels, px, (size_t)w * h * ch) == 0;
yapf_free(chk);
double t_src, t_one, t_all;
int dw, dh, dc;
YAPF_TIME_BEST(t_src, stbi_image_free(stbi_load_from_memory(src, (int)fsz, &dw, &dh, &dc, 0)));
YAPF_TIME_BEST(t_one, yapf_free(yapf_load_memory(enc, esz)));
YAPF_TIME_BEST(t_all, yapf_free(yapf_load_memory_mt(enc, esz, 0)));
double raw = (double)w * h * ch;
char a[32], b[32], c[32];
static const char *chn[] = { "", "gray", "gray+alpha", "RGB", "RGBA" };
printf("%s (%d x %d, %s)\n", path, w, h, chn[ch]);
printf(" %-5s %10s\n", "raw", fmt_kb(raw, c));
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms\n", ext_name(path), fmt_kb((double)fsz, a),
100.0 * fsz / raw, t_src * 1e3);
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms %.1fx faster, %.0f%% of the %s size\n",
"YAPF", fmt_kb((double)esz, b), 100.0 * esz / raw, t_one * 1e3,
t_src / t_one, 100.0 * esz / fsz, ext_name(path));
printf(" YAPF on all cores: decode %.3f ms (%.1f GB/s), encode %.0f ms, %s\n\n",
t_all * 1e3, raw / t_all / 1e9, t_enc * 1e3,
lossless ? "lossless: yes" : "LOSSLESS CHECK FAILED");
if (!lossless) rc = 1;
tot_src += (double)fsz; tot_yapf += (double)esz; tot_src_t += t_src; tot_yapf_t += t_one;
yapf_free_buffer(enc);
stbi_image_free(px);
free(src);
}
if (n > 1 && tot_yapf_t > 0) {
char a[32], b[32];
printf("Total: originals %s, YAPF %s (%.0f%%); decoding %.1fx faster on one core\n",
fmt_kb(tot_src, a), fmt_kb(tot_yapf, b), 100.0 * tot_yapf / tot_src, tot_src_t / tot_yapf_t);
}
return rc;
}
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"
" yapf compare <image.png> [more...] size and speed vs the original format\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 >= 3 && strcmp(argv[1], "compare") == 0) return cmd_compare(argc - 2, 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;
}

1735
yapf.c

File diff suppressed because it is too large Load diff

142
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
@ -197,6 +232,17 @@ yapf_image_t *yapf_load(const char *filename);
*/ */
int yapf_save(const char *filename, const yapf_image_t *img); int yapf_save(const char *filename, const yapf_image_t *img);
/*
* yapf_encode — encode an image into memory.
* On success *out_data points to *out_size bytes of a complete .yapf file;
* release it with yapf_free_buffer(). Same rules and return codes as
* yapf_save() (no YAPF_ERR_IO).
*/
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
/* yapf_free_buffer — release memory returned by yapf_encode(). NULL-safe. */
void yapf_free_buffer(void *data);
/* /*
* yapf_free — release all memory owned by an image returned by yapf_load(). * yapf_free — release all memory owned by an image returned by yapf_load().
* NULL-safe. * NULL-safe.