mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
Merge YAPF v1: fast format, SIMD, tools, plugins and language bindings
YAPF v1: fast byte-aligned format, SIMD, tools and plugins
This commit is contained in:
commit
5642b8c85d
85 changed files with 16727 additions and 712 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# build outputs
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.vsix
|
||||||
|
test_yapf_tmp.yapf
|
||||||
|
|
||||||
|
# generated: copied from bindings/js/yapf.js when packaging
|
||||||
|
/extensions/vscode/media/yapf.js
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
/bindings/rust/target/
|
||||||
|
/bindings/rust/Cargo.lock
|
||||||
|
# C# / Go / Java build outputs
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.class
|
||||||
65
Makefile
Normal file
65
Makefile
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# YAPF — simple build for Linux, macOS and other Unix systems.
|
||||||
|
#
|
||||||
|
# make library (static + shared) and the `yapf` tool, in build/
|
||||||
|
# make test run the C test suite (and the JS one if node is present)
|
||||||
|
# sudo make install install header, libraries and tool under $(PREFIX)
|
||||||
|
# make NO_SIMD=1 plain C only
|
||||||
|
#
|
||||||
|
# Windows: use build.bat (LLVM) or any MinGW / MSVC compiler on yapf.c.
|
||||||
|
|
||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -O2
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
BUILD ?= build
|
||||||
|
|
||||||
|
YAPF_CFLAGS = -std=c99 -Wall -Wextra $(CFLAGS) $(if $(NO_SIMD),-DYAPF_NO_SIMD)
|
||||||
|
LDLIBS = -lm -pthread
|
||||||
|
|
||||||
|
ifeq ($(shell uname -s),Darwin)
|
||||||
|
SHARED = libyapf.dylib
|
||||||
|
SHARED_FLAGS = -dynamiclib -install_name $(PREFIX)/lib/libyapf.dylib
|
||||||
|
else
|
||||||
|
SHARED = libyapf.so
|
||||||
|
SHARED_FLAGS = -shared
|
||||||
|
endif
|
||||||
|
|
||||||
|
all: $(BUILD)/libyapf.a $(BUILD)/$(SHARED) $(BUILD)/yapf
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
mkdir -p $(BUILD)
|
||||||
|
|
||||||
|
$(BUILD)/yapf.o: yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) -fPIC -c yapf.c -o $@
|
||||||
|
|
||||||
|
$(BUILD)/libyapf.a: $(BUILD)/yapf.o
|
||||||
|
$(AR) rcs $@ $^
|
||||||
|
|
||||||
|
$(BUILD)/$(SHARED): $(BUILD)/yapf.o
|
||||||
|
$(CC) $(SHARED_FLAGS) $^ -o $@ -pthread
|
||||||
|
|
||||||
|
$(BUILD)/yapf: tools/yapf_cli.c yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) tools/yapf_cli.c yapf.c -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
$(BUILD)/test_yapf: tests/test_yapf.c yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) tests/test_yapf.c yapf.c -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
test: $(BUILD)/test_yapf
|
||||||
|
$(BUILD)/test_yapf other/YAPF.YAPF
|
||||||
|
@if command -v node >/dev/null 2>&1; then node bindings/js/test.js; \
|
||||||
|
else echo "(node not found: skipped the JavaScript tests)"; fi
|
||||||
|
|
||||||
|
install: all
|
||||||
|
install -d $(DESTDIR)$(PREFIX)/include $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/bin
|
||||||
|
install -m 644 yapf.h $(DESTDIR)$(PREFIX)/include/yapf.h
|
||||||
|
install -m 644 $(BUILD)/libyapf.a $(DESTDIR)$(PREFIX)/lib/libyapf.a
|
||||||
|
install -m 755 $(BUILD)/$(SHARED) $(DESTDIR)$(PREFIX)/lib/$(SHARED)
|
||||||
|
install -m 755 $(BUILD)/yapf $(DESTDIR)$(PREFIX)/bin/yapf
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(DESTDIR)$(PREFIX)/include/yapf.h $(DESTDIR)$(PREFIX)/lib/libyapf.a \
|
||||||
|
$(DESTDIR)$(PREFIX)/lib/$(SHARED) $(DESTDIR)$(PREFIX)/bin/yapf
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD)
|
||||||
|
|
||||||
|
.PHONY: all test install uninstall clean
|
||||||
400
README.md
400
README.md
|
|
@ -1,90 +1,137 @@
|
||||||
# YAPF — Yet Another Picture Format
|
# YAPF — Yet Another Picture Format
|
||||||
|
|
||||||
**Fast. Small. Easy.**
|
**Lossless images that decode several times faster than PNG — on one core.**
|
||||||
|
|
||||||
YAPF is a lossless image codec built around a single principle: decoding should
|
YAPF is a lossless image format for photos, UI and game assets, built for
|
||||||
be as close to a memory copy as possible. Instead of running a general-purpose
|
decode speed first. There is no entropy coder, no bit reader and no context
|
||||||
compressor like DEFLATE over pixel data, YAPF encodes images with five cheap
|
model: every structure is byte aligned, and the decoder runs short loops that
|
||||||
operations — a pixel cache lookup, two flavours of delta encoding, run-length
|
modern CPUs execute at memory speed — with SSE / NEON where available. The
|
||||||
encoding, and verbatim pixel storage — producing files that are smaller than
|
encoder does the hard work, so the decoder only follows instructions.
|
||||||
uncompressed formats while decoding nearly as fast as `memcpy`.
|
|
||||||
|
- **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 /
|
||||||
|
premultiplied flags, and random access to 64×64 tiles.
|
||||||
|
- **Tiny and portable**: two C99 files, no dependencies. Plus a CLI, Python,
|
||||||
|
JavaScript and plugins for the tools you already use.
|
||||||
|
|
||||||
|
This is **YAPF version 1**, the first stable format.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why YAPF?
|
## Use it in…
|
||||||
|
|
||||||
Standard lossless formats like PNG compress well, but decompressing DEFLATE
|
| Where | What you get | Guide | Status |
|
||||||
streams is CPU-intensive. On large images (4K and above) or in
|
| :---- | :----------- | :---- | :----- |
|
||||||
latency-sensitive applications (game engines, real-time pipelines, embedded
|
| **C / C++** | `yapf.h` + `yapf.c`, drop into any project (compiles as C or C++) | [below](#c--c) | ✅ tested — see [platforms](#platforms) |
|
||||||
systems) this is a meaningful bottleneck.
|
| **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 |
|
||||||
|
| **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** |
|
||||||
|
|
||||||
YAPF trades a modest compression ratio advantage for dramatically lower decode
|
Pre-built downloads (command-line tool and libraries for Windows, macOS and
|
||||||
complexity. There are no Huffman tables to build, no sliding windows to
|
Linux, plus the plugin packages) are published on the
|
||||||
maintain, and no bit-level unpacking. The decoder is a tight byte-dispatch
|
[Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can
|
||||||
loop that any modern CPU handles at memory bandwidth.
|
also be built from source as described below.
|
||||||
|
|
||||||
### Performance
|
### Platforms
|
||||||
|
|
||||||
Benchmark: loading a 5K professional photograph (5120 × 2880 px).
|
| Platform | Status |
|
||||||
|
| :------- | :----- |
|
||||||
| Format | Load time |
|
| Linux x64 | ✅ built and fully tested |
|
||||||
| :----- | ----------: |
|
| Windows x64 | ✅ built and fully tested (MinGW and Zig builds, run under Wine) |
|
||||||
| **YAPF** | **0.111 s** |
|
| Linux ARM64 | ✅ built and fully tested (NEON, run under qemu) |
|
||||||
| PNG | 0.194 s |
|
| Windows ARM64 | ⚠️ built only, not run |
|
||||||
|
| macOS x64 / ARM64 | ⚠️ **cross-compiled only, not yet run on a Mac** |
|
||||||
YAPF decodes approximately **1.7× faster** than PNG on the same hardware
|
|
||||||
without SIMD or threading.
|
|
||||||
|
|
||||||
### When YAPF excels
|
|
||||||
|
|
||||||
- **Pixel art, sprites, UI textures** — large flat regions of identical colour
|
|
||||||
compress to single RUN bytes; palette-like images benefit heavily from the
|
|
||||||
cache.
|
|
||||||
- **Screenshots and diagrams** — solid backgrounds and repeated gradients hit
|
|
||||||
DIFF/LUMA and the cache constantly.
|
|
||||||
- **Game asset pipelines** — load textures at startup without paying a
|
|
||||||
decompression tax every frame.
|
|
||||||
- **Embedded / resource-constrained targets** — the entire codec fits in ~200
|
|
||||||
lines of portable C with zero external dependencies.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Integration
|
## Command line
|
||||||
|
|
||||||
The core library is two files. Drop them into your project and compile:
|
```sh
|
||||||
|
yapf photo.png photo.yapf # encode (formats by file extension)
|
||||||
|
yapf texture.png texture.yapf --mips # with a full mip chain
|
||||||
|
yapf photo.yapf photo.png # decode to PNG / JPG / BMP / TGA
|
||||||
|
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),
|
||||||
|
`--premult` (alpha is premultiplied).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benchmark
|
||||||
|
|
||||||
|
`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 | 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×** |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C / C++
|
||||||
|
|
||||||
|
The library is two files. Add them to your project:
|
||||||
|
|
||||||
```
|
```
|
||||||
yapf.h — public API and format documentation
|
yapf.h — public API and the complete format specification
|
||||||
yapf.c — encoder / decoder implementation
|
yapf.c — encoder and decoder
|
||||||
```
|
```
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "yapf.h"
|
#include "yapf.h"
|
||||||
|
|
||||||
int main(void) {
|
yapf_image_t *img = yapf_load("texture.yapf"); /* or yapf_load_memory(buf, len) */
|
||||||
/* Load */
|
if (img) {
|
||||||
yapf_image_t *img = yapf_load("photo.yapf");
|
/* img->pixels: width × height × channels bytes, rows top to bottom */
|
||||||
if (!img) { /* file not found or not a valid YAPF stream */ }
|
upload_texture(img->width, img->height, img->channels, img->pixels);
|
||||||
|
|
||||||
printf("%u x %u, %u channel(s)\n",
|
|
||||||
img->width, img->height, img->channels);
|
|
||||||
|
|
||||||
/* Save */
|
|
||||||
int rc = yapf_save("output.yapf", img);
|
|
||||||
if (rc != YAPF_OK) { /* write error */ }
|
|
||||||
|
|
||||||
yapf_free(img);
|
yapf_free(img);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yapf_image_t out = { .width = w, .height = h, .channels = 4,
|
||||||
|
.mip_levels = 1, .pixels = rgba, .flags = YAPF_FLAG_SRGB };
|
||||||
|
yapf_save("out.yapf", &out);
|
||||||
```
|
```
|
||||||
|
|
||||||
Compile with any C99-capable compiler — no flags required for the baseline:
|
Compile with any C99 compiler (`-pthread` on Unix for the `_mt` loaders):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gcc -O2 -std=c99 main.c yapf.c -o myapp
|
cc -O2 -std=c99 main.c yapf.c -o app -pthread
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
SIMD is on by default: SSSE3 on x86 / x64 (chosen at run time with CPUID,
|
||||||
|
so binaries still run on older CPUs) and NEON on ARM64, with GCC, Clang,
|
||||||
|
MSVC and MinGW.
|
||||||
|
|
||||||
|
| Define | Effect |
|
||||||
|
| :------------------ | :----- |
|
||||||
|
| `-DYAPF_NO_SIMD` | plain C only |
|
||||||
|
| `-DYAPF_NO_THREADS` | no threads; the `_mt` loaders decode on the calling thread, and only the C standard library is needed |
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
|
|
@ -92,15 +139,20 @@ gcc -O2 -std=c99 main.c yapf.c -o myapp
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t width; /* pixels per row */
|
uint32_t width; /* pixels per row */
|
||||||
uint32_t height; /* rows */
|
uint32_t height; /* rows */
|
||||||
uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */
|
uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */
|
||||||
uint8_t *pixels; /* raw pixel buffer, malloc-owned */
|
uint8_t gpu_format; /* YAPF_GPU_* hint */
|
||||||
|
uint8_t flags; /* YAPF_FLAG_* (premultiplied alpha, sRGB) */
|
||||||
|
uint8_t mip_levels; /* 1 = base only, up to 16 */
|
||||||
|
uint8_t *pixels; /* base level, malloc-owned */
|
||||||
|
uint8_t **mips; /* mips[0] == pixels, mips[1..] smaller levels */
|
||||||
} yapf_image_t;
|
} yapf_image_t;
|
||||||
```
|
```
|
||||||
|
|
||||||
`pixels` is a tightly packed, row-major byte array of `width × height × channels`
|
`pixels` is a tightly packed, row-major byte array of `width × height × channels`
|
||||||
bytes. The layout per pixel matches the `channels` value:
|
bytes. Mip level *m* is `max(1, width >> m) × max(1, height >> m)`. The
|
||||||
|
layout per pixel matches the `channels` value:
|
||||||
|
|
||||||
| `channels` | Layout per pixel |
|
| `channels` | Layout per pixel |
|
||||||
| :--------- | :--------------- |
|
| :--------- | :--------------- |
|
||||||
|
|
@ -113,29 +165,47 @@ bytes. The layout per pixel matches the `channels` value:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
yapf_image_t *yapf_load(const char *filename);
|
yapf_image_t *yapf_load(const char *filename);
|
||||||
|
yapf_image_t *yapf_load_memory(const void *buffer, size_t size);
|
||||||
```
|
```
|
||||||
Decodes a YAPF file from disk. Returns a heap-allocated `yapf_image_t` on
|
Decode a YAPF file from disk or from memory (e.g. a game archive). Return a
|
||||||
success, `NULL` on any failure (file not found, invalid magic, unsupported
|
heap-allocated image on success, `NULL` on any failure (file not found,
|
||||||
version, out of memory). Free with `yapf_free()`.
|
invalid or corrupt data, out of memory). Corrupt input is rejected, never
|
||||||
|
read out of bounds.
|
||||||
|
|
||||||
|
```c
|
||||||
|
yapf_image_t *yapf_load_mt(const char *filename, int threads);
|
||||||
|
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
|
||||||
|
```
|
||||||
|
Same, decoding tiles on up to `threads` threads (`0` = one per CPU core).
|
||||||
|
The output is identical to the single-threaded loaders.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
int yapf_save(const char *filename, const yapf_image_t *img);
|
int yapf_save(const char *filename, const yapf_image_t *img);
|
||||||
```
|
```
|
||||||
Encodes an image to disk. Returns `YAPF_OK` (0) on success or a negative
|
Encodes an image to disk. Writes `img->mip_levels`
|
||||||
`YAPF_ERR_*` code on failure.
|
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
|
```c
|
||||||
void yapf_free(yapf_image_t *img);
|
void yapf_free(yapf_image_t *img);
|
||||||
```
|
```
|
||||||
Releases memory returned by `yapf_load()`. Passing `NULL` is safe.
|
Releases memory returned by the loaders. Passing `NULL` is safe.
|
||||||
|
|
||||||
### Return codes
|
### Return codes
|
||||||
|
|
||||||
| Constant | Value | Meaning |
|
| Constant | Value | Meaning |
|
||||||
| :---------------- | ----: | :----------------------------------- |
|
| :----------------- | ----: | :----------------------------------- |
|
||||||
| `YAPF_OK` | 0 | Success |
|
| `YAPF_OK` | 0 | Success |
|
||||||
| `YAPF_ERR_INVALID`| –1 | NULL or logically invalid argument |
|
| `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
|
||||||
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
||||||
|
| `YAPF_ERR_OOM` | –3 | Out of memory |
|
||||||
|
|
||||||
### Channel constants
|
### Channel constants
|
||||||
|
|
||||||
|
|
@ -148,115 +218,109 @@ YAPF_CHANNELS_RGBA 4
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Building the libraries
|
## Building
|
||||||
|
|
||||||
The build scripts uses the [LLVM](https://releases.llvm.org/) compiler
|
| System | Command | Output |
|
||||||
|
| :----- | :------ | :----- |
|
||||||
|
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
|
||||||
|
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
|
||||||
|
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
|
||||||
|
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |
|
||||||
|
| All platforms at once | `tools/build_release.sh` (needs [zig](https://ziglang.org)) | `dist/release/`: every download on the Releases page |
|
||||||
|
|
||||||
|
The command-line tool is `tools/yapf_cli.c` + `yapf.c`; it uses the public
|
||||||
|
domain [stb](https://github.com/nothings/stb) headers in `third_party/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Two colour transforms** — per 64×64 tile, YCoCg-R or subtract-green
|
||||||
|
(`G, R−G, B−G`), whichever codes smaller. Both run mod 256, so every
|
||||||
|
plane stays one byte per sample.
|
||||||
|
2. **Twelve row filters** — per band of 8 rows: Left, Gradient (`L + A − D`),
|
||||||
|
Up-right, Up-average, or the row 1–8 rows above (catches repeating
|
||||||
|
textures and dither patterns). None depends on the previous sample by
|
||||||
|
more than one add, and the vertical ones decode as plain vector code.
|
||||||
|
3. **Groups of 8** — residuals are packed in groups of 8 at the group's bit
|
||||||
|
width: 8 values × *w* bits = exactly *w* bytes, so every group is byte
|
||||||
|
aligned and unpacks with one 64-bit load (or one SIMD shuffle).
|
||||||
|
4. **4-bit control codes** — one per group, or one per run of up to 64 groups
|
||||||
|
repeating the previous width; the encoder picks the cheapest sequence with
|
||||||
|
a small dynamic program.
|
||||||
|
5. **Validate first, then run** — all sizes in a tile are checked before the
|
||||||
|
hot loops start, so the loops need no bounds checks. Corrupt files are
|
||||||
|
rejected, never read out of bounds.
|
||||||
|
6. **Constant and stored tiles** — a plane that is constant in a tile costs
|
||||||
|
one byte; a tile that would not shrink is stored raw (noise never grows).
|
||||||
|
7. **Independent tiles** — any tile can be decoded on its own, in any order,
|
||||||
|
on any thread.
|
||||||
|
|
||||||
|
The complete specification is the comment at the top of [`yapf.h`](yapf.h):
|
||||||
|
|
||||||
|
```
|
||||||
|
HEADER (20 bytes) "YAPF", version 1, channels, GPU hint, mip count,
|
||||||
|
flags, 3 reserved, width u32, height u32
|
||||||
|
MIP INDEX per level: offset u32, size u32
|
||||||
|
LEVEL DATA tile_size u32 × tile_count, then the tiles
|
||||||
|
TILE (64×64) header byte: stored raw, or constant-plane mask + colour transform
|
||||||
|
per plane: filter byte per 8 rows, control nibbles,
|
||||||
|
w bytes per group of 8 values
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Unix (Linux / macOS)
|
make test # C suite: 786 checks, plus JavaScript if node is installed
|
||||||
chmod +x build.sh
|
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
|
||||||
./build.sh
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
build.bat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Output goes to `dist/` with one subdirectory per platform:
|
What has been verified:
|
||||||
|
|
||||||
```
|
- exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
|
||||||
dist/
|
single- and multi-threaded decoding, in-memory and file encoding;
|
||||||
├── windows-x64/ yapf.dll yapf.lib
|
- the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux
|
||||||
├── windows-arm64/ yapf.dll yapf.lib
|
ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as
|
||||||
├── macos-x64/ libyapf.dylib
|
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
||||||
├── macos-arm64/ libyapf.dylib
|
ThreadSanitizer;
|
||||||
├── linux-x64/ libyapf.so libyapf.a
|
- thousands of corrupted and truncated files are rejected without crashing;
|
||||||
└── linux-arm64/ libyapf.so libyapf.a
|
- 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.
|
||||||
|
|
||||||
### Optional build flags
|
**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
|
||||||
| Flag | Effect |
|
live Dolphin / GNOME file manager.
|
||||||
| :---------------- | :-------------------------------------------------- |
|
|
||||||
| `-DYAPF_USE_SIMD` | Reserved for future SIMD acceleration paths |
|
|
||||||
|
|
||||||
The interactive build script will prompt for these options.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technical Specification
|
## Project layout
|
||||||
|
|
||||||
### File header — 14 bytes
|
|
||||||
|
|
||||||
| Offset | Size | Type | Description |
|
|
||||||
| :----- | ---: | :--------- | :-------------------------------------- |
|
|
||||||
| 0x00 | 4 | `char[4]` | Magic: `"YAPF"` |
|
|
||||||
| 0x04 | 1 | `uint8_t` | Format version (currently `1`) |
|
|
||||||
| 0x05 | 4 | `uint32_t` | Width in pixels, little-endian |
|
|
||||||
| 0x09 | 4 | `uint32_t` | Height in pixels, little-endian |
|
|
||||||
| 0x0D | 1 | `uint8_t` | Channel count: 1 / 2 / 3 / 4 |
|
|
||||||
|
|
||||||
All multi-byte integers are always stored little-endian, regardless of host
|
|
||||||
architecture.
|
|
||||||
|
|
||||||
### Byte-stream opcodes
|
|
||||||
|
|
||||||
The payload begins immediately after the header and is decoded left-to-right.
|
|
||||||
A 64-slot pixel cache (zero-initialised) and a running "previous pixel" state
|
|
||||||
(`r=0, g=0, b=0, a=255`) are maintained throughout.
|
|
||||||
|
|
||||||
| Byte range | Opcode | Size | Description |
|
|
||||||
| :---------- | :----- | ------: | :-------------------------------------------- |
|
|
||||||
| 0x00–0x3F | INDEX | 1 byte | Restore pixel from cache slot `byte & 0x3F` |
|
|
||||||
| 0x40–0x7F | DIFF | 1 byte | Per-channel deltas: dr = bits[5:4]–2, dg = bits[3:2]–2, db = bits[1:0]–2. Range –2..+1. Alpha unchanged. |
|
|
||||||
| 0x80–0xBF | LUMA | 2 bytes | Green delta dg = (byte & 0x3F)–32. Second byte: dr = ((b1>>4)&0xF)–8+dg, db = (b1&0xF)–8+dg. Alpha unchanged. |
|
|
||||||
| 0xC0–0xFD | RUN | 1 byte | Repeat previous pixel (byte&0x3F)+1 times (1–62). Cache not updated. |
|
|
||||||
| 0xFE | RGB | 4 bytes | Next 3 bytes = R, G, B. Alpha unchanged. |
|
|
||||||
| 0xFF | RGBA | 5 bytes | Next 4 bytes = R, G, B, A. |
|
|
||||||
|
|
||||||
After every non-RUN opcode, the cache slot `HASH(r,g,b,a)` is updated and the
|
|
||||||
pixel becomes the new previous pixel.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
HASH(r,g,b,a) = (r×3 + g×5 + b×7 + a×11) mod 64
|
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
|
||||||
|
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
|
||||||
|
third_party/ stb_image / stb_image_write (public domain, CLI only)
|
||||||
|
other/YAPF.YAPF sample image (the YAPF logo)
|
||||||
```
|
```
|
||||||
|
|
||||||
The stream ends when exactly `width × height` pixels have been decoded; no
|
|
||||||
end-of-stream marker is required.
|
|
||||||
|
|
||||||
### Pixel buffer layout
|
|
||||||
|
|
||||||
Within the decoded buffer, each pixel occupies `channels` consecutive bytes in
|
|
||||||
the order shown in the API table above. Grayscale images store a single
|
|
||||||
luminance byte per pixel; the encoder internally promotes gray values to
|
|
||||||
`r = g = b = Y` for uniform opcode selection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GIMP Plugin
|
|
||||||
|
|
||||||
A plugin for GIMP is located in `extensions/gimp/`. See the
|
|
||||||
[build instructions](extensions/gimp/howtocompile.txt) for details.
|
|
||||||
|
|
||||||
Supported colour modes: Gray, Gray+Alpha, RGB, RGBA.
|
|
||||||
File extension: `.yapf`. MIME type: `image/x-yapf`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Platforms
|
|
||||||
|
|
||||||
| Platform | Architectures | Library format |
|
|
||||||
| :------------- | :------------ | :------------------ |
|
|
||||||
| Windows | x64, ARM64 | `.dll` + `.lib` |
|
|
||||||
| macOS | ARM64 | `.dylib` |
|
|
||||||
| Linux (glibc) | x64, ARM64 | `.so` |
|
|
||||||
| Linux (musl) | x64, ARM64 | `.a` (static) |
|
|
||||||
|
|
||||||
The core library (`yapf.c` / `yapf.h`) uses only the C99 standard library
|
|
||||||
(`<stdio.h>`, `<stdlib.h>`, `<string.h>`, `<stdint.h>`) and compiles cleanly
|
|
||||||
on any conforming C99 or C++ toolchain with no configuration required.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache License 2.0 — see [LICENSE](LICENSE).
|
Apache License 2.0 — see [LICENSE](LICENSE). `third_party/` contains
|
||||||
|
stb_image and stb_image_write by Sean Barrett (public domain / MIT).
|
||||||
|
|
|
||||||
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)); }
|
||||||
|
}
|
||||||
24
bindings/js/README.md
Normal file
24
bindings/js/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# yapf.js — YAPF in JavaScript
|
||||||
|
|
||||||
|
Encoder and decoder for YAPF v1 in one dependency-free file. Works in
|
||||||
|
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
|
||||||
|
produces exactly the same bytes as the C library.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const YAPF = require('./yapf.js'); // or <script src="yapf.js"> → window.YAPF
|
||||||
|
|
||||||
|
const img = YAPF.decode(bytes); // Uint8Array → image
|
||||||
|
// img = { width, height, channels, pixels, gpuFormat, flags, mipLevels, mips }
|
||||||
|
|
||||||
|
const file = YAPF.encode({ width, height, channels: 4, pixels }); // → Uint8Array
|
||||||
|
|
||||||
|
// draw on a canvas
|
||||||
|
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).
|
||||||
|
|
||||||
|
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}`);
|
||||||
12
bindings/js/package.json
Normal file
12
bindings/js/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "yapf-image",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies",
|
||||||
|
"main": "yapf.js",
|
||||||
|
"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",
|
||||||
|
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git", "directory": "bindings/js" }
|
||||||
|
}
|
||||||
57
bindings/js/test.js
Normal file
57
bindings/js/test.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* node bindings/js/test.js — round-trips, the sample file, and corrupt input. */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const YAPF = require('./yapf.js');
|
||||||
|
|
||||||
|
let tests = 0, fails = 0;
|
||||||
|
const check = (ok, msg) => { tests++; if (!ok) { fails++; console.log('FAIL:', msg); } };
|
||||||
|
|
||||||
|
let seed = 12345;
|
||||||
|
const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) >>> 0) >>> 8;
|
||||||
|
|
||||||
|
function fill(w, h, ch, kind) {
|
||||||
|
const p = new Uint8Array(w * h * ch);
|
||||||
|
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) for (let c = 0; c < ch; c++) {
|
||||||
|
const i = (y * w + x) * ch + c;
|
||||||
|
p[i] = kind === 0 ? rnd() : kind === 1 ? x * 3 + y * 5 + c * 40
|
||||||
|
: kind === 2 ? (((x / 7 | 0) + (y / 5 | 0) + c) & 1 ? 200 : 30)
|
||||||
|
: kind === 3 ? ((x * x + y * y) / 50 | 0) + rnd() % 5
|
||||||
|
: c === ch - 1 ? 255 : x ^ y;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [w, h] of [[1, 1], [7, 1], [1, 9], [63, 65], [64, 64], [97, 61], [200, 130]])
|
||||||
|
for (let ch = 1; ch <= 4; ch++) for (let kind = 0; kind < 5; kind++) {
|
||||||
|
const pixels = fill(w, h, ch, kind);
|
||||||
|
const back = YAPF.decode(YAPF.encode({ width: w, height: h, channels: ch, pixels }));
|
||||||
|
check(back.width === w && back.height === h && back.channels === ch &&
|
||||||
|
Buffer.compare(Buffer.from(back.pixels), Buffer.from(pixels)) === 0,
|
||||||
|
`roundtrip ${w}x${h} ch${ch} kind${kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The bundled sample: decode to the reference pixels, re-encode to the same bytes. */
|
||||||
|
const samplePath = path.join(__dirname, '..', '..', 'other', 'YAPF.YAPF');
|
||||||
|
if (fs.existsSync(samplePath)) {
|
||||||
|
const file = fs.readFileSync(samplePath);
|
||||||
|
const img = YAPF.decode(file);
|
||||||
|
let h = 2166136261;
|
||||||
|
for (const v of img.pixels) h = Math.imul(h ^ v, 16777619) >>> 0;
|
||||||
|
check(h === 0x8891c2b4, 'sample decodes to the reference pixels');
|
||||||
|
check(Buffer.compare(Buffer.from(YAPF.encode(img)), file) === 0,
|
||||||
|
'sample re-encodes to byte-identical output (matches the C encoder)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corrupt input must throw, never hang or return garbage silently. */
|
||||||
|
const good = YAPF.encode({ width: 130, height: 70, channels: 4, pixels: fill(130, 70, 4, 3) });
|
||||||
|
let threwOnTruncation = true;
|
||||||
|
for (let i = 0; i < 2000; i++) {
|
||||||
|
const bad = good.slice(0, i % 3 === 1 ? rnd() % good.length : good.length);
|
||||||
|
if (i % 3 !== 1) bad[rnd() % bad.length] ^= 1 << (rnd() % 8);
|
||||||
|
try { YAPF.decode(bad); if (i % 3 === 1) threwOnTruncation = false; } catch (e) { /* expected */ }
|
||||||
|
}
|
||||||
|
check(threwOnTruncation, 'truncated files are rejected');
|
||||||
|
|
||||||
|
console.log(`${tests} tests, ${fails} failures`);
|
||||||
|
process.exit(fails ? 1 : 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;
|
||||||
463
bindings/js/yapf.js
Normal file
463
bindings/js/yapf.js
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
/*
|
||||||
|
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript.
|
||||||
|
*
|
||||||
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Works in browsers, Node.js, Figma plugins and VS Code webviews; no
|
||||||
|
* dependencies. Produces exactly the same bytes as the C library.
|
||||||
|
*
|
||||||
|
* const img = YAPF.decode(bytes); // Uint8Array → image
|
||||||
|
* const file = YAPF.encode(img); // image → Uint8Array
|
||||||
|
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
||||||
|
*
|
||||||
|
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
||||||
|
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
||||||
|
* The specification is the comment at the top of yapf.h.
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.YAPF = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TILE = 64, TILE_PX = TILE * TILE, GROUP = 8, BAND = 8, RING = 16;
|
||||||
|
const MAX_GROUPS = TILE_PX / GROUP, MAX_DIM = 65535, MAX_MIPS = 16;
|
||||||
|
const STORED = 0x80, SUBGREEN = 0x10, NIB_REPEAT = 9;
|
||||||
|
const F_LEFT = 0, F_GRAD = 1, F_UPRIGHT = 2, F_UPAVG = 3, F_UP = 16;
|
||||||
|
const NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64];
|
||||||
|
const RUN_LENS = [2, 3, 4, 8, 16, 32, 64];
|
||||||
|
/* Encoder try order: cheapest to decode first, ties go to the earlier. */
|
||||||
|
const FILTERS = [F_UP, F_UP + 1, F_UP + 2, F_UP + 3, F_UP + 4, F_UP + 5,
|
||||||
|
F_UP + 6, F_UP + 7, F_UPRIGHT, F_UPAVG, F_LEFT, F_GRAD];
|
||||||
|
|
||||||
|
const GPU = { RGBA8: 0, RGB8: 1, RG8: 2, R8: 3, SRGB8_A8: 4, SRGB8: 5 };
|
||||||
|
const FLAG = { PREMULT_ALPHA: 1, SRGB: 2 };
|
||||||
|
|
||||||
|
const ZERO_ROW = new Uint8Array(TILE + 1);
|
||||||
|
|
||||||
|
const zz = (r) => ((r << 1) ^ ((r << 24) >> 31)) & 255; /* r is 0..255 */
|
||||||
|
const unzz = (z) => ((z >>> 1) ^ -(z & 1)) & 255;
|
||||||
|
const width = (z) => (z === 0 ? 0 : 32 - Math.clz32(z));
|
||||||
|
const s8 = (v) => (v << 24) >> 24;
|
||||||
|
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
||||||
|
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
||||||
|
|
||||||
|
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); }
|
||||||
|
|
||||||
|
/* ── Decoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function unpackPlane(b, p, end, z, tw, th) {
|
||||||
|
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
|
||||||
|
if (end - p < nb) corrupt();
|
||||||
|
const filt = b.subarray(p, p + nb);
|
||||||
|
p += nb;
|
||||||
|
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) corrupt();
|
||||||
|
|
||||||
|
const gw = new Uint8Array(ng + 64);
|
||||||
|
let data = 0, g = 0, hi = 0, w = 0;
|
||||||
|
while (g < ng) {
|
||||||
|
if (p >= end) corrupt();
|
||||||
|
const c = (b[p] >> (hi * 4)) & 15, n = NIB_GROUPS[c];
|
||||||
|
p += hi; hi ^= 1;
|
||||||
|
if (c < NIB_REPEAT) w = c;
|
||||||
|
gw.fill(w, g, g + n);
|
||||||
|
data += w * n; g += n;
|
||||||
|
}
|
||||||
|
if (g !== ng) corrupt();
|
||||||
|
if (hi) { if (b[p] >> 4) corrupt(); p++; }
|
||||||
|
if (end - p < data) corrupt();
|
||||||
|
|
||||||
|
for (g = 0; g < ng; g++) {
|
||||||
|
w = gw[g];
|
||||||
|
const o = g * GROUP, mask = (1 << w) - 1;
|
||||||
|
let acc = 0, bits = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) {
|
||||||
|
while (bits < w) { acc |= b[p++] << bits; bits += 8; }
|
||||||
|
z[o + i] = acc & mask;
|
||||||
|
acc >>>= w; bits -= w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { p, filt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfilterRow(f, R, r0, U, u0, K, k0, Z, z0, tw) {
|
||||||
|
if (f === F_LEFT) {
|
||||||
|
let L = 0;
|
||||||
|
for (let x = 0; x < tw; x++) R[r0 + x] = L = (L + unzz(Z[z0 + x])) & 255;
|
||||||
|
} else if (f === F_GRAD) {
|
||||||
|
let L = (U[u0] + unzz(Z[z0])) & 255;
|
||||||
|
R[r0] = L;
|
||||||
|
for (let x = 1; x < tw; x++)
|
||||||
|
R[r0 + x] = L = (L + U[u0 + x] - U[u0 + x - 1] + unzz(Z[z0 + x])) & 255;
|
||||||
|
} else {
|
||||||
|
/* pred = (a + b) >> 1: Up k a = b = K; Up-right a = b = U + 1;
|
||||||
|
* Up-average a = U, b = U + 1. Rows carry one extra sample. */
|
||||||
|
let A, a0, B, b0;
|
||||||
|
if (f >= F_UP) { A = K; a0 = k0; B = K; b0 = k0; }
|
||||||
|
else if (f === F_UPAVG) { A = U; a0 = u0; B = U; b0 = u0 + 1; }
|
||||||
|
else { A = U; a0 = u0 + 1; B = U; b0 = u0 + 1; }
|
||||||
|
for (let x = 0; x < tw; x++)
|
||||||
|
R[r0 + x] = (((A[a0 + x] + B[b0 + x]) >> 1) + unzz(Z[z0 + x])) & 255;
|
||||||
|
}
|
||||||
|
R[r0 + tw] = R[r0 + tw - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) {
|
||||||
|
if (end <= p) corrupt();
|
||||||
|
let hdr = b[p++];
|
||||||
|
if (hdr === STORED) {
|
||||||
|
const rowb = tw * ch;
|
||||||
|
if (end - p !== rowb * th) corrupt();
|
||||||
|
for (let y = 0; y < th; y++)
|
||||||
|
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hdr & 0xE0) corrupt(); /* reserved bits set */
|
||||||
|
const xf = hdr & SUBGREEN;
|
||||||
|
hdr &= 0x0F;
|
||||||
|
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
||||||
|
|
||||||
|
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
||||||
|
const rows = new Uint8Array(4 * planeLen);
|
||||||
|
const z = [], filt = [null, null, null, null];
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (!(hdr & (1 << c))) continue;
|
||||||
|
if (p >= end) corrupt();
|
||||||
|
rows.fill(b[p++], c * planeLen, (c + 1) * planeLen);
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (hdr & (1 << c)) continue;
|
||||||
|
z[c] = new Uint8Array(TILE_PX + GROUP);
|
||||||
|
const r = unpackPlane(b, p, end, z[c], tw, th);
|
||||||
|
p = r.p; filt[c] = r.filt;
|
||||||
|
}
|
||||||
|
if (p !== end) corrupt();
|
||||||
|
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const ri = (y % RING) * rowLen;
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (!filt[c]) continue;
|
||||||
|
const f = filt[c][y >> 3], k = upK(f), base = c * planeLen;
|
||||||
|
unfilterRow(f, rows, base + ri,
|
||||||
|
y ? rows : ZERO_ROW, y ? base + ((y - 1) % RING) * rowLen : 0,
|
||||||
|
y >= k ? rows : ZERO_ROW, y >= k ? base + ((y - k) % RING) * rowLen : 0,
|
||||||
|
z[c], y * tw, tw);
|
||||||
|
}
|
||||||
|
let d = ((ty + y) * imgW + tx) * ch;
|
||||||
|
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
||||||
|
for (let x = 0; x < tw; x++, d += ch) {
|
||||||
|
if (ch >= 3) {
|
||||||
|
if (xf) {
|
||||||
|
const g = rows[P0 + x];
|
||||||
|
out[d] = rows[P1 + x] + g - 128;
|
||||||
|
out[d + 1] = g;
|
||||||
|
out[d + 2] = rows[P2 + x] + g - 128;
|
||||||
|
} else {
|
||||||
|
const co = (rows[P1 + x] - 128) & 255, cg = (rows[P2 + x] - 128) & 255;
|
||||||
|
const t = (rows[P0 + x] - (s8(cg) >> 1)) & 255;
|
||||||
|
const bb = (t - (s8(co) >> 1)) & 255;
|
||||||
|
out[d] = bb + co;
|
||||||
|
out[d + 1] = cg + t;
|
||||||
|
out[d + 2] = bb;
|
||||||
|
}
|
||||||
|
if (ch === 4) out[d + 3] = rows[P3 + x];
|
||||||
|
} else {
|
||||||
|
out[d] = rows[P0 + x];
|
||||||
|
if (ch === 2) out[d + 1] = rows[P1 + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
||||||
|
|
||||||
|
function decode(input) {
|
||||||
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
|
throw new Error('YAPF: not a YAPF file');
|
||||||
|
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
|
||||||
|
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
||||||
|
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
||||||
|
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
||||||
|
const idxEnd = 20 + 8 * mipLevels;
|
||||||
|
if (b.length < idxEnd) corrupt();
|
||||||
|
|
||||||
|
const mips = [];
|
||||||
|
for (let m = 0; m < mipLevels; m++) {
|
||||||
|
const off = r32(b, 20 + 8 * m), len = r32(b, 24 + 8 * m);
|
||||||
|
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
|
||||||
|
if (off < idxEnd || off > b.length || len > b.length - off) corrupt();
|
||||||
|
const out = new Uint8Array(mw * mh * ch);
|
||||||
|
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
|
||||||
|
if (len / 4 < tcount) corrupt();
|
||||||
|
let pos = off + tcount * 4;
|
||||||
|
for (let t = 0; t < tcount; t++) {
|
||||||
|
const sz = r32(b, off + t * 4);
|
||||||
|
if (sz > off + len - pos) corrupt();
|
||||||
|
const tx = (t % tcx) * TILE, ty = Math.floor(t / tcx) * TILE;
|
||||||
|
decodeTile(b, pos, pos + sz, out, mw, tx, ty,
|
||||||
|
Math.min(TILE, mw - tx), Math.min(TILE, mh - ty), ch);
|
||||||
|
pos += sz;
|
||||||
|
}
|
||||||
|
mips.push(out);
|
||||||
|
}
|
||||||
|
return { width: W, height: H, channels: ch, gpuFormat: b[6], flags: b[8],
|
||||||
|
mipLevels, pixels: mips[0], mips };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Encoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
class Out {
|
||||||
|
constructor(n) { this.buf = new Uint8Array(Math.max(n, 1024)); this.len = 0; }
|
||||||
|
reserve(n) {
|
||||||
|
if (this.len + n <= this.buf.length) return;
|
||||||
|
let c = this.buf.length * 2;
|
||||||
|
while (c < this.len + n) c *= 2;
|
||||||
|
const nb = new Uint8Array(c); nb.set(this.buf.subarray(0, this.len)); this.buf = nb;
|
||||||
|
}
|
||||||
|
push(v) { this.reserve(1); this.buf[this.len++] = v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function predictRow(f, P, r0, u0, k0, pred, tw, U, K) {
|
||||||
|
switch (f) {
|
||||||
|
case F_LEFT: pred[0] = 0; for (let x = 1; x < tw; x++) pred[x] = P[r0 + x - 1]; break;
|
||||||
|
case F_GRAD:
|
||||||
|
pred[0] = U[u0];
|
||||||
|
for (let x = 1; x < tw; x++) pred[x] = (P[r0 + x - 1] + U[u0 + x] - U[u0 + x - 1]) & 255;
|
||||||
|
break;
|
||||||
|
case F_UPRIGHT:
|
||||||
|
for (let x = 0; x + 1 < tw; x++) pred[x] = U[u0 + x + 1];
|
||||||
|
pred[tw - 1] = U[u0 + tw - 1];
|
||||||
|
break;
|
||||||
|
case F_UPAVG:
|
||||||
|
for (let x = 0; x + 1 < tw; x++) pred[x] = (U[u0 + x] + U[u0 + x + 1]) >> 1;
|
||||||
|
pred[tw - 1] = U[u0 + tw - 1];
|
||||||
|
break;
|
||||||
|
default: for (let x = 0; x < tw; x++) pred[x] = K[k0 + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Picks a filter per band; fills z and filt; returns estimated cost. */
|
||||||
|
function chooseFilters(P, tw, th, z, filt) {
|
||||||
|
const total = tw * th, nb = (th + BAND - 1) >> 3;
|
||||||
|
const tmp = new Uint8Array(BAND * TILE + GROUP), pred = new Uint8Array(TILE);
|
||||||
|
let sum = 0, prevFi = 0;
|
||||||
|
for (let b = 0; b < nb; b++) {
|
||||||
|
const y0 = b * BAND, y1 = Math.min(y0 + BAND, th), n = (y1 - y0) * tw;
|
||||||
|
let bestCost = Infinity, bestFi = 0;
|
||||||
|
for (let t = 0; t < FILTERS.length; t++) {
|
||||||
|
const fi = t === 0 ? prevFi : (t <= prevFi ? t - 1 : t);
|
||||||
|
const f = FILTERS[fi], k = upK(f), serial = f === F_LEFT || f === F_GRAD;
|
||||||
|
let cost = 0, done = 0, lost = false;
|
||||||
|
for (let y = y0; y < y1 && !lost; y++) {
|
||||||
|
const r0 = y * tw;
|
||||||
|
const U = y ? P : ZERO_ROW, u0 = y ? r0 - tw : 0;
|
||||||
|
const K = y >= k ? P : ZERO_ROW, k0 = y >= k ? r0 - k * tw : 0;
|
||||||
|
predictRow(f, P, r0, u0, k0, pred, tw, U, K);
|
||||||
|
const T = (y - y0) * tw;
|
||||||
|
for (let x = 0; x < tw; x++) tmp[T + x] = zz((P[r0 + x] - pred[x]) & 255);
|
||||||
|
const last = y + 1 === y1, end = last ? n : (y + 1 - y0) * tw;
|
||||||
|
if (last) tmp.fill(0, n, n + GROUP);
|
||||||
|
for (; done + GROUP <= end || (last && done < end); done += GROUP) {
|
||||||
|
let m = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) m |= tmp[done + i];
|
||||||
|
const w = width(m);
|
||||||
|
cost += w ? 16 * w + 8 : 2;
|
||||||
|
}
|
||||||
|
const c = serial ? cost + Math.floor(cost / 25) : cost;
|
||||||
|
if (c > bestCost || (c === bestCost && fi > bestFi)) lost = true;
|
||||||
|
}
|
||||||
|
if (lost) continue;
|
||||||
|
if (serial) cost += Math.floor(cost / 25);
|
||||||
|
if (cost < bestCost || (cost === bestCost && fi < bestFi)) {
|
||||||
|
bestCost = cost; bestFi = fi;
|
||||||
|
z.set(tmp.subarray(0, n), y0 * tw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevFi = bestFi;
|
||||||
|
filt[b] = FILTERS[bestFi];
|
||||||
|
sum += bestCost;
|
||||||
|
}
|
||||||
|
z.fill(0, total, total + GROUP);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitPlane(o, z, filt, tw, th) {
|
||||||
|
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
|
||||||
|
o.reserve(nb + ng + ng * 8);
|
||||||
|
for (let i = 0; i < nb; i++) o.buf[o.len++] = filt[i];
|
||||||
|
|
||||||
|
const need = new Uint8Array(ng);
|
||||||
|
for (let g = 0; g < ng; g++) {
|
||||||
|
let m = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) m |= z[g * GROUP + i];
|
||||||
|
need[g] = width(m);
|
||||||
|
}
|
||||||
|
/* span[j][g] = widest need in [g, g + 2^j) */
|
||||||
|
const span = [need.slice()];
|
||||||
|
for (let j = 1; j < 7; j++) {
|
||||||
|
const prev = span[j - 1], cur = new Uint8Array(ng), h = 1 << (j - 1);
|
||||||
|
for (let g = 0; g + (1 << j) <= ng; g++) cur[g] = Math.max(prev[g], prev[g + h]);
|
||||||
|
span.push(cur);
|
||||||
|
}
|
||||||
|
const cost = new Uint32Array((ng + 1) * 9), pick = new Uint8Array(ng * 9);
|
||||||
|
const mx = new Uint8Array(7);
|
||||||
|
for (let g = ng - 1; g >= 0; g--) {
|
||||||
|
let lc = Infinity, lk = 0;
|
||||||
|
for (let w = need[g]; w <= 8; w++) {
|
||||||
|
const c = 1 + 2 * w + cost[(g + 1) * 9 + w];
|
||||||
|
if (c < lc) { lc = c; lk = w; }
|
||||||
|
}
|
||||||
|
let nl = 0;
|
||||||
|
for (; nl < 7 && g + RUN_LENS[nl] <= ng; nl++) {
|
||||||
|
const L = RUN_LENS[nl];
|
||||||
|
mx[nl] = L === 3 ? Math.max(span[1][g], need[g + 2]) : span[31 - Math.clz32(L)][g];
|
||||||
|
}
|
||||||
|
for (let pw = 0; pw <= 8; pw++) {
|
||||||
|
let bc = lc, bk = lk;
|
||||||
|
for (let li = 0; li < nl && mx[li] <= pw; li++) {
|
||||||
|
const L = RUN_LENS[li], c = 1 + 2 * pw * L + cost[(g + L) * 9 + pw];
|
||||||
|
if (c < bc) { bc = c; bk = NIB_REPEAT + li; }
|
||||||
|
}
|
||||||
|
cost[g * 9 + pw] = bc;
|
||||||
|
pick[g * 9 + pw] = bk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gw = new Uint8Array(ng), nib = [];
|
||||||
|
let pw = 0;
|
||||||
|
for (let g = 0; g < ng;) {
|
||||||
|
const k = pick[g * 9 + pw];
|
||||||
|
nib.push(k);
|
||||||
|
if (k < NIB_REPEAT) { pw = k; gw[g++] = k; }
|
||||||
|
else for (let n = 0; n < NIB_GROUPS[k]; n++) gw[g++] = pw;
|
||||||
|
}
|
||||||
|
for (let k = 0; k < nib.length; k += 2)
|
||||||
|
o.buf[o.len++] = nib[k] | (k + 1 < nib.length ? nib[k + 1] << 4 : 0);
|
||||||
|
|
||||||
|
for (let g = 0; g < ng; g++) {
|
||||||
|
const w = gw[g];
|
||||||
|
let acc = 0, bits = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) {
|
||||||
|
acc |= z[g * GROUP + i] << bits; bits += w;
|
||||||
|
while (bits >= 8) { o.buf[o.len++] = acc & 255; acc >>>= 8; bits -= 8; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeTile(o, sc, px, imgW, tx, ty, tw, th, ch) {
|
||||||
|
const total = tw * th, start = o.len, raw = total * ch, tries = ch >= 3 ? 2 : 1;
|
||||||
|
const mask = [0, 0], est = [0, 0];
|
||||||
|
for (let t = 0; t < tries; t++) {
|
||||||
|
const plane = sc.plane[t];
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
let s = ((ty + y) * imgW + tx) * ch;
|
||||||
|
for (let x = 0; x < tw; x++, s += ch) {
|
||||||
|
const i = y * tw + x;
|
||||||
|
if (ch < 3) { for (let c = 0; c < ch; c++) plane[c][i] = px[s + c]; continue; }
|
||||||
|
const r = px[s], g = px[s + 1], b = px[s + 2];
|
||||||
|
if (t === 0) {
|
||||||
|
const oo = (r - b) & 255, tt = (b + (s8(oo) >> 1)) & 255, q = (g - tt) & 255;
|
||||||
|
plane[0][i] = (tt + (s8(q) >> 1)) & 255;
|
||||||
|
plane[1][i] = (oo + 128) & 255;
|
||||||
|
plane[2][i] = (q + 128) & 255;
|
||||||
|
} else {
|
||||||
|
plane[0][i] = g;
|
||||||
|
plane[1][i] = (r - g + 128) & 255;
|
||||||
|
plane[2][i] = (b - g + 128) & 255;
|
||||||
|
}
|
||||||
|
if (ch === 4) plane[3][i] = px[s + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
const P = plane[c];
|
||||||
|
let i = 1;
|
||||||
|
while (i < total && P[i] === P[0]) i++;
|
||||||
|
if (i === total) { mask[t] |= 1 << c; est[t] += 16; continue; }
|
||||||
|
est[t] += chooseFilters(P, tw, th, sc.z[t][c], sc.filt[t][c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const t = tries === 2 && est[1] < est[0] ? 1 : 0;
|
||||||
|
o.reserve(5);
|
||||||
|
o.buf[o.len++] = mask[t] | (t ? SUBGREEN : 0);
|
||||||
|
for (let c = 0; c < ch; c++) if (mask[t] & (1 << c)) o.buf[o.len++] = sc.plane[t][c][0];
|
||||||
|
for (let c = 0; c < ch; c++)
|
||||||
|
if (!(mask[t] & (1 << c))) emitPlane(o, sc.z[t][c], sc.filt[t][c], tw, th);
|
||||||
|
|
||||||
|
if (o.len - start > raw + 1) {
|
||||||
|
o.len = start;
|
||||||
|
o.reserve(raw + 1);
|
||||||
|
o.buf[o.len++] = STORED;
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const s = ((ty + y) * imgW + tx) * ch;
|
||||||
|
o.buf.set(px.subarray(s, s + tw * ch), o.len);
|
||||||
|
o.len += tw * ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeLevel(px, mw, mh, ch) {
|
||||||
|
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
|
||||||
|
const o = new Out(tcount * 4 + (mw * mh * ch >> 1));
|
||||||
|
o.reserve(tcount * 4);
|
||||||
|
o.len = tcount * 4;
|
||||||
|
const mk = (n) => [0, 1, 2, 3].map(() => new Uint8Array(n));
|
||||||
|
const sc = {
|
||||||
|
plane: [mk(TILE_PX), mk(TILE_PX)],
|
||||||
|
z: [mk(TILE_PX + GROUP), mk(TILE_PX + GROUP)],
|
||||||
|
filt: [mk(TILE / BAND), mk(TILE / BAND)],
|
||||||
|
};
|
||||||
|
let t = 0;
|
||||||
|
for (let ty = 0; ty < tcy; ty++)
|
||||||
|
for (let tx = 0; tx < tcx; tx++, t++) {
|
||||||
|
const x0 = tx * TILE, y0 = ty * TILE, start = o.len;
|
||||||
|
encodeTile(o, sc, px, mw, x0, y0, Math.min(TILE, mw - x0), Math.min(TILE, mh - y0), ch);
|
||||||
|
const n = o.len - start;
|
||||||
|
o.buf[t * 4] = n & 255; o.buf[t * 4 + 1] = (n >>> 8) & 255;
|
||||||
|
o.buf[t * 4 + 2] = (n >>> 16) & 255; o.buf[t * 4 + 3] = (n >>> 24) & 255;
|
||||||
|
}
|
||||||
|
return o.buf.subarray(0, o.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(img) {
|
||||||
|
const { width: W, height: H, channels: ch } = img;
|
||||||
|
const mips = img.mips && img.mips.length ? img.mips : [img.pixels];
|
||||||
|
const mipLevels = img.mipLevels || mips.length;
|
||||||
|
if (!(W >= 1 && W <= MAX_DIM && H >= 1 && H <= MAX_DIM && ch >= 1 && ch <= 4 &&
|
||||||
|
mipLevels >= 1 && mipLevels <= MAX_MIPS && mips.length >= mipLevels))
|
||||||
|
throw new Error('YAPF: invalid image');
|
||||||
|
const levels = [];
|
||||||
|
for (let m = 0; m < mipLevels; m++) {
|
||||||
|
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
|
||||||
|
const px = m === 0 ? img.pixels : mips[m];
|
||||||
|
if (!px || px.length < mw * mh * ch) throw new Error('YAPF: mip ' + m + ' has too few pixels');
|
||||||
|
levels.push(encodeLevel(px, mw, mh, ch));
|
||||||
|
}
|
||||||
|
const idxEnd = 20 + 8 * mipLevels;
|
||||||
|
let total = idxEnd;
|
||||||
|
for (const l of levels) total += l.length;
|
||||||
|
const out = new Uint8Array(total);
|
||||||
|
out.set([0x59, 0x41, 0x50, 0x46, 1, ch, img.gpuFormat || 0, mipLevels, img.flags || 0]);
|
||||||
|
const w32 = (p, v) => { out[p] = v & 255; out[p + 1] = (v >>> 8) & 255;
|
||||||
|
out[p + 2] = (v >>> 16) & 255; out[p + 3] = (v >>> 24) & 255; };
|
||||||
|
w32(12, W); w32(16, H);
|
||||||
|
let off = idxEnd;
|
||||||
|
levels.forEach((l, m) => { w32(20 + 8 * m, off); w32(24 + 8 * m, l.length); out.set(l, off); off += l.length; });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Any channel count → RGBA (for canvas ImageData). */
|
||||||
|
function toRGBA(img, level) {
|
||||||
|
const m = level || 0, px = img.mips ? img.mips[m] : img.pixels;
|
||||||
|
const w = Math.max(1, img.width >>> m), h = Math.max(1, img.height >>> m), ch = img.channels;
|
||||||
|
if (ch === 4) return new Uint8ClampedArray(px.buffer, px.byteOffset, w * h * 4);
|
||||||
|
const out = new Uint8ClampedArray(w * h * 4);
|
||||||
|
for (let i = 0, s = 0; i < w * h; i++, s += ch) {
|
||||||
|
const o = i * 4;
|
||||||
|
if (ch >= 3) { out[o] = px[s]; out[o + 1] = px[s + 1]; out[o + 2] = px[s + 2]; out[o + 3] = 255; }
|
||||||
|
else { out[o] = out[o + 1] = out[o + 2] = px[s]; out[o + 3] = ch === 2 ? px[s + 1] : 255; }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { decode, encode, toRGBA, GPU, FLAG, 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
|
||||||
25
bindings/python/README.md
Normal file
25
bindings/python/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# yapf.py — YAPF in Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import yapf
|
||||||
|
|
||||||
|
img = yapf.load("texture.yapf") # or yapf.decode(data)
|
||||||
|
print(img.width, img.height, img.channels, img.mip_levels)
|
||||||
|
img.pixels # bytes, rows top to bottom
|
||||||
|
|
||||||
|
yapf.save("copy.yapf", img) # or yapf.encode(img) -> bytes
|
||||||
|
yapf.save("new.yapf", yapf.Image(w, h, 4, rgba_bytes))
|
||||||
|
```
|
||||||
|
|
||||||
|
It uses the C library through `ctypes` when it finds one:
|
||||||
|
|
||||||
|
1. the file named by the `YAPF_LIBRARY` environment variable,
|
||||||
|
2. `libyapf.so` / `libyapf.dylib` / `yapf.dll` next to `yapf.py`, or in
|
||||||
|
`lib/<platform>/` beside it (e.g. `lib/windows-x64/yapf.dll`),
|
||||||
|
3. the system library path (`sudo make install` installs 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")
|
||||||
314
bindings/python/yapf.py
Normal file
314
bindings/python/yapf.py
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
"""
|
||||||
|
yapf.py — Python bindings for YAPF v1 images.
|
||||||
|
|
||||||
|
Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
|
||||||
|
import yapf
|
||||||
|
img = yapf.load("texture.yapf") # or yapf.decode(data)
|
||||||
|
img.width, img.height, img.channels # pixels: bytes, row-major
|
||||||
|
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
|
||||||
|
|
||||||
|
Uses the C library through ctypes when one can be found (fast, can
|
||||||
|
encode); otherwise falls back to a pure-Python decoder that needs numpy
|
||||||
|
(decode only). The library is looked up in this order:
|
||||||
|
|
||||||
|
1. the path in the YAPF_LIBRARY environment variable
|
||||||
|
2. next to this file: libyapf.so / libyapf.dylib / yapf.dll
|
||||||
|
(also in a platform subfolder such as lib/linux-x64/)
|
||||||
|
3. the system library path
|
||||||
|
|
||||||
|
Build the library with: cc -O2 -shared -fPIC yapf.c -o libyapf.so -pthread
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__all__ = ["Image", "load", "save", "decode", "encode", "backend",
|
||||||
|
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
|
||||||
|
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
|
||||||
|
|
||||||
|
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
|
||||||
|
FLAG_PREMULT_ALPHA, FLAG_SRGB = 1, 2
|
||||||
|
|
||||||
|
|
||||||
|
class YapfError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Image:
|
||||||
|
"""A decoded image. `pixels` holds width*height*channels bytes, rows top
|
||||||
|
to bottom; `mips` holds every mip level (mips[0] is pixels)."""
|
||||||
|
|
||||||
|
def __init__(self, width, height, channels, pixels,
|
||||||
|
gpu_format=None, flags=FLAG_SRGB, mips=None):
|
||||||
|
self.width, self.height, self.channels = int(width), int(height), int(channels)
|
||||||
|
self.pixels = bytes(pixels)
|
||||||
|
if gpu_format is None:
|
||||||
|
gpu_format = {1: GPU_R8, 2: GPU_RG8, 3: GPU_SRGB8, 4: GPU_SRGB8_A8}[self.channels]
|
||||||
|
self.gpu_format, self.flags = gpu_format, flags
|
||||||
|
self.mips = [self.pixels] + [bytes(m) for m in (mips or [])[1:]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mip_levels(self):
|
||||||
|
return len(self.mips)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<yapf.Image %dx%d, %d channel(s), %d mip level(s)>" % (
|
||||||
|
self.width, self.height, self.channels, self.mip_levels)
|
||||||
|
|
||||||
|
|
||||||
|
# ── ctypes backend ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _CImage(ctypes.Structure):
|
||||||
|
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32),
|
||||||
|
("channels", ctypes.c_uint8), ("gpu_format", ctypes.c_uint8),
|
||||||
|
("flags", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
|
||||||
|
("pixels", ctypes.POINTER(ctypes.c_uint8)),
|
||||||
|
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_dir():
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
|
||||||
|
system = {"win32": "windows", "darwin": "macos"}.get(sys.platform, "linux")
|
||||||
|
return "%s-%s" % (system, arch)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_library():
|
||||||
|
names = {"win32": ["yapf.dll", "libyapf.dll"],
|
||||||
|
"darwin": ["libyapf.dylib"]}.get(sys.platform, ["libyapf.so"])
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidates = []
|
||||||
|
if os.environ.get("YAPF_LIBRARY"):
|
||||||
|
candidates.append(os.environ["YAPF_LIBRARY"])
|
||||||
|
for folder in (here, os.path.join(here, "lib", _platform_dir())):
|
||||||
|
candidates += [os.path.join(folder, n) for n in names]
|
||||||
|
candidates += names
|
||||||
|
for path in candidates:
|
||||||
|
try:
|
||||||
|
lib = ctypes.CDLL(path)
|
||||||
|
lib.yapf_load_memory_mt.restype = ctypes.POINTER(_CImage)
|
||||||
|
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
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_lib = _find_library()
|
||||||
|
|
||||||
|
|
||||||
|
def backend():
|
||||||
|
"""'c' when the C library is used, 'python' for the fallback decoder."""
|
||||||
|
return "c" if _lib else "python"
|
||||||
|
|
||||||
|
|
||||||
|
def _c_decode(data):
|
||||||
|
data = bytes(data)
|
||||||
|
p = _lib.yapf_load_memory_mt(data, len(data), 0)
|
||||||
|
if not p:
|
||||||
|
raise YapfError("not a valid YAPF v1 file")
|
||||||
|
try:
|
||||||
|
c = p.contents
|
||||||
|
mips = []
|
||||||
|
for m in range(c.mip_levels):
|
||||||
|
w, h = max(1, c.width >> m), max(1, c.height >> m)
|
||||||
|
mips.append(ctypes.string_at(c.mips[m], w * h * c.channels))
|
||||||
|
return Image(c.width, c.height, c.channels, mips[0], c.gpu_format, c.flags, mips)
|
||||||
|
finally:
|
||||||
|
_lib.yapf_free(p)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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_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) ──────────────────────────────
|
||||||
|
|
||||||
|
_NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64]
|
||||||
|
|
||||||
|
|
||||||
|
def _py_decode(data):
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
except ImportError:
|
||||||
|
raise YapfError("the YAPF C library was not found and numpy is not "
|
||||||
|
"installed; build the library (see yapf.py) or install numpy")
|
||||||
|
b = np.frombuffer(bytes(data), dtype=np.uint8)
|
||||||
|
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
|
||||||
|
raise YapfError("not a YAPF file")
|
||||||
|
if b[4] != 1:
|
||||||
|
raise YapfError("unsupported YAPF version %d" % b[4])
|
||||||
|
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
|
||||||
|
W, H = struct.unpack_from("<II", bytes(b[12:20]))
|
||||||
|
if not (1 <= ch <= 4 and 1 <= mip_levels <= 16 and 1 <= W <= 65535 and 1 <= H <= 65535):
|
||||||
|
raise YapfError("corrupt header")
|
||||||
|
|
||||||
|
def bad():
|
||||||
|
raise YapfError("corrupt YAPF data")
|
||||||
|
|
||||||
|
mips = []
|
||||||
|
for m in range(mip_levels):
|
||||||
|
off, ln = struct.unpack_from("<II", bytes(b[20 + 8 * m: 28 + 8 * m]))
|
||||||
|
mw, mh = max(1, W >> m), max(1, H >> m)
|
||||||
|
if off > len(b) or ln > len(b) - off:
|
||||||
|
bad()
|
||||||
|
out = np.zeros((mh, mw, ch), dtype=np.uint8)
|
||||||
|
tcx, tcy = (mw + 63) // 64, (mh + 63) // 64
|
||||||
|
sizes = np.frombuffer(bytes(b[off: off + 4 * tcx * tcy]), dtype="<u4")
|
||||||
|
pos = off + 4 * tcx * tcy
|
||||||
|
for t in range(tcx * tcy):
|
||||||
|
tx, ty = (t % tcx) * 64, (t // tcx) * 64
|
||||||
|
tw, th = min(64, mw - tx), min(64, mh - ty)
|
||||||
|
end = pos + int(sizes[t])
|
||||||
|
if end > off + ln:
|
||||||
|
bad()
|
||||||
|
try:
|
||||||
|
out[ty:ty + th, tx:tx + tw] = _py_tile(np, b, pos, end, tw, th, ch, bad)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
bad()
|
||||||
|
pos = end
|
||||||
|
mips.append(out.tobytes())
|
||||||
|
return Image(W, H, ch, mips[0], gpu, flags, mips)
|
||||||
|
|
||||||
|
|
||||||
|
def _py_tile(np, b, p, end, tw, th, ch, bad):
|
||||||
|
hdr = int(b[p]); p += 1
|
||||||
|
if hdr == 0x80:
|
||||||
|
if end - p != tw * th * ch:
|
||||||
|
bad()
|
||||||
|
return b[p:end].reshape(th, tw, ch)
|
||||||
|
if hdr & 0xE0:
|
||||||
|
bad()
|
||||||
|
xf, mask = hdr & 0x10, hdr & 0x0F
|
||||||
|
planes = np.zeros((ch, th, tw), dtype=np.uint8)
|
||||||
|
for c in range(ch):
|
||||||
|
if mask >> c & 1:
|
||||||
|
planes[c, :, :] = b[p]; p += 1
|
||||||
|
for c in range(ch):
|
||||||
|
if mask >> c & 1:
|
||||||
|
continue
|
||||||
|
nb, ng = (th + 7) // 8, (tw * th + 7) // 8
|
||||||
|
filt = b[p:p + nb]; p += nb
|
||||||
|
widths, g, hi, w = [], 0, 0, 0
|
||||||
|
while g < ng:
|
||||||
|
code = (int(b[p]) >> (4 * hi)) & 15
|
||||||
|
p += hi; hi ^= 1
|
||||||
|
if code < 9:
|
||||||
|
w = code
|
||||||
|
n = _NIB_GROUPS[code]
|
||||||
|
widths += [w] * n; g += n
|
||||||
|
if g != ng:
|
||||||
|
bad()
|
||||||
|
if hi:
|
||||||
|
p += 1
|
||||||
|
z = np.zeros(ng * 8, dtype=np.uint8)
|
||||||
|
widths = np.array(widths, dtype=np.int64)
|
||||||
|
starts = p + np.concatenate(([0], np.cumsum(widths)[:-1]))
|
||||||
|
p += int(widths.sum())
|
||||||
|
if p > end:
|
||||||
|
bad()
|
||||||
|
for wv in range(1, 9):
|
||||||
|
sel = np.nonzero(widths == wv)[0]
|
||||||
|
if not len(sel):
|
||||||
|
continue
|
||||||
|
raw = b[starts[sel][:, None] + np.arange(wv)].astype(np.uint64)
|
||||||
|
word = (raw << (8 * np.arange(wv, dtype=np.uint64))).sum(axis=1, dtype=np.uint64)
|
||||||
|
shifts = np.arange(8, dtype=np.uint64) * np.uint64(wv)
|
||||||
|
vals = (word[:, None] >> shifts) & np.uint64((1 << wv) - 1)
|
||||||
|
z[(sel[:, None] * 8 + np.arange(8)).ravel()] = vals.ravel().astype(np.uint8)
|
||||||
|
r = ((z[:tw * th] >> 1) ^ (0 - (z[:tw * th] & 1)).astype(np.uint8)).reshape(th, tw).astype(np.int64)
|
||||||
|
P = planes[c].astype(np.int64)
|
||||||
|
for y in range(th):
|
||||||
|
f = int(filt[y // 8])
|
||||||
|
U = P[y - 1] if y else np.zeros(tw, np.int64)
|
||||||
|
if f == 0:
|
||||||
|
P[y] = np.cumsum(r[y]) & 255
|
||||||
|
elif f == 1:
|
||||||
|
inc = r[y] + U - np.concatenate(([0], U[:-1]))
|
||||||
|
P[y] = np.cumsum(inc) & 255
|
||||||
|
elif f in (2, 3):
|
||||||
|
AR = np.concatenate((U[1:], U[-1:]))
|
||||||
|
pred = AR if f == 2 else (U + AR) >> 1
|
||||||
|
P[y] = (pred + r[y]) & 255
|
||||||
|
elif 16 <= f < 24:
|
||||||
|
k = f - 15
|
||||||
|
K = P[y - k] if y >= k else np.zeros(tw, np.int64)
|
||||||
|
P[y] = (K + r[y]) & 255
|
||||||
|
else:
|
||||||
|
bad()
|
||||||
|
planes[c] = P.astype(np.uint8)
|
||||||
|
if p != end:
|
||||||
|
bad()
|
||||||
|
if ch < 3:
|
||||||
|
return planes.transpose(1, 2, 0)
|
||||||
|
P0, P1, P2 = (planes[i].astype(np.int64) for i in range(3))
|
||||||
|
if xf:
|
||||||
|
R, G, B = P1 + P0 - 128, P0, P2 + P0 - 128
|
||||||
|
else:
|
||||||
|
co, cg = (P1 - 128) & 255, (P2 - 128) & 255
|
||||||
|
s8 = lambda v: np.where(v > 127, v - 256, v)
|
||||||
|
t = (P0 - (s8(cg) >> 1)) & 255
|
||||||
|
B = (t - (s8(co) >> 1)) & 255
|
||||||
|
R, G = B + co, cg + t
|
||||||
|
out = [R & 255, G & 255, B & 255] + ([planes[3].astype(np.int64)] if ch == 4 else [])
|
||||||
|
return np.stack(out, axis=-1).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ── public API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def decode(data):
|
||||||
|
"""Decode YAPF bytes into an Image."""
|
||||||
|
return _c_decode(data) if _lib else _py_decode(data)
|
||||||
|
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
"""Read and decode a .yapf file."""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return decode(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
if not isinstance(img, Image):
|
||||||
|
img = Image(img.width, img.height, img.channels, img.pixels)
|
||||||
|
expected = img.width * img.height * img.channels
|
||||||
|
if len(img.pixels) != expected:
|
||||||
|
raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels)))
|
||||||
|
return _c_encode(img)
|
||||||
|
|
||||||
|
|
||||||
|
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__":
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
print(path, load(path), "backend:", backend())
|
||||||
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 });
|
||||||
|
}
|
||||||
32
build.bat
32
build.bat
|
|
@ -10,7 +10,7 @@ setlocal enabledelayedexpansion
|
||||||
::
|
::
|
||||||
:: Options:
|
:: Options:
|
||||||
:: --debug Debug build (no optimisation, full symbols)
|
:: --debug Debug build (no optimisation, full symbols)
|
||||||
:: --simd Enable SIMD paths (-DYAPF_USE_SIMD)
|
:: --no-simd Plain C only (SIMD is on by default, picked at run time)
|
||||||
:: --static-only Build only static libraries
|
:: --static-only Build only static libraries
|
||||||
:: --shared-only Build only shared libraries (DLL)
|
:: --shared-only Build only shared libraries (DLL)
|
||||||
:: --help Show this message
|
:: --help Show this message
|
||||||
|
|
@ -45,13 +45,13 @@ if !ANSI_OK!==1 (
|
||||||
|
|
||||||
:: ── Option parsing ───────────────────────────────────────────────────
|
:: ── Option parsing ───────────────────────────────────────────────────
|
||||||
set OPT_DEBUG=0
|
set OPT_DEBUG=0
|
||||||
set OPT_SIMD=0
|
set OPT_NOSIMD=0
|
||||||
set OPT_STATIC_ONLY=0
|
set OPT_STATIC_ONLY=0
|
||||||
set OPT_SHARED_ONLY=0
|
set OPT_SHARED_ONLY=0
|
||||||
|
|
||||||
for %%A in (%*) do (
|
for %%A in (%*) do (
|
||||||
if /i "%%A"=="--debug" set OPT_DEBUG=1
|
if /i "%%A"=="--debug" set OPT_DEBUG=1
|
||||||
if /i "%%A"=="--simd" set OPT_SIMD=1
|
if /i "%%A"=="--no-simd" set OPT_NOSIMD=1
|
||||||
if /i "%%A"=="--static-only" set OPT_STATIC_ONLY=1
|
if /i "%%A"=="--static-only" set OPT_STATIC_ONLY=1
|
||||||
if /i "%%A"=="--shared-only" set OPT_SHARED_ONLY=1
|
if /i "%%A"=="--shared-only" set OPT_SHARED_ONLY=1
|
||||||
if /i "%%A"=="--help" goto :show_help
|
if /i "%%A"=="--help" goto :show_help
|
||||||
|
|
@ -115,9 +115,11 @@ if !OPT_DEBUG!==1 (
|
||||||
echo !C_CYN![info]!C_RST! Mode : release
|
echo !C_CYN![info]!C_RST! Mode : release
|
||||||
)
|
)
|
||||||
|
|
||||||
if !OPT_SIMD!==1 (
|
if !OPT_NOSIMD!==1 (
|
||||||
set CFLAGS=!CFLAGS! -DYAPF_USE_SIMD
|
set CFLAGS=!CFLAGS! -DYAPF_NO_SIMD
|
||||||
echo !C_CYN![info]!C_RST! SIMD : enabled
|
echo !C_CYN![info]!C_RST! SIMD : disabled
|
||||||
|
) else (
|
||||||
|
echo !C_CYN![info]!C_RST! SIMD : SSSE3 / NEON, chosen at run time
|
||||||
)
|
)
|
||||||
|
|
||||||
set BUILD_SHARED=1
|
set BUILD_SHARED=1
|
||||||
|
|
@ -139,8 +141,13 @@ echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h
|
||||||
echo LIBRARY yapf
|
echo LIBRARY yapf
|
||||||
echo EXPORTS
|
echo EXPORTS
|
||||||
echo yapf_load
|
echo yapf_load
|
||||||
|
echo yapf_load_memory
|
||||||
|
echo yapf_load_mt
|
||||||
|
echo yapf_load_memory_mt
|
||||||
echo yapf_save
|
echo yapf_save
|
||||||
|
echo yapf_encode
|
||||||
echo yapf_free
|
echo yapf_free
|
||||||
|
echo yapf_free_buffer
|
||||||
) > dist\_yapf.def
|
) > dist\_yapf.def
|
||||||
|
|
||||||
:: ====================================================================
|
:: ====================================================================
|
||||||
|
|
@ -183,6 +190,15 @@ if !BUILD_STATIC!==1 (
|
||||||
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\libyapf.a (static)
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\libyapf.a (static)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
:: Command-line tool
|
||||||
|
clang --target=x86_64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
|
||||||
|
tools\yapf_cli.c yapf.c -o dist\windows-x64\yapf.exe
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_YLW![warn]!C_RST! yapf.exe build failed (non-fatal)
|
||||||
|
) else (
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\yapf.exe (command-line tool)
|
||||||
|
)
|
||||||
|
|
||||||
:: ====================================================================
|
:: ====================================================================
|
||||||
:: Build: Windows ARM64
|
:: Build: Windows ARM64
|
||||||
:: ====================================================================
|
:: ====================================================================
|
||||||
|
|
@ -227,6 +243,10 @@ if !BUILD_STATIC!==1 (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clang --target=aarch64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
|
||||||
|
tools\yapf_cli.c yapf.c -o dist\windows-arm64\yapf.exe
|
||||||
|
if not errorlevel 1 echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\yapf.exe (command-line tool)
|
||||||
|
|
||||||
:skip_arm64
|
:skip_arm64
|
||||||
|
|
||||||
:: ── Cleanup temp files ───────────────────────────────────────────────
|
:: ── Cleanup temp files ───────────────────────────────────────────────
|
||||||
|
|
|
||||||
21
build.sh
Normal file → Executable file
21
build.sh
Normal file → Executable file
|
|
@ -8,7 +8,7 @@
|
||||||
#
|
#
|
||||||
# Options:
|
# Options:
|
||||||
# --debug Debug build: no optimisation, full symbols
|
# --debug Debug build: no optimisation, full symbols
|
||||||
# --simd Enable SIMD paths (-DYAPF_USE_SIMD)
|
# --no-simd Plain C only (SIMD is on by default, picked at run time)
|
||||||
# --static-only Build only static libraries
|
# --static-only Build only static libraries
|
||||||
# --shared-only Build only shared libraries
|
# --shared-only Build only shared libraries
|
||||||
# --help Show this message
|
# --help Show this message
|
||||||
|
|
@ -40,14 +40,15 @@ step() { echo; echo "${BLD}──── $* ────${RST}"; }
|
||||||
|
|
||||||
# ── Option parsing ───────────────────────────────────────────────────
|
# ── Option parsing ───────────────────────────────────────────────────
|
||||||
OPT_DEBUG=0
|
OPT_DEBUG=0
|
||||||
OPT_SIMD=0
|
OPT_NOSIMD=0
|
||||||
OPT_STATIC_ONLY=0
|
OPT_STATIC_ONLY=0
|
||||||
OPT_SHARED_ONLY=0
|
OPT_SHARED_ONLY=0
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--debug) OPT_DEBUG=1 ;;
|
--debug) OPT_DEBUG=1 ;;
|
||||||
--simd) OPT_SIMD=1 ;;
|
--no-simd) OPT_NOSIMD=1 ;;
|
||||||
|
--simd) ;; # SIMD is the default now; kept for old scripts
|
||||||
--static-only) OPT_STATIC_ONLY=1 ;;
|
--static-only) OPT_STATIC_ONLY=1 ;;
|
||||||
--shared-only) OPT_SHARED_ONLY=1 ;;
|
--shared-only) OPT_SHARED_ONLY=1 ;;
|
||||||
--help)
|
--help)
|
||||||
|
|
@ -89,7 +90,7 @@ fi
|
||||||
[[ -n "$STRIP" ]] && info "Strip : $STRIP"
|
[[ -n "$STRIP" ]] && info "Strip : $STRIP"
|
||||||
|
|
||||||
# ── Compiler flags ───────────────────────────────────────────────────
|
# ── Compiler flags ───────────────────────────────────────────────────
|
||||||
CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -fPIC"
|
CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -fPIC -pthread"
|
||||||
LFLAGS=""
|
LFLAGS=""
|
||||||
|
|
||||||
if [[ $OPT_DEBUG -eq 1 ]]; then
|
if [[ $OPT_DEBUG -eq 1 ]]; then
|
||||||
|
|
@ -101,7 +102,8 @@ else
|
||||||
info "Mode : release"
|
info "Mode : release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ $OPT_SIMD -eq 1 ]] && { CFLAGS="$CFLAGS -DYAPF_USE_SIMD"; info "SIMD : enabled"; }
|
if [[ $OPT_NOSIMD -eq 1 ]]; then CFLAGS="$CFLAGS -DYAPF_NO_SIMD"; info "SIMD : disabled"
|
||||||
|
else info "SIMD : SSSE3 / NEON, chosen at run time"; fi
|
||||||
|
|
||||||
# ── Detect OS ────────────────────────────────────────────────────────
|
# ── Detect OS ────────────────────────────────────────────────────────
|
||||||
OS=$(uname -s)
|
OS=$(uname -s)
|
||||||
|
|
@ -176,6 +178,13 @@ if [[ "$OS" == "Darwin" ]]; then
|
||||||
ok "dist/macos/libyapf.a (universal)"
|
ok "dist/macos/libyapf.a (universal)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
step "Building the yapf command-line tool"
|
||||||
|
"$CLANG" $CFLAGS --target=arm64-apple-macos11 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_arm64 -lm
|
||||||
|
"$CLANG" $CFLAGS --target=x86_64-apple-macos10.15 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_x64 -lm
|
||||||
|
"$LIPO" -create /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64 -output dist/macos/yapf
|
||||||
|
rm -f /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64
|
||||||
|
ok "dist/macos/yapf (universal)"
|
||||||
|
|
||||||
# ── Linux build ──────────────────────────────────────────────────────
|
# ── Linux build ──────────────────────────────────────────────────────
|
||||||
elif [[ "$OS" == "Linux" ]]; then
|
elif [[ "$OS" == "Linux" ]]; then
|
||||||
|
|
||||||
|
|
@ -205,6 +214,8 @@ elif [[ "$OS" == "Linux" ]]; then
|
||||||
compile_static "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.a"
|
compile_static "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.a"
|
||||||
ok "dist/$NATIVE_DIR/libyapf.a"
|
ok "dist/$NATIVE_DIR/libyapf.a"
|
||||||
fi
|
fi
|
||||||
|
"$CLANG" $CFLAGS --target="$NATIVE_TRIPLE" tools/yapf_cli.c yapf.c -o "dist/$NATIVE_DIR/yapf" -lm
|
||||||
|
ok "dist/$NATIVE_DIR/yapf (command-line tool)"
|
||||||
|
|
||||||
# ── Cross-compile to the other common arch ────────────────────────
|
# ── Cross-compile to the other common arch ────────────────────────
|
||||||
if [[ "$HOST_ARCH" == "x86_64" ]]; then
|
if [[ "$HOST_ARCH" == "x86_64" ]]; then
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
36
extensions/blender/README.md
Normal file
36
extensions/blender/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# YAPF for Blender
|
||||||
|
|
||||||
|
Import and export `.yapf` images in **Blender 4.2 or later** (tested with
|
||||||
|
Blender 5.1).
|
||||||
|
|
||||||
|
- **File → Import → YAPF Image (.yapf)** — optionally as a **plane** with a
|
||||||
|
material that shows the image.
|
||||||
|
- **Image Editor → Image → Open YAPF… / Save as YAPF…** (with mipmaps option).
|
||||||
|
- **Drag and drop** `.yapf` files into the 3D Viewport (creates a plane), the
|
||||||
|
Image Editor or a node editor.
|
||||||
|
|
||||||
|
Blender cannot read `.yapf` on its own, so imported images are **packed into
|
||||||
|
the .blend** and survive saving and reopening.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Get `yapf_image-blender.zip` from the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases), or build it (below).
|
||||||
|
2. Blender → **Edit → Preferences → Get Extensions → ⌄ → Install from Disk…**
|
||||||
|
and pick the zip.
|
||||||
|
|
||||||
|
The zip contains the native YAPF library for Windows, macOS and Linux (x64
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Build the zip
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 extensions/blender/build.py # library for this machine only
|
||||||
|
tools/build_release.sh # all platforms (needs zig)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both write `dist/yapf_image-blender.zip`.
|
||||||
59
extensions/blender/build.py
Normal file
59
extensions/blender/build.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Package the Blender extension: python3 extensions/blender/build.py [libs_dir]
|
||||||
|
|
||||||
|
Writes dist/yapf_image-blender.zip, ready for Blender's
|
||||||
|
Edit > Preferences > Get Extensions > Install from Disk.
|
||||||
|
|
||||||
|
libs_dir (optional) holds prebuilt libraries as <platform>/<file>, e.g.
|
||||||
|
linux-x64/libyapf.so, windows-x64/yapf.dll, macos-arm64/libyapf.dylib
|
||||||
|
(tools/build_libs.sh makes them). Without libraries the extension still
|
||||||
|
imports .yapf files using numpy, but cannot export. If no libs_dir is
|
||||||
|
given, a library for this machine is built with `cc` when available.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
SRC = os.path.join(ROOT, "extensions", "blender", "yapf_image")
|
||||||
|
sys.path.insert(0, os.path.join(ROOT, "bindings", "python"))
|
||||||
|
import yapf # noqa: E402 (for the platform folder name)
|
||||||
|
|
||||||
|
|
||||||
|
def host_lib(tmp):
|
||||||
|
name = {"win32": "yapf.dll", "darwin": "libyapf.dylib"}.get(sys.platform, "libyapf.so")
|
||||||
|
out = os.path.join(tmp, yapf._platform_dir(), name)
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||||
|
if not cc:
|
||||||
|
return None
|
||||||
|
cmd = [cc, "-O2", "-std=c99", "-shared", "-fPIC", os.path.join(ROOT, "yapf.c"), "-o", out, "-pthread"]
|
||||||
|
return tmp if subprocess.call(cmd) == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
libs = sys.argv[1] if len(sys.argv) > 1 else host_lib(tmp)
|
||||||
|
os.makedirs(os.path.join(ROOT, "dist"), exist_ok=True)
|
||||||
|
out = os.path.join(ROOT, "dist", "yapf_image-blender.zip")
|
||||||
|
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for f in ("__init__.py", "blender_manifest.toml"):
|
||||||
|
z.write(os.path.join(SRC, f), f)
|
||||||
|
z.write(os.path.join(ROOT, "bindings", "python", "yapf.py"), "yapf.py")
|
||||||
|
z.write(os.path.join(ROOT, "LICENSE"), "LICENSE")
|
||||||
|
if libs:
|
||||||
|
for plat in sorted(os.listdir(libs)):
|
||||||
|
for f in sorted(os.listdir(os.path.join(libs, plat))):
|
||||||
|
if f.endswith((".so", ".dll", ".dylib")):
|
||||||
|
z.write(os.path.join(libs, plat, f), "lib/%s/%s" % (plat, f))
|
||||||
|
print(" bundled lib/%s/%s" % (plat, f))
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
print("wrote", out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
251
extensions/blender/yapf_image/__init__.py
Normal file
251
extensions/blender/yapf_image/__init__.py
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
# YAPF image support for Blender.
|
||||||
|
#
|
||||||
|
# Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
#
|
||||||
|
# File > Import > YAPF Image (.yapf) open an image, optionally as a plane
|
||||||
|
# Image Editor > Image > Open YAPF… open into the Image Editor
|
||||||
|
# Image Editor > Image > Save as YAPF… export the current image
|
||||||
|
# Drag a .yapf file into the 3D Viewport, Image Editor or a node editor.
|
||||||
|
#
|
||||||
|
# Blender cannot read .yapf files by itself, so imported images are packed
|
||||||
|
# into the .blend file; they survive saving and reopening.
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "YAPF Image Format",
|
||||||
|
"author": "Nexoniarz",
|
||||||
|
"version": (1, 0, 0),
|
||||||
|
"blender": (4, 2, 0),
|
||||||
|
"location": "File > Import > YAPF Image, Image Editor > Image menu",
|
||||||
|
"description": "Import and export YAPF (.yapf) lossless images",
|
||||||
|
"doc_url": "https://github.com/Nexoniarz/YAPF",
|
||||||
|
"category": "Import-Export",
|
||||||
|
}
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
from bpy.props import BoolProperty, CollectionProperty, StringProperty
|
||||||
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
|
|
||||||
|
from . import yapf
|
||||||
|
|
||||||
|
|
||||||
|
# ── conversion helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_image(filepath):
|
||||||
|
"""Read a .yapf file into a packed bpy.types.Image."""
|
||||||
|
img = yapf.load(filepath)
|
||||||
|
w, h, ch = img.width, img.height, img.channels
|
||||||
|
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
|
||||||
|
rgba = np.ones((h, w, 4), dtype=np.float32)
|
||||||
|
if ch >= 3:
|
||||||
|
rgba[..., :3] = src[..., :3]
|
||||||
|
else:
|
||||||
|
rgba[..., :3] = src[..., :1]
|
||||||
|
if ch in (2, 4):
|
||||||
|
rgba[..., 3] = src[..., ch - 1]
|
||||||
|
|
||||||
|
name = os.path.basename(filepath)
|
||||||
|
bimg = bpy.data.images.new(name, w, h, alpha=ch in (2, 4))
|
||||||
|
bimg.colorspace_settings.name = "sRGB" if img.flags & yapf.FLAG_SRGB else "Non-Color"
|
||||||
|
if img.flags & yapf.FLAG_PREMULT_ALPHA:
|
||||||
|
bimg.alpha_mode = "PREMUL"
|
||||||
|
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
|
||||||
|
bimg.file_format = "PNG"
|
||||||
|
bimg.pack() # keep it inside the .blend
|
||||||
|
bimg.use_fake_user = True # …even before anything uses it
|
||||||
|
bimg["yapf_source"] = filepath
|
||||||
|
bimg["yapf_alpha"] = ch in (2, 4)
|
||||||
|
return bimg
|
||||||
|
|
||||||
|
|
||||||
|
def save_image(bimg, filepath, mips=False):
|
||||||
|
"""Write a bpy.types.Image to a .yapf file."""
|
||||||
|
w, h = bimg.size
|
||||||
|
if w == 0 or h == 0:
|
||||||
|
raise yapf.YapfError("image '%s' has no pixels" % bimg.name)
|
||||||
|
buf = np.empty(w * h * 4, dtype=np.float32)
|
||||||
|
bimg.pixels.foreach_get(buf)
|
||||||
|
px = (np.clip(buf.reshape(h, w, 4)[::-1], 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
|
||||||
|
has_alpha = bool((px[..., 3] != 255).any())
|
||||||
|
ch = 4 if has_alpha else 3
|
||||||
|
px = np.ascontiguousarray(px[..., :ch])
|
||||||
|
|
||||||
|
srgb = bimg.colorspace_settings.name not in ("Non-Color", "Linear", "Linear Rec.709", "Raw")
|
||||||
|
flags = (yapf.FLAG_SRGB if srgb else 0) | (yapf.FLAG_PREMULT_ALPHA if bimg.alpha_mode == "PREMUL" else 0)
|
||||||
|
levels = [px]
|
||||||
|
if mips:
|
||||||
|
cur = px.astype(np.uint32)
|
||||||
|
while cur.shape[0] > 1 or cur.shape[1] > 1:
|
||||||
|
hh, ww = max(1, cur.shape[0] // 2), max(1, cur.shape[1] // 2)
|
||||||
|
p = np.pad(cur, ((0, cur.shape[0] % 2), (0, cur.shape[1] % 2), (0, 0)), mode="edge")
|
||||||
|
cur = (p[0::2, 0::2] + p[1::2, 0::2] + p[0::2, 1::2] + p[1::2, 1::2] + 2) // 4
|
||||||
|
cur = cur[:hh, :ww]
|
||||||
|
levels.append(cur.astype(np.uint8))
|
||||||
|
if len(levels) == 16:
|
||||||
|
break
|
||||||
|
out = yapf.Image(w, h, ch, levels[0].tobytes(), flags=flags,
|
||||||
|
mips=[l.tobytes() for l in levels])
|
||||||
|
if srgb:
|
||||||
|
out.gpu_format = yapf.GPU_SRGB8_A8 if ch == 4 else yapf.GPU_SRGB8
|
||||||
|
else:
|
||||||
|
out.gpu_format = yapf.GPU_RGBA8 if ch == 4 else yapf.GPU_RGB8
|
||||||
|
yapf.save(filepath, out)
|
||||||
|
|
||||||
|
|
||||||
|
def add_image_plane(context, bimg):
|
||||||
|
"""A plane with the image's aspect ratio and a material that shows it."""
|
||||||
|
w, h = bimg.size
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=1.0, location=context.scene.cursor.location)
|
||||||
|
obj = context.active_object
|
||||||
|
obj.name = os.path.splitext(bimg.name)[0]
|
||||||
|
obj.scale = (w / max(w, h), h / max(w, h), 1.0)
|
||||||
|
|
||||||
|
mat = bpy.data.materials.new(obj.name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||||
|
bsdf = next(n for n in nodes if n.type == "BSDF_PRINCIPLED")
|
||||||
|
tex = nodes.new("ShaderNodeTexImage")
|
||||||
|
tex.image = bimg
|
||||||
|
tex.location = (bsdf.location.x - 350, bsdf.location.y)
|
||||||
|
links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
|
||||||
|
if bimg.get("yapf_alpha"):
|
||||||
|
links.new(tex.outputs["Alpha"], bsdf.inputs["Alpha"])
|
||||||
|
obj.data.materials.append(mat)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def show_in_image_editor(context, bimg):
|
||||||
|
for area in context.screen.areas if context.screen else []:
|
||||||
|
if area.type == "IMAGE_EDITOR":
|
||||||
|
area.spaces.active.image = bimg
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ── operators ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class IMPORT_IMAGE_OT_yapf(bpy.types.Operator, ImportHelper):
|
||||||
|
"""Import YAPF (.yapf) images"""
|
||||||
|
bl_idname = "import_image.yapf"
|
||||||
|
bl_label = "Import YAPF Image"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
filename_ext = ".yapf"
|
||||||
|
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
|
||||||
|
files: CollectionProperty(type=bpy.types.OperatorFileListElement,
|
||||||
|
options={"HIDDEN", "SKIP_SAVE"})
|
||||||
|
directory: StringProperty(subtype="DIR_PATH", options={"HIDDEN", "SKIP_SAVE"})
|
||||||
|
as_plane: BoolProperty(name="Create Plane",
|
||||||
|
description="Add a plane showing the image to the scene",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
def paths(self):
|
||||||
|
if self.files and self.directory:
|
||||||
|
return [os.path.join(self.directory, f.name) for f in self.files if f.name]
|
||||||
|
return [self.filepath]
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
done = 0
|
||||||
|
for path in self.paths():
|
||||||
|
try:
|
||||||
|
bimg = load_image(path)
|
||||||
|
except (OSError, yapf.YapfError) as e:
|
||||||
|
self.report({"ERROR"}, "%s: %s" % (os.path.basename(path), e))
|
||||||
|
continue
|
||||||
|
done += 1
|
||||||
|
if self.as_plane and context.mode == "OBJECT":
|
||||||
|
add_image_plane(context, bimg)
|
||||||
|
else:
|
||||||
|
show_in_image_editor(context, bimg)
|
||||||
|
if done:
|
||||||
|
self.report({"INFO"}, "Imported %d YAPF image(s)" % done)
|
||||||
|
return {"FINISHED"} if done else {"CANCELLED"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Dropped files arrive with filepath/directory already set.
|
||||||
|
if self.directory and self.files:
|
||||||
|
if context.area and context.area.type == "VIEW_3D":
|
||||||
|
self.as_plane = True
|
||||||
|
return self.execute(context)
|
||||||
|
return ImportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
|
|
||||||
|
class EXPORT_IMAGE_OT_yapf(bpy.types.Operator, ExportHelper):
|
||||||
|
"""Save the current image as a YAPF (.yapf) file"""
|
||||||
|
bl_idname = "export_image.yapf"
|
||||||
|
bl_label = "Save as YAPF"
|
||||||
|
|
||||||
|
filename_ext = ".yapf"
|
||||||
|
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
|
||||||
|
image_name: StringProperty(name="Image", description="Image to save (default: the one in the Image Editor)")
|
||||||
|
mips: BoolProperty(name="Store Mipmaps", description="Also store a full mip chain", default=False)
|
||||||
|
|
||||||
|
def target(self, context):
|
||||||
|
if self.image_name:
|
||||||
|
return bpy.data.images.get(self.image_name)
|
||||||
|
space = context.space_data
|
||||||
|
return getattr(space, "image", None)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
img = self.target(context)
|
||||||
|
if img is None:
|
||||||
|
self.report({"ERROR"}, "Open an image in the Image Editor first")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
self.image_name = img.name
|
||||||
|
self.filepath = os.path.splitext(img.name)[0] + ".yapf"
|
||||||
|
return ExportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
img = self.target(context)
|
||||||
|
if img is None:
|
||||||
|
self.report({"ERROR"}, "No image to save")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
try:
|
||||||
|
save_image(img, self.filepath, self.mips)
|
||||||
|
except (OSError, yapf.YapfError) as e:
|
||||||
|
self.report({"ERROR"}, str(e))
|
||||||
|
return {"CANCELLED"}
|
||||||
|
self.report({"INFO"}, "Saved %s" % os.path.basename(self.filepath))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class IMAGE_FH_yapf(bpy.types.FileHandler):
|
||||||
|
"""Drag and drop .yapf files into Blender"""
|
||||||
|
bl_idname = "IMAGE_FH_yapf"
|
||||||
|
bl_label = "YAPF Image"
|
||||||
|
bl_import_operator = "import_image.yapf"
|
||||||
|
bl_file_extensions = ".yapf"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll_drop(cls, context):
|
||||||
|
return context.area is not None and context.area.type in {
|
||||||
|
"VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR"}
|
||||||
|
|
||||||
|
|
||||||
|
def menu_import(self, context):
|
||||||
|
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="YAPF Image (.yapf)")
|
||||||
|
|
||||||
|
|
||||||
|
def menu_image(self, context):
|
||||||
|
self.layout.separator()
|
||||||
|
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="Open YAPF…")
|
||||||
|
self.layout.operator(EXPORT_IMAGE_OT_yapf.bl_idname, text="Save as YAPF…")
|
||||||
|
|
||||||
|
|
||||||
|
classes = (IMPORT_IMAGE_OT_yapf, EXPORT_IMAGE_OT_yapf, IMAGE_FH_yapf)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for c in classes:
|
||||||
|
bpy.utils.register_class(c)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.append(menu_import)
|
||||||
|
bpy.types.IMAGE_MT_image.append(menu_image)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.IMAGE_MT_image.remove(menu_image)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
|
||||||
|
for c in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(c)
|
||||||
16
extensions/blender/yapf_image/blender_manifest.toml
Normal file
16
extensions/blender/yapf_image/blender_manifest.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "yapf_image"
|
||||||
|
version = "1.0.0"
|
||||||
|
name = "YAPF Image Format"
|
||||||
|
tagline = "Import and export YAPF lossless images"
|
||||||
|
maintainer = "Nexoniarz"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://github.com/Nexoniarz/YAPF"
|
||||||
|
tags = ["Import-Export"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
license = ["SPDX:Apache-2.0"]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Read and write .yapf image files"
|
||||||
39
extensions/figma/README.md
Normal file
39
extensions/figma/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# YAPF for Figma
|
||||||
|
|
||||||
|
- **Plugins → YAPF Image Format → Import YAPF image…** — choose or drop
|
||||||
|
`.yapf` files; each becomes a rectangle filled with the image.
|
||||||
|
- **Plugins → YAPF Image Format → Export selection as YAPF…** — every
|
||||||
|
selected layer is exported at 1×, 2× or 4× and downloaded as `.yapf`.
|
||||||
|
|
||||||
|
Both directions are pixel-exact: the plugin carries its own small PNG
|
||||||
|
encoder/decoder instead of going through a canvas, which would alter
|
||||||
|
semi-transparent pixels.
|
||||||
|
|
||||||
|
## Install (development plugin)
|
||||||
|
|
||||||
|
1. Use the **Figma desktop app**.
|
||||||
|
2. **Plugins → Development → Import plugin from manifest…** and choose
|
||||||
|
`extensions/figma/manifest.json` (or the one inside `yapf-figma.zip`
|
||||||
|
from the [Releases](https://github.com/Nexoniarz/YAPF/releases)).
|
||||||
|
3. Run it from **Plugins → Development → YAPF Image Format**.
|
||||||
|
|
||||||
|
To publish it to the Figma Community, use **Publish** in the same menu;
|
||||||
|
Figma then assigns the plugin its real `id`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
`ui.html` is generated: it inlines `bindings/js/yapf.js` and `src/png.js`
|
||||||
|
into `src/ui.src.html`, because Figma loads the UI as a single file.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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.**
|
||||||
9
extensions/figma/build.js
Normal file
9
extensions/figma/build.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// node build.js — inlines the YAPF and PNG codecs into ui.html (Figma needs one file).
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const read = (p) => fs.readFileSync(path.join(__dirname, p), 'utf8');
|
||||||
|
const html = read('src/ui.src.html')
|
||||||
|
.replace('/*__YAPF_JS__*/', () => read('../../bindings/js/yapf.js'))
|
||||||
|
.replace('/*__PNG_JS__*/', () => read('src/png.js'));
|
||||||
|
fs.writeFileSync(path.join(__dirname, 'ui.html'), html);
|
||||||
|
console.log('wrote ui.html (' + html.length + ' bytes)');
|
||||||
54
extensions/figma/code.js
Normal file
54
extensions/figma/code.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// YAPF for Figma — plugin main thread (has the document, no DOM).
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// Decoding/encoding happens in the UI (ui.html), which has a DOM.
|
||||||
|
|
||||||
|
figma.showUI(__html__, { width: 380, height: 330, themeColors: true });
|
||||||
|
figma.ui.postMessage({ type: 'mode', mode: figma.command || 'import' });
|
||||||
|
|
||||||
|
function sendSelection() {
|
||||||
|
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
|
||||||
|
figma.ui.postMessage({ type: 'selection', names: nodes.map((n) => n.name) });
|
||||||
|
}
|
||||||
|
figma.on('selectionchange', sendSelection);
|
||||||
|
sendSelection();
|
||||||
|
|
||||||
|
figma.ui.onmessage = async (msg) => {
|
||||||
|
if (msg.type === 'import') {
|
||||||
|
// msg.images: [{ name, width, height, png: Uint8Array }]
|
||||||
|
const created = [];
|
||||||
|
const center = figma.viewport.center;
|
||||||
|
let x = center.x;
|
||||||
|
for (const im of msg.images) {
|
||||||
|
try {
|
||||||
|
const image = figma.createImage(im.png);
|
||||||
|
const rect = figma.createRectangle();
|
||||||
|
rect.name = im.name;
|
||||||
|
rect.resize(im.width, im.height);
|
||||||
|
rect.fills = [{ type: 'IMAGE', imageHash: image.hash, scaleMode: 'FILL' }];
|
||||||
|
rect.x = Math.round(x - (created.length ? 0 : im.width / 2));
|
||||||
|
rect.y = Math.round(center.y - im.height / 2);
|
||||||
|
x = rect.x + im.width + 40;
|
||||||
|
figma.currentPage.appendChild(rect);
|
||||||
|
created.push(rect);
|
||||||
|
} catch (e) {
|
||||||
|
figma.notify(im.name + ': ' + (e.message || e) + ' (Figma accepts images up to 4096×4096)', { error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (created.length) {
|
||||||
|
figma.currentPage.selection = created;
|
||||||
|
figma.viewport.scrollAndZoomIntoView(created);
|
||||||
|
figma.notify('Imported ' + created.length + ' YAPF image' + (created.length > 1 ? 's' : ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'export') {
|
||||||
|
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
|
||||||
|
if (!nodes.length) { figma.notify('Select one or more layers to export', { error: true }); return; }
|
||||||
|
for (const node of nodes) {
|
||||||
|
const png = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: msg.scale || 1 } });
|
||||||
|
figma.ui.postMessage({ type: 'exported', name: node.name, png });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'close') figma.closePlugin();
|
||||||
|
};
|
||||||
14
extensions/figma/manifest.json
Normal file
14
extensions/figma/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "YAPF Image Format",
|
||||||
|
"id": "yapf-image-format",
|
||||||
|
"api": "1.0.0",
|
||||||
|
"main": "code.js",
|
||||||
|
"ui": "ui.html",
|
||||||
|
"editorType": ["figma", "figjam"],
|
||||||
|
"documentAccess": "dynamic-page",
|
||||||
|
"networkAccess": { "allowedDomains": ["none"] },
|
||||||
|
"menu": [
|
||||||
|
{ "name": "Import YAPF image…", "command": "import" },
|
||||||
|
{ "name": "Export selection as YAPF…", "command": "export" }
|
||||||
|
]
|
||||||
|
}
|
||||||
126
extensions/figma/src/png.js
Normal file
126
extensions/figma/src/png.js
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* png.js — exact 8-bit PNG encode / decode for the browser and Node 18+.
|
||||||
|
* Uses the platform's CompressionStream / DecompressionStream for zlib.
|
||||||
|
* Canvas round-trips premultiply alpha and are not lossless; this is.
|
||||||
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.PNG = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CRC = new Uint32Array(256);
|
||||||
|
for (let n = 0; n < 256; n++) {
|
||||||
|
let c = n;
|
||||||
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
CRC[n] = c >>> 0;
|
||||||
|
}
|
||||||
|
function crc32(bytes, start, end) {
|
||||||
|
let c = 0xffffffff;
|
||||||
|
for (let i = start; i < end; i++) c = CRC[(c ^ bytes[i]) & 255] ^ (c >>> 8);
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zlib(data, mode) {
|
||||||
|
const stream = new Blob([data]).stream().pipeThrough(
|
||||||
|
mode === 'deflate' ? new CompressionStream('deflate') : new DecompressionStream('deflate'));
|
||||||
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 }; // channels → PNG colour type
|
||||||
|
const CHANNELS = { 0: 1, 4: 2, 2: 3, 6: 4 };
|
||||||
|
|
||||||
|
function paeth(a, b, c) {
|
||||||
|
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
||||||
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pixels: Uint8Array of w*h*ch bytes, rows top to bottom. */
|
||||||
|
async function encode(pixels, w, h, ch) {
|
||||||
|
const stride = w * ch, raw = new Uint8Array((stride + 1) * h), row = new Uint8Array(stride);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const cur = pixels.subarray(y * stride, (y + 1) * stride);
|
||||||
|
const prev = y ? pixels.subarray((y - 1) * stride, y * stride) : null;
|
||||||
|
let best = 0, bestSum = Infinity;
|
||||||
|
for (let f = 0; f < 5; f++) { // pick the filter with the smallest sum
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
const v = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
sum += v < 128 ? v : 256 - v;
|
||||||
|
}
|
||||||
|
if (sum < bestSum) { bestSum = sum; best = f; }
|
||||||
|
}
|
||||||
|
const o = y * (stride + 1);
|
||||||
|
raw[o] = best;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
row[i] = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][best]) & 255;
|
||||||
|
}
|
||||||
|
raw.set(row, o + 1);
|
||||||
|
}
|
||||||
|
const idat = await zlib(raw, 'deflate');
|
||||||
|
const chunks = [];
|
||||||
|
const chunk = (type, data) => {
|
||||||
|
const c = new Uint8Array(12 + data.length), dv = new DataView(c.buffer);
|
||||||
|
dv.setUint32(0, data.length);
|
||||||
|
for (let i = 0; i < 4; i++) c[4 + i] = type.charCodeAt(i);
|
||||||
|
c.set(data, 8);
|
||||||
|
dv.setUint32(8 + data.length, crc32(c, 4, 8 + data.length));
|
||||||
|
chunks.push(c);
|
||||||
|
};
|
||||||
|
const ihdr = new Uint8Array(13), dv = new DataView(ihdr.buffer);
|
||||||
|
dv.setUint32(0, w); dv.setUint32(4, h);
|
||||||
|
ihdr.set([8, COLOR_TYPE[ch], 0, 0, 0], 8);
|
||||||
|
chunk('IHDR', ihdr); chunk('IDAT', idat); chunk('IEND', new Uint8Array(0));
|
||||||
|
const total = 8 + chunks.reduce((n, c) => n + c.length, 0), out = new Uint8Array(total);
|
||||||
|
out.set([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
let p = 8;
|
||||||
|
for (const c of chunks) { out.set(c, p); p += c.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns { width, height, channels, pixels } or throws for PNGs it does
|
||||||
|
* not handle (16-bit, interlaced); callers can fall back to a canvas. */
|
||||||
|
async function decode(bytes) {
|
||||||
|
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
|
if (bytes.length < 8 || sig.some((v, i) => bytes[i] !== v)) throw new Error('not a PNG');
|
||||||
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
let w = 0, h = 0, depth = 0, type = 0, interlace = 0, palette = null, trns = null;
|
||||||
|
const idat = [];
|
||||||
|
for (let p = 8; p + 8 <= bytes.length;) {
|
||||||
|
const len = dv.getUint32(p), name = String.fromCharCode(...bytes.subarray(p + 4, p + 8));
|
||||||
|
const data = bytes.subarray(p + 8, p + 8 + len);
|
||||||
|
if (name === 'IHDR') { w = dv.getUint32(p + 8); h = dv.getUint32(p + 12); depth = data[8]; type = data[9]; interlace = data[12]; }
|
||||||
|
else if (name === 'PLTE') palette = data;
|
||||||
|
else if (name === 'tRNS') trns = data;
|
||||||
|
else if (name === 'IDAT') idat.push(data);
|
||||||
|
else if (name === 'IEND') break;
|
||||||
|
p += 12 + len;
|
||||||
|
}
|
||||||
|
if (depth !== 8 || interlace !== 0 || !(type in CHANNELS || type === 3))
|
||||||
|
throw new Error('unsupported PNG (needs 8-bit, non-interlaced)');
|
||||||
|
const inCh = type === 3 ? 1 : CHANNELS[type], stride = w * inCh;
|
||||||
|
const raw = await zlib(new Uint8Array(await new Blob(idat).arrayBuffer()), 'inflate');
|
||||||
|
const px = new Uint8Array(stride * h);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const f = raw[y * (stride + 1)], src = y * (stride + 1) + 1, o = y * stride;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= inCh ? px[o + i - inCh] : 0, b = y ? px[o - stride + i] : 0;
|
||||||
|
const c = y && i >= inCh ? px[o - stride + i - inCh] : 0;
|
||||||
|
px[o + i] = (raw[src + i] + [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type !== 3) return { width: w, height: h, channels: inCh, pixels: px };
|
||||||
|
const ch = trns ? 4 : 3, out = new Uint8Array(w * h * ch); // expand a palette
|
||||||
|
for (let i = 0; i < w * h; i++) {
|
||||||
|
const k = px[i];
|
||||||
|
out.set(palette.subarray(k * 3, k * 3 + 3), i * ch);
|
||||||
|
if (trns) out[i * ch + 3] = k < trns.length ? trns[k] : 255;
|
||||||
|
}
|
||||||
|
return { width: w, height: h, channels: ch, pixels: out };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { encode, decode };
|
||||||
|
}));
|
||||||
124
extensions/figma/src/ui.src.html
Normal file
124
extensions/figma/src/ui.src.html
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font: 12px Inter, system-ui, sans-serif; margin: 0; padding: 14px;
|
||||||
|
color: var(--figma-color-text, #333); background: var(--figma-color-bg, #fff); }
|
||||||
|
h2 { font-size: 13px; margin: 0 0 10px; }
|
||||||
|
.tabs { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.tabs button { flex: 1; }
|
||||||
|
button { font: inherit; padding: 7px 10px; border-radius: 6px; cursor: pointer;
|
||||||
|
border: 1px solid var(--figma-color-border, #ccc);
|
||||||
|
background: var(--figma-color-bg-secondary, #f5f5f5); color: inherit; }
|
||||||
|
button.primary, .tabs button.on { background: var(--figma-color-bg-brand, #0d99ff);
|
||||||
|
color: var(--figma-color-text-onbrand, #fff); border-color: transparent; }
|
||||||
|
#drop { border: 2px dashed var(--figma-color-border, #ccc); border-radius: 8px; padding: 28px 10px;
|
||||||
|
text-align: center; cursor: pointer; color: var(--figma-color-text-secondary, #777); }
|
||||||
|
#drop.hover { border-color: var(--figma-color-border-brand, #0d99ff); }
|
||||||
|
#status { margin-top: 10px; min-height: 32px; color: var(--figma-color-text-secondary, #777);
|
||||||
|
white-space: pre-line; }
|
||||||
|
.row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
|
||||||
|
select { font: inherit; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tabs">
|
||||||
|
<button id="tab-import">Import</button>
|
||||||
|
<button id="tab-export">Export</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="import">
|
||||||
|
<h2>Import YAPF images</h2>
|
||||||
|
<div id="drop">Drop .yapf files here<br>or click to choose</div>
|
||||||
|
<input id="file" type="file" accept=".yapf" multiple hidden>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="export" hidden>
|
||||||
|
<h2>Export selection as YAPF</h2>
|
||||||
|
<div id="sel">Nothing selected</div>
|
||||||
|
<div class="row">Scale
|
||||||
|
<select id="scale"><option>1</option><option>2</option><option>4</option></select>
|
||||||
|
</div>
|
||||||
|
<button id="go" class="primary">Export</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script>/*__YAPF_JS__*/</script>
|
||||||
|
<script>/*__PNG_JS__*/</script>
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const post = (m) => parent.postMessage({ pluginMessage: m }, '*');
|
||||||
|
const status = (t) => { $('status').textContent = t; };
|
||||||
|
|
||||||
|
function tab(name) {
|
||||||
|
$('import').hidden = name !== 'import';
|
||||||
|
$('export').hidden = name !== 'export';
|
||||||
|
$('tab-import').classList.toggle('on', name === 'import');
|
||||||
|
$('tab-export').classList.toggle('on', name === 'export');
|
||||||
|
status('');
|
||||||
|
}
|
||||||
|
$('tab-import').onclick = () => tab('import');
|
||||||
|
$('tab-export').onclick = () => tab('export');
|
||||||
|
|
||||||
|
/* Import: .yapf → pixels → exact PNG → Figma image */
|
||||||
|
async function importFiles(files) {
|
||||||
|
const images = [];
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer()));
|
||||||
|
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
||||||
|
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
||||||
|
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')');
|
||||||
|
} catch (e) {
|
||||||
|
status(f.name + ': ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (images.length) post({ type: 'import', images });
|
||||||
|
}
|
||||||
|
$('drop').onclick = () => $('file').click();
|
||||||
|
$('file').onchange = (e) => importFiles([...e.target.files]);
|
||||||
|
$('drop').ondragover = (e) => { e.preventDefault(); $('drop').classList.add('hover'); };
|
||||||
|
$('drop').ondragleave = () => $('drop').classList.remove('hover');
|
||||||
|
$('drop').ondrop = (e) => { e.preventDefault(); $('drop').classList.remove('hover'); importFiles([...e.dataTransfer.files]); };
|
||||||
|
|
||||||
|
/* Export: Figma PNG → exact pixels → .yapf download */
|
||||||
|
$('go').onclick = () => { status('Exporting…'); post({ type: 'export', scale: +$('scale').value }); };
|
||||||
|
|
||||||
|
async function exported(name, pngBytes) {
|
||||||
|
let img = await PNG.decode(pngBytes);
|
||||||
|
let { pixels, channels } = img;
|
||||||
|
if (channels === 4) { // drop alpha when fully opaque
|
||||||
|
let opaque = true;
|
||||||
|
for (let i = 3; i < pixels.length && opaque; i += 4) opaque = pixels[i] === 255;
|
||||||
|
if (opaque) {
|
||||||
|
const rgb = new Uint8Array(img.width * img.height * 3);
|
||||||
|
for (let i = 0, j = 0; i < pixels.length; i += 4, j += 3) rgb.set(pixels.subarray(i, i + 3), j);
|
||||||
|
pixels = rgb; channels = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const file = YAPF.encode({ width: img.width, height: img.height, channels, pixels,
|
||||||
|
flags: YAPF.FLAG.SRGB,
|
||||||
|
gpuFormat: channels === 4 ? YAPF.GPU.SRGB8_A8 : YAPF.GPU.SRGB8 });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([file], { type: 'image/x-yapf' }));
|
||||||
|
a.download = name.replace(/[\\/:*?"<>|]/g, '_') + '.yapf';
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
status('Saved ' + a.download + '\n' + img.width + '×' + img.height + ', ' +
|
||||||
|
(file.length / 1024).toFixed(1) + ' KB (' +
|
||||||
|
(100 * file.length / (img.width * img.height * channels)).toFixed(1) + '% of raw)');
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
const m = e.data.pluginMessage;
|
||||||
|
if (!m) return;
|
||||||
|
if (m.type === 'mode') tab(m.mode === 'export' ? 'export' : 'import');
|
||||||
|
if (m.type === 'selection')
|
||||||
|
$('sel').textContent = m.names.length ? 'Selected: ' + m.names.join(', ') : 'Nothing selected';
|
||||||
|
if (m.type === 'exported') exported(m.name, m.png).catch((err) => status(m.name + ': ' + err.message));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
713
extensions/figma/ui.html
Normal file
713
extensions/figma/ui.html
Normal file
|
|
@ -0,0 +1,713 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font: 12px Inter, system-ui, sans-serif; margin: 0; padding: 14px;
|
||||||
|
color: var(--figma-color-text, #333); background: var(--figma-color-bg, #fff); }
|
||||||
|
h2 { font-size: 13px; margin: 0 0 10px; }
|
||||||
|
.tabs { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.tabs button { flex: 1; }
|
||||||
|
button { font: inherit; padding: 7px 10px; border-radius: 6px; cursor: pointer;
|
||||||
|
border: 1px solid var(--figma-color-border, #ccc);
|
||||||
|
background: var(--figma-color-bg-secondary, #f5f5f5); color: inherit; }
|
||||||
|
button.primary, .tabs button.on { background: var(--figma-color-bg-brand, #0d99ff);
|
||||||
|
color: var(--figma-color-text-onbrand, #fff); border-color: transparent; }
|
||||||
|
#drop { border: 2px dashed var(--figma-color-border, #ccc); border-radius: 8px; padding: 28px 10px;
|
||||||
|
text-align: center; cursor: pointer; color: var(--figma-color-text-secondary, #777); }
|
||||||
|
#drop.hover { border-color: var(--figma-color-border-brand, #0d99ff); }
|
||||||
|
#status { margin-top: 10px; min-height: 32px; color: var(--figma-color-text-secondary, #777);
|
||||||
|
white-space: pre-line; }
|
||||||
|
.row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
|
||||||
|
select { font: inherit; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tabs">
|
||||||
|
<button id="tab-import">Import</button>
|
||||||
|
<button id="tab-export">Export</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="import">
|
||||||
|
<h2>Import YAPF images</h2>
|
||||||
|
<div id="drop">Drop .yapf files here<br>or click to choose</div>
|
||||||
|
<input id="file" type="file" accept=".yapf" multiple hidden>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="export" hidden>
|
||||||
|
<h2>Export selection as YAPF</h2>
|
||||||
|
<div id="sel">Nothing selected</div>
|
||||||
|
<div class="row">Scale
|
||||||
|
<select id="scale"><option>1</option><option>2</option><option>4</option></select>
|
||||||
|
</div>
|
||||||
|
<button id="go" class="primary">Export</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script>/*
|
||||||
|
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript.
|
||||||
|
*
|
||||||
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Works in browsers, Node.js, Figma plugins and VS Code webviews; no
|
||||||
|
* dependencies. Produces exactly the same bytes as the C library.
|
||||||
|
*
|
||||||
|
* const img = YAPF.decode(bytes); // Uint8Array → image
|
||||||
|
* const file = YAPF.encode(img); // image → Uint8Array
|
||||||
|
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
||||||
|
*
|
||||||
|
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
||||||
|
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
||||||
|
* The specification is the comment at the top of yapf.h.
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.YAPF = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TILE = 64, TILE_PX = TILE * TILE, GROUP = 8, BAND = 8, RING = 16;
|
||||||
|
const MAX_GROUPS = TILE_PX / GROUP, MAX_DIM = 65535, MAX_MIPS = 16;
|
||||||
|
const STORED = 0x80, SUBGREEN = 0x10, NIB_REPEAT = 9;
|
||||||
|
const F_LEFT = 0, F_GRAD = 1, F_UPRIGHT = 2, F_UPAVG = 3, F_UP = 16;
|
||||||
|
const NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64];
|
||||||
|
const RUN_LENS = [2, 3, 4, 8, 16, 32, 64];
|
||||||
|
/* Encoder try order: cheapest to decode first, ties go to the earlier. */
|
||||||
|
const FILTERS = [F_UP, F_UP + 1, F_UP + 2, F_UP + 3, F_UP + 4, F_UP + 5,
|
||||||
|
F_UP + 6, F_UP + 7, F_UPRIGHT, F_UPAVG, F_LEFT, F_GRAD];
|
||||||
|
|
||||||
|
const GPU = { RGBA8: 0, RGB8: 1, RG8: 2, R8: 3, SRGB8_A8: 4, SRGB8: 5 };
|
||||||
|
const FLAG = { PREMULT_ALPHA: 1, SRGB: 2 };
|
||||||
|
|
||||||
|
const ZERO_ROW = new Uint8Array(TILE + 1);
|
||||||
|
|
||||||
|
const zz = (r) => ((r << 1) ^ ((r << 24) >> 31)) & 255; /* r is 0..255 */
|
||||||
|
const unzz = (z) => ((z >>> 1) ^ -(z & 1)) & 255;
|
||||||
|
const width = (z) => (z === 0 ? 0 : 32 - Math.clz32(z));
|
||||||
|
const s8 = (v) => (v << 24) >> 24;
|
||||||
|
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
||||||
|
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
||||||
|
|
||||||
|
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); }
|
||||||
|
|
||||||
|
/* ── Decoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function unpackPlane(b, p, end, z, tw, th) {
|
||||||
|
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
|
||||||
|
if (end - p < nb) corrupt();
|
||||||
|
const filt = b.subarray(p, p + nb);
|
||||||
|
p += nb;
|
||||||
|
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) corrupt();
|
||||||
|
|
||||||
|
const gw = new Uint8Array(ng + 64);
|
||||||
|
let data = 0, g = 0, hi = 0, w = 0;
|
||||||
|
while (g < ng) {
|
||||||
|
if (p >= end) corrupt();
|
||||||
|
const c = (b[p] >> (hi * 4)) & 15, n = NIB_GROUPS[c];
|
||||||
|
p += hi; hi ^= 1;
|
||||||
|
if (c < NIB_REPEAT) w = c;
|
||||||
|
gw.fill(w, g, g + n);
|
||||||
|
data += w * n; g += n;
|
||||||
|
}
|
||||||
|
if (g !== ng) corrupt();
|
||||||
|
if (hi) { if (b[p] >> 4) corrupt(); p++; }
|
||||||
|
if (end - p < data) corrupt();
|
||||||
|
|
||||||
|
for (g = 0; g < ng; g++) {
|
||||||
|
w = gw[g];
|
||||||
|
const o = g * GROUP, mask = (1 << w) - 1;
|
||||||
|
let acc = 0, bits = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) {
|
||||||
|
while (bits < w) { acc |= b[p++] << bits; bits += 8; }
|
||||||
|
z[o + i] = acc & mask;
|
||||||
|
acc >>>= w; bits -= w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { p, filt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfilterRow(f, R, r0, U, u0, K, k0, Z, z0, tw) {
|
||||||
|
if (f === F_LEFT) {
|
||||||
|
let L = 0;
|
||||||
|
for (let x = 0; x < tw; x++) R[r0 + x] = L = (L + unzz(Z[z0 + x])) & 255;
|
||||||
|
} else if (f === F_GRAD) {
|
||||||
|
let L = (U[u0] + unzz(Z[z0])) & 255;
|
||||||
|
R[r0] = L;
|
||||||
|
for (let x = 1; x < tw; x++)
|
||||||
|
R[r0 + x] = L = (L + U[u0 + x] - U[u0 + x - 1] + unzz(Z[z0 + x])) & 255;
|
||||||
|
} else {
|
||||||
|
/* pred = (a + b) >> 1: Up k a = b = K; Up-right a = b = U + 1;
|
||||||
|
* Up-average a = U, b = U + 1. Rows carry one extra sample. */
|
||||||
|
let A, a0, B, b0;
|
||||||
|
if (f >= F_UP) { A = K; a0 = k0; B = K; b0 = k0; }
|
||||||
|
else if (f === F_UPAVG) { A = U; a0 = u0; B = U; b0 = u0 + 1; }
|
||||||
|
else { A = U; a0 = u0 + 1; B = U; b0 = u0 + 1; }
|
||||||
|
for (let x = 0; x < tw; x++)
|
||||||
|
R[r0 + x] = (((A[a0 + x] + B[b0 + x]) >> 1) + unzz(Z[z0 + x])) & 255;
|
||||||
|
}
|
||||||
|
R[r0 + tw] = R[r0 + tw - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) {
|
||||||
|
if (end <= p) corrupt();
|
||||||
|
let hdr = b[p++];
|
||||||
|
if (hdr === STORED) {
|
||||||
|
const rowb = tw * ch;
|
||||||
|
if (end - p !== rowb * th) corrupt();
|
||||||
|
for (let y = 0; y < th; y++)
|
||||||
|
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hdr & 0xE0) corrupt(); /* reserved bits set */
|
||||||
|
const xf = hdr & SUBGREEN;
|
||||||
|
hdr &= 0x0F;
|
||||||
|
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
||||||
|
|
||||||
|
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
||||||
|
const rows = new Uint8Array(4 * planeLen);
|
||||||
|
const z = [], filt = [null, null, null, null];
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (!(hdr & (1 << c))) continue;
|
||||||
|
if (p >= end) corrupt();
|
||||||
|
rows.fill(b[p++], c * planeLen, (c + 1) * planeLen);
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (hdr & (1 << c)) continue;
|
||||||
|
z[c] = new Uint8Array(TILE_PX + GROUP);
|
||||||
|
const r = unpackPlane(b, p, end, z[c], tw, th);
|
||||||
|
p = r.p; filt[c] = r.filt;
|
||||||
|
}
|
||||||
|
if (p !== end) corrupt();
|
||||||
|
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const ri = (y % RING) * rowLen;
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
if (!filt[c]) continue;
|
||||||
|
const f = filt[c][y >> 3], k = upK(f), base = c * planeLen;
|
||||||
|
unfilterRow(f, rows, base + ri,
|
||||||
|
y ? rows : ZERO_ROW, y ? base + ((y - 1) % RING) * rowLen : 0,
|
||||||
|
y >= k ? rows : ZERO_ROW, y >= k ? base + ((y - k) % RING) * rowLen : 0,
|
||||||
|
z[c], y * tw, tw);
|
||||||
|
}
|
||||||
|
let d = ((ty + y) * imgW + tx) * ch;
|
||||||
|
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
||||||
|
for (let x = 0; x < tw; x++, d += ch) {
|
||||||
|
if (ch >= 3) {
|
||||||
|
if (xf) {
|
||||||
|
const g = rows[P0 + x];
|
||||||
|
out[d] = rows[P1 + x] + g - 128;
|
||||||
|
out[d + 1] = g;
|
||||||
|
out[d + 2] = rows[P2 + x] + g - 128;
|
||||||
|
} else {
|
||||||
|
const co = (rows[P1 + x] - 128) & 255, cg = (rows[P2 + x] - 128) & 255;
|
||||||
|
const t = (rows[P0 + x] - (s8(cg) >> 1)) & 255;
|
||||||
|
const bb = (t - (s8(co) >> 1)) & 255;
|
||||||
|
out[d] = bb + co;
|
||||||
|
out[d + 1] = cg + t;
|
||||||
|
out[d + 2] = bb;
|
||||||
|
}
|
||||||
|
if (ch === 4) out[d + 3] = rows[P3 + x];
|
||||||
|
} else {
|
||||||
|
out[d] = rows[P0 + x];
|
||||||
|
if (ch === 2) out[d + 1] = rows[P1 + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
||||||
|
|
||||||
|
function decode(input) {
|
||||||
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
|
throw new Error('YAPF: not a YAPF file');
|
||||||
|
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
|
||||||
|
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
||||||
|
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
||||||
|
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
||||||
|
const idxEnd = 20 + 8 * mipLevels;
|
||||||
|
if (b.length < idxEnd) corrupt();
|
||||||
|
|
||||||
|
const mips = [];
|
||||||
|
for (let m = 0; m < mipLevels; m++) {
|
||||||
|
const off = r32(b, 20 + 8 * m), len = r32(b, 24 + 8 * m);
|
||||||
|
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
|
||||||
|
if (off < idxEnd || off > b.length || len > b.length - off) corrupt();
|
||||||
|
const out = new Uint8Array(mw * mh * ch);
|
||||||
|
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
|
||||||
|
if (len / 4 < tcount) corrupt();
|
||||||
|
let pos = off + tcount * 4;
|
||||||
|
for (let t = 0; t < tcount; t++) {
|
||||||
|
const sz = r32(b, off + t * 4);
|
||||||
|
if (sz > off + len - pos) corrupt();
|
||||||
|
const tx = (t % tcx) * TILE, ty = Math.floor(t / tcx) * TILE;
|
||||||
|
decodeTile(b, pos, pos + sz, out, mw, tx, ty,
|
||||||
|
Math.min(TILE, mw - tx), Math.min(TILE, mh - ty), ch);
|
||||||
|
pos += sz;
|
||||||
|
}
|
||||||
|
mips.push(out);
|
||||||
|
}
|
||||||
|
return { width: W, height: H, channels: ch, gpuFormat: b[6], flags: b[8],
|
||||||
|
mipLevels, pixels: mips[0], mips };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Encoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
class Out {
|
||||||
|
constructor(n) { this.buf = new Uint8Array(Math.max(n, 1024)); this.len = 0; }
|
||||||
|
reserve(n) {
|
||||||
|
if (this.len + n <= this.buf.length) return;
|
||||||
|
let c = this.buf.length * 2;
|
||||||
|
while (c < this.len + n) c *= 2;
|
||||||
|
const nb = new Uint8Array(c); nb.set(this.buf.subarray(0, this.len)); this.buf = nb;
|
||||||
|
}
|
||||||
|
push(v) { this.reserve(1); this.buf[this.len++] = v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function predictRow(f, P, r0, u0, k0, pred, tw, U, K) {
|
||||||
|
switch (f) {
|
||||||
|
case F_LEFT: pred[0] = 0; for (let x = 1; x < tw; x++) pred[x] = P[r0 + x - 1]; break;
|
||||||
|
case F_GRAD:
|
||||||
|
pred[0] = U[u0];
|
||||||
|
for (let x = 1; x < tw; x++) pred[x] = (P[r0 + x - 1] + U[u0 + x] - U[u0 + x - 1]) & 255;
|
||||||
|
break;
|
||||||
|
case F_UPRIGHT:
|
||||||
|
for (let x = 0; x + 1 < tw; x++) pred[x] = U[u0 + x + 1];
|
||||||
|
pred[tw - 1] = U[u0 + tw - 1];
|
||||||
|
break;
|
||||||
|
case F_UPAVG:
|
||||||
|
for (let x = 0; x + 1 < tw; x++) pred[x] = (U[u0 + x] + U[u0 + x + 1]) >> 1;
|
||||||
|
pred[tw - 1] = U[u0 + tw - 1];
|
||||||
|
break;
|
||||||
|
default: for (let x = 0; x < tw; x++) pred[x] = K[k0 + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Picks a filter per band; fills z and filt; returns estimated cost. */
|
||||||
|
function chooseFilters(P, tw, th, z, filt) {
|
||||||
|
const total = tw * th, nb = (th + BAND - 1) >> 3;
|
||||||
|
const tmp = new Uint8Array(BAND * TILE + GROUP), pred = new Uint8Array(TILE);
|
||||||
|
let sum = 0, prevFi = 0;
|
||||||
|
for (let b = 0; b < nb; b++) {
|
||||||
|
const y0 = b * BAND, y1 = Math.min(y0 + BAND, th), n = (y1 - y0) * tw;
|
||||||
|
let bestCost = Infinity, bestFi = 0;
|
||||||
|
for (let t = 0; t < FILTERS.length; t++) {
|
||||||
|
const fi = t === 0 ? prevFi : (t <= prevFi ? t - 1 : t);
|
||||||
|
const f = FILTERS[fi], k = upK(f), serial = f === F_LEFT || f === F_GRAD;
|
||||||
|
let cost = 0, done = 0, lost = false;
|
||||||
|
for (let y = y0; y < y1 && !lost; y++) {
|
||||||
|
const r0 = y * tw;
|
||||||
|
const U = y ? P : ZERO_ROW, u0 = y ? r0 - tw : 0;
|
||||||
|
const K = y >= k ? P : ZERO_ROW, k0 = y >= k ? r0 - k * tw : 0;
|
||||||
|
predictRow(f, P, r0, u0, k0, pred, tw, U, K);
|
||||||
|
const T = (y - y0) * tw;
|
||||||
|
for (let x = 0; x < tw; x++) tmp[T + x] = zz((P[r0 + x] - pred[x]) & 255);
|
||||||
|
const last = y + 1 === y1, end = last ? n : (y + 1 - y0) * tw;
|
||||||
|
if (last) tmp.fill(0, n, n + GROUP);
|
||||||
|
for (; done + GROUP <= end || (last && done < end); done += GROUP) {
|
||||||
|
let m = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) m |= tmp[done + i];
|
||||||
|
const w = width(m);
|
||||||
|
cost += w ? 16 * w + 8 : 2;
|
||||||
|
}
|
||||||
|
const c = serial ? cost + Math.floor(cost / 25) : cost;
|
||||||
|
if (c > bestCost || (c === bestCost && fi > bestFi)) lost = true;
|
||||||
|
}
|
||||||
|
if (lost) continue;
|
||||||
|
if (serial) cost += Math.floor(cost / 25);
|
||||||
|
if (cost < bestCost || (cost === bestCost && fi < bestFi)) {
|
||||||
|
bestCost = cost; bestFi = fi;
|
||||||
|
z.set(tmp.subarray(0, n), y0 * tw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevFi = bestFi;
|
||||||
|
filt[b] = FILTERS[bestFi];
|
||||||
|
sum += bestCost;
|
||||||
|
}
|
||||||
|
z.fill(0, total, total + GROUP);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitPlane(o, z, filt, tw, th) {
|
||||||
|
const total = tw * th, ng = (total + GROUP - 1) >> 3, nb = (th + BAND - 1) >> 3;
|
||||||
|
o.reserve(nb + ng + ng * 8);
|
||||||
|
for (let i = 0; i < nb; i++) o.buf[o.len++] = filt[i];
|
||||||
|
|
||||||
|
const need = new Uint8Array(ng);
|
||||||
|
for (let g = 0; g < ng; g++) {
|
||||||
|
let m = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) m |= z[g * GROUP + i];
|
||||||
|
need[g] = width(m);
|
||||||
|
}
|
||||||
|
/* span[j][g] = widest need in [g, g + 2^j) */
|
||||||
|
const span = [need.slice()];
|
||||||
|
for (let j = 1; j < 7; j++) {
|
||||||
|
const prev = span[j - 1], cur = new Uint8Array(ng), h = 1 << (j - 1);
|
||||||
|
for (let g = 0; g + (1 << j) <= ng; g++) cur[g] = Math.max(prev[g], prev[g + h]);
|
||||||
|
span.push(cur);
|
||||||
|
}
|
||||||
|
const cost = new Uint32Array((ng + 1) * 9), pick = new Uint8Array(ng * 9);
|
||||||
|
const mx = new Uint8Array(7);
|
||||||
|
for (let g = ng - 1; g >= 0; g--) {
|
||||||
|
let lc = Infinity, lk = 0;
|
||||||
|
for (let w = need[g]; w <= 8; w++) {
|
||||||
|
const c = 1 + 2 * w + cost[(g + 1) * 9 + w];
|
||||||
|
if (c < lc) { lc = c; lk = w; }
|
||||||
|
}
|
||||||
|
let nl = 0;
|
||||||
|
for (; nl < 7 && g + RUN_LENS[nl] <= ng; nl++) {
|
||||||
|
const L = RUN_LENS[nl];
|
||||||
|
mx[nl] = L === 3 ? Math.max(span[1][g], need[g + 2]) : span[31 - Math.clz32(L)][g];
|
||||||
|
}
|
||||||
|
for (let pw = 0; pw <= 8; pw++) {
|
||||||
|
let bc = lc, bk = lk;
|
||||||
|
for (let li = 0; li < nl && mx[li] <= pw; li++) {
|
||||||
|
const L = RUN_LENS[li], c = 1 + 2 * pw * L + cost[(g + L) * 9 + pw];
|
||||||
|
if (c < bc) { bc = c; bk = NIB_REPEAT + li; }
|
||||||
|
}
|
||||||
|
cost[g * 9 + pw] = bc;
|
||||||
|
pick[g * 9 + pw] = bk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gw = new Uint8Array(ng), nib = [];
|
||||||
|
let pw = 0;
|
||||||
|
for (let g = 0; g < ng;) {
|
||||||
|
const k = pick[g * 9 + pw];
|
||||||
|
nib.push(k);
|
||||||
|
if (k < NIB_REPEAT) { pw = k; gw[g++] = k; }
|
||||||
|
else for (let n = 0; n < NIB_GROUPS[k]; n++) gw[g++] = pw;
|
||||||
|
}
|
||||||
|
for (let k = 0; k < nib.length; k += 2)
|
||||||
|
o.buf[o.len++] = nib[k] | (k + 1 < nib.length ? nib[k + 1] << 4 : 0);
|
||||||
|
|
||||||
|
for (let g = 0; g < ng; g++) {
|
||||||
|
const w = gw[g];
|
||||||
|
let acc = 0, bits = 0;
|
||||||
|
for (let i = 0; i < GROUP; i++) {
|
||||||
|
acc |= z[g * GROUP + i] << bits; bits += w;
|
||||||
|
while (bits >= 8) { o.buf[o.len++] = acc & 255; acc >>>= 8; bits -= 8; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeTile(o, sc, px, imgW, tx, ty, tw, th, ch) {
|
||||||
|
const total = tw * th, start = o.len, raw = total * ch, tries = ch >= 3 ? 2 : 1;
|
||||||
|
const mask = [0, 0], est = [0, 0];
|
||||||
|
for (let t = 0; t < tries; t++) {
|
||||||
|
const plane = sc.plane[t];
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
let s = ((ty + y) * imgW + tx) * ch;
|
||||||
|
for (let x = 0; x < tw; x++, s += ch) {
|
||||||
|
const i = y * tw + x;
|
||||||
|
if (ch < 3) { for (let c = 0; c < ch; c++) plane[c][i] = px[s + c]; continue; }
|
||||||
|
const r = px[s], g = px[s + 1], b = px[s + 2];
|
||||||
|
if (t === 0) {
|
||||||
|
const oo = (r - b) & 255, tt = (b + (s8(oo) >> 1)) & 255, q = (g - tt) & 255;
|
||||||
|
plane[0][i] = (tt + (s8(q) >> 1)) & 255;
|
||||||
|
plane[1][i] = (oo + 128) & 255;
|
||||||
|
plane[2][i] = (q + 128) & 255;
|
||||||
|
} else {
|
||||||
|
plane[0][i] = g;
|
||||||
|
plane[1][i] = (r - g + 128) & 255;
|
||||||
|
plane[2][i] = (b - g + 128) & 255;
|
||||||
|
}
|
||||||
|
if (ch === 4) plane[3][i] = px[s + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
const P = plane[c];
|
||||||
|
let i = 1;
|
||||||
|
while (i < total && P[i] === P[0]) i++;
|
||||||
|
if (i === total) { mask[t] |= 1 << c; est[t] += 16; continue; }
|
||||||
|
est[t] += chooseFilters(P, tw, th, sc.z[t][c], sc.filt[t][c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const t = tries === 2 && est[1] < est[0] ? 1 : 0;
|
||||||
|
o.reserve(5);
|
||||||
|
o.buf[o.len++] = mask[t] | (t ? SUBGREEN : 0);
|
||||||
|
for (let c = 0; c < ch; c++) if (mask[t] & (1 << c)) o.buf[o.len++] = sc.plane[t][c][0];
|
||||||
|
for (let c = 0; c < ch; c++)
|
||||||
|
if (!(mask[t] & (1 << c))) emitPlane(o, sc.z[t][c], sc.filt[t][c], tw, th);
|
||||||
|
|
||||||
|
if (o.len - start > raw + 1) {
|
||||||
|
o.len = start;
|
||||||
|
o.reserve(raw + 1);
|
||||||
|
o.buf[o.len++] = STORED;
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const s = ((ty + y) * imgW + tx) * ch;
|
||||||
|
o.buf.set(px.subarray(s, s + tw * ch), o.len);
|
||||||
|
o.len += tw * ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeLevel(px, mw, mh, ch) {
|
||||||
|
const tcx = Math.ceil(mw / TILE), tcy = Math.ceil(mh / TILE), tcount = tcx * tcy;
|
||||||
|
const o = new Out(tcount * 4 + (mw * mh * ch >> 1));
|
||||||
|
o.reserve(tcount * 4);
|
||||||
|
o.len = tcount * 4;
|
||||||
|
const mk = (n) => [0, 1, 2, 3].map(() => new Uint8Array(n));
|
||||||
|
const sc = {
|
||||||
|
plane: [mk(TILE_PX), mk(TILE_PX)],
|
||||||
|
z: [mk(TILE_PX + GROUP), mk(TILE_PX + GROUP)],
|
||||||
|
filt: [mk(TILE / BAND), mk(TILE / BAND)],
|
||||||
|
};
|
||||||
|
let t = 0;
|
||||||
|
for (let ty = 0; ty < tcy; ty++)
|
||||||
|
for (let tx = 0; tx < tcx; tx++, t++) {
|
||||||
|
const x0 = tx * TILE, y0 = ty * TILE, start = o.len;
|
||||||
|
encodeTile(o, sc, px, mw, x0, y0, Math.min(TILE, mw - x0), Math.min(TILE, mh - y0), ch);
|
||||||
|
const n = o.len - start;
|
||||||
|
o.buf[t * 4] = n & 255; o.buf[t * 4 + 1] = (n >>> 8) & 255;
|
||||||
|
o.buf[t * 4 + 2] = (n >>> 16) & 255; o.buf[t * 4 + 3] = (n >>> 24) & 255;
|
||||||
|
}
|
||||||
|
return o.buf.subarray(0, o.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(img) {
|
||||||
|
const { width: W, height: H, channels: ch } = img;
|
||||||
|
const mips = img.mips && img.mips.length ? img.mips : [img.pixels];
|
||||||
|
const mipLevels = img.mipLevels || mips.length;
|
||||||
|
if (!(W >= 1 && W <= MAX_DIM && H >= 1 && H <= MAX_DIM && ch >= 1 && ch <= 4 &&
|
||||||
|
mipLevels >= 1 && mipLevels <= MAX_MIPS && mips.length >= mipLevels))
|
||||||
|
throw new Error('YAPF: invalid image');
|
||||||
|
const levels = [];
|
||||||
|
for (let m = 0; m < mipLevels; m++) {
|
||||||
|
const mw = Math.max(1, W >>> m), mh = Math.max(1, H >>> m);
|
||||||
|
const px = m === 0 ? img.pixels : mips[m];
|
||||||
|
if (!px || px.length < mw * mh * ch) throw new Error('YAPF: mip ' + m + ' has too few pixels');
|
||||||
|
levels.push(encodeLevel(px, mw, mh, ch));
|
||||||
|
}
|
||||||
|
const idxEnd = 20 + 8 * mipLevels;
|
||||||
|
let total = idxEnd;
|
||||||
|
for (const l of levels) total += l.length;
|
||||||
|
const out = new Uint8Array(total);
|
||||||
|
out.set([0x59, 0x41, 0x50, 0x46, 1, ch, img.gpuFormat || 0, mipLevels, img.flags || 0]);
|
||||||
|
const w32 = (p, v) => { out[p] = v & 255; out[p + 1] = (v >>> 8) & 255;
|
||||||
|
out[p + 2] = (v >>> 16) & 255; out[p + 3] = (v >>> 24) & 255; };
|
||||||
|
w32(12, W); w32(16, H);
|
||||||
|
let off = idxEnd;
|
||||||
|
levels.forEach((l, m) => { w32(20 + 8 * m, off); w32(24 + 8 * m, l.length); out.set(l, off); off += l.length; });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Any channel count → RGBA (for canvas ImageData). */
|
||||||
|
function toRGBA(img, level) {
|
||||||
|
const m = level || 0, px = img.mips ? img.mips[m] : img.pixels;
|
||||||
|
const w = Math.max(1, img.width >>> m), h = Math.max(1, img.height >>> m), ch = img.channels;
|
||||||
|
if (ch === 4) return new Uint8ClampedArray(px.buffer, px.byteOffset, w * h * 4);
|
||||||
|
const out = new Uint8ClampedArray(w * h * 4);
|
||||||
|
for (let i = 0, s = 0; i < w * h; i++, s += ch) {
|
||||||
|
const o = i * 4;
|
||||||
|
if (ch >= 3) { out[o] = px[s]; out[o + 1] = px[s + 1]; out[o + 2] = px[s + 2]; out[o + 3] = 255; }
|
||||||
|
else { out[o] = out[o + 1] = out[o + 2] = px[s]; out[o + 3] = ch === 2 ? px[s + 1] : 255; }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 };
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
<script>/*
|
||||||
|
* png.js — exact 8-bit PNG encode / decode for the browser and Node 18+.
|
||||||
|
* Uses the platform's CompressionStream / DecompressionStream for zlib.
|
||||||
|
* Canvas round-trips premultiply alpha and are not lossless; this is.
|
||||||
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.PNG = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CRC = new Uint32Array(256);
|
||||||
|
for (let n = 0; n < 256; n++) {
|
||||||
|
let c = n;
|
||||||
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
CRC[n] = c >>> 0;
|
||||||
|
}
|
||||||
|
function crc32(bytes, start, end) {
|
||||||
|
let c = 0xffffffff;
|
||||||
|
for (let i = start; i < end; i++) c = CRC[(c ^ bytes[i]) & 255] ^ (c >>> 8);
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zlib(data, mode) {
|
||||||
|
const stream = new Blob([data]).stream().pipeThrough(
|
||||||
|
mode === 'deflate' ? new CompressionStream('deflate') : new DecompressionStream('deflate'));
|
||||||
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 }; // channels → PNG colour type
|
||||||
|
const CHANNELS = { 0: 1, 4: 2, 2: 3, 6: 4 };
|
||||||
|
|
||||||
|
function paeth(a, b, c) {
|
||||||
|
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
||||||
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pixels: Uint8Array of w*h*ch bytes, rows top to bottom. */
|
||||||
|
async function encode(pixels, w, h, ch) {
|
||||||
|
const stride = w * ch, raw = new Uint8Array((stride + 1) * h), row = new Uint8Array(stride);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const cur = pixels.subarray(y * stride, (y + 1) * stride);
|
||||||
|
const prev = y ? pixels.subarray((y - 1) * stride, y * stride) : null;
|
||||||
|
let best = 0, bestSum = Infinity;
|
||||||
|
for (let f = 0; f < 5; f++) { // pick the filter with the smallest sum
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
const v = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
sum += v < 128 ? v : 256 - v;
|
||||||
|
}
|
||||||
|
if (sum < bestSum) { bestSum = sum; best = f; }
|
||||||
|
}
|
||||||
|
const o = y * (stride + 1);
|
||||||
|
raw[o] = best;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
row[i] = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][best]) & 255;
|
||||||
|
}
|
||||||
|
raw.set(row, o + 1);
|
||||||
|
}
|
||||||
|
const idat = await zlib(raw, 'deflate');
|
||||||
|
const chunks = [];
|
||||||
|
const chunk = (type, data) => {
|
||||||
|
const c = new Uint8Array(12 + data.length), dv = new DataView(c.buffer);
|
||||||
|
dv.setUint32(0, data.length);
|
||||||
|
for (let i = 0; i < 4; i++) c[4 + i] = type.charCodeAt(i);
|
||||||
|
c.set(data, 8);
|
||||||
|
dv.setUint32(8 + data.length, crc32(c, 4, 8 + data.length));
|
||||||
|
chunks.push(c);
|
||||||
|
};
|
||||||
|
const ihdr = new Uint8Array(13), dv = new DataView(ihdr.buffer);
|
||||||
|
dv.setUint32(0, w); dv.setUint32(4, h);
|
||||||
|
ihdr.set([8, COLOR_TYPE[ch], 0, 0, 0], 8);
|
||||||
|
chunk('IHDR', ihdr); chunk('IDAT', idat); chunk('IEND', new Uint8Array(0));
|
||||||
|
const total = 8 + chunks.reduce((n, c) => n + c.length, 0), out = new Uint8Array(total);
|
||||||
|
out.set([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
let p = 8;
|
||||||
|
for (const c of chunks) { out.set(c, p); p += c.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns { width, height, channels, pixels } or throws for PNGs it does
|
||||||
|
* not handle (16-bit, interlaced); callers can fall back to a canvas. */
|
||||||
|
async function decode(bytes) {
|
||||||
|
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
|
if (bytes.length < 8 || sig.some((v, i) => bytes[i] !== v)) throw new Error('not a PNG');
|
||||||
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
let w = 0, h = 0, depth = 0, type = 0, interlace = 0, palette = null, trns = null;
|
||||||
|
const idat = [];
|
||||||
|
for (let p = 8; p + 8 <= bytes.length;) {
|
||||||
|
const len = dv.getUint32(p), name = String.fromCharCode(...bytes.subarray(p + 4, p + 8));
|
||||||
|
const data = bytes.subarray(p + 8, p + 8 + len);
|
||||||
|
if (name === 'IHDR') { w = dv.getUint32(p + 8); h = dv.getUint32(p + 12); depth = data[8]; type = data[9]; interlace = data[12]; }
|
||||||
|
else if (name === 'PLTE') palette = data;
|
||||||
|
else if (name === 'tRNS') trns = data;
|
||||||
|
else if (name === 'IDAT') idat.push(data);
|
||||||
|
else if (name === 'IEND') break;
|
||||||
|
p += 12 + len;
|
||||||
|
}
|
||||||
|
if (depth !== 8 || interlace !== 0 || !(type in CHANNELS || type === 3))
|
||||||
|
throw new Error('unsupported PNG (needs 8-bit, non-interlaced)');
|
||||||
|
const inCh = type === 3 ? 1 : CHANNELS[type], stride = w * inCh;
|
||||||
|
const raw = await zlib(new Uint8Array(await new Blob(idat).arrayBuffer()), 'inflate');
|
||||||
|
const px = new Uint8Array(stride * h);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const f = raw[y * (stride + 1)], src = y * (stride + 1) + 1, o = y * stride;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= inCh ? px[o + i - inCh] : 0, b = y ? px[o - stride + i] : 0;
|
||||||
|
const c = y && i >= inCh ? px[o - stride + i - inCh] : 0;
|
||||||
|
px[o + i] = (raw[src + i] + [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type !== 3) return { width: w, height: h, channels: inCh, pixels: px };
|
||||||
|
const ch = trns ? 4 : 3, out = new Uint8Array(w * h * ch); // expand a palette
|
||||||
|
for (let i = 0; i < w * h; i++) {
|
||||||
|
const k = px[i];
|
||||||
|
out.set(palette.subarray(k * 3, k * 3 + 3), i * ch);
|
||||||
|
if (trns) out[i * ch + 3] = k < trns.length ? trns[k] : 255;
|
||||||
|
}
|
||||||
|
return { width: w, height: h, channels: ch, pixels: out };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { encode, decode };
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const post = (m) => parent.postMessage({ pluginMessage: m }, '*');
|
||||||
|
const status = (t) => { $('status').textContent = t; };
|
||||||
|
|
||||||
|
function tab(name) {
|
||||||
|
$('import').hidden = name !== 'import';
|
||||||
|
$('export').hidden = name !== 'export';
|
||||||
|
$('tab-import').classList.toggle('on', name === 'import');
|
||||||
|
$('tab-export').classList.toggle('on', name === 'export');
|
||||||
|
status('');
|
||||||
|
}
|
||||||
|
$('tab-import').onclick = () => tab('import');
|
||||||
|
$('tab-export').onclick = () => tab('export');
|
||||||
|
|
||||||
|
/* Import: .yapf → pixels → exact PNG → Figma image */
|
||||||
|
async function importFiles(files) {
|
||||||
|
const images = [];
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer()));
|
||||||
|
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
||||||
|
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
||||||
|
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')');
|
||||||
|
} catch (e) {
|
||||||
|
status(f.name + ': ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (images.length) post({ type: 'import', images });
|
||||||
|
}
|
||||||
|
$('drop').onclick = () => $('file').click();
|
||||||
|
$('file').onchange = (e) => importFiles([...e.target.files]);
|
||||||
|
$('drop').ondragover = (e) => { e.preventDefault(); $('drop').classList.add('hover'); };
|
||||||
|
$('drop').ondragleave = () => $('drop').classList.remove('hover');
|
||||||
|
$('drop').ondrop = (e) => { e.preventDefault(); $('drop').classList.remove('hover'); importFiles([...e.dataTransfer.files]); };
|
||||||
|
|
||||||
|
/* Export: Figma PNG → exact pixels → .yapf download */
|
||||||
|
$('go').onclick = () => { status('Exporting…'); post({ type: 'export', scale: +$('scale').value }); };
|
||||||
|
|
||||||
|
async function exported(name, pngBytes) {
|
||||||
|
let img = await PNG.decode(pngBytes);
|
||||||
|
let { pixels, channels } = img;
|
||||||
|
if (channels === 4) { // drop alpha when fully opaque
|
||||||
|
let opaque = true;
|
||||||
|
for (let i = 3; i < pixels.length && opaque; i += 4) opaque = pixels[i] === 255;
|
||||||
|
if (opaque) {
|
||||||
|
const rgb = new Uint8Array(img.width * img.height * 3);
|
||||||
|
for (let i = 0, j = 0; i < pixels.length; i += 4, j += 3) rgb.set(pixels.subarray(i, i + 3), j);
|
||||||
|
pixels = rgb; channels = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const file = YAPF.encode({ width: img.width, height: img.height, channels, pixels,
|
||||||
|
flags: YAPF.FLAG.SRGB,
|
||||||
|
gpuFormat: channels === 4 ? YAPF.GPU.SRGB8_A8 : YAPF.GPU.SRGB8 });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([file], { type: 'image/x-yapf' }));
|
||||||
|
a.download = name.replace(/[\\/:*?"<>|]/g, '_') + '.yapf';
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
status('Saved ' + a.download + '\n' + img.width + '×' + img.height + ', ' +
|
||||||
|
(file.length / 1024).toFixed(1) + ' KB (' +
|
||||||
|
(100 * file.length / (img.width * img.height * channels)).toFixed(1) + '% of raw)');
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
const m = e.data.pluginMessage;
|
||||||
|
if (!m) return;
|
||||||
|
if (m.type === 'mode') tab(m.mode === 'export' ? 'export' : 'import');
|
||||||
|
if (m.type === 'selection')
|
||||||
|
$('sel').textContent = m.names.length ? 'Selected: ' + m.names.join(', ') : 'Nothing selected';
|
||||||
|
if (m.type === 'exported') exported(m.name, m.png).catch((err) => status(m.name + ': ' + err.message));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
extensions/gimp/README.md
Normal file
48
extensions/gimp/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# YAPF for GIMP
|
||||||
|
|
||||||
|
Open and export `.yapf` images in **GIMP 3.0 or later**.
|
||||||
|
|
||||||
|
- **File → Open** reads `.yapf` (Gray, Gray + Alpha, RGB, RGBA).
|
||||||
|
- **File → Export As… → `name.yapf`** writes the visible image (layers are
|
||||||
|
flattened into a copy; your document is untouched), with an optional
|
||||||
|
**Store mipmaps** checkbox for game engines.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
You need GIMP 3 with its development files (`gimptool`).
|
||||||
|
|
||||||
|
| System | Development files |
|
||||||
|
| :-------------- | :-------------------------------------------------- |
|
||||||
|
| Arch / Manjaro | `sudo pacman -S gimp` (includes them) |
|
||||||
|
| Debian / Ubuntu | `sudo apt install libgimp-3.0-dev` (GIMP 3 packages) |
|
||||||
|
| Fedora | `sudo dnf install gimp-devel` |
|
||||||
|
| Windows | MSYS2: `pacman -S mingw-w64-ucrt-x86_64-gimp` |
|
||||||
|
| macOS | build GIMP 3 from source or use MacPorts/Homebrew |
|
||||||
|
|
||||||
|
Then, from this folder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gimptool --install yapf-gimp.c
|
||||||
|
```
|
||||||
|
|
||||||
|
That compiles the plugin (it pulls in `../../yapf.c` itself) and installs it
|
||||||
|
into your personal GIMP plug-ins folder. Restart GIMP.
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gcc -O2 $(pkg-config --cflags gimpui-3.0) yapf-gimp.c $(pkg-config --libs gimpui-3.0) -o yapf-gimp
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `yapf-gimp` to `<GIMP config>/plug-ins/yapf-gimp/yapf-gimp`, where the
|
||||||
|
GIMP config folder is `~/.config/GIMP/3.0` on Linux,
|
||||||
|
`%APPDATA%\GIMP\3.0` on Windows and
|
||||||
|
`~/Library/Application Support/GIMP/3.0` on macOS.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
|
||||||
|
gamma encoded. Files marked linear load as linear.
|
||||||
|
- When a file has mipmaps, GIMP opens the full-size image.
|
||||||
|
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
|
||||||
|
pixel for pixel, and export equals GIMP's own flattened PNG of the image.
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
YAPF GIMP Plugin — Build Instructions
|
|
||||||
======================================
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
------------
|
|
||||||
- GIMP 3.0 or later (with development headers)
|
|
||||||
- gimptool (included with GIMP 3 dev packages)
|
|
||||||
- yapf.c and yapf.h must be in the SAME directory as yapf-gimp.c
|
|
||||||
(the plugin embeds the codec via a direct #include)
|
|
||||||
|
|
||||||
On Arch/Manjaro: sudo pacman -S gimp
|
|
||||||
On Debian/Ubuntu: sudo apt install gimp libgmp3-dev
|
|
||||||
On Fedora: sudo dnf install gimp gimp-devel
|
|
||||||
|
|
||||||
Installation (automatic)
|
|
||||||
-------------------------
|
|
||||||
Open a terminal in this directory and run:
|
|
||||||
|
|
||||||
$ gimptool --install yapf-gimp.c
|
|
||||||
|
|
||||||
This compiles the plugin and installs it into your GIMP plug-ins folder
|
|
||||||
automatically. Restart GIMP and .yapf files will appear in the "Open" and
|
|
||||||
"Export As" dialogs under "YAPF image (*.yapf)".
|
|
||||||
|
|
||||||
Manual compilation
|
|
||||||
------------------
|
|
||||||
$ gcc $(gimptool --cflags) -O2 yapf-gimp.c \
|
|
||||||
$(gimptool --libs) -o yapf-gimp
|
|
||||||
$ install -Dm755 yapf-gimp \
|
|
||||||
~/.config/GIMP/3.(your version here)/plug-ins/yapf-gimp/yapf-gimp
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
- The plugin registers two procedures: file-yapf-load and file-yapf-save.
|
|
||||||
- Supported colour modes: Gray, Gray+Alpha, RGB, RGBA.
|
|
||||||
- The MIME type is image/x-yapf and the file extension is .yapf.
|
|
||||||
- RGB and RGBA images are saved with the sRGB GPU hint (YAPF_FLAG_SRGB)
|
|
||||||
because GIMP's native 8-bit buffers are always gamma-corrected sRGB.
|
|
||||||
- Mip levels are NOT generated on export — GIMP does not expose them.
|
|
||||||
Use the YAPF C API directly if you need a mip chain.
|
|
||||||
|
|
@ -21,22 +21,24 @@
|
||||||
*
|
*
|
||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
*
|
*
|
||||||
* Compilation:
|
* Compilation (from this folder):
|
||||||
* gimptool --install yapf-gimp.c
|
* gimptool --install yapf-gimp.c
|
||||||
*
|
*
|
||||||
* yapf.c is embedded via #include below because gimptool compiles a single
|
* yapf.c (two folders up) is embedded via #include below because gimptool
|
||||||
* source file. Do NOT link separately — the include IS the build.
|
* compiles a single source file. Do NOT link it separately.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <libgimp/gimp.h>
|
#include <libgimp/gimp.h>
|
||||||
|
#include <libgimp/gimpui.h>
|
||||||
|
|
||||||
/* Pull in the entire encoder/decoder as a single translation unit. */
|
/* Pull in the entire encoder/decoder as a single translation unit. */
|
||||||
#include "yapf.c"
|
#include "../../yapf.c"
|
||||||
|
|
||||||
/* ── GIMP procedure names ────────────────────────────────────────────── */
|
/* ── GIMP procedure names ────────────────────────────────────────────── */
|
||||||
|
|
||||||
#define LOAD_PROC "file-yapf-load"
|
#define LOAD_PROC "file-yapf-load"
|
||||||
#define SAVE_PROC "file-yapf-save"
|
#define SAVE_PROC "file-yapf-save"
|
||||||
|
#define PLUG_IN_BINARY "yapf-gimp"
|
||||||
|
|
||||||
/* ── Plugin type boilerplate ─────────────────────────────────────────── */
|
/* ── Plugin type boilerplate ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -123,6 +125,14 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
|
||||||
"image/x-yapf");
|
"image/x-yapf");
|
||||||
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
|
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
|
||||||
"yapf");
|
"yapf");
|
||||||
|
/* GIMP flattens / converts a copy of the image for us as needed. */
|
||||||
|
gimp_export_procedure_set_capabilities(GIMP_EXPORT_PROCEDURE(procedure),
|
||||||
|
GIMP_EXPORT_CAN_HANDLE_RGB | GIMP_EXPORT_CAN_HANDLE_GRAY |
|
||||||
|
GIMP_EXPORT_CAN_HANDLE_ALPHA, NULL, NULL, NULL);
|
||||||
|
gimp_procedure_add_boolean_argument(procedure, "mipmaps",
|
||||||
|
"Store _mipmaps",
|
||||||
|
"Also store a full mip chain (for game engines and GPUs)",
|
||||||
|
FALSE, G_PARAM_READWRITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return procedure;
|
return procedure;
|
||||||
|
|
@ -142,7 +152,7 @@ static GimpValueArray *yapf_load_run(
|
||||||
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
|
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
|
||||||
|
|
||||||
gchar *path = g_file_get_path(file);
|
gchar *path = g_file_get_path(file);
|
||||||
yapf_image_t *img = yapf_load(path);
|
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */
|
||||||
g_free(path);
|
g_free(path);
|
||||||
|
|
||||||
if (!img) {
|
if (!img) {
|
||||||
|
|
@ -164,12 +174,12 @@ static GimpValueArray *yapf_load_run(
|
||||||
case YAPF_CHANNELS_GRAY:
|
case YAPF_CHANNELS_GRAY:
|
||||||
base_type = GIMP_GRAY;
|
base_type = GIMP_GRAY;
|
||||||
layer_type = GIMP_GRAY_IMAGE;
|
layer_type = GIMP_GRAY_IMAGE;
|
||||||
babl_fmt = "Y' u8";
|
babl_fmt = is_srgb ? "Y' u8" : "Y u8";
|
||||||
break;
|
break;
|
||||||
case YAPF_CHANNELS_GRAY_ALPHA:
|
case YAPF_CHANNELS_GRAY_ALPHA:
|
||||||
base_type = GIMP_GRAY;
|
base_type = GIMP_GRAY;
|
||||||
layer_type = GIMP_GRAYA_IMAGE;
|
layer_type = GIMP_GRAYA_IMAGE;
|
||||||
babl_fmt = "Y'A u8";
|
babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
|
||||||
break;
|
break;
|
||||||
case YAPF_CHANNELS_RGB:
|
case YAPF_CHANNELS_RGB:
|
||||||
base_type = GIMP_RGB;
|
base_type = GIMP_RGB;
|
||||||
|
|
@ -204,6 +214,44 @@ static GimpValueArray *yapf_load_run(
|
||||||
|
|
||||||
/* ── Save handler ────────────────────────────────────────────────────── */
|
/* ── Save handler ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes).
|
||||||
|
* Fills img->mips / img->mip_levels; mips[0] aliases img->pixels. */
|
||||||
|
static gboolean yapf_build_mips(yapf_image_t *img) {
|
||||||
|
int levels = 1;
|
||||||
|
while (levels < (int)YAPF_MAX_MIPS &&
|
||||||
|
((img->width >> levels) > 0 || (img->height >> levels) > 0))
|
||||||
|
levels++;
|
||||||
|
img->mips = (uint8_t **)calloc((size_t)levels, sizeof(uint8_t *));
|
||||||
|
if (!img->mips) return FALSE;
|
||||||
|
img->mips[0] = img->pixels;
|
||||||
|
img->mip_levels = (uint8_t)levels;
|
||||||
|
|
||||||
|
int ch = img->channels;
|
||||||
|
for (int m = 1; m < levels; m++) {
|
||||||
|
uint32_t pw = MAX(1u, img->width >> (m - 1)), ph = MAX(1u, img->height >> (m - 1));
|
||||||
|
uint32_t w = MAX(1u, img->width >> m), h = MAX(1u, img->height >> m);
|
||||||
|
const uint8_t *src = img->mips[m - 1];
|
||||||
|
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
|
||||||
|
if (!dst) return FALSE;
|
||||||
|
for (uint32_t y = 0; y < h; y++)
|
||||||
|
for (uint32_t x = 0; x < w; x++)
|
||||||
|
for (int c = 0; c < ch; c++) {
|
||||||
|
uint32_t x0 = MIN(2 * x, pw - 1), x1 = MIN(2 * x + 1, pw - 1);
|
||||||
|
uint32_t y0 = MIN(2 * y, ph - 1), y1 = MIN(2 * y + 1, ph - 1);
|
||||||
|
unsigned sum = src[((size_t)y0 * pw + x0) * ch + c] + src[((size_t)y0 * pw + x1) * ch + c]
|
||||||
|
+ src[((size_t)y1 * pw + x0) * ch + c] + src[((size_t)y1 * pw + x1) * ch + c];
|
||||||
|
dst[((size_t)y * w + x) * ch + c] = (uint8_t)((sum + 2) / 4);
|
||||||
|
}
|
||||||
|
img->mips[m] = dst;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg) {
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR,
|
||||||
|
g_error_new_literal(GIMP_PLUG_IN_ERROR, 0, msg));
|
||||||
|
}
|
||||||
|
|
||||||
static GimpValueArray *yapf_save_run(
|
static GimpValueArray *yapf_save_run(
|
||||||
GimpProcedure *procedure,
|
GimpProcedure *procedure,
|
||||||
GimpRunMode run_mode,
|
GimpRunMode run_mode,
|
||||||
|
|
@ -214,93 +262,88 @@ static GimpValueArray *yapf_save_run(
|
||||||
GimpProcedureConfig *config,
|
GimpProcedureConfig *config,
|
||||||
gpointer run_data)
|
gpointer run_data)
|
||||||
{
|
{
|
||||||
(void)run_mode; (void)options; (void)metadata; (void)config; (void)run_data;
|
(void)metadata; (void)run_data;
|
||||||
|
|
||||||
/* Retrieve the top-most layer. */
|
if (run_mode == GIMP_RUN_INTERACTIVE) {
|
||||||
GimpLayer **layers = gimp_image_get_layers(image);
|
gimp_ui_init(PLUG_IN_BINARY);
|
||||||
gint n_layers = 0;
|
GtkWidget *dialog = gimp_export_procedure_dialog_new(
|
||||||
if (layers) { while (layers[n_layers]) n_layers++; }
|
GIMP_EXPORT_PROCEDURE(procedure), config, image);
|
||||||
|
gimp_procedure_dialog_fill(GIMP_PROCEDURE_DIALOG(dialog), NULL);
|
||||||
if (!layers || n_layers == 0) {
|
gboolean ok = gimp_procedure_dialog_run(GIMP_PROCEDURE_DIALOG(dialog));
|
||||||
g_free(layers);
|
gtk_widget_destroy(dialog);
|
||||||
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
|
if (!ok)
|
||||||
"No layers to export.");
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL);
|
||||||
return gimp_procedure_new_return_values(procedure,
|
|
||||||
GIMP_PDB_EXECUTION_ERROR, err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GimpDrawable *drawable = GIMP_DRAWABLE(layers[0]);
|
gboolean mipmaps = FALSE;
|
||||||
g_free(layers);
|
g_object_get(config, "mipmaps", &mipmaps, NULL);
|
||||||
|
|
||||||
/* Determine channel layout and GPU format hint. */
|
/* A flattened copy when the image has several layers; the visible
|
||||||
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
|
* result is what gets exported. */
|
||||||
gboolean is_gray = gimp_drawable_is_gray(drawable);
|
GimpExportReturn export = gimp_export_options_get_image(options, &image);
|
||||||
|
GList *layers = gimp_image_list_layers(image);
|
||||||
|
if (!layers) {
|
||||||
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
|
return yapf_error(procedure, "No layers to export.");
|
||||||
|
}
|
||||||
|
GimpDrawable *drawable = GIMP_DRAWABLE(layers->data);
|
||||||
|
g_list_free(layers);
|
||||||
|
|
||||||
|
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
|
||||||
|
gboolean is_gray = gimp_drawable_is_gray(drawable);
|
||||||
const gchar *babl_fmt;
|
const gchar *babl_fmt;
|
||||||
uint8_t ch;
|
uint8_t ch, gpu_fmt;
|
||||||
uint8_t gpu_fmt;
|
|
||||||
uint8_t flags = YAPF_FLAG_SRGB; /* GIMP's native u8 formats are sRGB */
|
|
||||||
|
|
||||||
|
/* GIMP's 8-bit buffers are gamma encoded, so pixels are written as sRGB. */
|
||||||
if (is_gray) {
|
if (is_gray) {
|
||||||
if (has_alpha) {
|
ch = has_alpha ? YAPF_CHANNELS_GRAY_ALPHA : YAPF_CHANNELS_GRAY;
|
||||||
ch = YAPF_CHANNELS_GRAY_ALPHA; babl_fmt = "Y'A u8";
|
babl_fmt = has_alpha ? "Y'A u8" : "Y' u8";
|
||||||
gpu_fmt = YAPF_GPU_RG8;
|
gpu_fmt = has_alpha ? YAPF_GPU_RG8 : YAPF_GPU_R8;
|
||||||
flags = 0; /* grayscale has no sRGB GPU hint */
|
|
||||||
} else {
|
|
||||||
ch = YAPF_CHANNELS_GRAY; babl_fmt = "Y' u8";
|
|
||||||
gpu_fmt = YAPF_GPU_R8;
|
|
||||||
flags = 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (has_alpha) {
|
ch = has_alpha ? YAPF_CHANNELS_RGBA : YAPF_CHANNELS_RGB;
|
||||||
ch = YAPF_CHANNELS_RGBA; babl_fmt = "R'G'B'A u8";
|
babl_fmt = has_alpha ? "R'G'B'A u8" : "R'G'B' u8";
|
||||||
gpu_fmt = YAPF_GPU_SRGB8_A8;
|
gpu_fmt = has_alpha ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_SRGB8;
|
||||||
} else {
|
|
||||||
ch = YAPF_CHANNELS_RGB; babl_fmt = "R'G'B' u8";
|
|
||||||
gpu_fmt = YAPF_GPU_SRGB8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gint w = gimp_drawable_get_width(drawable);
|
gint w = gimp_drawable_get_width(drawable);
|
||||||
gint h = gimp_drawable_get_height(drawable);
|
gint h = gimp_drawable_get_height(drawable);
|
||||||
|
|
||||||
/* Read pixel data from GIMP's buffer. */
|
|
||||||
uint8_t *pixels = (uint8_t *)malloc((size_t)w * (size_t)h * ch);
|
uint8_t *pixels = (uint8_t *)malloc((size_t)w * (size_t)h * ch);
|
||||||
if (!pixels) {
|
if (!pixels) {
|
||||||
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Out of memory.");
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
return gimp_procedure_new_return_values(procedure,
|
return yapf_error(procedure, "Out of memory.");
|
||||||
GIMP_PDB_EXECUTION_ERROR, err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GeglBuffer *buffer = gimp_drawable_get_buffer(drawable);
|
GeglBuffer *buffer = gimp_drawable_get_buffer(drawable);
|
||||||
gegl_buffer_get(buffer,
|
gegl_buffer_get(buffer, GEGL_RECTANGLE(0, 0, w, h), 1.0,
|
||||||
GEGL_RECTANGLE(0, 0, w, h),
|
babl_format(babl_fmt), pixels,
|
||||||
1.0, babl_format(babl_fmt),
|
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
||||||
pixels, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
|
||||||
g_object_unref(buffer);
|
g_object_unref(buffer);
|
||||||
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
|
|
||||||
/* Build yapf_image_t — base level only, no mip chain from GIMP. */
|
|
||||||
yapf_image_t img;
|
yapf_image_t img;
|
||||||
|
memset(&img, 0, sizeof(img));
|
||||||
img.width = (uint32_t)w;
|
img.width = (uint32_t)w;
|
||||||
img.height = (uint32_t)h;
|
img.height = (uint32_t)h;
|
||||||
img.channels = ch;
|
img.channels = ch;
|
||||||
img.gpu_format = gpu_fmt;
|
img.gpu_format = gpu_fmt;
|
||||||
img.flags = flags;
|
img.flags = YAPF_FLAG_SRGB;
|
||||||
img.mip_levels = 1;
|
img.mip_levels = 1;
|
||||||
img.pixels = pixels;
|
img.pixels = pixels;
|
||||||
img.mips = NULL;
|
|
||||||
|
|
||||||
gchar *path = g_file_get_path(file);
|
int result = YAPF_ERR_OOM;
|
||||||
int result = yapf_save(path, &img);
|
if (!mipmaps || yapf_build_mips(&img)) {
|
||||||
g_free(path);
|
gchar *path = g_file_get_path(file);
|
||||||
|
result = yapf_save(path, &img);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
if (img.mips) {
|
||||||
|
for (int m = 1; m < img.mip_levels; m++) free(img.mips[m]);
|
||||||
|
free(img.mips);
|
||||||
|
}
|
||||||
free(pixels);
|
free(pixels);
|
||||||
|
|
||||||
if (result != YAPF_OK) {
|
if (result != YAPF_OK)
|
||||||
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
|
return yapf_error(procedure, "Failed to export YAPF file.");
|
||||||
"Failed to export YAPF file.");
|
|
||||||
return gimp_procedure_new_return_values(procedure,
|
|
||||||
GIMP_PDB_EXECUTION_ERROR, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL);
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
extensions/linux/README.md
Normal file
15
extensions/linux/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Linux desktop integration
|
||||||
|
|
||||||
|
Makes file managers recognise `.yapf` files (MIME type `image/x-yapf`) and
|
||||||
|
show **thumbnails** for them.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh extensions/linux/install.sh # for your user
|
||||||
|
sudo sh extensions/linux/install.sh --system
|
||||||
|
```
|
||||||
|
|
||||||
|
Thumbnails are made by `yapf thumbnail` (from the `yapf` tool, which must be
|
||||||
|
on your `PATH` — `sudo make install` puts it in `/usr/local/bin`). They work
|
||||||
|
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
|
||||||
|
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
|
||||||
|
system and is not covered yet.
|
||||||
18
extensions/linux/install.sh
Executable file
18
extensions/linux/install.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Registers the YAPF MIME type and thumbnailer for the current user, so file
|
||||||
|
# managers that use freedesktop thumbnailers (GNOME Files, Nemo, Caja,
|
||||||
|
# Thunar with tumbler) show .yapf previews. Needs `yapf` on your PATH.
|
||||||
|
# sh extensions/linux/install.sh (current user)
|
||||||
|
# sudo sh extensions/linux/install.sh --system
|
||||||
|
set -e
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
if [ "$1" = "--system" ]; then data=/usr/share; else data=${XDG_DATA_HOME:-$HOME/.local/share}; fi
|
||||||
|
|
||||||
|
command -v yapf >/dev/null 2>&1 || echo "warning: 'yapf' is not on your PATH; thumbnails need it" >&2
|
||||||
|
|
||||||
|
mkdir -p "$data/mime/packages" "$data/thumbnailers"
|
||||||
|
cp "$here/yapf-mime.xml" "$data/mime/packages/yapf.xml"
|
||||||
|
cp "$here/yapf.thumbnailer" "$data/thumbnailers/yapf.thumbnailer"
|
||||||
|
update-mime-database "$data/mime" >/dev/null 2>&1 || true
|
||||||
|
echo "Installed the image/x-yapf MIME type and thumbnailer into $data"
|
||||||
|
echo "Restart your file manager (e.g. 'nautilus -q') and clear ~/.cache/thumbnails/fail if needed."
|
||||||
12
extensions/linux/yapf-mime.xml
Normal file
12
extensions/linux/yapf-mime.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Registers .yapf files as image/x-yapf (shared-mime-info). -->
|
||||||
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
|
<mime-type type="image/x-yapf">
|
||||||
|
<comment>YAPF image</comment>
|
||||||
|
<generic-icon name="image-x-generic"/>
|
||||||
|
<magic priority="60">
|
||||||
|
<match type="string" value="YAPF" offset="0"/>
|
||||||
|
</magic>
|
||||||
|
<glob pattern="*.yapf"/>
|
||||||
|
</mime-type>
|
||||||
|
</mime-info>
|
||||||
4
extensions/linux/yapf.thumbnailer
Normal file
4
extensions/linux/yapf.thumbnailer
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=yapf
|
||||||
|
Exec=yapf thumbnail %i %o %s
|
||||||
|
MimeType=image/x-yapf;
|
||||||
28
extensions/photoshop/Open YAPF.jsx
Normal file
28
extensions/photoshop/Open YAPF.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Open YAPF — opens .yapf images in Photoshop.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// Install: copy this folder's .jsx / .jsxinc files (and yapf / yapf.exe)
|
||||||
|
// into Photoshop's Presets/Scripts folder; then File > Scripts > Open YAPF.
|
||||||
|
|
||||||
|
#target photoshop
|
||||||
|
#include "yapf_common.jsxinc"
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var filter = YAPF_IS_WINDOWS ? "YAPF images:*.yapf,All files:*.*"
|
||||||
|
: function (f) { return f instanceof Folder || /\.yapf$/i.test(f.name); };
|
||||||
|
var files = File.openDialog("Open YAPF image", filter, true);
|
||||||
|
if (!files) return;
|
||||||
|
if (!(files instanceof Array)) files = [files];
|
||||||
|
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
var tmp = yapfTempFile(".png");
|
||||||
|
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
|
||||||
|
alert("Could not open " + files[i].name + ".\n\n" + YAPF_TOOL_HELP);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var doc = app.open(tmp);
|
||||||
|
// The temporary PNG is no longer needed once the document is open;
|
||||||
|
// use File > Scripts > Save as YAPF to write a .yapf again.
|
||||||
|
doc.info.caption = files[i].fsName;
|
||||||
|
tmp.remove();
|
||||||
|
}
|
||||||
|
})();
|
||||||
33
extensions/photoshop/README.md
Normal file
33
extensions/photoshop/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# YAPF for Photoshop
|
||||||
|
|
||||||
|
Two scripts that appear under **File → Scripts**:
|
||||||
|
|
||||||
|
- **Open YAPF** — opens one or more `.yapf` files as documents.
|
||||||
|
- **Save as YAPF** — saves the visible image of the active document (a
|
||||||
|
flattened 8-bit copy; your document is not changed), keeping
|
||||||
|
transparency, with an optional mip chain.
|
||||||
|
|
||||||
|
They convert through the `yapf` command-line tool, so Photoshop needs no
|
||||||
|
native plugin.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Download `yapf-<your platform>.zip` from the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases) (or build the tool:
|
||||||
|
`make` → `build/yapf`).
|
||||||
|
2. Copy `Open YAPF.jsx`, `Save as YAPF.jsx`, `yapf_common.jsxinc` **and**
|
||||||
|
`yapf` / `yapf.exe` into Photoshop's scripts folder:
|
||||||
|
- Windows: `C:\Program Files\Adobe\Adobe Photoshop <version>\Presets\Scripts`
|
||||||
|
- macOS: `/Applications/Adobe Photoshop <version>/Presets/Scripts`
|
||||||
|
3. Restart Photoshop. The scripts are under **File → Scripts**; you can give
|
||||||
|
them keyboard shortcuts in **Edit → Keyboard Shortcuts** or record them
|
||||||
|
in Actions.
|
||||||
|
|
||||||
|
Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The scripts are written for Photoshop CC's ExtendScript and are not yet
|
||||||
|
tested inside Photoshop (the converter itself is fully tested). A native
|
||||||
|
format plugin (`.8bi` / `.plugin`, so `.yapf` shows up directly in the Open
|
||||||
|
and Save dialogs) needs Adobe's Photoshop SDK and is planned.
|
||||||
40
extensions/photoshop/Save as YAPF.jsx
Normal file
40
extensions/photoshop/Save as YAPF.jsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Save as YAPF — writes the active document as a .yapf image.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// The visible result is saved: a flattened 8-bit RGB (or grayscale) copy,
|
||||||
|
// transparency kept. Your document is not modified.
|
||||||
|
|
||||||
|
#target photoshop
|
||||||
|
#include "yapf_common.jsxinc"
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
if (!app.documents.length) { alert("Open a document first."); return; }
|
||||||
|
var doc = app.activeDocument;
|
||||||
|
|
||||||
|
var base = doc.name.replace(/\.[^.]+$/, "");
|
||||||
|
var start = new File((doc.saved && doc.path ? doc.path.fsName : Folder.desktop.fsName) + "/" + base + ".yapf");
|
||||||
|
var out = start.saveDlg("Save as YAPF", YAPF_IS_WINDOWS ? "YAPF images:*.yapf" : undefined);
|
||||||
|
if (!out) return;
|
||||||
|
if (!/\.yapf$/i.test(out.name)) out = new File(out.fsName + ".yapf");
|
||||||
|
var mips = confirm("Also store mipmaps (for game engines / GPUs)?", true, "YAPF");
|
||||||
|
|
||||||
|
var tmp = yapfTempFile(".png");
|
||||||
|
var copy = doc.duplicate(base + " (YAPF export)", true); // merged copy
|
||||||
|
try {
|
||||||
|
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
|
||||||
|
copy.changeMode(ChangeMode.RGB);
|
||||||
|
if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT)
|
||||||
|
copy.bitsPerChannel = BitsPerChannelType.EIGHT;
|
||||||
|
var opts = new PNGSaveOptions();
|
||||||
|
opts.interlaced = false;
|
||||||
|
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
|
||||||
|
} finally {
|
||||||
|
copy.close(SaveOptions.DONOTSAVECHANGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = [tmp.fsName, out.fsName];
|
||||||
|
if (mips) args.push("--mips");
|
||||||
|
var ok = yapfRun(args, out);
|
||||||
|
tmp.remove();
|
||||||
|
if (!ok) { alert("Could not write " + out.name + ".\n\n" + YAPF_TOOL_HELP); return; }
|
||||||
|
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
|
||||||
|
})();
|
||||||
41
extensions/photoshop/yapf_common.jsxinc
Normal file
41
extensions/photoshop/yapf_common.jsxinc
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Shared helpers for the YAPF Photoshop scripts.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
//
|
||||||
|
// The scripts convert through the `yapf` command-line tool. It is looked
|
||||||
|
// up next to these scripts first (yapf.exe / yapf), then on the PATH.
|
||||||
|
|
||||||
|
var YAPF_IS_WINDOWS = $.os.indexOf("Windows") !== -1;
|
||||||
|
|
||||||
|
function yapfQuote(s) {
|
||||||
|
return '"' + String(s).replace(/"/g, '\\"') + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfTool() {
|
||||||
|
var here = new File($.fileName).parent;
|
||||||
|
var names = YAPF_IS_WINDOWS ? ["yapf.exe"] : ["yapf"];
|
||||||
|
for (var i = 0; i < names.length; i++) {
|
||||||
|
var f = new File(here.fsName + "/" + names[i]);
|
||||||
|
if (f.exists) return f.fsName;
|
||||||
|
}
|
||||||
|
return names[0]; // rely on the PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the tool; returns true when `expectOut` was produced.
|
||||||
|
function yapfRun(args, expectOut) {
|
||||||
|
var cmd = yapfQuote(yapfTool());
|
||||||
|
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
|
||||||
|
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
|
||||||
|
if (expectOut.exists) expectOut.remove();
|
||||||
|
app.system(cmd);
|
||||||
|
return expectOut.exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfTempFile(ext) {
|
||||||
|
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
|
||||||
|
Math.floor(Math.random() * 1e6) + ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
var YAPF_TOOL_HELP =
|
||||||
|
"The 'yapf' command-line tool was not found or failed.\n\n" +
|
||||||
|
"Put yapf" + (YAPF_IS_WINDOWS ? ".exe" : "") + " in the same folder as this script, " +
|
||||||
|
"or add it to your PATH.\nDownload or build it: https://github.com/Nexoniarz/YAPF";
|
||||||
34
extensions/vscode/README.md
Normal file
34
extensions/vscode/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# YAPF Image Preview for VS Code
|
||||||
|
|
||||||
|
Click any `.yapf` file in VS Code to see the image.
|
||||||
|
|
||||||
|
- Checkerboard behind transparent pixels.
|
||||||
|
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
|
||||||
|
to zoom (pixels stay sharp when enlarged).
|
||||||
|
- Picker for every **mip level** stored in the file.
|
||||||
|
- Info bar: size, channels, sRGB / linear, GPU format hint, file size and
|
||||||
|
compression ratio, decode time.
|
||||||
|
- Reloads automatically when the file changes on disk.
|
||||||
|
|
||||||
|
Decoding runs in plain JavaScript inside the preview; no native code.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
- From a release: download `yapf-preview.vsix`, then in VS Code run
|
||||||
|
**Extensions: Install from VSIX…** (or `code --install-extension yapf-preview.vsix`).
|
||||||
|
- From source (needs Node.js):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd extensions/vscode
|
||||||
|
npm run package # → yapf-preview-1.0.0.vsix
|
||||||
|
code --install-extension yapf-preview-1.0.0.vsix
|
||||||
|
```
|
||||||
|
|
||||||
|
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.**
|
||||||
65
extensions/vscode/extension.js
Normal file
65
extensions/vscode/extension.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// YAPF image preview for VS Code — a read-only custom editor for *.yapf.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
const vscode = require('vscode');
|
||||||
|
|
||||||
|
class YapfPreviewProvider {
|
||||||
|
constructor(context) { this.context = context; }
|
||||||
|
|
||||||
|
openCustomDocument(uri) { return { uri, dispose() {} }; }
|
||||||
|
|
||||||
|
async resolveCustomEditor(document, panel) {
|
||||||
|
const media = vscode.Uri.joinPath(this.context.extensionUri, 'media');
|
||||||
|
panel.webview.options = { enableScripts: true, localResourceRoots: [media] };
|
||||||
|
panel.webview.html = this.html(panel.webview, media);
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
try {
|
||||||
|
const bytes = await vscode.workspace.fs.readFile(document.uri);
|
||||||
|
panel.webview.postMessage({ type: 'file', name: document.uri.path.split('/').pop(), bytes });
|
||||||
|
} catch (e) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: String(e.message || e) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
panel.webview.onDidReceiveMessage((m) => { if (m.type === 'ready') send(); });
|
||||||
|
|
||||||
|
// Reload when the file changes on disk.
|
||||||
|
const folder = vscode.Uri.joinPath(document.uri, '..');
|
||||||
|
const name = document.uri.path.split('/').pop();
|
||||||
|
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, name));
|
||||||
|
watcher.onDidChange(send);
|
||||||
|
panel.onDidDispose(() => watcher.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
html(webview, media) {
|
||||||
|
const uri = (f) => webview.asWebviewUri(vscode.Uri.joinPath(media, f));
|
||||||
|
const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'none'; img-src ${webview.cspSource} data: blob:; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="${uri('preview.css')}">
|
||||||
|
<title>YAPF Preview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="bar"><span id="info">Loading…</span><span id="controls"></span></div>
|
||||||
|
<div id="stage"><canvas id="canvas"></canvas></div>
|
||||||
|
<div id="error" hidden></div>
|
||||||
|
<script nonce="${nonce}" src="${uri('yapf.js')}"></script>
|
||||||
|
<script nonce="${nonce}" src="${uri('preview.js')}"></script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(context) {
|
||||||
|
context.subscriptions.push(vscode.window.registerCustomEditorProvider(
|
||||||
|
'yapf.preview', new YapfPreviewProvider(context),
|
||||||
|
{ webviewOptions: { retainContextWhenHidden: true }, supportsMultipleEditorsPerDocument: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate() {}
|
||||||
|
|
||||||
|
module.exports = { activate, deactivate };
|
||||||
BIN
extensions/vscode/media/icon.png
Normal file
BIN
extensions/vscode/media/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
26
extensions/vscode/media/preview.css
Normal file
26
extensions/vscode/media/preview.css
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
background: var(--vscode-editor-background, #1e1e1e);
|
||||||
|
color: var(--vscode-foreground, #ccc);
|
||||||
|
font: 12px var(--vscode-font-family, system-ui, sans-serif);
|
||||||
|
}
|
||||||
|
#bar {
|
||||||
|
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||||||
|
padding: 6px 12px; border-bottom: 1px solid var(--vscode-panel-border, #333);
|
||||||
|
}
|
||||||
|
#bar select {
|
||||||
|
background: var(--vscode-dropdown-background, #3c3c3c);
|
||||||
|
color: var(--vscode-dropdown-foreground, #ccc);
|
||||||
|
border: 1px solid var(--vscode-dropdown-border, #555);
|
||||||
|
}
|
||||||
|
#stage { flex: 1; overflow: auto; display: grid; place-items: center; padding: 16px; cursor: zoom-in; }
|
||||||
|
body:not(.fit) #stage { cursor: zoom-out; }
|
||||||
|
canvas {
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: conic-gradient(#ccc 25%, #fff 0 50%, #ccc 0 75%, #fff 0);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
box-shadow: 0 0 0 1px var(--vscode-panel-border, #333);
|
||||||
|
}
|
||||||
|
canvas.pixelated { image-rendering: pixelated; }
|
||||||
|
#error { padding: 16px; color: var(--vscode-errorForeground, #f48771); }
|
||||||
91
extensions/vscode/media/preview.js
Normal file
91
extensions/vscode/media/preview.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Webview side of the YAPF preview: decode, draw, zoom, mip picker, info.
|
||||||
|
(function () {
|
||||||
|
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const info = document.getElementById('info');
|
||||||
|
const controls = document.getElementById('controls');
|
||||||
|
const errorBox = document.getElementById('error');
|
||||||
|
const GPU_NAMES = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8'];
|
||||||
|
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
|
||||||
|
|
||||||
|
let image = null, level = 0, fit = true, zoom = 1;
|
||||||
|
|
||||||
|
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
const w = Math.max(1, image.width >>> level), h = Math.max(1, image.height >>> level);
|
||||||
|
canvas.width = w; canvas.height = h;
|
||||||
|
canvas.getContext('2d').putImageData(new ImageData(YAPF.toRGBA(image, level), w, h), 0, 0);
|
||||||
|
layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function layout() {
|
||||||
|
if (!image) return;
|
||||||
|
const w = canvas.width, h = canvas.height;
|
||||||
|
const stage = document.getElementById('stage');
|
||||||
|
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
|
||||||
|
canvas.style.width = Math.max(1, Math.round(w * scale)) + 'px';
|
||||||
|
canvas.style.height = Math.max(1, Math.round(h * scale)) + 'px';
|
||||||
|
canvas.classList.toggle('pixelated', scale >= 2);
|
||||||
|
document.body.classList.toggle('fit', fit);
|
||||||
|
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(name, bytes) {
|
||||||
|
errorBox.hidden = true;
|
||||||
|
const t = performance.now();
|
||||||
|
try {
|
||||||
|
image = YAPF.decode(bytes);
|
||||||
|
} catch (e) {
|
||||||
|
image = null;
|
||||||
|
errorBox.hidden = false;
|
||||||
|
errorBox.textContent = name + ': ' + e.message;
|
||||||
|
info.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ms = performance.now() - t;
|
||||||
|
const raw = image.width * image.height * image.channels;
|
||||||
|
level = Math.min(level, image.mipLevels - 1);
|
||||||
|
info.textContent = [
|
||||||
|
image.width + ' × ' + image.height,
|
||||||
|
CH_NAMES[image.channels],
|
||||||
|
(image.flags & 2 ? 'sRGB' : 'linear') + (image.flags & 1 ? ', premultiplied' : ''),
|
||||||
|
'GPU ' + (GPU_NAMES[image.gpuFormat] || '?'),
|
||||||
|
kb(bytes.length) + ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)',
|
||||||
|
'decoded in ' + ms.toFixed(1) + ' ms',
|
||||||
|
].join(' · ');
|
||||||
|
|
||||||
|
controls.textContent = '';
|
||||||
|
if (image.mipLevels > 1) {
|
||||||
|
const sel = document.createElement('select');
|
||||||
|
for (let m = 0; m < image.mipLevels; m++) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = m;
|
||||||
|
o.textContent = 'Mip ' + m + ' (' + Math.max(1, image.width >>> m) + '×' + Math.max(1, image.height >>> m) + ')';
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.value = level;
|
||||||
|
sel.onchange = () => { level = +sel.value; draw(); };
|
||||||
|
controls.appendChild(sel);
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
|
||||||
|
document.getElementById('stage').addEventListener('wheel', (e) => {
|
||||||
|
if (!e.ctrlKey || !image) return;
|
||||||
|
e.preventDefault();
|
||||||
|
fit = false;
|
||||||
|
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
|
||||||
|
layout();
|
||||||
|
}, { passive: false });
|
||||||
|
window.addEventListener('resize', layout);
|
||||||
|
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
const m = e.data;
|
||||||
|
if (m.type === 'file') show(m.name, m.bytes instanceof Uint8Array ? m.bytes : new Uint8Array(m.bytes));
|
||||||
|
if (m.type === 'error') { errorBox.hidden = false; errorBox.textContent = m.message; }
|
||||||
|
});
|
||||||
|
if (vscode) vscode.postMessage({ type: 'ready' });
|
||||||
|
window.yapfPreviewShow = show; // used by the browser test page
|
||||||
|
})();
|
||||||
29
extensions/vscode/package.json
Normal file
29
extensions/vscode/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "yapf-preview",
|
||||||
|
"displayName": "YAPF Image Preview",
|
||||||
|
"description": "View YAPF (.yapf) lossless images inside VS Code",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "Nexoniarz",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"icon": "media/icon.png",
|
||||||
|
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git" },
|
||||||
|
"engines": { "vscode": "^1.75.0" },
|
||||||
|
"categories": ["Visualization", "Other"],
|
||||||
|
"keywords": ["yapf", "image", "texture", "preview", "viewer"],
|
||||||
|
"main": "./extension.js",
|
||||||
|
"activationEvents": [],
|
||||||
|
"contributes": {
|
||||||
|
"customEditors": [
|
||||||
|
{
|
||||||
|
"viewType": "yapf.preview",
|
||||||
|
"displayName": "YAPF Image Preview",
|
||||||
|
"selector": [{ "filenamePattern": "*.yapf" }],
|
||||||
|
"priority": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "node scripts/copy-codec.js",
|
||||||
|
"package": "node scripts/copy-codec.js && npx --yes @vscode/vsce package --allow-missing-repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
extensions/vscode/scripts/copy-codec.js
Normal file
6
extensions/vscode/scripts/copy-codec.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copies the shared JavaScript codec into media/ so the extension is self-contained.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = path.join(__dirname, '..', '..', '..', 'bindings', 'js', 'yapf.js');
|
||||||
|
fs.copyFileSync(src, path.join(__dirname, '..', 'media', 'yapf.js'));
|
||||||
|
console.log('copied', path.relative(process.cwd(), src), '-> media/yapf.js');
|
||||||
BIN
other/YAPF.YAPF
BIN
other/YAPF.YAPF
Binary file not shown.
202
tests/test_yapf.c
Normal file
202
tests/test_yapf.c
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
* test_yapf.c — self-contained test suite for yapf.c
|
||||||
|
*
|
||||||
|
* cc -O2 -std=c99 tests/test_yapf.c yapf.c -o test_yapf -pthread
|
||||||
|
* ./test_yapf [path/to/other/YAPF.YAPF]
|
||||||
|
*
|
||||||
|
* Round-trips many sizes, channel counts and image kinds, checks mip
|
||||||
|
* chains, single- vs multi-threaded decode, the bundled sample file, and
|
||||||
|
* that corrupted or truncated files are rejected without crashing.
|
||||||
|
* Exit code 0 means every test passed.
|
||||||
|
*/
|
||||||
|
#include "../yapf.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
|
||||||
|
#define SAMPLE_HASH 0x8891c2b4u
|
||||||
|
|
||||||
|
static int tests, fails;
|
||||||
|
|
||||||
|
static uint32_t rng = 12345;
|
||||||
|
static uint32_t rnd(void) { rng = rng * 1103515245u + 12345u; return rng >> 8; }
|
||||||
|
|
||||||
|
#define CHECK(cond, ...) do { tests++; if (!(cond)) { fails++; \
|
||||||
|
printf("FAIL: "); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||||
|
|
||||||
|
/* Five kinds of content: noise, smooth gradient, hard-edged blocks,
|
||||||
|
* noisy curves, and an image with an opaque alpha channel. */
|
||||||
|
static void fill(uint8_t *p, int w, int h, int ch, int kind) {
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int c = 0; c < ch; c++) {
|
||||||
|
uint8_t *d = p + ((size_t)y * w + x) * ch + c;
|
||||||
|
switch (kind) {
|
||||||
|
case 0: *d = (uint8_t)rnd(); break;
|
||||||
|
case 1: *d = (uint8_t)(x * 3 + y * 5 + c * 40); break;
|
||||||
|
case 2: *d = ((x / 7 + y / 5 + c) & 1) ? 200 : 30; break;
|
||||||
|
case 3: *d = (uint8_t)((x * x + y * y) / 50 + rnd() % 5); break;
|
||||||
|
default: *d = (c == ch - 1) ? 255 : (uint8_t)(x ^ y); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
size_t n = (size_t)w * h * ch, len = 0;
|
||||||
|
uint8_t *p = (uint8_t *)malloc(n);
|
||||||
|
fill(p, w, h, ch, kind);
|
||||||
|
|
||||||
|
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 = p;
|
||||||
|
|
||||||
|
uint8_t *file = encode_mem(&img, &len);
|
||||||
|
CHECK(file != NULL, "encode %dx%d ch%d kind%d", w, h, ch, kind);
|
||||||
|
if (file) {
|
||||||
|
yapf_image_t *a = yapf_load_memory(file, len);
|
||||||
|
yapf_image_t *b = yapf_load_memory_mt(file, len, 0);
|
||||||
|
CHECK(a && memcmp(a->pixels, p, n) == 0,
|
||||||
|
"roundtrip %dx%d ch%d kind%d", w, h, ch, kind);
|
||||||
|
CHECK(b && memcmp(b->pixels, p, n) == 0,
|
||||||
|
"roundtrip (threads) %dx%d ch%d kind%d", w, h, ch, kind);
|
||||||
|
yapf_free(a);
|
||||||
|
yapf_free(b);
|
||||||
|
free(file);
|
||||||
|
}
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mips(void) {
|
||||||
|
const int w = 300, h = 200, levels = 9;
|
||||||
|
uint8_t *mp[9];
|
||||||
|
yapf_image_t img;
|
||||||
|
memset(&img, 0, sizeof(img));
|
||||||
|
img.width = w; img.height = h; img.channels = 4; img.mip_levels = levels;
|
||||||
|
for (int l = 0; l < levels; l++) {
|
||||||
|
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
|
||||||
|
mp[l] = (uint8_t *)malloc((size_t)mw * mh * 4);
|
||||||
|
fill(mp[l], mw, mh, 4, l % 5);
|
||||||
|
}
|
||||||
|
img.pixels = mp[0];
|
||||||
|
img.mips = mp;
|
||||||
|
|
||||||
|
size_t len = 0;
|
||||||
|
uint8_t *file = encode_mem(&img, &len);
|
||||||
|
yapf_image_t *d = file ? yapf_load_memory_mt(file, len, 3) : NULL;
|
||||||
|
int ok = d && d->mip_levels == levels;
|
||||||
|
for (int l = 0; ok && l < levels; l++) {
|
||||||
|
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
|
||||||
|
ok = memcmp(d->mips[l], mp[l], (size_t)mw * mh * 4) == 0;
|
||||||
|
}
|
||||||
|
CHECK(ok, "mip chain of %d levels", levels);
|
||||||
|
yapf_free(d);
|
||||||
|
free(file);
|
||||||
|
for (int l = 0; l < levels; l++) free(mp[l]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flip bits, truncate, scribble: the decoder must never crash, and a
|
||||||
|
* truncated file must be rejected. */
|
||||||
|
static void corruption(void) {
|
||||||
|
const int w = 130, h = 70, ch = 4;
|
||||||
|
uint8_t *p = (uint8_t *)malloc((size_t)w * h * ch);
|
||||||
|
fill(p, w, h, ch, 3);
|
||||||
|
yapf_image_t img;
|
||||||
|
memset(&img, 0, sizeof(img));
|
||||||
|
img.width = w; img.height = h; img.channels = ch; img.mip_levels = 1; img.pixels = p;
|
||||||
|
size_t len = 0;
|
||||||
|
uint8_t *file = encode_mem(&img, &len);
|
||||||
|
uint8_t *bad = (uint8_t *)malloc(len);
|
||||||
|
int truncated_ok = 1;
|
||||||
|
for (int it = 0; it < 3000 && file; it++) {
|
||||||
|
memcpy(bad, file, len);
|
||||||
|
size_t n = len;
|
||||||
|
switch (it % 3) {
|
||||||
|
case 0: for (int k = 0; k < 1 + it % 7; k++) bad[rnd() % len] ^= (uint8_t)(1u << (rnd() % 8)); break;
|
||||||
|
case 1: n = rnd() % len; break;
|
||||||
|
default: bad[rnd() % len] = (uint8_t)rnd(); break;
|
||||||
|
}
|
||||||
|
yapf_image_t *d = (it & 1) ? yapf_load_memory_mt(bad, n, 2) : yapf_load_memory(bad, n);
|
||||||
|
if (it % 3 == 1 && d) truncated_ok = 0;
|
||||||
|
yapf_free(d);
|
||||||
|
}
|
||||||
|
CHECK(file != NULL, "corruption test setup");
|
||||||
|
CHECK(truncated_ok, "truncated files are rejected");
|
||||||
|
free(bad);
|
||||||
|
free(file);
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t fnv(const uint8_t *p, size_t n) {
|
||||||
|
uint32_t h = 2166136261u;
|
||||||
|
for (size_t i = 0; i < n; i++) h = (h ^ p[i]) * 16777619u;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
static const int sizes[][2] = {
|
||||||
|
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
|
||||||
|
{1, 129}, {97, 61}, {200, 130}, {256, 256}, {513, 77},
|
||||||
|
{1000, 700} /* 176 tiles: big enough for the threaded decoder */
|
||||||
|
};
|
||||||
|
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++)
|
||||||
|
for (int ch = 1; ch <= 4; ch++)
|
||||||
|
for (int kind = 0; kind < 5; kind++)
|
||||||
|
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);
|
||||||
|
if (img) {
|
||||||
|
uint32_t h = fnv(img->pixels, (size_t)img->width * img->height * img->channels);
|
||||||
|
CHECK(img->width == 512 && img->height == 512 && img->channels == 4 &&
|
||||||
|
h == SAMPLE_HASH, "sample %s decodes to the reference pixels (hash %08x)",
|
||||||
|
sample, (unsigned)h);
|
||||||
|
yapf_free(img);
|
||||||
|
} else {
|
||||||
|
printf("note: sample %s not found, skipped\n", sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("%d tests, %d failures\n", tests, fails);
|
||||||
|
return fails != 0;
|
||||||
|
}
|
||||||
7988
third_party/stb_image.h
vendored
Normal file
7988
third_party/stb_image.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
1724
third_party/stb_image_write.h
vendored
Normal file
1724
third_party/stb_image_write.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
60
tools/build_release.sh
Executable file
60
tools/build_release.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds every release download into dist/release/ — for maintainers.
|
||||||
|
#
|
||||||
|
# tools/build_release.sh
|
||||||
|
#
|
||||||
|
# Needs: zig (cross-compiles all six platforms from one machine), python3,
|
||||||
|
# and node + npx for the VS Code package. Outputs:
|
||||||
|
# yapf-<platform>.zip command-line tool + shared/static library + header
|
||||||
|
# yapf_image-blender.zip Blender extension with libraries for all platforms
|
||||||
|
# yapf-preview.vsix VS Code extension
|
||||||
|
# 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
|
||||||
|
LIBS=dist/libs
|
||||||
|
rm -rf "$OUT" "$LIBS"
|
||||||
|
mkdir -p "$OUT" "$LIBS"
|
||||||
|
|
||||||
|
command -v zig >/dev/null || { echo "zig not found: https://ziglang.org/download/" >&2; exit 1; }
|
||||||
|
export ZIG_GLOBAL_CACHE_DIR=${ZIG_GLOBAL_CACHE_DIR:-$PWD/dist/.zig-cache}
|
||||||
|
|
||||||
|
# zig target folder library tool
|
||||||
|
targets="x86_64-linux-gnu.2.17 linux-x64 libyapf.so yapf
|
||||||
|
aarch64-linux-gnu.2.17 linux-arm64 libyapf.so yapf
|
||||||
|
x86_64-windows-gnu windows-x64 yapf.dll yapf.exe
|
||||||
|
aarch64-windows-gnu windows-arm64 yapf.dll yapf.exe
|
||||||
|
x86_64-macos macos-x64 libyapf.dylib yapf
|
||||||
|
aarch64-macos macos-arm64 libyapf.dylib yapf"
|
||||||
|
|
||||||
|
echo "$targets" | while read -r tgt dir lib tool; do
|
||||||
|
pkg=dist/pkg/yapf-$dir
|
||||||
|
rm -rf "$pkg" && mkdir -p "$pkg" "$LIBS/$dir"
|
||||||
|
zig cc -target "$tgt" -O2 -std=c99 -shared -fPIC -s yapf.c -o "$LIBS/$dir/$lib"
|
||||||
|
zig cc -target "$tgt" -O2 -std=c99 -s tools/yapf_cli.c yapf.c -o "$pkg/$tool" -lm
|
||||||
|
zig cc -target "$tgt" -O2 -std=c99 -c yapf.c -o "$pkg/yapf.o"
|
||||||
|
zig ar rcs "$pkg/libyapf.a" "$pkg/yapf.o" && rm "$pkg/yapf.o"
|
||||||
|
cp "$LIBS/$dir/$lib" yapf.h LICENSE "$pkg/"
|
||||||
|
rm -f "$pkg"/*.lib "$pkg"/*.pdb "$LIBS/$dir"/*.lib "$LIBS/$dir"/*.pdb
|
||||||
|
(cd dist/pkg && python3 -m zipfile -c "../../$OUT/yapf-$dir.zip" "yapf-$dir")
|
||||||
|
echo "built yapf-$dir.zip"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 extensions/blender/build.py "$LIBS"
|
||||||
|
mv dist/yapf_image-blender.zip "$OUT/"
|
||||||
|
|
||||||
|
if command -v npx >/dev/null; then
|
||||||
|
(cd extensions/vscode && npm run --silent package && mv yapf-preview-*.vsix "../../$OUT/yapf-preview.vsix")
|
||||||
|
else
|
||||||
|
echo "node/npx not found: skipped the VS Code package" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd extensions/figma && node build.js 2>/dev/null || true)
|
||||||
|
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"
|
||||||
454
tools/yapf_cli.c
Normal file
454
tools/yapf_cli.c
Normal file
|
|
@ -0,0 +1,454 @@
|
||||||
|
/*
|
||||||
|
* yapf_cli.c — the `yapf` command-line tool.
|
||||||
|
*
|
||||||
|
* Copyright 2026 Nexoniarz
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* yapf <input> <output> [options] convert; formats chosen by extension
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Options for writing YAPF:
|
||||||
|
* --mips also store a full mip chain (2×2 box filter)
|
||||||
|
* --linear mark pixels as linear instead of sRGB
|
||||||
|
* --premult mark alpha as premultiplied
|
||||||
|
* Options for reading YAPF:
|
||||||
|
* --mip N export mip level N instead of the base image
|
||||||
|
*
|
||||||
|
* Build: cc -O2 -std=c99 -Ithird_party tools/yapf_cli.c yapf.c -o yapf -lm -pthread
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
|
||||||
|
# define _POSIX_C_SOURCE 200809L
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "../yapf.h"
|
||||||
|
|
||||||
|
/* Third-party, public domain; keep their warnings out of our build. */
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
# pragma GCC diagnostic push
|
||||||
|
# pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
|
# pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
||||||
|
# pragma GCC diagnostic ignored "-Wsign-compare"
|
||||||
|
#endif
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#define STBI_FAILURE_USERMSG
|
||||||
|
#include "../third_party/stb_image.h"
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "../third_party/stb_image_write.h"
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
# pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static int ends_with(const char *s, const char *ext) {
|
||||||
|
size_t n = strlen(s), m = strlen(ext);
|
||||||
|
if (n < m) return 0;
|
||||||
|
for (size_t i = 0; i < m; i++)
|
||||||
|
if (tolower((unsigned char)s[n - m + i]) != ext[i]) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_yapf(const char *path) { return ends_with(path, ".yapf"); }
|
||||||
|
|
||||||
|
static double now_s(void) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return (double)clock() / CLOCKS_PER_SEC;
|
||||||
|
#else
|
||||||
|
struct timespec t;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||||
|
return (double)t.tv_sec + (double)t.tv_nsec * 1e-9;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t gpu_hint(int ch, int srgb) {
|
||||||
|
switch (ch) {
|
||||||
|
case 1: return YAPF_GPU_R8;
|
||||||
|
case 2: return YAPF_GPU_RG8;
|
||||||
|
case 3: return srgb ? YAPF_GPU_SRGB8 : YAPF_GPU_RGB8;
|
||||||
|
default: return srgb ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_RGBA8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes). */
|
||||||
|
static int build_mips(yapf_image_t *img) {
|
||||||
|
int levels = 1;
|
||||||
|
while (levels < (int)YAPF_MAX_MIPS &&
|
||||||
|
((img->width >> levels) > 0 || (img->height >> levels) > 0))
|
||||||
|
levels++;
|
||||||
|
img->mips = (uint8_t **)calloc((size_t)levels, sizeof(uint8_t *));
|
||||||
|
if (!img->mips) return -1;
|
||||||
|
img->mips[0] = img->pixels;
|
||||||
|
img->mip_levels = (uint8_t)levels;
|
||||||
|
|
||||||
|
int ch = img->channels;
|
||||||
|
for (int m = 1; m < levels; m++) {
|
||||||
|
uint32_t pw = img->width >> (m - 1), ph = img->height >> (m - 1);
|
||||||
|
uint32_t w = img->width >> m, h = img->height >> m;
|
||||||
|
if (pw < 1) pw = 1;
|
||||||
|
if (ph < 1) ph = 1;
|
||||||
|
if (w < 1) w = 1;
|
||||||
|
if (h < 1) h = 1;
|
||||||
|
uint8_t *src = img->mips[m - 1];
|
||||||
|
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
|
||||||
|
if (!dst) return -1;
|
||||||
|
for (uint32_t y = 0; y < h; y++)
|
||||||
|
for (uint32_t x = 0; x < w; x++)
|
||||||
|
for (int c = 0; c < ch; c++) {
|
||||||
|
uint32_t x0 = 2 * x < pw ? 2 * x : pw - 1, x1 = 2 * x + 1 < pw ? 2 * x + 1 : pw - 1;
|
||||||
|
uint32_t y0 = 2 * y < ph ? 2 * y : ph - 1, y1 = 2 * y + 1 < ph ? 2 * y + 1 : ph - 1;
|
||||||
|
unsigned s = src[((size_t)y0 * pw + x0) * ch + c] + src[((size_t)y0 * pw + x1) * ch + c]
|
||||||
|
+ src[((size_t)y1 * pw + x0) * ch + c] + src[((size_t)y1 * pw + x1) * ch + c];
|
||||||
|
dst[((size_t)y * w + x) * ch + c] = (uint8_t)((s + 2) / 4);
|
||||||
|
}
|
||||||
|
img->mips[m] = dst;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_image(const char *path, const uint8_t *px, int w, int h, int ch) {
|
||||||
|
if (ends_with(path, ".png")) return stbi_write_png(path, w, h, ch, px, w * ch);
|
||||||
|
if (ends_with(path, ".bmp")) return stbi_write_bmp(path, w, h, ch, px);
|
||||||
|
if (ends_with(path, ".tga")) return stbi_write_tga(path, w, h, ch, px);
|
||||||
|
if (ends_with(path, ".jpg") || ends_with(path, ".jpeg"))
|
||||||
|
return stbi_write_jpg(path, w, h, ch, px, 95);
|
||||||
|
fprintf(stderr, "yapf: unsupported output format: %s (use .yapf .png .bmp .tga .jpg)\n", path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *gpu_name(int g) {
|
||||||
|
static const char *n[] = { "RGBA8", "RGB8", "RG8", "R8", "SRGB8_A8", "SRGB8" };
|
||||||
|
return g >= 0 && g < 6 ? n[g] : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_info(const char *path) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
uint8_t hdr[20];
|
||||||
|
if (!f || fread(hdr, 1, 20, f) != 20 || memcmp(hdr, "YAPF", 4) != 0) {
|
||||||
|
fprintf(stderr, "yapf: %s is not a YAPF file\n", path);
|
||||||
|
if (f) fclose(f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long size = ftell(f);
|
||||||
|
fclose(f);
|
||||||
|
unsigned w = hdr[12] | hdr[13] << 8 | (unsigned)hdr[14] << 16 | (unsigned)hdr[15] << 24;
|
||||||
|
unsigned h = hdr[16] | hdr[17] << 8 | (unsigned)hdr[18] << 16 | (unsigned)hdr[19] << 24;
|
||||||
|
printf("%s\n", path);
|
||||||
|
printf(" format version : %u\n", hdr[4]);
|
||||||
|
printf(" size : %u x %u, %u channel(s)\n", w, h, hdr[5]);
|
||||||
|
printf(" mip levels : %u\n", hdr[7]);
|
||||||
|
printf(" gpu format : %s\n", gpu_name(hdr[6]));
|
||||||
|
printf(" flags : %s%s\n", hdr[8] & YAPF_FLAG_SRGB ? "sRGB " : "linear ",
|
||||||
|
hdr[8] & YAPF_FLAG_PREMULT_ALPHA ? "premultiplied-alpha" : "");
|
||||||
|
printf(" file size : %ld bytes (%.1f%% of raw)\n", size,
|
||||||
|
100.0 * (double)size / ((double)w * h * (hdr[5] ? hdr[5] : 1)));
|
||||||
|
yapf_image_t *img = yapf_load(path);
|
||||||
|
printf(" decodes : %s\n", img ? "ok" : "FAILED (corrupt or unsupported)");
|
||||||
|
yapf_free(img);
|
||||||
|
return img ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_bench(const char *path) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) { fprintf(stderr, "yapf: cannot open %s\n", path); return 1; }
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long n = ftell(f);
|
||||||
|
rewind(f);
|
||||||
|
uint8_t *buf = (uint8_t *)malloc((size_t)n);
|
||||||
|
if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return 1; }
|
||||||
|
fclose(f);
|
||||||
|
yapf_image_t *img = yapf_load_memory(buf, (size_t)n);
|
||||||
|
if (!img) { fprintf(stderr, "yapf: %s does not decode\n", path); free(buf); return 1; }
|
||||||
|
double raw = (double)img->width * img->height * img->channels;
|
||||||
|
yapf_free(img);
|
||||||
|
for (int mt = 0; mt < 2; mt++) {
|
||||||
|
/* Best of 15 rounds; each round repeats enough decodes to last
|
||||||
|
* ~20 ms, so short images are measured warm and not timer-bound. */
|
||||||
|
double one = now_s();
|
||||||
|
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0) : yapf_load_memory(buf, (size_t)n));
|
||||||
|
one = now_s() - one;
|
||||||
|
int reps = (int)(0.02 / (one > 1e-6 ? one : 1e-6)) + 1;
|
||||||
|
double best = 1e9;
|
||||||
|
for (int r = 0; r < 15; r++) {
|
||||||
|
double t = now_s();
|
||||||
|
for (int k = 0; k < reps; k++)
|
||||||
|
yapf_free(mt ? yapf_load_memory_mt(buf, (size_t)n, 0)
|
||||||
|
: yapf_load_memory(buf, (size_t)n));
|
||||||
|
t = (now_s() - t) / reps;
|
||||||
|
if (t < best) best = t;
|
||||||
|
}
|
||||||
|
printf(" %-12s %8.3f ms %6.2f GB/s\n", mt ? "all cores" : "1 thread", best * 1e3, raw / best / 1e9);
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview no larger than size×size: start from the smallest mip that is
|
||||||
|
* still big enough, then average boxes of source pixels. */
|
||||||
|
static int cmd_thumbnail(const char *in, const char *out, int size) {
|
||||||
|
yapf_image_t *img = yapf_load_mt(in, 0);
|
||||||
|
if (!img) { fprintf(stderr, "yapf: cannot read %s\n", in); return 1; }
|
||||||
|
if (size < 1) size = 256;
|
||||||
|
int m = 0;
|
||||||
|
while (m + 1 < img->mip_levels &&
|
||||||
|
(int)(img->width >> (m + 1)) >= size && (int)(img->height >> (m + 1)) >= size)
|
||||||
|
m++;
|
||||||
|
uint32_t sw = img->width >> m ? img->width >> m : 1, sh = img->height >> m ? img->height >> m : 1;
|
||||||
|
const uint8_t *src = img->mips[m];
|
||||||
|
double k = (double)size / (double)(sw > sh ? sw : sh);
|
||||||
|
if (k > 1.0) k = 1.0;
|
||||||
|
uint32_t dw = (uint32_t)(sw * k + 0.5), dh = (uint32_t)(sh * k + 0.5);
|
||||||
|
if (dw < 1) dw = 1;
|
||||||
|
if (dh < 1) dh = 1;
|
||||||
|
int ch = img->channels;
|
||||||
|
uint8_t *dst = (uint8_t *)malloc((size_t)dw * dh * ch);
|
||||||
|
if (!dst) { yapf_free(img); return 1; }
|
||||||
|
for (uint32_t y = 0; y < dh; y++) {
|
||||||
|
uint32_t y0 = (uint32_t)((uint64_t)y * sh / dh), y1 = (uint32_t)((uint64_t)(y + 1) * sh / dh);
|
||||||
|
if (y1 <= y0) y1 = y0 + 1;
|
||||||
|
for (uint32_t x = 0; x < dw; x++) {
|
||||||
|
uint32_t x0 = (uint32_t)((uint64_t)x * sw / dw), x1 = (uint32_t)((uint64_t)(x + 1) * sw / dw);
|
||||||
|
if (x1 <= x0) x1 = x0 + 1;
|
||||||
|
for (int c = 0; c < ch; c++) {
|
||||||
|
uint64_t sum = 0;
|
||||||
|
for (uint32_t yy = y0; yy < y1; yy++)
|
||||||
|
for (uint32_t xx = x0; xx < x1; xx++)
|
||||||
|
sum += src[((size_t)yy * sw + xx) * ch + c];
|
||||||
|
uint64_t n = (uint64_t)(y1 - y0) * (x1 - x0);
|
||||||
|
dst[((size_t)y * dw + x) * ch + c] = (uint8_t)((sum + n / 2) / n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int ok = stbi_write_png(out, (int)dw, (int)dh, ch, dst, (int)dw * ch);
|
||||||
|
free(dst);
|
||||||
|
yapf_free(img);
|
||||||
|
if (!ok) fprintf(stderr, "yapf: cannot write %s\n", out);
|
||||||
|
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"
|
||||||
|
"\n"
|
||||||
|
"options when writing .yapf:\n"
|
||||||
|
" --mips store a full mip chain\n"
|
||||||
|
" --linear pixels are linear, not sRGB\n"
|
||||||
|
" --premult alpha is premultiplied\n"
|
||||||
|
"options when reading .yapf:\n"
|
||||||
|
" --mip N export mip level N\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
const char *in = argv[1], *out = argv[2];
|
||||||
|
int mips = 0, srgb = 1, premult = 0, mip_out = 0;
|
||||||
|
for (int i = 3; i < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--mips") == 0) mips = 1;
|
||||||
|
else if (strcmp(argv[i], "--linear") == 0) srgb = 0;
|
||||||
|
else if (strcmp(argv[i], "--premult") == 0) premult = 1;
|
||||||
|
else if (strcmp(argv[i], "--mip") == 0 && i + 1 < argc) mip_out = atoi(argv[++i]);
|
||||||
|
else { usage(); return 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read */
|
||||||
|
yapf_image_t src;
|
||||||
|
yapf_image_t *loaded = NULL;
|
||||||
|
uint8_t *stb_px = NULL;
|
||||||
|
memset(&src, 0, sizeof(src));
|
||||||
|
if (is_yapf(in)) {
|
||||||
|
loaded = yapf_load_mt(in, 0);
|
||||||
|
if (!loaded) { fprintf(stderr, "yapf: cannot read %s (missing, corrupt or not YAPF v1)\n", in); return 1; }
|
||||||
|
src = *loaded;
|
||||||
|
} else {
|
||||||
|
int w, h, ch;
|
||||||
|
if (stbi_is_16_bit(in))
|
||||||
|
fprintf(stderr, "yapf: note: %s has 16-bit channels; YAPF stores 8 bits per channel\n", in);
|
||||||
|
stb_px = stbi_load(in, &w, &h, &ch, 0);
|
||||||
|
if (!stb_px) { fprintf(stderr, "yapf: cannot read %s: %s\n", in, stbi_failure_reason()); return 1; }
|
||||||
|
src.width = (uint32_t)w; src.height = (uint32_t)h; src.channels = (uint8_t)ch;
|
||||||
|
src.pixels = stb_px; src.mip_levels = 1;
|
||||||
|
src.flags = srgb ? YAPF_FLAG_SRGB : 0;
|
||||||
|
src.gpu_format = gpu_hint(ch, srgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write */
|
||||||
|
int rc = 0;
|
||||||
|
if (is_yapf(out)) {
|
||||||
|
yapf_image_t dst = src;
|
||||||
|
if (!loaded) {
|
||||||
|
if (premult) dst.flags |= YAPF_FLAG_PREMULT_ALPHA;
|
||||||
|
}
|
||||||
|
dst.mips = NULL;
|
||||||
|
dst.mip_levels = 1;
|
||||||
|
if (mips && build_mips(&dst) != 0) { fprintf(stderr, "yapf: out of memory\n"); rc = 1; }
|
||||||
|
if (!rc && yapf_save(out, &dst) != YAPF_OK) { fprintf(stderr, "yapf: cannot write %s\n", out); rc = 1; }
|
||||||
|
if (dst.mips) {
|
||||||
|
for (int m = 1; m < dst.mip_levels; m++) free(dst.mips[m]);
|
||||||
|
free(dst.mips);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const uint8_t *px = src.pixels;
|
||||||
|
uint32_t w = src.width, h = src.height;
|
||||||
|
if (mip_out > 0) {
|
||||||
|
if (!loaded || mip_out >= src.mip_levels) {
|
||||||
|
fprintf(stderr, "yapf: %s has no mip level %d\n", in, mip_out);
|
||||||
|
rc = 1;
|
||||||
|
} else {
|
||||||
|
px = src.mips[mip_out];
|
||||||
|
w = src.width >> mip_out ? src.width >> mip_out : 1;
|
||||||
|
h = src.height >> mip_out ? src.height >> mip_out : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rc && !write_image(out, px, (int)w, (int)h, src.channels)) {
|
||||||
|
fprintf(stderr, "yapf: cannot write %s\n", out);
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yapf_free(loaded);
|
||||||
|
stbi_image_free(stb_px);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
142
yapf.h
142
yapf.h
|
|
@ -20,21 +20,30 @@
|
||||||
*
|
*
|
||||||
* YAPF — lossless, GPU-ready image format.
|
* YAPF — lossless, GPU-ready image format.
|
||||||
*
|
*
|
||||||
* Space savings come from two cheap mathematical steps — no entropy coder,
|
* Designed for single-core decode speed. Everything is byte aligned:
|
||||||
* no lookup tables, no opcode dispatch. Decode is straight arithmetic:
|
* no bit reader, no entropy coder, no context model. Per row the decoder
|
||||||
|
* dispatches one filter; per 8 samples it reads a 4-bit control code and
|
||||||
|
* exactly w bytes of payload, unpacked with one 64-bit load and shifts.
|
||||||
|
* The encoder does the hard work (filter search, colour transform choice,
|
||||||
|
* optimal control codes); the decoder only follows instructions.
|
||||||
*
|
*
|
||||||
* 1. YCoCg-R color transform (RGB↔Y/Co/Cg, fully reversible integers)
|
* 1. Per tile: YCoCg-R or subtract-green colour transform, 8-bit modular
|
||||||
* 2. 2-D neighbor prediction + per-tile adaptive bit-packing
|
* 2. Per band of 8 rows: one of 12 row filters
|
||||||
|
* 3. Residuals in groups of 8, each group at its bit width (0–8);
|
||||||
|
* 4-bit control codes, with codes that repeat the previous width
|
||||||
|
* 4. Independent 64×64 tiles with a size table → random access and
|
||||||
|
* optional parallel decode
|
||||||
*
|
*
|
||||||
* The file carries GPU metadata (format hint, mip levels, flags) so a
|
* The file carries GPU metadata (format hint, mip levels, flags) so a
|
||||||
* loader can upload the decoded buffer straight to a texture without any
|
* loader can upload the decoded buffer straight to a texture without any
|
||||||
* additional transcoding step.
|
* additional transcoding step.
|
||||||
*
|
*
|
||||||
* =========================================================================
|
* =========================================================================
|
||||||
* FILE FORMAT (version 2)
|
* FILE FORMAT (version 1)
|
||||||
* =========================================================================
|
* =========================================================================
|
||||||
*
|
*
|
||||||
* All multi-byte integers are little-endian.
|
* All multi-byte integers are little-endian. All arithmetic on samples
|
||||||
|
* is mod 256.
|
||||||
*
|
*
|
||||||
* ┌─────────────────────────────────────────────────────────────────────┐
|
* ┌─────────────────────────────────────────────────────────────────────┐
|
||||||
* │ HEADER — 20 bytes │
|
* │ HEADER — 20 bytes │
|
||||||
|
|
@ -42,7 +51,7 @@
|
||||||
* │ Offset │ Size │ Field │
|
* │ Offset │ Size │ Field │
|
||||||
* ├────────┼──────┼────────────────────────────────────────────────────┤
|
* ├────────┼──────┼────────────────────────────────────────────────────┤
|
||||||
* │ 0x00 │ 4 │ Magic: "YAPF" │
|
* │ 0x00 │ 4 │ Magic: "YAPF" │
|
||||||
* │ 0x04 │ 1 │ Version: 2 │
|
* │ 0x04 │ 1 │ Version: 1 │
|
||||||
* │ 0x05 │ 1 │ Channels: 1 gray / 2 gray+A / 3 RGB / 4 RGBA │
|
* │ 0x05 │ 1 │ Channels: 1 gray / 2 gray+A / 3 RGB / 4 RGBA │
|
||||||
* │ 0x06 │ 1 │ GPU format hint (YAPF_GPU_* constant) │
|
* │ 0x06 │ 1 │ GPU format hint (YAPF_GPU_* constant) │
|
||||||
* │ 0x07 │ 1 │ Mip levels (1 = base only, max 16) │
|
* │ 0x07 │ 1 │ Mip levels (1 = base only, max 16) │
|
||||||
|
|
@ -58,53 +67,64 @@
|
||||||
* │ +0 │ 4 │ Byte offset of mip data from start of file (uint32) │
|
* │ +0 │ 4 │ Byte offset of mip data from start of file (uint32) │
|
||||||
* │ +4 │ 4 │ Byte size of mip data (uint32) │
|
* │ +4 │ 4 │ Byte size of mip data (uint32) │
|
||||||
* └────────┴──────┴────────────────────────────────────────────────────┘
|
* └────────┴──────┴────────────────────────────────────────────────────┘
|
||||||
|
* Mip m has size max(1, width >> m) × max(1, height >> m).
|
||||||
*
|
*
|
||||||
* ┌─────────────────────────────────────────────────────────────────────┐
|
* ┌─────────────────────────────────────────────────────────────────────┐
|
||||||
* │ PIXEL DATA — per mip level, referenced by the mip index │
|
* │ MIP LEVEL DATA │
|
||||||
* ├─────────────────────────────────────────────────────────────────────┤
|
* ├─────────────────────────────────────────────────────────────────────┤
|
||||||
* │ Pixels are divided into 32×32 tiles, stored in row-major order. │
|
* │ Pixels are divided into 64×64 tiles in row-major order; the last │
|
||||||
* │ The last column / row of tiles may be smaller (no padding). │
|
* │ column / row of tiles may be smaller (no padding). │
|
||||||
* │ │
|
* │ │
|
||||||
* │ Encoding pipeline (encoder): │
|
* │ uint32 tile_size[tile_count] byte size of each tile │
|
||||||
* │ a) RGB→YCoCg-R (skipped for gray/gray-alpha channels) │
|
* │ tile data, back to back │
|
||||||
* │ b) 2-D prediction residuals (see below) │
|
|
||||||
* │ c) Determine per-channel bit depth from max |residual| in tile │
|
|
||||||
* │ d) Write tile header + packed residuals │
|
|
||||||
* │ │
|
* │ │
|
||||||
* │ Tile layout: │
|
* │ TILE │
|
||||||
* │ [bit_depth per channel] 1 byte each, channels bytes total │
|
* │ 1 byte header │
|
||||||
* │ [first pixel, raw] 1 byte per channel │
|
* │ 0x80 stored: tile_w × tile_h × channels raw bytes │
|
||||||
* │ [packed residuals] bit_depth[c] bits per sample, │
|
* │ follow, interleaved, in the original channel order │
|
||||||
* │ stored MSB-first, zero-padded to the │
|
* │ otherwise coded: │
|
||||||
* │ next byte boundary at the end of tile │
|
* │ bits 0–3 bit c set = plane c is constant (only bits below │
|
||||||
|
* │ the channel count may be set) │
|
||||||
|
* │ bit 4 colour transform: 0 = YCoCg-R, 1 = subtract-green │
|
||||||
|
* │ (RGB / RGBA only; must be 0 for gray) │
|
||||||
|
* │ bits 5–7 must be 0 │
|
||||||
|
* │ coded tiles then hold one value byte per constant plane, and a │
|
||||||
|
* │ plane payload per other plane, both in plane order. The tile │
|
||||||
|
* │ must end exactly where its last payload ends. │
|
||||||
* │ │
|
* │ │
|
||||||
* │ Prediction (lossless 2-D): │
|
* │ PLANES │
|
||||||
* │ pixel (0,0) in tile → stored raw, no residual │
|
* │ gray / gray+A: the channels as stored. │
|
||||||
* │ pixel (x,0) → predict = left │
|
* │ RGB(A), YCoCg-R (>> is an arithmetic shift of the value as int8):│
|
||||||
* │ pixel (0,y) → predict = above │
|
* │ planes Y, Co, Cg (+ A) │
|
||||||
* │ pixel (x,y) → predict = left + above − diag │
|
* │ forward: o = R − B; t = B + (o >> 1); q = G − t; │
|
||||||
* │ residual = (actual − predict) wrapped mod 256, stored signed │
|
* │ Y = t + (q >> 1); Co = o + 128; Cg = q + 128 │
|
||||||
|
* │ inverse: o = Co − 128; q = Cg − 128; t = Y − (q >> 1); │
|
||||||
|
* │ G = q + t; B = t − (o >> 1); R = B + o │
|
||||||
|
* │ RGB(A), subtract-green: │
|
||||||
|
* │ planes G, R − G + 128, B − G + 128 (+ A) │
|
||||||
* │ │
|
* │ │
|
||||||
* │ Bit packing: │
|
* │ PLANE PAYLOAD │
|
||||||
* │ bits_needed = bit_depth[c] (0 means all residuals are 0) │
|
* │ filter[ceil(tile_h / 8)] one byte per band of 8 rows. │
|
||||||
* │ Each residual is stored as a (bits_needed)-bit two's-complement │
|
* │ L / A / D / AR = left, above, above-left, above-right sample; │
|
||||||
* │ signed integer, MSB first, packed across byte boundaries. │
|
* │ samples outside the tile read as 0, except that AR in the │
|
||||||
* │ bit_depth 1 → only the sign bit (value is −1 or 0) │
|
* │ last column reads as A. │
|
||||||
* │ bit_depth 0 → no bits written; all samples in channel = 0 │
|
* │ 0 Left pred = L │
|
||||||
* │ │
|
* │ 1 Gradient pred = L + A − D │
|
||||||
* │ YCoCg-R forward transform (applied before prediction): │
|
* │ 2 Up-right pred = AR │
|
||||||
* │ Co = R − B │
|
* │ 3 Up-average pred = (A + AR) >> 1 (not mod 256) │
|
||||||
* │ t = B + (Co >> 1) │
|
* │ 16 + k−1 Up k pred = sample k rows above, k = 1..8 │
|
||||||
* │ Cg = G − t │
|
* │ control nibbles, low nibble of each byte first, covering │
|
||||||
* │ Y = t + (Cg >> 1) │
|
* │ exactly ceil(tile_w × tile_h / 8) groups of 8 consecutive │
|
||||||
* │ Stored channel order: Y, Co, Cg (replaces R, G, B) │
|
* │ residuals in raster order: │
|
||||||
* │ Alpha is left unchanged. │
|
* │ 0–8 one group of width w │
|
||||||
* │ │
|
* │ 9–15 2, 3, 4, 8, 16, 32 or 64 groups at the width of │
|
||||||
* │ YCoCg-R inverse transform (applied after prediction): │
|
* │ the last 0–8 code in this plane (0 if none yet) │
|
||||||
* │ t = Y − (Cg >> 1) │
|
* │ an odd nibble count is padded with a zero high nibble │
|
||||||
* │ G = Cg + t │
|
* │ group data: w bytes per group, the 8 values packed little- │
|
||||||
* │ B = t − (Co >> 1) │
|
* │ endian, value i in bits [i·w, i·w + w) │
|
||||||
* │ R = B + Co │
|
* │ Residual r = (sample − pred) mod 256 is stored folded: │
|
||||||
|
* │ v = (r << 1) ^ (r >> 7) (0, −1, 1, −2, … → 0, 1, 2, 3, …) │
|
||||||
|
* │ The last group is padded with zero values. │
|
||||||
* └─────────────────────────────────────────────────────────────────────┘
|
* └─────────────────────────────────────────────────────────────────────┘
|
||||||
*
|
*
|
||||||
* GPU FORMAT HINTS (YAPF_GPU_* values for the gpu_format field)
|
* GPU FORMAT HINTS (YAPF_GPU_* values for the gpu_format field)
|
||||||
|
|
@ -161,7 +181,7 @@
|
||||||
|
|
||||||
#define YAPF_MAX_DIM 65535u
|
#define YAPF_MAX_DIM 65535u
|
||||||
#define YAPF_MAX_MIPS 16u
|
#define YAPF_MAX_MIPS 16u
|
||||||
#define YAPF_TILE_SIZE 32u
|
#define YAPF_TILE_SIZE 64u
|
||||||
|
|
||||||
/* ── Image descriptor ────────────────────────────────────────────────── */
|
/* ── Image descriptor ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -189,6 +209,21 @@ extern "C" {
|
||||||
*/
|
*/
|
||||||
yapf_image_t *yapf_load(const char *filename);
|
yapf_image_t *yapf_load(const char *filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* yapf_load_memory — decode a .yapf file already in memory (e.g. from a
|
||||||
|
* game archive). Same ownership and failure rules as yapf_load().
|
||||||
|
*/
|
||||||
|
yapf_image_t *yapf_load_memory(const void *buffer, size_t size);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* yapf_load_mt / yapf_load_memory_mt — same as above, but decode tiles on
|
||||||
|
* up to `threads` threads (0 = one per CPU core). Output is identical.
|
||||||
|
* Without thread support (YAPF_NO_THREADS, or an unknown platform) these
|
||||||
|
* decode on the calling thread.
|
||||||
|
*/
|
||||||
|
yapf_image_t *yapf_load_mt(const char *filename, int threads);
|
||||||
|
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* yapf_save — encode an image to disk.
|
* yapf_save — encode an image to disk.
|
||||||
* img->mip_levels controls how many levels are written; set to 1 to write
|
* img->mip_levels controls how many levels are written; set to 1 to write
|
||||||
|
|
@ -197,6 +232,17 @@ yapf_image_t *yapf_load(const char *filename);
|
||||||
*/
|
*/
|
||||||
int yapf_save(const char *filename, const yapf_image_t *img);
|
int yapf_save(const char *filename, const yapf_image_t *img);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* yapf_encode — encode an image into memory.
|
||||||
|
* On success *out_data points to *out_size bytes of a complete .yapf file;
|
||||||
|
* release it with yapf_free_buffer(). Same rules and return codes as
|
||||||
|
* yapf_save() (no YAPF_ERR_IO).
|
||||||
|
*/
|
||||||
|
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||||
|
|
||||||
|
/* yapf_free_buffer — release memory returned by yapf_encode(). NULL-safe. */
|
||||||
|
void yapf_free_buffer(void *data);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* yapf_free — release all memory owned by an image returned by yapf_load().
|
* yapf_free — release all memory owned by an image returned by yapf_load().
|
||||||
* NULL-safe.
|
* NULL-safe.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue