mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
Language bindings, KDE thumbnails, yapf compare, in-memory encoding
- yapf_encode() / yapf_free_buffer(): encode to memory; yapf_save uses it - yapf.c also compiles as C++ - `yapf compare`: size and decode speed of the original format vs YAPF, with a lossless check - bindings with tested examples: C++ (header-only), C# (P/Invoke), Rust (crate, builds the C code), Go (cgo), Java (FFM API, Java 22+), Zig (@cImport), LuaJIT (FFI), TypeScript types for the JS port; Python now encodes in memory - docs/LANGUAGES.md and examples/ with measured results - KDE Dolphin thumbnail plugin (KF6, CMake) - README: honest status for Photoshop / VS Code / Figma (not run in the apps yet) and macOS (cross-compiled only), platform table Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ca726e3af
commit
d7f494a0c9
52 changed files with 1926 additions and 106 deletions
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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue