mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
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:
parent
2ca726e3af
commit
d7f494a0c9
52 changed files with 1926 additions and 106 deletions
18
bindings/cpp/README.md
Normal file
18
bindings/cpp/README.md
Normal 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
33
bindings/cpp/example.cpp
Normal 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
98
bindings/cpp/yapf.hpp
Normal 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
|
||||
11
bindings/csharp/Example/Example.csproj
Normal file
11
bindings/csharp/Example/Example.csproj
Normal 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>
|
||||
25
bindings/csharp/Example/Program.cs
Normal file
25
bindings/csharp/Example/Program.cs
Normal 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
19
bindings/csharp/README.md
Normal 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
121
bindings/csharp/Yapf.cs
Normal 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
17
bindings/go/README.md
Normal 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`.
|
||||
47
bindings/go/example/main.go
Normal file
47
bindings/go/example/main.go
Normal 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
3
bindings/go/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/Nexoniarz/YAPF/bindings/go
|
||||
|
||||
go 1.21
|
||||
111
bindings/go/yapf.go
Normal file
111
bindings/go/yapf.go
Normal 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
2
bindings/go/yapf_c.c
Normal 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
46
bindings/go/yapf_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
29
bindings/java/Example.java
Normal file
29
bindings/java/Example.java
Normal 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
16
bindings/java/README.md
Normal 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
121
bindings/java/Yapf.java
Normal 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)); }
|
||||
}
|
||||
|
|
@ -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
19
bindings/js/example.mjs
Normal 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}`);
|
||||
|
|
@ -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
25
bindings/js/yapf.d.ts
vendored
Normal 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
13
bindings/lua/README.md
Normal 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
17
bindings/lua/example.lua
Normal 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
71
bindings/lua/yapf.lua
Normal 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
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
33
bindings/python/example.py
Normal file
33
bindings/python/example.py
Normal 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")
|
||||
|
|
@ -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
12
bindings/rust/Cargo.toml
Normal 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
18
bindings/rust/README.md
Normal 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
12
bindings/rust/build.rs
Normal 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());
|
||||
}
|
||||
26
bindings/rust/examples/demo.rs
Normal file
26
bindings/rust/examples/demo.rs
Normal 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
156
bindings/rust/src/lib.rs
Normal 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
13
bindings/zig/README.md
Normal 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
33
bindings/zig/example.zig
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in a new issue