mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-02 20:44:27 +00:00
- 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>
121 lines
6.1 KiB
Java
121 lines
6.1 KiB
Java
// 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)); }
|
|
}
|