mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-02 20:44:27 +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
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -15,3 +15,11 @@ test_yapf_tmp.yapf
|
|||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Rust
|
||||
/bindings/rust/target/
|
||||
/bindings/rust/Cargo.lock
|
||||
# C# / Go / Java build outputs
|
||||
bin/
|
||||
obj/
|
||||
*.class
|
||||
|
|
|
|||
104
README.md
104
README.md
|
|
@ -8,8 +8,8 @@ model: every structure is byte aligned, and the decoder runs short loops that
|
|||
modern CPUs execute at memory speed — with SSE / NEON where available. The
|
||||
encoder does the hard work, so the decoder only follows instructions.
|
||||
|
||||
- **6–10× faster decoding than PNG** (stb_image), single-threaded; a 4K image
|
||||
decodes in about 6 ms (5.6 GB/s). Multithreaded decoding is optional.
|
||||
- **5–12× faster decoding than PNG** (stb_image), single-threaded; a 4K image
|
||||
decodes in about 6–7 ms (around 5 GB/s). Multithreaded decoding is optional.
|
||||
- **Smaller than PNG** on UI, icons and screenshots; 20–45 % larger on photos
|
||||
and natural textures — the price of having no entropy coder.
|
||||
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
|
||||
|
|
@ -25,22 +25,32 @@ This is **YAPF version 1**, the first stable format.
|
|||
|
||||
| Where | What you get | Guide | Status |
|
||||
| :---- | :----------- | :---- | :----- |
|
||||
| **C / C++** | `yapf.h` + `yapf.c`, drop into any project | [below](#c--c) | ✅ tested on Linux x64, Windows x64, Linux ARM64 |
|
||||
| **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux, Windows |
|
||||
| **GIMP 3** | open, export (with mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 |
|
||||
| **C / C++** | `yapf.h` + `yapf.c`, drop into any project (compiles as C or C++) | [below](#c--c) | ✅ tested — see [platforms](#platforms) |
|
||||
| **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `compare`, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux and Windows |
|
||||
| **Your language** | Python, C#, Rust, Go, Java, Zig, Lua, JavaScript / TypeScript, … | [docs/LANGUAGES.md](docs/LANGUAGES.md) | ✅ every binding tested |
|
||||
| **GIMP 3** | open, export (flattened, optional mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 |
|
||||
| **Blender 4.2+** | import, export, drag & drop, image as plane | [extensions/blender](extensions/blender) | ✅ tested in Blender 5.1 |
|
||||
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ✅ packaged, preview tested in Chromium |
|
||||
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ✅ plugin UI tested in Chromium |
|
||||
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ not yet tested inside Photoshop |
|
||||
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnailer tested, not in a file manager |
|
||||
| **Python** | `yapf.load()` / `yapf.save()` | [bindings/python](bindings/python) | ✅ tested |
|
||||
| **JavaScript** | browsers, Node.js, workers | [bindings/js](bindings/js) | ✅ tested, byte-identical to C |
|
||||
| **KDE Dolphin** | thumbnails for `.yapf` files | [extensions/kde](extensions/kde) | ⚠️ tested through KDE's plugin loader (KF6); not yet inside a running Dolphin |
|
||||
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnail command tested; not yet in a file manager |
|
||||
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ⚠️ packaged; preview code tested in a browser engine (Chromium); **not yet run inside VS Code** |
|
||||
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ⚠️ plugin UI tested in a browser engine with a simulated Figma; **not yet run inside Figma** |
|
||||
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ **syntax-checked only; not yet run in Photoshop** |
|
||||
|
||||
Pre-built downloads (command-line tool and libraries for Windows, macOS and
|
||||
Linux, plus the plugin packages) are published on the
|
||||
[Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can
|
||||
also be built from source as described below.
|
||||
|
||||
### Platforms
|
||||
|
||||
| Platform | Status |
|
||||
| :------- | :----- |
|
||||
| Linux x64 | ✅ built and fully tested |
|
||||
| Windows x64 | ✅ built and fully tested (MinGW and Zig builds, run under Wine) |
|
||||
| Linux ARM64 | ✅ built and fully tested (NEON, run under qemu) |
|
||||
| Windows ARM64 | ⚠️ built only, not run |
|
||||
| macOS x64 / ARM64 | ⚠️ **cross-compiled only, not yet run on a Mac** |
|
||||
|
||||
---
|
||||
|
||||
## Command line
|
||||
|
|
@ -53,6 +63,7 @@ yapf texture.yapf mip2.png --mip 2 # export one mip level
|
|||
yapf info photo.yapf # size, channels, mips, flags, ratio
|
||||
yapf bench photo.yapf # decode speed, 1 thread and all cores
|
||||
yapf thumbnail photo.yapf thumb.png 256
|
||||
yapf compare photo.png ui.png # size and decode speed vs the original format
|
||||
```
|
||||
|
||||
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
|
||||
|
|
@ -62,25 +73,24 @@ Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
|
|||
|
||||
## Benchmark
|
||||
|
||||
Single core, AMD Ryzen 7 5700G, `gcc -O2`. PNG is decoded with `stb_image`;
|
||||
the PNG files are the ones shipped with the respective projects. "Plain C"
|
||||
is the library built with `-DYAPF_NO_SIMD`; the default build uses SSE / NEON.
|
||||
`yapf compare` on a mixed set of images: one binary (`make`, gcc -O2) for
|
||||
both formats, PNG decoded with stb_image, one core, AMD Ryzen 7 5700G.
|
||||
Every result is checked to decode back to the exact original pixels.
|
||||
|
||||
| Image | Raw | PNG | **YAPF** | vs PNG | PNG decode | YAPF, plain C | **YAPF (default)** |
|
||||
| :--------------------- | ------: | -----: | -------: | -----: | ---------: | ------------: | -----------------: |
|
||||
| Wallpaper 3840×2160 | 32400 K | 1812 K | 2267 K | 125 % | 44.7 ms¹ | 10.3 ms | **5.9 ms** (5.6 GB/s) |
|
||||
| UI screenshot 261×430 | 438 K | 176 K | 143 K | 81 % | 1.6 ms | 0.31 ms | **0.17 ms** |
|
||||
| UI screenshot 305×529 | 630 K | 201 K | 185 K | 92 % | 2.1 ms | 0.45 ms | **0.24 ms** |
|
||||
| App icon 128×128 | 64 K | 24 K | 23 K | 93 % | 0.26 ms | 0.06 ms | **0.03 ms** |
|
||||
| Rock texture 512×512 | 768 K | 182 K | 221 K | 121 % | 1.4 ms | 0.35 ms | **0.16 ms** |
|
||||
| Wood texture 512×512 | 768 K | 185 K | 234 K | 126 % | 1.4 ms | 0.46 ms | **0.16 ms** |
|
||||
| Canvas texture 512×512 | 768 K | 171 K | 245 K | 143 % | 1.0 ms | 0.35 ms | **0.15 ms** |
|
||||
| Pixel art 512×512 | 1024 K | — | 66 K | — | — | 0.74 ms | **0.32 ms** |
|
||||
| Random noise 512×512 | 1024 K | — | 1024 K | — | — | 0.1 ms | 0.1 ms |
|
||||
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
|
||||
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
|
||||
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
|
||||
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
|
||||
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
|
||||
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
|
||||
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
|
||||
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
|
||||
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
|
||||
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
|
||||
|
||||
¹ PNG decode of the wallpaper measured between 44.7 and 66 ms depending on
|
||||
the harness; the table uses the fastest. `-O2` and `-O3` builds of YAPF
|
||||
perform the same.
|
||||
Smaller than PNG on UI-style images, 20–50 % larger on photos and textures
|
||||
(the price of having no entropy coder), and 5–12× faster to decode. More
|
||||
in [examples/](examples) — and run `yapf compare` on your own images.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -176,6 +186,13 @@ Encodes an image to disk. Writes `img->mip_levels`
|
|||
levels; `img->mips` may be `NULL` when `mip_levels == 1`. Returns `YAPF_OK`
|
||||
(0) on success or a negative `YAPF_ERR_*` code on failure.
|
||||
|
||||
```c
|
||||
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||
void yapf_free_buffer(void *data);
|
||||
```
|
||||
Encodes into memory: `*out_data` receives a complete `.yapf` file of
|
||||
`*out_size` bytes, to be released with `yapf_free_buffer()`.
|
||||
|
||||
```c
|
||||
void yapf_free(yapf_image_t *img);
|
||||
```
|
||||
|
|
@ -256,22 +273,27 @@ TILE (64×64) header byte: stored raw, or constant-plane mask + colour tr
|
|||
## Testing
|
||||
|
||||
```sh
|
||||
make test # C suite: 784 checks, plus JavaScript if node is installed
|
||||
make test # C suite: 786 checks, plus JavaScript if node is installed
|
||||
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
|
||||
```
|
||||
|
||||
What has been verified for this release:
|
||||
What has been verified:
|
||||
|
||||
- exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
|
||||
single- and multi-threaded decoding;
|
||||
- the same tests pass on **Linux x64**, **Windows x64** (MinGW, run under
|
||||
Wine), **Linux ARM64** (NEON, run under qemu), with and without SIMD, and
|
||||
under AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer;
|
||||
single- and multi-threaded decoding, in-memory and file encoding;
|
||||
- the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux
|
||||
ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as
|
||||
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
||||
ThreadSanitizer;
|
||||
- thousands of corrupted and truncated files are rejected without crashing;
|
||||
- the JavaScript and Python versions produce byte-identical files to C;
|
||||
- the GIMP plugin and the Blender extension in the real applications.
|
||||
- every language binding runs its example; JavaScript, Python, C++, C#,
|
||||
Rust, Go, Java and Lua re-encode the sample to byte-identical output;
|
||||
- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1;
|
||||
- the KDE thumbnailer through KDE's own plugin loader.
|
||||
|
||||
The macOS builds are cross-compiled and not yet run on a Mac.
|
||||
**Not yet verified:** running on a Mac (macOS builds are cross-compiled
|
||||
only), Windows on ARM, and running inside Photoshop, VS Code, Figma or a
|
||||
live Dolphin / GNOME file manager.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -282,14 +304,16 @@ yapf.h, yapf.c the library (format spec in yapf.h)
|
|||
tools/yapf_cli.c the `yapf` command-line tool
|
||||
tools/build_release.sh builds every download for all platforms
|
||||
tests/test_yapf.c C test suite
|
||||
bindings/js/ JavaScript encoder / decoder
|
||||
bindings/python/ Python module
|
||||
examples/ speed and size comparison, C example
|
||||
docs/LANGUAGES.md using YAPF from other languages
|
||||
bindings/ cpp, python, csharp, rust, go, java, zig, lua, js
|
||||
extensions/gimp/ GIMP 3 plugin
|
||||
extensions/blender/ Blender extension
|
||||
extensions/kde/ Dolphin thumbnail plugin (KF6)
|
||||
extensions/linux/ MIME type and freedesktop thumbnailer
|
||||
extensions/vscode/ VS Code preview
|
||||
extensions/figma/ Figma plugin
|
||||
extensions/photoshop/ Photoshop scripts
|
||||
extensions/linux/ MIME type and thumbnailer
|
||||
third_party/ stb_image / stb_image_write (public domain, CLI only)
|
||||
other/YAPF.YAPF sample image (the YAPF logo)
|
||||
```
|
||||
|
|
|
|||
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 });
|
||||
}
|
||||
|
|
@ -145,7 +145,9 @@ echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h
|
|||
echo yapf_load_mt
|
||||
echo yapf_load_memory_mt
|
||||
echo yapf_save
|
||||
echo yapf_encode
|
||||
echo yapf_free
|
||||
echo yapf_free_buffer
|
||||
) > dist\_yapf.def
|
||||
|
||||
:: ====================================================================
|
||||
|
|
|
|||
144
docs/LANGUAGES.md
Normal file
144
docs/LANGUAGES.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Using YAPF from your language
|
||||
|
||||
YAPF is a small C library with six functions, so every language that can
|
||||
call C can use it. Ready-made bindings, each with a runnable example:
|
||||
|
||||
| Language | Folder | How it works | Tested | Decode¹ |
|
||||
| :------- | :----- | :----------- | :----- | ------: |
|
||||
| **C** | [`yapf.h`](../yapf.h), [`examples/c`](../examples/c) | the library itself | ✅ | 0.26 ms |
|
||||
| **C++** | [`bindings/cpp`](../bindings/cpp) | header-only RAII wrapper (C++17) | ✅ g++ | 0.25 ms |
|
||||
| **Python** | [`bindings/python`](../bindings/python) | ctypes; numpy fallback without the library | ✅ CPython 3.13 | 0.23 ms |
|
||||
| **C# / .NET** | [`bindings/csharp`](../bindings/csharp) | P/Invoke, single file | ✅ .NET 8 | 0.30 ms |
|
||||
| **Rust** | [`bindings/rust`](../bindings/rust) | crate, builds the C code with `cc` | ✅ Rust 1.95 | 0.28 ms |
|
||||
| **Go** | [`bindings/go`](../bindings/go) | cgo package | ✅ Go 1.26 | 0.31 ms |
|
||||
| **Java / Kotlin** | [`bindings/java`](../bindings/java) | Foreign Function & Memory API (Java 22+) | ✅ JDK 25 | 0.42 ms |
|
||||
| **Zig** | [`bindings/zig`](../bindings/zig) | `@cImport` of `yapf.h` | ✅ Zig 0.16 | 0.28 ms |
|
||||
| **Lua** | [`bindings/lua`](../bindings/lua) | LuaJIT FFI (also LÖVE) | ✅ LuaJIT 2.1 | 0.28 ms |
|
||||
| **JavaScript / TypeScript** | [`bindings/js`](../bindings/js) | pure JS port, `.d.ts` types | ✅ Node 24, Chromium | 5.9 ms |
|
||||
|
||||
¹ Decoding the 512×512 RGBA YAPF logo (`other/YAPF.YAPF`), best of 10 after
|
||||
a warm-up, on an AMD Ryzen 7 5700G. Every binding except JavaScript runs
|
||||
the same native code, so they are all about equally fast; the JavaScript
|
||||
port needs no native library at all. Every example also re-encodes the
|
||||
image and checks that the result is byte-identical to the original file.
|
||||
|
||||
## Getting the native library
|
||||
|
||||
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
|
||||
library at run time:
|
||||
|
||||
| System | File | Get it |
|
||||
| :------ | :-------------- | :----- |
|
||||
| Windows | `yapf.dll` | [Releases](https://github.com/Nexoniarz/YAPF/releases) or `build.bat` |
|
||||
| Linux | `libyapf.so` | Releases, `make`, or `sudo make install` |
|
||||
| macOS | `libyapf.dylib` | Releases or `make` |
|
||||
|
||||
Put it next to your program, on the library path (`PATH`,
|
||||
`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`), or pass its path explicitly
|
||||
(`YAPF_LIBRARY` for Python and Lua, `-Dyapf.library=` for Java).
|
||||
|
||||
## Quick look
|
||||
|
||||
**C**
|
||||
```c
|
||||
yapf_image_t *img = yapf_load("texture.yapf");
|
||||
/* img->width, img->height, img->channels, img->pixels */
|
||||
yapf_free(img);
|
||||
```
|
||||
|
||||
**C++**
|
||||
```cpp
|
||||
auto img = yapf::Image::load("texture.yapf");
|
||||
auto bytes = img.encode();
|
||||
```
|
||||
|
||||
**Python**
|
||||
```python
|
||||
img = yapf.load("texture.yapf")
|
||||
yapf.save("copy.yapf", img)
|
||||
```
|
||||
|
||||
**C#**
|
||||
```csharp
|
||||
var img = YapfImage.Load("texture.yapf");
|
||||
File.WriteAllBytes("copy.yapf", img.Encode());
|
||||
```
|
||||
|
||||
**Rust**
|
||||
```rust
|
||||
let img = yapf::Image::load("texture.yapf")?;
|
||||
img.save("copy.yapf")?;
|
||||
```
|
||||
|
||||
**Go**
|
||||
```go
|
||||
img, err := yapf.Load("texture.yapf")
|
||||
data, err := yapf.Encode(img)
|
||||
```
|
||||
|
||||
**Java**
|
||||
```java
|
||||
Yapf.Image img = Yapf.load(Path.of("texture.yapf"));
|
||||
Yapf.save(Path.of("copy.yapf"), img);
|
||||
```
|
||||
|
||||
**Zig**
|
||||
```zig
|
||||
const img = c.yapf_load("texture.yapf") orelse return error.CannotLoad;
|
||||
defer c.yapf_free(img);
|
||||
```
|
||||
|
||||
**Lua**
|
||||
```lua
|
||||
local img = yapf.load("texture.yapf")
|
||||
yapf.save("copy.yapf", img)
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
```js
|
||||
const img = YAPF.decode(bytes);
|
||||
const file = YAPF.encode(img);
|
||||
```
|
||||
|
||||
## Any other language
|
||||
|
||||
Swift, Kotlin/Native, Dart, PHP, Ruby, Julia, Nim, D, Odin, Haskell, … —
|
||||
anything with a C foreign-function interface can call the library
|
||||
directly. These are the whole API:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint32_t width, height;
|
||||
uint8_t channels; /* 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA */
|
||||
uint8_t gpu_format; /* hint, see yapf.h */
|
||||
uint8_t flags; /* 1 premultiplied alpha, 2 sRGB */
|
||||
uint8_t mip_levels;
|
||||
uint8_t *pixels; /* width*height*channels bytes, rows top to bottom */
|
||||
uint8_t **mips; /* mips[0] == pixels */
|
||||
} yapf_image_t; /* 32 bytes on 64-bit systems: pixels at offset 16, mips at 24 */
|
||||
|
||||
yapf_image_t *yapf_load_memory_mt(const void *data, size_t size, int threads); /* threads: 1 or 0 = all cores */
|
||||
void yapf_free(yapf_image_t *img);
|
||||
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||
void yapf_free_buffer(void *data);
|
||||
yapf_image_t *yapf_load(const char *path);
|
||||
int yapf_save(const char *path, const yapf_image_t *img);
|
||||
```
|
||||
|
||||
Returned images and buffers must be released with `yapf_free` /
|
||||
`yapf_free_buffer` from the same library. Ready-made bindings for these
|
||||
languages are not included yet; the snippets below show the starting point
|
||||
and are **not tested**.
|
||||
|
||||
**Swift** — add a module map for `yapf.h` (or a bridging header) and compile
|
||||
`yapf.c` in the target; then `let img = yapf_load(path)` works as is.
|
||||
|
||||
**Dart** — `dart:ffi`: `DynamicLibrary.open('libyapf.so')` and
|
||||
`lookupFunction<Pointer<YapfImage> Function(Pointer<Uint8>, IntPtr, Int32), …>('yapf_load_memory_mt')`.
|
||||
|
||||
**PHP 7.4+** — `$ffi = FFI::cdef(file_get_contents('yapf_api.h'), 'libyapf.so');`
|
||||
with the declarations above in `yapf_api.h`.
|
||||
|
||||
**Ruby** — `Fiddle::Function.new(lib['yapf_load'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)`.
|
||||
|
||||
**Julia** — `ccall((:yapf_load, "libyapf"), Ptr{Cvoid}, (Cstring,), path)`.
|
||||
62
examples/README.md
Normal file
62
examples/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Examples: how fast, how small
|
||||
|
||||
## Try it on your own images
|
||||
|
||||
```sh
|
||||
make # builds build/yapf
|
||||
build/yapf compare photo.png ui.png # any PNG / JPG / TGA / BMP
|
||||
```
|
||||
|
||||
`yapf compare` encodes each image to YAPF in memory, checks that it decodes
|
||||
back to exactly the same pixels, and times decoding the original format
|
||||
against YAPF on one core and on all cores:
|
||||
|
||||
```
|
||||
ui_reverb.png (261 x 430, RGBA)
|
||||
raw 438.4 KB
|
||||
PNG 176.6 KB 40.3% of raw decode 2.049 ms
|
||||
YAPF 143.7 KB 32.8% of raw decode 0.170 ms 12.1x faster, 81% of the PNG size
|
||||
YAPF on all cores: decode 0.176 ms (2.5 GB/s), encode 8 ms, lossless: yes
|
||||
```
|
||||
|
||||
## Results on a mixed set of images
|
||||
|
||||
AMD Ryzen 7 5700G, `make` (gcc -O2), PNG decoded with stb_image. The test
|
||||
images (a 4K wallpaper, UI screenshots, an app icon and seamless textures
|
||||
from open-source projects) are not redistributed here; run `yapf compare`
|
||||
on your own files to reproduce.
|
||||
|
||||
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
|
||||
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
|
||||
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
|
||||
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
|
||||
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
|
||||
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
|
||||
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
|
||||
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
|
||||
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
|
||||
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
|
||||
|
||||
In short: **smaller than PNG on UI-style images, 20–50 % larger on photos
|
||||
and textures, and 5–12× faster to decode** — always lossless. The decoder
|
||||
reaches 5–6 GB/s on large images on one core, and more with
|
||||
`yapf_load_mt`.
|
||||
|
||||
## Code examples
|
||||
|
||||
| Language | Example |
|
||||
| :------- | :------ |
|
||||
| C | [`examples/c/example.c`](c/example.c) |
|
||||
| C++ | [`bindings/cpp/example.cpp`](../bindings/cpp/example.cpp) |
|
||||
| Python | [`bindings/python/example.py`](../bindings/python/example.py) |
|
||||
| C# | [`bindings/csharp/Example`](../bindings/csharp/Example) |
|
||||
| Rust | [`bindings/rust/examples/demo.rs`](../bindings/rust/examples/demo.rs) |
|
||||
| Go | [`bindings/go/example`](../bindings/go/example) |
|
||||
| Java | [`bindings/java/Example.java`](../bindings/java/Example.java) |
|
||||
| Zig | [`bindings/zig/example.zig`](../bindings/zig/example.zig) |
|
||||
| Lua | [`bindings/lua/example.lua`](../bindings/lua/example.lua) |
|
||||
| JavaScript | [`bindings/js/example.mjs`](../bindings/js/example.mjs) |
|
||||
|
||||
Each one loads a `.yapf` file, prints its size and decode time, re-encodes
|
||||
it (byte-identical to the file) and writes a new image from its own pixels.
|
||||
See [docs/LANGUAGES.md](../docs/LANGUAGES.md) for setup in each language.
|
||||
61
examples/c/example.c
Normal file
61
examples/c/example.c
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* example.c — load, inspect, time and write YAPF images from C.
|
||||
*
|
||||
* cc -O2 -std=c99 -I../.. example.c ../../yapf.c -o example -pthread
|
||||
* ./example ../../other/YAPF.YAPF
|
||||
*/
|
||||
#include "yapf.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static double seconds(void) { return (double)clock() / CLOCKS_PER_SEC; }
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = argc > 1 ? argv[1] : "../../other/YAPF.YAPF";
|
||||
|
||||
/* 1. Load a file (yapf_load_memory works the same on a buffer). */
|
||||
yapf_image_t *img = yapf_load(path);
|
||||
if (!img) { fprintf(stderr, "cannot load %s\n", path); return 1; }
|
||||
double t = 1e9; /* decode time: best of 10 */
|
||||
for (int i = 0; i < 10; i++) {
|
||||
double t0 = seconds();
|
||||
yapf_free(yapf_load(path));
|
||||
t0 = seconds() - t0;
|
||||
if (t0 < t) t = t0;
|
||||
}
|
||||
printf("%s: %u x %u, %u channels, %u mip level(s), decoded in %.2f ms\n",
|
||||
path, img->width, img->height, img->channels, img->mip_levels, t * 1e3);
|
||||
|
||||
/* 2. Pixels: width * height * channels bytes, rows top to bottom. */
|
||||
const uint8_t *p = img->pixels;
|
||||
printf("top-left pixel: %u %u %u %u\n", p[0], p[1], p[2], img->channels == 4 ? p[3] : 255);
|
||||
|
||||
/* 3. Encode to memory and to a file. */
|
||||
void *data = NULL;
|
||||
size_t size = 0;
|
||||
if (yapf_encode(img, &data, &size) == YAPF_OK) {
|
||||
printf("re-encoded: %zu bytes (%.1f%% of raw)\n", size,
|
||||
100.0 * size / ((double)img->width * img->height * img->channels));
|
||||
yapf_free_buffer(data);
|
||||
}
|
||||
|
||||
/* 4. Create an image from your own pixels. */
|
||||
enum { W = 256, H = 256 };
|
||||
static uint8_t rgba[W * H * 4];
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int x = 0; x < W; x++) {
|
||||
uint8_t *q = rgba + (y * W + x) * 4;
|
||||
q[0] = (uint8_t)x; q[1] = (uint8_t)y; q[2] = 128; q[3] = 255;
|
||||
}
|
||||
yapf_image_t mine;
|
||||
memset(&mine, 0, sizeof(mine));
|
||||
mine.width = W; mine.height = H; mine.channels = 4; mine.mip_levels = 1;
|
||||
mine.pixels = rgba; mine.flags = YAPF_FLAG_SRGB; mine.gpu_format = YAPF_GPU_SRGB8_A8;
|
||||
if (yapf_save("gradient.yapf", &mine) == YAPF_OK) printf("wrote gradient.yapf\n");
|
||||
|
||||
yapf_free(img);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -20,7 +20,9 @@ the .blend** and survive saving and reopening.
|
|||
and pick the zip.
|
||||
|
||||
The zip contains the native YAPF library for Windows, macOS and Linux (x64
|
||||
and ARM64). If no library matches your system, the extension still
|
||||
and ARM64). Tested in Blender 5.1 on Linux; the Windows library is tested
|
||||
outside Blender, and the macOS libraries are cross-compiled and **not yet run
|
||||
on a Mac**. If no library matches your system, the extension still
|
||||
**imports** using a pure-Python decoder (numpy, bundled with Blender), but
|
||||
cannot export.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,3 +30,10 @@ node build.js
|
|||
```
|
||||
|
||||
Figma limits images to 4096 × 4096; larger `.yapf` files report an error.
|
||||
|
||||
## Status
|
||||
|
||||
The plugin UI was tested in a browser engine (headless Chromium) with a
|
||||
simulated Figma main thread: import hands Figma a pixel-exact PNG, and
|
||||
export downloads a pixel-exact `.yapf`. `code.js` is syntax-checked.
|
||||
**It has not yet been run inside Figma itself.**
|
||||
|
|
|
|||
26
extensions/kde/CMakeLists.txt
Normal file
26
extensions/kde/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# YAPF thumbnails for KDE Dolphin (KF6 KIO thumbnail plugin).
|
||||
# cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
# cmake --build build
|
||||
# sudo cmake --install build (or see README.md for a per-user install)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(yapfthumbnail LANGUAGES C CXX)
|
||||
|
||||
find_package(ECM 6.0 REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui)
|
||||
find_package(KF6 6.0 REQUIRED COMPONENTS KIO)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
kcoreaddons_add_plugin(yapfthumbnail
|
||||
SOURCES yapfthumbnail.cpp ../../yapf.c
|
||||
INSTALL_NAMESPACE "kf6/thumbcreator")
|
||||
set_source_files_properties(../../yapf.c PROPERTIES LANGUAGE C)
|
||||
target_include_directories(yapfthumbnail PRIVATE ../..)
|
||||
target_link_libraries(yapfthumbnail PRIVATE KF6::KIOGui Qt6::Gui Threads::Threads)
|
||||
|
||||
# Register the image/x-yapf file type so Dolphin recognises .yapf files.
|
||||
install(FILES ../linux/yapf-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR} RENAME yapf.xml)
|
||||
50
extensions/kde/README.md
Normal file
50
extensions/kde/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# YAPF thumbnails for KDE (Dolphin)
|
||||
|
||||
Shows previews of `.yapf` files in **Dolphin**, the KDE file dialogs and
|
||||
anything else that uses KIO thumbnails. KDE Frameworks 6 / Plasma 6.
|
||||
Files with mipmaps are thumbnailed from the nearest mip level, so even huge
|
||||
textures preview instantly.
|
||||
|
||||
## Build and install
|
||||
|
||||
You need CMake, a C/C++ compiler and the KDE development packages:
|
||||
|
||||
| System | Packages |
|
||||
| :-------------- | :------- |
|
||||
| Arch / Manjaro | `sudo pacman -S cmake extra-cmake-modules kio` |
|
||||
| Debian / Ubuntu | `sudo apt install cmake extra-cmake-modules libkf6kio-dev qt6-base-dev` |
|
||||
| Fedora | `sudo dnf install cmake extra-cmake-modules kf6-kio-devel qt6-qtbase-devel` |
|
||||
| openSUSE | `sudo zypper install cmake kf6-extra-cmake-modules kf6-kio-devel qt6-base-devel` |
|
||||
| NixOS | `nix-shell kde-shell.nix` (in this folder) |
|
||||
|
||||
```sh
|
||||
cd extensions/kde
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
update-mime-database /usr/share/mime
|
||||
```
|
||||
|
||||
This installs the plugin (`kf6/thumbcreator/yapfthumbnail.so`) and the
|
||||
`image/x-yapf` file type. Then in Dolphin: **Settings → Configure Dolphin →
|
||||
Interface → Previews**, tick **YAPF Images**, and turn previews on.
|
||||
|
||||
### Without root (per user, also NixOS)
|
||||
|
||||
```sh
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/.local
|
||||
cmake --build build && cmake --install build
|
||||
update-mime-database ~/.local/share/mime
|
||||
mkdir -p ~/.config/plasma-workspace/env
|
||||
echo 'export QT_PLUGIN_PATH="$HOME/.local/lib/plugins:$QT_PLUGIN_PATH"' > ~/.config/plasma-workspace/env/yapf.sh
|
||||
```
|
||||
|
||||
Log out and back in, then enable **YAPF Images** in Dolphin's preview settings.
|
||||
(Some distributions use `lib64/plugins` or `lib/qt6/plugins`; `cmake
|
||||
--install` prints the exact folder.)
|
||||
|
||||
## Status
|
||||
|
||||
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
|
||||
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
|
||||
corrupt files are rejected). Not yet tried inside a running Dolphin.
|
||||
6
extensions/kde/kde-shell.nix
Normal file
6
extensions/kde/kde-shell.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Development shell for building the plugin on NixOS: nix-shell kde-shell.nix
|
||||
with import <nixpkgs> {};
|
||||
mkShell {
|
||||
nativeBuildInputs = [ cmake kdePackages.extra-cmake-modules pkg-config ];
|
||||
buildInputs = [ kdePackages.kio kdePackages.qtbase kdePackages.kcoreaddons ];
|
||||
}
|
||||
79
extensions/kde/yapfthumbnail.cpp
Normal file
79
extensions/kde/yapfthumbnail.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// YAPF thumbnails for KDE Dolphin and other KIO file dialogs.
|
||||
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||
|
||||
#include <KIO/ThumbnailCreator>
|
||||
#include <KPluginFactory>
|
||||
#include <QImage>
|
||||
|
||||
extern "C" {
|
||||
#include "yapf.h"
|
||||
}
|
||||
|
||||
class YapfThumbnailCreator : public KIO::ThumbnailCreator
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
YapfThumbnailCreator(QObject *parent, const QVariantList &args)
|
||||
: KIO::ThumbnailCreator(parent, args)
|
||||
{
|
||||
}
|
||||
|
||||
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
|
||||
{
|
||||
const QByteArray path = request.url().toLocalFile().toLocal8Bit();
|
||||
yapf_image_t *img = yapf_load(path.constData());
|
||||
if (!img) {
|
||||
return KIO::ThumbnailResult::fail();
|
||||
}
|
||||
|
||||
// Start from the smallest stored mip level that still covers the
|
||||
// requested size; Qt scales the rest.
|
||||
const QSize want = request.targetSize();
|
||||
int m = 0;
|
||||
while (m + 1 < img->mip_levels &&
|
||||
int(img->width >> (m + 1)) >= want.width() &&
|
||||
int(img->height >> (m + 1)) >= want.height()) {
|
||||
++m;
|
||||
}
|
||||
const int w = qMax(1, int(img->width >> m));
|
||||
const int h = qMax(1, int(img->height >> m));
|
||||
const uchar *px = img->mips[m];
|
||||
|
||||
QImage image;
|
||||
switch (img->channels) {
|
||||
case 1:
|
||||
image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy();
|
||||
break;
|
||||
case 2: { // gray + alpha → RGBA
|
||||
image = QImage(w, h, QImage::Format_RGBA8888);
|
||||
for (int y = 0; y < h; ++y) {
|
||||
const uchar *s = px + size_t(y) * w * 2;
|
||||
uchar *d = image.scanLine(y);
|
||||
for (int x = 0; x < w; ++x, s += 2, d += 4) {
|
||||
d[0] = d[1] = d[2] = s[0];
|
||||
d[3] = s[1];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy();
|
||||
break;
|
||||
default:
|
||||
image = QImage(px, w, h, w * 4,
|
||||
(img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
|
||||
: QImage::Format_RGBA8888).copy();
|
||||
break;
|
||||
}
|
||||
yapf_free(img);
|
||||
|
||||
if (image.width() > want.width() || image.height() > want.height()) {
|
||||
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
return KIO::ThumbnailResult::pass(image);
|
||||
}
|
||||
};
|
||||
|
||||
K_PLUGIN_CLASS_WITH_JSON(YapfThumbnailCreator, "yapfthumbnail.json")
|
||||
|
||||
#include "yapfthumbnail.moc"
|
||||
7
extensions/kde/yapfthumbnail.json
Normal file
7
extensions/kde/yapfthumbnail.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"CacheThumbnail": true,
|
||||
"KPlugin": {
|
||||
"MimeTypes": ["image/x-yapf"],
|
||||
"Name": "YAPF Images"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,11 @@ Decoding runs in plain JavaScript inside the preview; no native code.
|
|||
code --install-extension yapf-preview-1.0.0.vsix
|
||||
```
|
||||
|
||||
Works in VS Code, VSCodium and other VS Code–based editors (1.75+).
|
||||
Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
|
||||
|
||||
## Status
|
||||
|
||||
The `.vsix` packages cleanly, and the preview code was tested in a browser
|
||||
engine (headless Chromium) with real files: correct pixels, mip picker,
|
||||
error message for corrupt files. **It has not yet been run inside VS Code
|
||||
itself.**
|
||||
|
|
|
|||
|
|
@ -44,18 +44,39 @@ static void fill(uint8_t *p, int w, int h, int ch, int kind) {
|
|||
}
|
||||
|
||||
static uint8_t *encode_mem(const yapf_image_t *img, size_t *n) {
|
||||
void *data = NULL;
|
||||
if (yapf_encode(img, &data, n) != YAPF_OK) return NULL;
|
||||
uint8_t *copy = (uint8_t *)malloc(*n);
|
||||
if (copy) memcpy(copy, data, *n);
|
||||
yapf_free_buffer(data);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* yapf_save() writes exactly what yapf_encode() returns. */
|
||||
static void save_matches_encode(void) {
|
||||
const char *tmp = "test_yapf_tmp.yapf";
|
||||
if (yapf_save(tmp, img) != YAPF_OK) return NULL;
|
||||
FILE *f = fopen(tmp, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
*n = (size_t)ftell(f);
|
||||
rewind(f);
|
||||
uint8_t *b = (uint8_t *)malloc(*n);
|
||||
if (b && fread(b, 1, *n, f) != *n) { free(b); b = NULL; }
|
||||
fclose(f);
|
||||
uint8_t px[37 * 23 * 3];
|
||||
fill(px, 37, 23, 3, 3);
|
||||
yapf_image_t img;
|
||||
memset(&img, 0, sizeof(img));
|
||||
img.width = 37; img.height = 23; img.channels = 3; img.mip_levels = 1; img.pixels = px;
|
||||
size_t len = 0;
|
||||
uint8_t *mem = encode_mem(&img, &len);
|
||||
int ok = mem && yapf_save(tmp, &img) == YAPF_OK;
|
||||
FILE *f = ok ? fopen(tmp, "rb") : NULL;
|
||||
if (f) {
|
||||
uint8_t *disk = (uint8_t *)malloc(len + 1);
|
||||
ok = fread(disk, 1, len + 1, f) == len && memcmp(disk, mem, len) == 0;
|
||||
free(disk);
|
||||
fclose(f);
|
||||
}
|
||||
remove(tmp);
|
||||
return b;
|
||||
free(mem);
|
||||
CHECK(ok, "yapf_save writes the same bytes as yapf_encode");
|
||||
void *d = (void *)1;
|
||||
size_t n = 1;
|
||||
CHECK(yapf_encode(NULL, &d, &n) == YAPF_ERR_INVALID && d == NULL && n == 0,
|
||||
"yapf_encode rejects a NULL image");
|
||||
}
|
||||
|
||||
static void roundtrip(int w, int h, int ch, int kind) {
|
||||
|
|
@ -162,6 +183,7 @@ int main(int argc, char **argv) {
|
|||
roundtrip(sizes[s][0], sizes[s][1], ch, kind);
|
||||
mips();
|
||||
corruption();
|
||||
save_matches_encode();
|
||||
|
||||
const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF";
|
||||
yapf_image_t *img = yapf_load(sample);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
# yapf-figma.zip Figma plugin (import via manifest.json)
|
||||
# yapf-photoshop.zip Photoshop scripts
|
||||
# yapf-gimp-source.zip GIMP plugin source (build with gimptool)
|
||||
# yapf-kde-source.zip Dolphin thumbnail plugin source (build with CMake)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=dist/release
|
||||
|
|
@ -55,4 +56,5 @@ fi
|
|||
python3 -m zipfile -c "$OUT/yapf-figma.zip" extensions/figma/manifest.json extensions/figma/code.js extensions/figma/ui.html
|
||||
python3 -m zipfile -c "$OUT/yapf-photoshop.zip" extensions/photoshop
|
||||
python3 -m zipfile -c "$OUT/yapf-gimp-source.zip" extensions/gimp yapf.c yapf.h
|
||||
python3 -m zipfile -c "$OUT/yapf-kde-source.zip" extensions/kde extensions/linux/yapf-mime.xml yapf.c yapf.h
|
||||
ls -la "$OUT"
|
||||
|
|
|
|||
109
tools/yapf_cli.c
109
tools/yapf_cli.c
|
|
@ -21,6 +21,7 @@
|
|||
* yapf info <file.yapf> print header information
|
||||
* yapf bench <file.yapf> time decoding
|
||||
* yapf thumbnail <in.yapf> <out.png> [size] small preview (default 256)
|
||||
* yapf compare <image.png> [more…] size and decode speed: original vs YAPF
|
||||
*
|
||||
* Reads PNG, JPEG, BMP, TGA, GIF (first frame), PSD (composite), PNM, HDR
|
||||
* (tone-clamped) and YAPF. Writes YAPF, PNG, BMP, TGA and JPEG.
|
||||
|
|
@ -249,12 +250,119 @@ static int cmd_thumbnail(const char *in, const char *out, int size) {
|
|||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
/* Best time over several rounds of a decode call, for fair small-image timing. */
|
||||
#define YAPF_TIME_BEST(result, expr) \
|
||||
do { \
|
||||
double one_ = now_s(); { expr; } one_ = now_s() - one_; \
|
||||
int reps_ = (int)(0.02 / (one_ > 1e-6 ? one_ : 1e-6)) + 1; \
|
||||
double best_ = 1e9; \
|
||||
for (int r_ = 0; r_ < 9; r_++) { \
|
||||
double t_ = now_s(); \
|
||||
for (int k_ = 0; k_ < reps_; k_++) { expr; } \
|
||||
t_ = (now_s() - t_) / reps_; \
|
||||
if (t_ < best_) best_ = t_; \
|
||||
} \
|
||||
(result) = best_; \
|
||||
} while (0)
|
||||
|
||||
static const char *fmt_kb(double bytes, char *buf) {
|
||||
if (bytes >= 1024.0 * 1024.0) sprintf(buf, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
else sprintf(buf, "%.1f KB", bytes / 1024.0);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static const char *ext_name(const char *path) {
|
||||
const char *dot = strrchr(path, '.');
|
||||
static char name[8];
|
||||
size_t i = 0;
|
||||
if (!dot) return "file";
|
||||
for (dot++; *dot && i < sizeof(name) - 1; dot++) name[i++] = (char)toupper((unsigned char)*dot);
|
||||
name[i] = 0;
|
||||
return name;
|
||||
}
|
||||
|
||||
/* Original format vs YAPF: size, decode time (1 thread / all cores),
|
||||
* and a lossless check. */
|
||||
static int cmd_compare(int n, char **paths) {
|
||||
int rc = 0;
|
||||
double tot_src = 0, tot_yapf = 0, tot_src_t = 0, tot_yapf_t = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *path = paths[i];
|
||||
if (is_yapf(path)) {
|
||||
fprintf(stderr, "yapf: compare takes the original image (PNG, JPG, ...), not %s\n", path);
|
||||
rc = 1;
|
||||
continue;
|
||||
}
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); rc = 1; continue; }
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsz = ftell(f);
|
||||
rewind(f);
|
||||
uint8_t *src = (uint8_t *)malloc((size_t)fsz);
|
||||
if (!src || fread(src, 1, (size_t)fsz, f) != (size_t)fsz) { fclose(f); free(src); rc = 1; continue; }
|
||||
fclose(f);
|
||||
|
||||
int w, h, ch;
|
||||
uint8_t *px = stbi_load_from_memory(src, (int)fsz, &w, &h, &ch, 0);
|
||||
if (!px) { fprintf(stderr, "yapf: cannot read %s: %s\n", path, stbi_failure_reason()); free(src); rc = 1; continue; }
|
||||
|
||||
yapf_image_t img;
|
||||
memset(&img, 0, sizeof(img));
|
||||
img.width = (uint32_t)w; img.height = (uint32_t)h; img.channels = (uint8_t)ch;
|
||||
img.mip_levels = 1; img.pixels = px; img.flags = YAPF_FLAG_SRGB;
|
||||
img.gpu_format = gpu_hint(ch, 1);
|
||||
|
||||
void *enc = NULL;
|
||||
size_t esz = 0;
|
||||
double t_enc = now_s();
|
||||
int erc = yapf_encode(&img, &enc, &esz);
|
||||
t_enc = now_s() - t_enc;
|
||||
if (erc != YAPF_OK) { fprintf(stderr, "yapf: cannot encode %s\n", path); stbi_image_free(px); free(src); rc = 1; continue; }
|
||||
|
||||
yapf_image_t *chk = yapf_load_memory(enc, esz);
|
||||
int lossless = chk && memcmp(chk->pixels, px, (size_t)w * h * ch) == 0;
|
||||
yapf_free(chk);
|
||||
|
||||
double t_src, t_one, t_all;
|
||||
int dw, dh, dc;
|
||||
YAPF_TIME_BEST(t_src, stbi_image_free(stbi_load_from_memory(src, (int)fsz, &dw, &dh, &dc, 0)));
|
||||
YAPF_TIME_BEST(t_one, yapf_free(yapf_load_memory(enc, esz)));
|
||||
YAPF_TIME_BEST(t_all, yapf_free(yapf_load_memory_mt(enc, esz, 0)));
|
||||
|
||||
double raw = (double)w * h * ch;
|
||||
char a[32], b[32], c[32];
|
||||
static const char *chn[] = { "", "gray", "gray+alpha", "RGB", "RGBA" };
|
||||
printf("%s (%d x %d, %s)\n", path, w, h, chn[ch]);
|
||||
printf(" %-5s %10s\n", "raw", fmt_kb(raw, c));
|
||||
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms\n", ext_name(path), fmt_kb((double)fsz, a),
|
||||
100.0 * fsz / raw, t_src * 1e3);
|
||||
printf(" %-5s %10s %5.1f%% of raw decode %8.3f ms %.1fx faster, %.0f%% of the %s size\n",
|
||||
"YAPF", fmt_kb((double)esz, b), 100.0 * esz / raw, t_one * 1e3,
|
||||
t_src / t_one, 100.0 * esz / fsz, ext_name(path));
|
||||
printf(" YAPF on all cores: decode %.3f ms (%.1f GB/s), encode %.0f ms, %s\n\n",
|
||||
t_all * 1e3, raw / t_all / 1e9, t_enc * 1e3,
|
||||
lossless ? "lossless: yes" : "LOSSLESS CHECK FAILED");
|
||||
if (!lossless) rc = 1;
|
||||
tot_src += (double)fsz; tot_yapf += (double)esz; tot_src_t += t_src; tot_yapf_t += t_one;
|
||||
yapf_free_buffer(enc);
|
||||
stbi_image_free(px);
|
||||
free(src);
|
||||
}
|
||||
if (n > 1 && tot_yapf_t > 0) {
|
||||
char a[32], b[32];
|
||||
printf("Total: originals %s, YAPF %s (%.0f%%); decoding %.1fx faster on one core\n",
|
||||
fmt_kb(tot_src, a), fmt_kb(tot_yapf, b), 100.0 * tot_yapf / tot_src, tot_src_t / tot_yapf_t);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void usage(void) {
|
||||
fprintf(stderr,
|
||||
"usage: yapf <input> <output> [options] convert (formats by extension)\n"
|
||||
" yapf info <file.yapf> show header information\n"
|
||||
" yapf bench <file.yapf> time decoding\n"
|
||||
" yapf thumbnail <in.yapf> <out.png> [size] preview, default 256 px\n"
|
||||
" yapf compare <image.png> [more...] size and speed vs the original format\n"
|
||||
"\n"
|
||||
"reads: .yapf .png .jpg .bmp .tga .gif .psd .pnm .hdr\n"
|
||||
"writes: .yapf .png .jpg .bmp .tga\n"
|
||||
|
|
@ -270,6 +378,7 @@ static void usage(void) {
|
|||
int main(int argc, char **argv) {
|
||||
if (argc == 3 && strcmp(argv[1], "info") == 0) return cmd_info(argv[2]);
|
||||
if (argc == 3 && strcmp(argv[1], "bench") == 0) return cmd_bench(argv[2]);
|
||||
if (argc >= 3 && strcmp(argv[1], "compare") == 0) return cmd_compare(argc - 2, argv + 2);
|
||||
if ((argc == 4 || argc == 5) && strcmp(argv[1], "thumbnail") == 0)
|
||||
return cmd_thumbnail(argv[2], argv[3], argc == 5 ? atoi(argv[4]) : 256);
|
||||
if (argc < 3) { usage(); return 2; }
|
||||
|
|
|
|||
81
yapf.c
81
yapf.c
|
|
@ -193,7 +193,7 @@ static int yapf__width(uint8_t z) {
|
|||
return w;
|
||||
}
|
||||
|
||||
static const uint8_t yapf__zero_row[YAPF_TILE_SIZE + 1];
|
||||
static const uint8_t yapf__zero_row[YAPF_TILE_SIZE + 1] = { 0 };
|
||||
|
||||
/* ── SIMD ────────────────────────────────────────────────────────────── */
|
||||
/*
|
||||
|
|
@ -1409,11 +1409,14 @@ yapf_image_t *yapf_load_mt(const char *filename, int threads) {
|
|||
}
|
||||
|
||||
/* ========================================================================
|
||||
* yapf_save
|
||||
* yapf_encode / yapf_save
|
||||
* ======================================================================== */
|
||||
|
||||
int yapf_save(const char *filename, const yapf_image_t *img) {
|
||||
if (!filename || !img || !img->pixels) return YAPF_ERR_INVALID;
|
||||
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size) {
|
||||
if (!out_data || !out_size) return YAPF_ERR_INVALID;
|
||||
*out_data = NULL;
|
||||
*out_size = 0;
|
||||
if (!img || !img->pixels) return YAPF_ERR_INVALID;
|
||||
if (img->width == 0 || img->width > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
|
||||
if (img->height == 0 || img->height > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
|
||||
if (img->channels < 1 || img->channels > 4) return YAPF_ERR_INVALID;
|
||||
|
|
@ -1440,47 +1443,63 @@ int yapf_save(const char *filename, const yapf_image_t *img) {
|
|||
}
|
||||
|
||||
/* Offsets are 32-bit: refuse files that would not fit. */
|
||||
uint32_t offsets[YAPF_MAX_MIPS];
|
||||
uint64_t cur = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY;
|
||||
uint64_t total = YAPF__HDR_LEN + (uint64_t)mip_levels * YAPF__MIP_ENTRY;
|
||||
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) {
|
||||
offsets[m] = (uint32_t)cur;
|
||||
cur += bws[m].len;
|
||||
if (cur > 0xFFFFFFFFull) err = YAPF_ERR_INVALID;
|
||||
total += bws[m].len;
|
||||
if (total > 0xFFFFFFFFull) err = YAPF_ERR_INVALID;
|
||||
}
|
||||
|
||||
FILE *f = NULL;
|
||||
if (err == YAPF_OK && !(f = fopen(filename, "wb"))) err = YAPF_ERR_IO;
|
||||
uint8_t *file = NULL;
|
||||
if (err == YAPF_OK && !(file = (uint8_t *)malloc((size_t)total))) err = YAPF_ERR_OOM;
|
||||
|
||||
if (err == YAPF_OK) {
|
||||
uint8_t hdr[YAPF__HDR_LEN];
|
||||
memset(hdr, 0, sizeof(hdr));
|
||||
memcpy(hdr, YAPF__MAGIC, 4);
|
||||
hdr[4] = YAPF__VERSION;
|
||||
hdr[5] = img->channels;
|
||||
hdr[6] = img->gpu_format;
|
||||
hdr[7] = mip_levels;
|
||||
hdr[8] = img->flags;
|
||||
yapf__w32le(hdr + 12, img->width);
|
||||
yapf__w32le(hdr + 16, img->height);
|
||||
if (fwrite(hdr, 1, YAPF__HDR_LEN, f) != YAPF__HDR_LEN) err = YAPF_ERR_IO;
|
||||
memset(file, 0, YAPF__HDR_LEN);
|
||||
memcpy(file, YAPF__MAGIC, 4);
|
||||
file[4] = YAPF__VERSION;
|
||||
file[5] = img->channels;
|
||||
file[6] = img->gpu_format;
|
||||
file[7] = mip_levels;
|
||||
file[8] = img->flags;
|
||||
yapf__w32le(file + 12, img->width);
|
||||
yapf__w32le(file + 16, img->height);
|
||||
|
||||
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++) {
|
||||
uint8_t entry[YAPF__MIP_ENTRY];
|
||||
yapf__w32le(entry + 0, offsets[m]);
|
||||
size_t pos = YAPF__HDR_LEN + (size_t)mip_levels * YAPF__MIP_ENTRY;
|
||||
for (uint8_t m = 0; m < mip_levels; m++) {
|
||||
uint8_t *entry = file + YAPF__HDR_LEN + (size_t)m * YAPF__MIP_ENTRY;
|
||||
yapf__w32le(entry + 0, (uint32_t)pos);
|
||||
yapf__w32le(entry + 4, (uint32_t)bws[m].len);
|
||||
if (fwrite(entry, 1, YAPF__MIP_ENTRY, f) != YAPF__MIP_ENTRY)
|
||||
err = YAPF_ERR_IO;
|
||||
memcpy(file + pos, bws[m].buf, bws[m].len);
|
||||
pos += bws[m].len;
|
||||
}
|
||||
for (uint8_t m = 0; m < mip_levels && err == YAPF_OK; m++)
|
||||
if (fwrite(bws[m].buf, 1, bws[m].len, f) != bws[m].len)
|
||||
err = YAPF_ERR_IO;
|
||||
if (fclose(f) != 0 && err == YAPF_OK) err = YAPF_ERR_IO;
|
||||
*out_data = file;
|
||||
*out_size = (size_t)total;
|
||||
}
|
||||
|
||||
for (uint8_t m = 0; m < mip_levels; m++) free(bws[m].buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
void yapf_free_buffer(void *data) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
int yapf_save(const char *filename, const yapf_image_t *img) {
|
||||
if (!filename) return YAPF_ERR_INVALID;
|
||||
void *data = NULL;
|
||||
size_t size = 0;
|
||||
int err = yapf_encode(img, &data, &size);
|
||||
if (err != YAPF_OK) return err;
|
||||
|
||||
FILE *f = fopen(filename, "wb");
|
||||
if (!f) err = YAPF_ERR_IO;
|
||||
else {
|
||||
if (fwrite(data, 1, size, f) != size) err = YAPF_ERR_IO;
|
||||
if (fclose(f) != 0 && err == YAPF_OK) err = YAPF_ERR_IO;
|
||||
}
|
||||
yapf_free_buffer(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* yapf_free
|
||||
* ======================================================================== */
|
||||
|
|
|
|||
11
yapf.h
11
yapf.h
|
|
@ -232,6 +232,17 @@ yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
|
|||
*/
|
||||
int yapf_save(const char *filename, const yapf_image_t *img);
|
||||
|
||||
/*
|
||||
* yapf_encode — encode an image into memory.
|
||||
* On success *out_data points to *out_size bytes of a complete .yapf file;
|
||||
* release it with yapf_free_buffer(). Same rules and return codes as
|
||||
* yapf_save() (no YAPF_ERR_IO).
|
||||
*/
|
||||
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||
|
||||
/* yapf_free_buffer — release memory returned by yapf_encode(). NULL-safe. */
|
||||
void yapf_free_buffer(void *data);
|
||||
|
||||
/*
|
||||
* yapf_free — release all memory owned by an image returned by yapf_load().
|
||||
* NULL-safe.
|
||||
|
|
|
|||
Loading…
Reference in a new issue