Language bindings, KDE thumbnails, yapf compare, in-memory encoding

- yapf_encode() / yapf_free_buffer(): encode to memory; yapf_save uses it
- yapf.c also compiles as C++
- `yapf compare`: size and decode speed of the original format vs YAPF,
  with a lossless check
- bindings with tested examples: C++ (header-only), C# (P/Invoke),
  Rust (crate, builds the C code), Go (cgo), Java (FFM API, Java 22+),
  Zig (@cImport), LuaJIT (FFI), TypeScript types for the JS port;
  Python now encodes in memory
- docs/LANGUAGES.md and examples/ with measured results
- KDE Dolphin thumbnail plugin (KF6, CMake)
- README: honest status for Photoshop / VS Code / Figma (not run in the
  apps yet) and macOS (cross-compiled only), platform table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-03 14:39:56 +02:00
commit d7f494a0c9
52 changed files with 1926 additions and 106 deletions

8
.gitignore vendored
View file

@ -15,3 +15,11 @@ test_yapf_tmp.yapf
node_modules/ node_modules/
__pycache__/ __pycache__/
*.pyc *.pyc
# Rust
/bindings/rust/target/
/bindings/rust/Cargo.lock
# C# / Go / Java build outputs
bin/
obj/
*.class

104
README.md
View file

@ -8,8 +8,8 @@ model: every structure is byte aligned, and the decoder runs short loops that
modern CPUs execute at memory speed — with SSE / NEON where available. The modern CPUs execute at memory speed — with SSE / NEON where available. The
encoder does the hard work, so the decoder only follows instructions. encoder does the hard work, so the decoder only follows instructions.
- **6–10× faster decoding than PNG** (stb_image), single-threaded; a 4K image - **5–12× faster decoding than PNG** (stb_image), single-threaded; a 4K image
decodes in about 6 ms (5.6 GB/s). Multithreaded decoding is optional. 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 - **Smaller than PNG** on UI, icons and screenshots; 20–45 % larger on photos
and natural textures — the price of having no entropy coder. and natural textures — the price of having no entropy coder.
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB / - **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
@ -25,22 +25,32 @@ This is **YAPF version 1**, the first stable format.
| Where | What you get | Guide | Status | | Where | What you get | Guide | Status |
| :---- | :----------- | :---- | :----- | | :---- | :----------- | :---- | :----- |
| **C / C++** | `yapf.h` + `yapf.c`, drop into any project | [below](#c--c) | ✅ tested on Linux x64, Windows x64, Linux ARM64 | | **C / C++** | `yapf.h` + `yapf.c`, drop into any project (compiles as C or C++) | [below](#c--c) | ✅ tested — see [platforms](#platforms) |
| **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux, Windows | | **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `compare`, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux and Windows |
| **GIMP 3** | open, export (with mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 | | **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 | | **Blender 4.2+** | import, export, drag & drop, image as plane | [extensions/blender](extensions/blender) | ✅ tested in Blender 5.1 |
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ✅ packaged, preview tested in Chromium | | **KDE Dolphin** | thumbnails for `.yapf` files | [extensions/kde](extensions/kde) | ⚠️ tested through KDE's plugin loader (KF6); not yet inside a running Dolphin |
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ✅ plugin UI tested in Chromium | | **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnail command tested; not yet in a file manager |
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ not yet tested inside Photoshop | | **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** |
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnailer tested, not in a file manager | | **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** |
| **Python** | `yapf.load()` / `yapf.save()` | [bindings/python](bindings/python) | ✅ tested | | **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ **syntax-checked only; not yet run in Photoshop** |
| **JavaScript** | browsers, Node.js, workers | [bindings/js](bindings/js) | ✅ tested, byte-identical to C |
Pre-built downloads (command-line tool and libraries for Windows, macOS and Pre-built downloads (command-line tool and libraries for Windows, macOS and
Linux, plus the plugin packages) are published on the Linux, plus the plugin packages) are published on the
[Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can [Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can
also be built from source as described below. also be built from source as described below.
### Platforms
| Platform | Status |
| :------- | :----- |
| Linux x64 | ✅ built and fully tested |
| Windows x64 | ✅ built and fully tested (MinGW and Zig builds, run under Wine) |
| Linux ARM64 | ✅ built and fully tested (NEON, run under qemu) |
| Windows ARM64 | ⚠️ built only, not run |
| macOS x64 / ARM64 | ⚠️ **cross-compiled only, not yet run on a Mac** |
--- ---
## Command line ## Command line
@ -53,6 +63,7 @@ yapf texture.yapf mip2.png --mip 2 # export one mip level
yapf info photo.yapf # size, channels, mips, flags, ratio yapf info photo.yapf # size, channels, mips, flags, ratio
yapf bench photo.yapf # decode speed, 1 thread and all cores yapf bench photo.yapf # decode speed, 1 thread and all cores
yapf thumbnail photo.yapf thumb.png 256 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), Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
@ -62,25 +73,24 @@ Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
## Benchmark ## Benchmark
Single core, AMD Ryzen 7 5700G, `gcc -O2`. PNG is decoded with `stb_image`; `yapf compare` on a mixed set of images: one binary (`make`, gcc -O2) for
the PNG files are the ones shipped with the respective projects. "Plain C" both formats, PNG decoded with stb_image, one core, AMD Ryzen 7 5700G.
is the library built with `-DYAPF_NO_SIMD`; the default build uses SSE / NEON. Every result is checked to decode back to the exact original pixels.
| Image | Raw | PNG | **YAPF** | vs PNG | PNG decode | YAPF, plain C | **YAPF (default)** | | Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
| :--------------------- | ------: | -----: | -------: | -----: | ---------: | ------------: | -----------------: | | :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
| Wallpaper 3840×2160 | 32400 K | 1812 K | 2267 K | 125 % | 44.7 ms¹ | 10.3 ms | **5.9 ms** (5.6 GB/s) | | UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
| UI screenshot 261×430 | 438 K | 176 K | 143 K | 81 % | 1.6 ms | 0.31 ms | **0.17 ms** | | UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
| UI screenshot 305×529 | 630 K | 201 K | 185 K | 92 % | 2.1 ms | 0.45 ms | **0.24 ms** | | App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
| App icon 128×128 | 64 K | 24 K | 23 K | 93 % | 0.26 ms | 0.06 ms | **0.03 ms** | | Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
| Rock texture 512×512 | 768 K | 182 K | 221 K | 121 % | 1.4 ms | 0.35 ms | **0.16 ms** | | Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
| Wood texture 512×512 | 768 K | 185 K | 234 K | 126 % | 1.4 ms | 0.46 ms | **0.16 ms** | | Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
| Canvas texture 512×512 | 768 K | 171 K | 245 K | 143 % | 1.0 ms | 0.35 ms | **0.15 ms** | | Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
| Pixel art 512×512 | 1024 K | — | 66 K | — | — | 0.74 ms | **0.32 ms** | | YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
| Random noise 512×512 | 1024 K | — | 1024 K | — | — | 0.1 ms | 0.1 ms |
¹ PNG decode of the wallpaper measured between 44.7 and 66 ms depending on Smaller than PNG on UI-style images, 20–50 % larger on photos and textures
the harness; the table uses the fastest. `-O2` and `-O3` builds of YAPF (the price of having no entropy coder), and 5–12× faster to decode. More
perform the same. in [examples/](examples) — and run `yapf compare` on your own images.
--- ---
@ -176,6 +186,13 @@ Encodes an image to disk. Writes `img->mip_levels`
levels; `img->mips` may be `NULL` when `mip_levels == 1`. Returns `YAPF_OK` levels; `img->mips` may be `NULL` when `mip_levels == 1`. Returns `YAPF_OK`
(0) on success or a negative `YAPF_ERR_*` code on failure. (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);
``` ```
@ -256,22 +273,27 @@ TILE (64×64) header byte: stored raw, or constant-plane mask + colour tr
## Testing ## Testing
```sh ```sh
make test # C suite: 784 checks, plus JavaScript if node is installed make test # C suite: 786 checks, plus JavaScript if node is installed
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
``` ```
What has been verified for this release: What has been verified:
- exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains, - exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
single- and multi-threaded decoding; single- and multi-threaded decoding, in-memory and file encoding;
- the same tests pass on **Linux x64**, **Windows x64** (MinGW, run under - the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux
Wine), **Linux ARM64** (NEON, run under qemu), with and without SIMD, and ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as
under AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer; C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
ThreadSanitizer;
- thousands of corrupted and truncated files are rejected without crashing; - thousands of corrupted and truncated files are rejected without crashing;
- the JavaScript and Python versions produce byte-identical files to C; - every language binding runs its example; JavaScript, Python, C++, C#,
- the GIMP plugin and the Blender extension in the real applications. 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.
The macOS builds are cross-compiled and not yet run on a Mac. **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
live Dolphin / GNOME file manager.
--- ---
@ -282,14 +304,16 @@ yapf.h, yapf.c the library (format spec in yapf.h)
tools/yapf_cli.c the `yapf` command-line tool tools/yapf_cli.c the `yapf` command-line tool
tools/build_release.sh builds every download for all platforms tools/build_release.sh builds every download for all platforms
tests/test_yapf.c C test suite tests/test_yapf.c C test suite
bindings/js/ JavaScript encoder / decoder examples/ speed and size comparison, C example
bindings/python/ Python module docs/LANGUAGES.md using YAPF from other languages
bindings/ cpp, python, csharp, rust, go, java, zig, lua, js
extensions/gimp/ GIMP 3 plugin extensions/gimp/ GIMP 3 plugin
extensions/blender/ Blender extension extensions/blender/ Blender extension
extensions/kde/ Dolphin thumbnail plugin (KF6)
extensions/linux/ MIME type and freedesktop thumbnailer
extensions/vscode/ VS Code preview extensions/vscode/ VS Code preview
extensions/figma/ Figma plugin extensions/figma/ Figma plugin
extensions/photoshop/ Photoshop scripts extensions/photoshop/ Photoshop scripts
extensions/linux/ MIME type and thumbnailer
third_party/ stb_image / stb_image_write (public domain, CLI only) third_party/ stb_image / stb_image_write (public domain, CLI only)
other/YAPF.YAPF sample image (the YAPF logo) other/YAPF.YAPF sample image (the YAPF logo)
``` ```

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

View file

@ -19,4 +19,6 @@ 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 `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). image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
Tests: `node test.js`. 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}`);

View file

@ -3,7 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies", "description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies",
"main": "yapf.js", "main": "yapf.js",
"files": ["yapf.js", "README.md"], "types": "yapf.d.ts",
"files": ["yapf.js", "yapf.d.ts", "README.md"],
"scripts": { "test": "node test.js" }, "scripts": { "test": "node test.js" },
"keywords": ["image", "codec", "lossless", "yapf", "texture"], "keywords": ["image", "codec", "lossless", "yapf", "texture"],
"license": "Apache-2.0", "license": "Apache-2.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;

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

View file

@ -21,3 +21,5 @@ It uses the C library through `ctypes` when it finds one:
Without the library it falls back to a pure-Python decoder that needs 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()` `numpy` (decoding only; about 1.4 s for a 4K image). `yapf.backend()`
tells you which one is in use. 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")

View file

@ -94,6 +94,9 @@ def _find_library():
lib.yapf_load_memory_mt.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int] 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_free.argtypes = [ctypes.POINTER(_CImage)]
lib.yapf_save.argtypes = [ctypes.c_char_p, 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 return lib
except (OSError, AttributeError): except (OSError, AttributeError):
continue continue
@ -124,15 +127,20 @@ def _c_decode(data):
_lib.yapf_free(p) _lib.yapf_free(p)
def _c_save(path, img): def _c_encode(img):
bufs = [ctypes.create_string_buffer(m, len(m)) for m in img.mips] bufs = [ctypes.create_string_buffer(m, len(m)) for m in img.mips]
ptrs = (ctypes.POINTER(ctypes.c_uint8) * len(bufs))( ptrs = (ctypes.POINTER(ctypes.c_uint8) * len(bufs))(
*[ctypes.cast(b, ctypes.POINTER(ctypes.c_uint8)) for b in 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, c = _CImage(img.width, img.height, img.channels, img.gpu_format, img.flags,
len(bufs), ptrs[0], ptrs) len(bufs), ptrs[0], ptrs)
rc = _lib.yapf_save(os.fsencode(path), ctypes.byref(c)) 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: if rc != 0:
raise YapfError("yapf_save failed with code %d" % rc) 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) ────────────────────────────── # ── pure-Python fallback decoder (numpy) ──────────────────────────────
@ -280,9 +288,9 @@ def load(path):
return decode(f.read()) return decode(f.read())
def save(path, img): def encode(img):
"""Encode an Image (or anything with width/height/channels/pixels) to a """Encode an Image (or anything with width/height/channels/pixels) to
.yapf file. Needs the C library.""" YAPF bytes. Needs the C library."""
if not _lib: if not _lib:
raise YapfError("saving .yapf needs the YAPF native library, which was not found " raise YapfError("saving .yapf needs the YAPF native library, which was not found "
"for this platform; see https://github.com/Nexoniarz/YAPF#python") "for this platform; see https://github.com/Nexoniarz/YAPF#python")
@ -291,20 +299,14 @@ def save(path, img):
expected = img.width * img.height * img.channels expected = img.width * img.height * img.channels
if len(img.pixels) != expected: if len(img.pixels) != expected:
raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels))) raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels)))
_c_save(path, img) return _c_encode(img)
def encode(img): def save(path, img):
"""Encode an Image to YAPF bytes. Needs the C library.""" """Encode an Image to a .yapf file. Needs the C library."""
import tempfile data = encode(img)
fd, tmp = tempfile.mkstemp(suffix=".yapf") with open(path, "wb") as f:
os.close(fd) f.write(data)
try:
save(tmp, img)
with open(tmp, "rb") as f:
return f.read()
finally:
os.remove(tmp)
if __name__ == "__main__": if __name__ == "__main__":

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

@ -145,7 +145,9 @@ echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h
echo yapf_load_mt echo yapf_load_mt
echo yapf_load_memory_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
:: ==================================================================== :: ====================================================================

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

@ -20,7 +20,9 @@ the .blend** and survive saving and reopening.
and pick the zip. and pick the zip.
The zip contains the native YAPF library for Windows, macOS and Linux (x64 The zip contains the native YAPF library for Windows, macOS and Linux (x64
and ARM64). If no library matches your system, the extension still 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 **imports** using a pure-Python decoder (numpy, bundled with Blender), but
cannot export. cannot export.

View file

@ -30,3 +30,10 @@ node build.js
``` ```
Figma limits images to 4096 × 4096; larger `.yapf` files report an error. 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,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

@ -24,4 +24,11 @@ Decoding runs in plain JavaScript inside the preview; no native code.
code --install-extension yapf-preview-1.0.0.vsix code --install-extension yapf-preview-1.0.0.vsix
``` ```
Works in VS Code, VSCodium and other VS Code–based editors (1.75+). 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

@ -44,18 +44,39 @@ static void fill(uint8_t *p, int w, int h, int ch, int kind) {
} }
static uint8_t *encode_mem(const yapf_image_t *img, size_t *n) { 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"; const char *tmp = "test_yapf_tmp.yapf";
if (yapf_save(tmp, img) != YAPF_OK) return NULL; uint8_t px[37 * 23 * 3];
FILE *f = fopen(tmp, "rb"); fill(px, 37, 23, 3, 3);
if (!f) return NULL; yapf_image_t img;
fseek(f, 0, SEEK_END); memset(&img, 0, sizeof(img));
*n = (size_t)ftell(f); img.width = 37; img.height = 23; img.channels = 3; img.mip_levels = 1; img.pixels = px;
rewind(f); size_t len = 0;
uint8_t *b = (uint8_t *)malloc(*n); uint8_t *mem = encode_mem(&img, &len);
if (b && fread(b, 1, *n, f) != *n) { free(b); b = NULL; } int ok = mem && yapf_save(tmp, &img) == YAPF_OK;
fclose(f); 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); remove(tmp);
return b; 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) { static void roundtrip(int w, int h, int ch, int kind) {
@ -162,6 +183,7 @@ int main(int argc, char **argv) {
roundtrip(sizes[s][0], sizes[s][1], ch, kind); roundtrip(sizes[s][0], sizes[s][1], ch, kind);
mips(); mips();
corruption(); corruption();
save_matches_encode();
const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF"; const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF";
yapf_image_t *img = yapf_load(sample); yapf_image_t *img = yapf_load(sample);

View file

@ -11,6 +11,7 @@
# yapf-figma.zip Figma plugin (import via manifest.json) # yapf-figma.zip Figma plugin (import via manifest.json)
# yapf-photoshop.zip Photoshop scripts # yapf-photoshop.zip Photoshop scripts
# yapf-gimp-source.zip GIMP plugin source (build with gimptool) # yapf-gimp-source.zip GIMP plugin source (build with gimptool)
# yapf-kde-source.zip Dolphin thumbnail plugin source (build with CMake)
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
OUT=dist/release OUT=dist/release
@ -55,4 +56,5 @@ fi
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-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-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-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" ls -la "$OUT"

View file

@ -21,6 +21,7 @@
* yapf info <file.yapf> print header information * yapf info <file.yapf> print header information
* yapf bench <file.yapf> time decoding * yapf bench <file.yapf> time decoding
* yapf thumbnail <in.yapf> <out.png> [size] small preview (default 256) * 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 * Reads PNG, JPEG, BMP, TGA, GIF (first frame), PSD (composite), PNM, HDR
* (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG. * (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG.
@ -249,12 +250,119 @@ static int cmd_thumbnail(const char *in, const char *out, int size) {
return ok ? 0 : 1; 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) { static void usage(void) {
fprintf(stderr, fprintf(stderr,
"usage: yapf <input> <output> [options] convert (formats by extension)\n" "usage: yapf <input> <output> [options] convert (formats by extension)\n"
" yapf info <file.yapf> show header information\n" " yapf info <file.yapf> show header information\n"
" yapf bench <file.yapf> time decoding\n" " yapf bench <file.yapf> time decoding\n"
" yapf thumbnail <in.yapf> <out.png> [size] preview, default 256 px\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" "\n"
"reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n" "reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n"
"writes: .yapf .png .jpg .bmp .tga\n" "writes: .yapf .png .jpg .bmp .tga\n"
@ -270,6 +378,7 @@ static void usage(void) {
int main(int argc, char **argv) { 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], "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], "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) if ((argc == 4 || argc == 5) && strcmp(argv[1], "thumbnail") == 0)
return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256); return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256);
if (argc < 3) { usage(); return 2; } if (argc < 3) { usage(); return 2; }

81
yapf.c
View file

@ -193,7 +193,7 @@ static int yapf__width(uint8_t z) {
return w; return w;
} }
static const uint8_t yapf__zero_row[YAPF_TILE_SIZE + 1]; static const uint8_t yapf__zero_row[YAPF_TILE_SIZE + 1] = { 0 };
/* ── SIMD ────────────────────────────────────────────────────────────── */ /* ── SIMD ────────────────────────────────────────────────────────────── */
/* /*
@ -1409,11 +1409,14 @@ yapf_image_t *yapf_load_mt(const char *filename, int threads) {
} }
/* ======================================================================== /* ========================================================================
* yapf_save * yapf_encode / yapf_save
* ======================================================================== */ * ======================================================================== */
int yapf_save(const char *filename, const yapf_image_t *img) { int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size) {
if (!filename || !img || !img->pixels) return YAPF_ERR_INVALID; if (!out_data || !out_size) return YAPF_ERR_INVALID;
*out_data = NULL;
*out_size = 0;
if (!img || !img->pixels) return YAPF_ERR_INVALID;
if (img->width == 0 || img->width > YAPF_MAX_DIM) return YAPF_ERR_INVALID; if (img->width == 0 || img->width > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
if (img->height == 0 || img->height > YAPF_MAX_DIM) return YAPF_ERR_INVALID; if (img->height == 0 || img->height > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
if (img->channels < 1 || img->channels > 4) return YAPF_ERR_INVALID; if (img->channels < 1 || img->channels > 4) return YAPF_ERR_INVALID;
@ -1440,47 +1443,63 @@ int yapf_save(const char *filename, const yapf_image_t *img) {
} }
/* Offsets are 32-bit: refuse files that would not fit. */ /* Offsets are 32-bit: refuse files that would not fit. */
uint32_t offsets[YAPF_MAX_MIPS]; uint64_t total = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY;
uint64_t cur = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY;
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) { for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) {
offsets[m] = (uint32_t)cur; total += bws[m].len;
cur += bws[m].len; if (total > 0xFFFFFFFFull) err = YAPF_ERR_INVALID;
if (cur > 0xFFFFFFFFull) err = YAPF_ERR_INVALID;
} }
FILE *f = NULL; uint8_t *file = NULL;
if (err == YAPF_OK && !(f = fopen(filename, "wb"))) err = YAPF_ERR_IO; if (err == YAPF_OK && !(file = (uint8_t *)malloc((size_t)total))) err = YAPF_ERR_OOM;
if (err == YAPF_OK) { if (err == YAPF_OK) {
uint8_t hdr[YAPF__HDR_LEN]; memset(file, 0, YAPF__HDR_LEN);
memset(hdr, 0, sizeof(hdr)); memcpy(file, YAPF__MAGIC, 4);
memcpy(hdr, YAPF__MAGIC, 4); file[4] = YAPF__VERSION;
hdr[4] = YAPF__VERSION; file[5] = img->channels;
hdr[5] = img->channels; file[6] = img->gpu_format;
hdr[6] = img->gpu_format; file[7] = mip_levels;
hdr[7] = mip_levels; file[8] = img->flags;
hdr[8] = img->flags; yapf__w32le(file + 12, img->width);
yapf__w32le(hdr + 12, img->width); yapf__w32le(file + 16, img->height);
yapf__w32le(hdr + 16, img->height);
if (fwrite(hdr, 1, YAPF__HDR_LEN, f) != YAPF__HDR_LEN) err = YAPF_ERR_IO;
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) { size_t pos = YAPF__HDR_LEN + (size_t)mip_levels * YAPF__MIP_ENTRY;
uint8_t entry[YAPF__MIP_ENTRY]; for (uint8_t m = 0; m < mip_levels; m++) {
yapf__w32le(entry + 0, offsets[m]); uint8_t *entry = file + YAPF__HDR_LEN + (size_t)m * YAPF__MIP_ENTRY;
yapf__w32le(entry + 0, (uint32_t)pos);
yapf__w32le(entry + 4, (uint32_t)bws[m].len); yapf__w32le(entry + 4, (uint32_t)bws[m].len);
if (fwrite(entry, 1, YAPF__MIP_ENTRY, f) != YAPF__MIP_ENTRY) memcpy(file + pos, bws[m].buf, bws[m].len);
err = YAPF_ERR_IO; pos += bws[m].len;
} }
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) *out_data = file;
if (fwrite(bws[m].buf, 1, bws[m].len, f) != bws[m].len) *out_size = (size_t)total;
err = YAPF_ERR_IO;
if (fclose(f) != 0 && err == YAPF_OK) err = YAPF_ERR_IO;
} }
for (uint8_t m = 0; m < mip_levels; m++) free(bws[m].buf); for (uint8_t m = 0; m < mip_levels; m++) free(bws[m].buf);
return err; return err;
} }
void yapf_free_buffer(void *data) {
free(data);
}
int yapf_save(const char *filename, const yapf_image_t *img) {
if (!filename) return YAPF_ERR_INVALID;
void *data = NULL;
size_t size = 0;
int err = yapf_encode(img, &data, &size);
if (err != YAPF_OK) return err;
FILE *f = fopen(filename, "wb");
if (!f) err = YAPF_ERR_IO;
else {
if (fwrite(data, 1, size, f) != size) err = YAPF_ERR_IO;
if (fclose(f) != 0 && err == YAPF_OK) err = YAPF_ERR_IO;
}
yapf_free_buffer(data);
return err;
}
/* ======================================================================== /* ========================================================================
* yapf_free * yapf_free
* ======================================================================== */ * ======================================================================== */

11
yapf.h
View file

@ -232,6 +232,17 @@ yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
*/ */
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.