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

View file

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

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

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

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

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