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

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

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

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

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

View file

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

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

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

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

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

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

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

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

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