diff --git a/.gitignore b/.gitignore index d93af31..7de58a5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,11 @@ test_yapf_tmp.yapf node_modules/ __pycache__/ *.pyc + +# Rust +/bindings/rust/target/ +/bindings/rust/Cargo.lock +# C# / Go / Java build outputs +bin/ +obj/ +*.class diff --git a/README.md b/README.md index e9bf4a0..2480e4d 100644 --- a/README.md +++ b/README.md @@ -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 encoder does the hard work, so the decoder only follows instructions. -- **6–10× faster decoding than PNG** (stb_image), single-threaded; a 4K image - decodes in about 6 ms (5.6 GB/s). Multithreaded decoding is optional. +- **5–12× faster decoding than PNG** (stb_image), single-threaded; a 4K image + decodes in about 6–7 ms (around 5 GB/s). Multithreaded decoding is optional. - **Smaller than PNG** on UI, icons and screenshots; 20–45 % larger on photos and natural textures — the price of having no entropy coder. - **Made for games and tools too**: mipmaps, GPU format hint, sRGB / @@ -25,22 +25,32 @@ This is **YAPF version 1**, the first stable format. | 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 | -| **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux, Windows | -| **GIMP 3** | open, export (with mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 | +| **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, `compare`, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux and Windows | +| **Your language** | Python, C#, Rust, Go, Java, Zig, Lua, JavaScript / TypeScript, … | [docs/LANGUAGES.md](docs/LANGUAGES.md) | ✅ every binding tested | +| **GIMP 3** | open, export (flattened, optional mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 | | **Blender 4.2+** | import, export, drag & drop, image as plane | [extensions/blender](extensions/blender) | ✅ tested in Blender 5.1 | -| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ✅ packaged, preview tested in Chromium | -| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ✅ plugin UI tested in Chromium | -| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ not yet tested inside Photoshop | -| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnailer tested, not in a file manager | -| **Python** | `yapf.load()` / `yapf.save()` | [bindings/python](bindings/python) | ✅ tested | -| **JavaScript** | browsers, Node.js, workers | [bindings/js](bindings/js) | ✅ tested, byte-identical to C | +| **KDE Dolphin** | thumbnails for `.yapf` files | [extensions/kde](extensions/kde) | ⚠️ tested through KDE's plugin loader (KF6); not yet inside a running Dolphin | +| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnail command tested; not yet in a file manager | +| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ⚠️ packaged; preview code tested in a browser engine (Chromium); **not yet run inside VS Code** | +| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ⚠️ plugin UI tested in a browser engine with a simulated Figma; **not yet run inside Figma** | +| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ **syntax-checked only; not yet run in Photoshop** | Pre-built downloads (command-line tool and libraries for Windows, macOS and Linux, plus the plugin packages) are published on the [Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can 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 @@ -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 bench photo.yapf # decode speed, 1 thread and all cores yapf thumbnail photo.yapf thumb.png 256 +yapf compare photo.png ui.png # size and decode speed vs the original format ``` Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB), @@ -62,25 +73,24 @@ Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB), ## Benchmark -Single core, AMD Ryzen 7 5700G, `gcc -O2`. PNG is decoded with `stb_image`; -the PNG files are the ones shipped with the respective projects. "Plain C" -is the library built with `-DYAPF_NO_SIMD`; the default build uses SSE / NEON. +`yapf compare` on a mixed set of images: one binary (`make`, gcc -O2) for +both formats, PNG decoded with stb_image, one core, AMD Ryzen 7 5700G. +Every result is checked to decode back to the exact original pixels. -| Image | Raw | PNG | **YAPF** | vs PNG | PNG decode | YAPF, plain C | **YAPF (default)** | -| :--------------------- | ------: | -----: | -------: | -----: | ---------: | ------------: | -----------------: | -| Wallpaper 3840×2160 | 32400 K | 1812 K | 2267 K | 125 % | 44.7 ms¹ | 10.3 ms | **5.9 ms** (5.6 GB/s) | -| UI screenshot 261×430 | 438 K | 176 K | 143 K | 81 % | 1.6 ms | 0.31 ms | **0.17 ms** | -| UI screenshot 305×529 | 630 K | 201 K | 185 K | 92 % | 2.1 ms | 0.45 ms | **0.24 ms** | -| App icon 128×128 | 64 K | 24 K | 23 K | 93 % | 0.26 ms | 0.06 ms | **0.03 ms** | -| Rock texture 512×512 | 768 K | 182 K | 221 K | 121 % | 1.4 ms | 0.35 ms | **0.16 ms** | -| Wood texture 512×512 | 768 K | 185 K | 234 K | 126 % | 1.4 ms | 0.46 ms | **0.16 ms** | -| Canvas texture 512×512 | 768 K | 171 K | 245 K | 143 % | 1.0 ms | 0.35 ms | **0.15 ms** | -| Pixel art 512×512 | 1024 K | — | 66 K | — | — | 0.74 ms | **0.32 ms** | -| Random noise 512×512 | 1024 K | — | 1024 K | — | — | 0.1 ms | 0.1 ms | +| 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×** | -¹ PNG decode of the wallpaper measured between 44.7 and 66 ms depending on -the harness; the table uses the fastest. `-O2` and `-O3` builds of YAPF -perform the same. +Smaller than PNG on UI-style images, 20–50 % larger on photos and textures +(the price of having no entropy coder), and 5–12× faster to decode. More +in [examples/](examples) — and run `yapf compare` on your own images. --- @@ -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` (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 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 ```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 ``` -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, - single- and multi-threaded decoding; -- the same tests pass on **Linux x64**, **Windows x64** (MinGW, run under - Wine), **Linux ARM64** (NEON, run under qemu), with and without SIMD, and - under AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer; + single- and multi-threaded decoding, in-memory and file encoding; +- the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux + ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as + C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and + ThreadSanitizer; - thousands of corrupted and truncated files are rejected without crashing; -- the JavaScript and Python versions produce byte-identical files to C; -- the GIMP plugin and the Blender extension in the real applications. +- every language binding runs its example; JavaScript, Python, C++, C#, + Rust, Go, Java and Lua re-encode the sample to byte-identical output; +- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1; +- the KDE thumbnailer through KDE's own plugin loader. -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/build_release.sh builds every download for all platforms tests/test_yapf.c C test suite -bindings/js/ JavaScript encoder / decoder -bindings/python/ Python module +examples/ speed and size comparison, C example +docs/LANGUAGES.md using YAPF from other languages +bindings/ cpp, python, csharp, rust, go, java, zig, lua, js extensions/gimp/ GIMP 3 plugin extensions/blender/ Blender extension +extensions/kde/ Dolphin thumbnail plugin (KF6) +extensions/linux/ MIME type and freedesktop thumbnailer extensions/vscode/ VS Code preview extensions/figma/ Figma plugin extensions/photoshop/ Photoshop scripts -extensions/linux/ MIME type and thumbnailer third_party/ stb_image / stb_image_write (public domain, CLI only) other/YAPF.YAPF sample image (the YAPF logo) ``` diff --git a/bindings/cpp/README.md b/bindings/cpp/README.md new file mode 100644 index 0000000..e6d9a77 --- /dev/null +++ b/bindings/cpp/README.md @@ -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 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` diff --git a/bindings/cpp/example.cpp b/bindings/cpp/example.cpp new file mode 100644 index 0000000..633114e --- /dev/null +++ b/bindings/cpp/example.cpp @@ -0,0 +1,33 @@ +// g++ -O2 -std=c++17 example.cpp ../../yapf.c -o example -pthread && ./example ../../other/YAPF.YAPF +#include "yapf.hpp" + +#include +#include +#include +#include +#include + +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 bytes((std::istreambuf_iterator(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(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 again = img.encode(); + std::printf("re-encoded: %zu bytes, identical to the file: %s\n", again.size(), + again == bytes ? "yes" : "no"); + + std::vector 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"); +} diff --git a/bindings/cpp/yapf.hpp b/bindings/cpp/yapf.hpp new file mode 100644 index 0000000..ffd09e5 --- /dev/null +++ b/bindings/cpp/yapf.hpp @@ -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 file = img.encode(); +// yapf::Image mine(256, 256, 4, my_rgba_bytes); mine.save("out.yapf"); +#pragma once + +extern "C" { +#include "../../yapf.h" +} + +#include +#include +#include +#include +#include +#include + +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 &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 encode() const { + void *data = nullptr; + size_t n = 0; + if (yapf_encode(&raw(), &data, &n) != YAPF_OK) throw Error("YAPF: encoding failed"); + std::vector out(static_cast(data), static_cast(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 own_; // or pixels we own + yapf_image_t view_{}; +}; + +} // namespace yapf diff --git a/bindings/csharp/Example/Example.csproj b/bindings/csharp/Example/Example.csproj new file mode 100644 index 0000000..f0d0e5a --- /dev/null +++ b/bindings/csharp/Example/Example.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + enable + enable + + + + + diff --git a/bindings/csharp/Example/Program.cs b/bindings/csharp/Example/Program.cs new file mode 100644 index 0000000..d6f1bcc --- /dev/null +++ b/bindings/csharp/Example/Program.cs @@ -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"); diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md new file mode 100644 index 0000000..0aeb9dc --- /dev/null +++ b/bindings/csharp/README.md @@ -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`). diff --git a/bindings/csharp/Yapf.cs b/bindings/csharp/Yapf.cs new file mode 100644 index 0000000..64d23ef --- /dev/null +++ b/bindings/csharp/Yapf.cs @@ -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); + } + } + + /// Decode YAPF bytes. threads: 1 = calling thread, 0 = all cores. + 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(p)); } + finally { NativeMethods.yapf_free(p); } + } + + public static YapfImage Load(string path, int threads = 1) => Decode(File.ReadAllBytes(path), threads); + + /// Encode to the bytes of a .yapf file. + 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); + } + } +} diff --git a/bindings/go/README.md b/bindings/go/README.md new file mode 100644 index 0000000..f889865 --- /dev/null +++ b/bindings/go/README.md @@ -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`. diff --git a/bindings/go/example/main.go b/bindings/go/example/main.go new file mode 100644 index 0000000..5fac6ea --- /dev/null +++ b/bindings/go/example/main.go @@ -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") +} diff --git a/bindings/go/go.mod b/bindings/go/go.mod new file mode 100644 index 0000000..13f58b7 --- /dev/null +++ b/bindings/go/go.mod @@ -0,0 +1,3 @@ +module github.com/Nexoniarz/YAPF/bindings/go + +go 1.21 diff --git a/bindings/go/yapf.go b/bindings/go/yapf.go new file mode 100644 index 0000000..5869d5b --- /dev/null +++ b/bindings/go/yapf.go @@ -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 +#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) +} diff --git a/bindings/go/yapf_c.c b/bindings/go/yapf_c.c new file mode 100644 index 0000000..006b23c --- /dev/null +++ b/bindings/go/yapf_c.c @@ -0,0 +1,2 @@ +// Builds the C library as part of this Go package (cgo compiles .c files here). +#include "../../yapf.c" diff --git a/bindings/go/yapf_test.go b/bindings/go/yapf_test.go new file mode 100644 index 0000000..7910b1c --- /dev/null +++ b/bindings/go/yapf_test.go @@ -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") + } +} diff --git a/bindings/java/Example.java b/bindings/java/Example.java new file mode 100644 index 0000000..d12f7ca --- /dev/null +++ b/bindings/java/Example.java @@ -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"); + } +} diff --git a/bindings/java/README.md b/bindings/java/README.md new file mode 100644 index 0000000..605ce11 --- /dev/null +++ b/bindings/java/README.md @@ -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` diff --git a/bindings/java/Yapf.java b/bindings/java/Yapf.java new file mode 100644 index 0000000..80e97c5 --- /dev/null +++ b/bindings/java/Yapf.java @@ -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)); } +} diff --git a/bindings/js/README.md b/bindings/js/README.md index 849f640..309c0c7 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -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 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`. diff --git a/bindings/js/example.mjs b/bindings/js/example.mjs new file mode 100644 index 0000000..4974404 --- /dev/null +++ b/bindings/js/example.mjs @@ -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}`); diff --git a/bindings/js/package.json b/bindings/js/package.json index 81c885c..bb4c1d7 100644 --- a/bindings/js/package.json +++ b/bindings/js/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies", "main": "yapf.js", - "files": ["yapf.js", "README.md"], + "types": "yapf.d.ts", + "files": ["yapf.js", "yapf.d.ts", "README.md"], "scripts": { "test": "node test.js" }, "keywords": ["image", "codec", "lossless", "yapf", "texture"], "license": "Apache-2.0", diff --git a/bindings/js/yapf.d.ts b/bindings/js/yapf.d.ts new file mode 100644 index 0000000..1e52691 --- /dev/null +++ b/bindings/js/yapf.d.ts @@ -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; +/** 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; diff --git a/bindings/lua/README.md b/bindings/lua/README.md new file mode 100644 index 0000000..5548cb9 --- /dev/null +++ b/bindings/lua/README.md @@ -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` diff --git a/bindings/lua/example.lua b/bindings/lua/example.lua new file mode 100644 index 0000000..35d4fe0 --- /dev/null +++ b/bindings/lua/example.lua @@ -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))) diff --git a/bindings/lua/yapf.lua b/bindings/lua/yapf.lua new file mode 100644 index 0000000..39c0a12 --- /dev/null +++ b/bindings/lua/yapf.lua @@ -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 diff --git a/bindings/python/README.md b/bindings/python/README.md index 3c93be7..33e52c8 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -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 `numpy` (decoding only; about 1.4 s for a 4K image). `yapf.backend()` tells you which one is in use. + +Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF` diff --git a/bindings/python/example.py b/bindings/python/example.py new file mode 100644 index 0000000..1b13fae --- /dev/null +++ b/bindings/python/example.py @@ -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") diff --git a/bindings/python/yapf.py b/bindings/python/yapf.py index 86472d1..9f8f3a1 100644 --- a/bindings/python/yapf.py +++ b/bindings/python/yapf.py @@ -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_free.argtypes = [ctypes.POINTER(_CImage)] lib.yapf_save.argtypes = [ctypes.c_char_p, ctypes.POINTER(_CImage)] + lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_size_t)] + lib.yapf_free_buffer.argtypes = [ctypes.c_void_p] return lib except (OSError, AttributeError): continue @@ -124,15 +127,20 @@ def _c_decode(data): _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] ptrs = (ctypes.POINTER(ctypes.c_uint8) * len(bufs))( *[ctypes.cast(b, ctypes.POINTER(ctypes.c_uint8)) for b in bufs]) c = _CImage(img.width, img.height, img.channels, img.gpu_format, img.flags, len(bufs), ptrs[0], ptrs) - rc = _lib.yapf_save(os.fsencode(path), ctypes.byref(c)) + data, size = ctypes.c_void_p(), ctypes.c_size_t() + rc = _lib.yapf_encode(ctypes.byref(c), ctypes.byref(data), ctypes.byref(size)) if rc != 0: - raise YapfError("yapf_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) ────────────────────────────── @@ -280,9 +288,9 @@ def load(path): return decode(f.read()) -def save(path, img): - """Encode an Image (or anything with width/height/channels/pixels) to a - .yapf file. Needs the C library.""" +def encode(img): + """Encode an Image (or anything with width/height/channels/pixels) to + YAPF bytes. Needs the C library.""" if not _lib: raise YapfError("saving .yapf needs the YAPF native library, which was not found " "for this platform; see https://github.com/Nexoniarz/YAPF#python") @@ -291,20 +299,14 @@ def save(path, img): expected = img.width * img.height * img.channels if len(img.pixels) != expected: raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels))) - _c_save(path, img) + return _c_encode(img) -def encode(img): - """Encode an Image to YAPF bytes. Needs the C library.""" - import tempfile - fd, tmp = tempfile.mkstemp(suffix=".yapf") - os.close(fd) - try: - save(tmp, img) - with open(tmp, "rb") as f: - return f.read() - finally: - os.remove(tmp) +def save(path, img): + """Encode an Image to a .yapf file. Needs the C library.""" + data = encode(img) + with open(path, "wb") as f: + f.write(data) if __name__ == "__main__": diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 0000000..759099c --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -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" diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 0000000..5eef983 --- /dev/null +++ b/bindings/rust/README.md @@ -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 +``` + +Example: `cargo run --release --example demo -- ../../other/YAPF.YAPF` · +tests: `cargo test`. diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs new file mode 100644 index 0000000..8d56bac --- /dev/null +++ b/bindings/rust/build.rs @@ -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()); +} diff --git a/bindings/rust/examples/demo.rs b/bindings/rust/examples/demo.rs new file mode 100644 index 0000000..3be07e1 --- /dev/null +++ b/bindings/rust/examples/demo.rs @@ -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 = (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(()) +} diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 0000000..7c4802e --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -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 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, + /// Smaller mip levels (1, 2, …); empty when the file stores only the base image. + pub mips: Vec>, +} + +impl Image { + pub fn new(width: u32, height: u32, channels: u8, pixels: Vec) -> 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 { + 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::decode_with_threads(data, 1) } + + pub fn load>(path: P) -> Result { + Image::decode(&std::fs::read(path)?) + } + + /// Encode to the bytes of a `.yapf` file. + pub fn encode(&self) -> Result, 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>(&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 = (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()); + } +} diff --git a/bindings/zig/README.md b/bindings/zig/README.md new file mode 100644 index 0000000..09c7271 --- /dev/null +++ b/bindings/zig/README.md @@ -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`. diff --git a/bindings/zig/example.zig b/bindings/zig/example.zig new file mode 100644 index 0000000..5a99540 --- /dev/null +++ b/bindings/zig/example.zig @@ -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 }); +} diff --git a/build.bat b/build.bat index 5c3d268..801253c 100644 --- a/build.bat +++ b/build.bat @@ -145,7 +145,9 @@ echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h echo yapf_load_mt echo yapf_load_memory_mt echo yapf_save + echo yapf_encode echo yapf_free + echo yapf_free_buffer ) > dist\_yapf.def :: ==================================================================== diff --git a/docs/LANGUAGES.md b/docs/LANGUAGES.md new file mode 100644 index 0000000..7029222 --- /dev/null +++ b/docs/LANGUAGES.md @@ -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 Function(Pointer, 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)`. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..57fd093 --- /dev/null +++ b/examples/README.md @@ -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. diff --git a/examples/c/example.c b/examples/c/example.c new file mode 100644 index 0000000..cf07d13 --- /dev/null +++ b/examples/c/example.c @@ -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 +#include +#include +#include + +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; +} diff --git a/extensions/blender/README.md b/extensions/blender/README.md index cacec89..046d5af 100644 --- a/extensions/blender/README.md +++ b/extensions/blender/README.md @@ -20,7 +20,9 @@ the .blend** and survive saving and reopening. and pick the zip. The zip contains the native YAPF library for Windows, macOS and Linux (x64 -and ARM64). If no library matches your system, the extension still +and ARM64). Tested in Blender 5.1 on Linux; the Windows library is tested +outside Blender, and the macOS libraries are cross-compiled and **not yet run +on a Mac**. If no library matches your system, the extension still **imports** using a pure-Python decoder (numpy, bundled with Blender), but cannot export. diff --git a/extensions/figma/README.md b/extensions/figma/README.md index 379cfb1..487853f 100644 --- a/extensions/figma/README.md +++ b/extensions/figma/README.md @@ -30,3 +30,10 @@ node build.js ``` Figma limits images to 4096 × 4096; larger `.yapf` files report an error. + +## Status + +The plugin UI was tested in a browser engine (headless Chromium) with a +simulated Figma main thread: import hands Figma a pixel-exact PNG, and +export downloads a pixel-exact `.yapf`. `code.js` is syntax-checked. +**It has not yet been run inside Figma itself.** diff --git a/extensions/kde/CMakeLists.txt b/extensions/kde/CMakeLists.txt new file mode 100644 index 0000000..453c78e --- /dev/null +++ b/extensions/kde/CMakeLists.txt @@ -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) diff --git a/extensions/kde/README.md b/extensions/kde/README.md new file mode 100644 index 0000000..6283557 --- /dev/null +++ b/extensions/kde/README.md @@ -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. diff --git a/extensions/kde/kde-shell.nix b/extensions/kde/kde-shell.nix new file mode 100644 index 0000000..bcb1544 --- /dev/null +++ b/extensions/kde/kde-shell.nix @@ -0,0 +1,6 @@ +# Development shell for building the plugin on NixOS: nix-shell kde-shell.nix +with import {}; +mkShell { + nativeBuildInputs = [ cmake kdePackages.extra-cmake-modules pkg-config ]; + buildInputs = [ kdePackages.kio kdePackages.qtbase kdePackages.kcoreaddons ]; +} diff --git a/extensions/kde/yapfthumbnail.cpp b/extensions/kde/yapfthumbnail.cpp new file mode 100644 index 0000000..6842a65 --- /dev/null +++ b/extensions/kde/yapfthumbnail.cpp @@ -0,0 +1,79 @@ +// YAPF thumbnails for KDE Dolphin and other KIO file dialogs. +// Copyright 2026 Nexoniarz — Apache License 2.0. + +#include +#include +#include + +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" diff --git a/extensions/kde/yapfthumbnail.json b/extensions/kde/yapfthumbnail.json new file mode 100644 index 0000000..4135ab1 --- /dev/null +++ b/extensions/kde/yapfthumbnail.json @@ -0,0 +1,7 @@ +{ + "CacheThumbnail": true, + "KPlugin": { + "MimeTypes": ["image/x-yapf"], + "Name": "YAPF Images" + } +} diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md index d004122..b0f3da0 100644 --- a/extensions/vscode/README.md +++ b/extensions/vscode/README.md @@ -24,4 +24,11 @@ Decoding runs in plain JavaScript inside the preview; no native code. 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.** diff --git a/tests/test_yapf.c b/tests/test_yapf.c index ed49421..2412684 100644 --- a/tests/test_yapf.c +++ b/tests/test_yapf.c @@ -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) { + 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"; - if (yapf_save(tmp, img) != YAPF_OK) return NULL; - FILE *f = fopen(tmp, "rb"); - if (!f) return NULL; - fseek(f, 0, SEEK_END); - *n = (size_t)ftell(f); - rewind(f); - uint8_t *b = (uint8_t *)malloc(*n); - if (b && fread(b, 1, *n, f) != *n) { free(b); b = NULL; } - fclose(f); + uint8_t px[37 * 23 * 3]; + fill(px, 37, 23, 3, 3); + yapf_image_t img; + memset(&img, 0, sizeof(img)); + img.width = 37; img.height = 23; img.channels = 3; img.mip_levels = 1; img.pixels = px; + size_t len = 0; + uint8_t *mem = encode_mem(&img, &len); + int ok = mem && yapf_save(tmp, &img) == YAPF_OK; + FILE *f = ok ? fopen(tmp, "rb") : NULL; + if (f) { + uint8_t *disk = (uint8_t *)malloc(len + 1); + ok = fread(disk, 1, len + 1, f) == len && memcmp(disk, mem, len) == 0; + free(disk); + fclose(f); + } remove(tmp); - 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) { @@ -162,6 +183,7 @@ int main(int argc, char **argv) { roundtrip(sizes[s][0], sizes[s][1], ch, kind); mips(); corruption(); + save_matches_encode(); const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF"; yapf_image_t *img = yapf_load(sample); diff --git a/tools/build_release.sh b/tools/build_release.sh index 1f78f67..540c332 100755 --- a/tools/build_release.sh +++ b/tools/build_release.sh @@ -11,6 +11,7 @@ # yapf-figma.zip Figma plugin (import via manifest.json) # yapf-photoshop.zip Photoshop scripts # yapf-gimp-source.zip GIMP plugin source (build with gimptool) +# yapf-kde-source.zip Dolphin thumbnail plugin source (build with CMake) set -euo pipefail cd "$(dirname "$0")/.." OUT=dist/release @@ -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-photoshop.zip" extensions/photoshop python3 -m zipfile -c "$OUT/yapf-gimp-source.zip" extensions/gimp yapf.c yapf.h +python3 -m zipfile -c "$OUT/yapf-kde-source.zip" extensions/kde extensions/linux/yapf-mime.xml yapf.c yapf.h ls -la "$OUT" diff --git a/tools/yapf_cli.c b/tools/yapf_cli.c index 7571436..effec05 100644 --- a/tools/yapf_cli.c +++ b/tools/yapf_cli.c @@ -21,6 +21,7 @@ * yapf info print header information * yapf bench time decoding * yapf thumbnail [size] small preview (default 256) + * yapf compare [more…] size and decode speed: original vs YAPF * * Reads PNG, JPEG, BMP, TGA, GIF (first frame), PSD (composite), PNM, HDR * (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG. @@ -249,12 +250,119 @@ static int cmd_thumbnail(const char *in, const char *out, int size) { return ok ? 0 : 1; } +/* Best time over several rounds of a decode call, for fair small-image timing. */ +#define YAPF_TIME_BEST(result, expr) \ + do { \ + double one_ = now_s(); { expr; } one_ = now_s() - one_; \ + int reps_ = (int)(0.02 / (one_ > 1e-6 ? one_ : 1e-6)) + 1; \ + double best_ = 1e9; \ + for (int r_ = 0; r_ < 9; r_++) { \ + double t_ = now_s(); \ + for (int k_ = 0; k_ < reps_; k_++) { expr; } \ + t_ = (now_s() - t_) / reps_; \ + if (t_ < best_) best_ = t_; \ + } \ + (result) = best_; \ + } while (0) + +static const char *fmt_kb(double bytes, char *buf) { + if (bytes >= 1024.0 * 1024.0) sprintf(buf, "%.2f MB", bytes / (1024.0 * 1024.0)); + else sprintf(buf, "%.1f KB", bytes / 1024.0); + return buf; +} + +static const char *ext_name(const char *path) { + const char *dot = strrchr(path, '.'); + static char name[8]; + size_t i = 0; + if (!dot) return "file"; + for (dot++; *dot && i < sizeof(name) - 1; dot++) name[i++] = (char)toupper((unsigned char)*dot); + name[i] = 0; + return name; +} + +/* Original format vs YAPF: size, decode time (1 thread / all cores), + * and a lossless check. */ +static int cmd_compare(int n, char **paths) { + int rc = 0; + double tot_src = 0, tot_yapf = 0, tot_src_t = 0, tot_yapf_t = 0; + for (int i = 0; i < n; i++) { + const char *path = paths[i]; + if (is_yapf(path)) { + fprintf(stderr, "yapf: compare takes the original image (PNG, JPG, ...), not %s\n", path); + rc = 1; + continue; + } + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); rc = 1; continue; } + fseek(f, 0, SEEK_END); + long fsz = ftell(f); + rewind(f); + uint8_t *src = (uint8_t *)malloc((size_t)fsz); + if (!src || fread(src, 1, (size_t)fsz, f) != (size_t)fsz) { fclose(f); free(src); rc = 1; continue; } + fclose(f); + + int w, h, ch; + uint8_t *px = stbi_load_from_memory(src, (int)fsz, &w, &h, &ch, 0); + if (!px) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); free(src); rc = 1; continue; } + + yapf_image_t img; + memset(&img, 0, sizeof(img)); + img.width = (uint32_t)w; img.height = (uint32_t)h; img.channels = (uint8_t)ch; + img.mip_levels = 1; img.pixels = px; img.flags = YAPF_FLAG_SRGB; + img.gpu_format = gpu_hint(ch, 1); + + void *enc = NULL; + size_t esz = 0; + double t_enc = now_s(); + int erc = yapf_encode(&img, &enc, &esz); + t_enc = now_s() - t_enc; + if (erc != YAPF_OK) { fprintf(stderr, "yapf: cannot encode %s\n", path); stbi_image_free(px); free(src); rc = 1; continue; } + + yapf_image_t *chk = yapf_load_memory(enc, esz); + int lossless = chk && memcmp(chk->pixels, px, (size_t)w * h * ch) == 0; + yapf_free(chk); + + double t_src, t_one, t_all; + int dw, dh, dc; + YAPF_TIME_BEST(t_src, stbi_image_free(stbi_load_from_memory(src, (int)fsz, &dw, &dh, &dc, 0))); + YAPF_TIME_BEST(t_one, yapf_free(yapf_load_memory(enc, esz))); + YAPF_TIME_BEST(t_all, yapf_free(yapf_load_memory_mt(enc, esz, 0))); + + double raw = (double)w * h * ch; + char a[32], b[32], c[32]; + static const char *chn[] = { "", "gray", "gray+alpha", "RGB", "RGBA" }; + printf("%s (%d x %d, %s)\n", path, w, h, chn[ch]); + printf(" %-5s %10s\n", "raw", fmt_kb(raw, c)); + printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms\n", ext_name(path), fmt_kb((double)fsz, a), + 100.0 * fsz / raw, t_src * 1e3); + printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms %.1fx faster, %.0f%% of the %s size\n", + "YAPF", fmt_kb((double)esz, b), 100.0 * esz / raw, t_one * 1e3, + t_src / t_one, 100.0 * esz / fsz, ext_name(path)); + printf(" YAPF on all cores: decode %.3f ms (%.1f GB/s), encode %.0f ms, %s\n\n", + t_all * 1e3, raw / t_all / 1e9, t_enc * 1e3, + lossless ? "lossless: yes" : "LOSSLESS CHECK FAILED"); + if (!lossless) rc = 1; + tot_src += (double)fsz; tot_yapf += (double)esz; tot_src_t += t_src; tot_yapf_t += t_one; + yapf_free_buffer(enc); + stbi_image_free(px); + free(src); + } + if (n > 1 && tot_yapf_t > 0) { + char a[32], b[32]; + printf("Total: originals %s, YAPF %s (%.0f%%); decoding %.1fx faster on one core\n", + fmt_kb(tot_src, a), fmt_kb(tot_yapf, b), 100.0 * tot_yapf / tot_src, tot_src_t / tot_yapf_t); + } + return rc; +} + static void usage(void) { fprintf(stderr, "usage: yapf [options] convert (formats by extension)\n" " yapf info show header information\n" " yapf bench time decoding\n" " yapf thumbnail [size] preview, default 256 px\n" + " yapf compare [more...] size and speed vs the original format\n" "\n" "reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n" "writes: .yapf .png .jpg .bmp .tga\n" @@ -270,6 +378,7 @@ static void usage(void) { int main(int argc, char **argv) { if (argc == 3 && strcmp(argv[1], "info") == 0) return cmd_info(argv[2]); if (argc == 3 && strcmp(argv[1], "bench") == 0) return cmd_bench(argv[2]); + if (argc >= 3 && strcmp(argv[1], "compare") == 0) return cmd_compare(argc - 2, argv + 2); if ((argc == 4 || argc == 5) && strcmp(argv[1], "thumbnail") == 0) return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256); if (argc < 3) { usage(); return 2; } diff --git a/yapf.c b/yapf.c index f27a52f..d926109 100644 --- a/yapf.c +++ b/yapf.c @@ -193,7 +193,7 @@ static int yapf__width(uint8_t z) { 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 ────────────────────────────────────────────────────────────── */ /* @@ -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) { - if (!filename || !img || !img->pixels) return YAPF_ERR_INVALID; +int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size) { + 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->height == 0 || img->height > YAPF_MAX_DIM) 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. */ - uint32_t offsets[YAPF_MAX_MIPS]; - uint64_t cur = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY; + uint64_t total = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY; for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) { - offsets[m] = (uint32_t)cur; - cur += bws[m].len; - if (cur > 0xFFFFFFFFull) err = YAPF_ERR_INVALID; + total += bws[m].len; + if (total > 0xFFFFFFFFull) err = YAPF_ERR_INVALID; } - FILE *f = NULL; - if (err == YAPF_OK && !(f = fopen(filename, "wb"))) err = YAPF_ERR_IO; + uint8_t *file = NULL; + if (err == YAPF_OK && !(file = (uint8_t *)malloc((size_t)total))) err = YAPF_ERR_OOM; if (err == YAPF_OK) { - uint8_t hdr[YAPF__HDR_LEN]; - memset(hdr, 0, sizeof(hdr)); - memcpy(hdr, YAPF__MAGIC, 4); - hdr[4] = YAPF__VERSION; - hdr[5] = img->channels; - hdr[6] = img->gpu_format; - hdr[7] = mip_levels; - hdr[8] = img->flags; - yapf__w32le(hdr + 12, img->width); - yapf__w32le(hdr + 16, img->height); - if (fwrite(hdr, 1, YAPF__HDR_LEN, f) != YAPF__HDR_LEN) err = YAPF_ERR_IO; + memset(file, 0, YAPF__HDR_LEN); + memcpy(file, YAPF__MAGIC, 4); + file[4] = YAPF__VERSION; + file[5] = img->channels; + file[6] = img->gpu_format; + file[7] = mip_levels; + file[8] = img->flags; + yapf__w32le(file + 12, img->width); + yapf__w32le(file + 16, img->height); - for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) { - uint8_t entry[YAPF__MIP_ENTRY]; - yapf__w32le(entry + 0, offsets[m]); + size_t pos = YAPF__HDR_LEN + (size_t)mip_levels * YAPF__MIP_ENTRY; + for (uint8_t m = 0; m < mip_levels; 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); - if (fwrite(entry, 1, YAPF__MIP_ENTRY, f) != YAPF__MIP_ENTRY) - err = YAPF_ERR_IO; + memcpy(file + pos, bws[m].buf, bws[m].len); + pos += bws[m].len; } - for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) - if (fwrite(bws[m].buf, 1, bws[m].len, f) != bws[m].len) - err = YAPF_ERR_IO; - if (fclose(f) != 0 && err == YAPF_OK) err = YAPF_ERR_IO; + *out_data = file; + *out_size = (size_t)total; } for (uint8_t m = 0; m < mip_levels; m++) free(bws[m].buf); 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 * ======================================================================== */ diff --git a/yapf.h b/yapf.h index 463d689..4e60939 100644 --- a/yapf.h +++ b/yapf.h @@ -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); +/* + * 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(). * NULL-safe.