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

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

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

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

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

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

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

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

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

View file

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

View file

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

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

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

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

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

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

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

View file

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

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

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

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

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

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

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

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

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

View file

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

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

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

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

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

View file

@ -19,4 +19,6 @@ ctx.putImageData(new ImageData(YAPF.toRGBA(img), img.width, img.height), 0, 0);
`decode` throws an `Error` for corrupt or unsupported data. Speed: a 4K
image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
Tests: `node test.js`.
TypeScript types are in `yapf.d.ts`.
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.

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

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

View file

@ -3,7 +3,8 @@
"version": "1.0.0",
"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",

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

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

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

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

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

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

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

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

View file

@ -21,3 +21,5 @@ It uses the C library through `ctypes` when it finds one:
Without the library it falls back to a pure-Python decoder that needs
`numpy` (decoding only; about 1.4 s for a 4K image). `yapf.backend()`
tells you which one is in use.
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`

View file

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

View file

@ -94,6 +94,9 @@ def _find_library():
lib.yapf_load_memory_mt.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int]
lib.yapf_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__":

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

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

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

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

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

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

View file

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

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

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

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

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

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

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