diff --git a/Cargo.lock b/Cargo.lock index f488319..81a8689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libtlg-rs" -version = "0.2.2" +version = "0.2.3" dependencies = [ "lazy_static", "overf", diff --git a/libtlg-rs/Cargo.toml b/libtlg-rs/Cargo.toml index e82ac80..dc39fc0 100644 --- a/libtlg-rs/Cargo.toml +++ b/libtlg-rs/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "libtlg-rs" -version = "0.2.2" +version = "0.2.3" description = "Rust version of libtlg" edition = "2024" license = "MIT" repository = "https://github.com/lifegpc/libtlg-rs" [features] +capi = [] encode = [] [dependencies] diff --git a/libtlg-rs/cbindgen.toml b/libtlg-rs/cbindgen.toml new file mode 100644 index 0000000..08094f2 --- /dev/null +++ b/libtlg-rs/cbindgen.toml @@ -0,0 +1 @@ +language = "C" diff --git a/libtlg-rs/src/capi.rs b/libtlg-rs/src/capi.rs new file mode 100644 index 0000000..aa73407 --- /dev/null +++ b/libtlg-rs/src/capi.rs @@ -0,0 +1,327 @@ +//! C API for libtlg-rs +//! +//! Use follow command to build the C API library +//! ```shell +//! cargo install cargo-c +//! cargo cbuild --release --features "encode" +//! ``` +use std::ffi::{c_char, c_void}; +#[cfg(feature = "encode")] +use std::io::Write; +use std::io::{Read, Seek, SeekFrom}; + +/// A readable stream for TLG processing. +pub struct ReadableStream { + /// User data pointer. + userdata: *mut c_void, + /// Read function pointer. + read_fn: extern "C" fn(*mut c_void, *mut u8, usize) -> usize, + /// Seek function pointer. + seek_fn: extern "C" fn(*mut c_void, i64, i32) -> u64, +} + +#[cfg(feature = "encode")] +/// A writeable stream for TLG processing. +pub struct WriteableStream { + /// User data pointer. + userdata: *mut c_void, + /// Write function pointer. + write_fn: extern "C" fn(*mut c_void, *const u8, usize) -> usize, + /// Flush function pointer. + flush_fn: extern "C" fn(*mut c_void) -> i32, + /// Seek function pointer. + seek_fn: extern "C" fn(*mut c_void, i64, i32) -> u64, +} + +#[repr(C)] +/// A tag in TLG file. +pub struct Tag { + /// Tag name. + pub name: *mut c_char, + /// Length of tag name. + pub name_length: usize, + /// Tag value. + pub value: *mut c_char, + /// Length of tag value. + pub value_length: usize, +} + +#[repr(C)] +/// Color type of TLG image. +pub enum TlgColorType { + /// Grayscale 8-bit. + Grayscale8, + /// BGR 24-bit. + Bgr24, + /// BGRA 32-bit. + Bgra32, +} + +#[repr(C)] +pub struct Tlg { + /// TLG version. 5 or 6. + pub version: u32, + /// Image width. + pub width: u32, + /// Image height. + pub height: u32, + /// Color type. + pub color_type: TlgColorType, + /// Pointer to image data. + pub data: *mut u8, + /// Number of bytes in data. + pub data_length: usize, + /// Pointer to tags, null if no tags. + pub tags: *mut Tag, + /// Number of tags. + pub tag_count: usize, +} + +impl Read for ReadableStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let ret = (self.read_fn)(self.userdata, buf.as_mut_ptr(), buf.len()); + Ok(ret) + } +} + +impl Seek for ReadableStream { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + let (offset, whence) = match pos { + SeekFrom::Start(off) => (off as i64, 0), + SeekFrom::End(off) => (off, 2), + SeekFrom::Current(off) => (off, 1), + }; + let ret = (self.seek_fn)(self.userdata, offset, whence); + Ok(ret) + } +} + +#[cfg(feature = "encode")] +impl Write for WriteableStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let ret = (self.write_fn)(self.userdata, buf.as_ptr(), buf.len()); + Ok(ret) + } + + fn flush(&mut self) -> std::io::Result<()> { + let ret = (self.flush_fn)(self.userdata); + if ret == 0 { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Flush failed", + )) + } + } +} + +#[cfg(feature = "encode")] +impl Seek for WriteableStream { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + let (offset, whence) = match pos { + SeekFrom::Start(off) => (off as i64, 0), + SeekFrom::End(off) => (off, 2), + SeekFrom::Current(off) => (off, 1), + }; + let ret = (self.seek_fn)(self.userdata, offset, whence); + Ok(ret) + } +} + +/// Create a readable stream. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn create_readable_stream( + userdata: *mut c_void, + read_fn: extern "C" fn(*mut c_void, *mut u8, usize) -> usize, + seek_fn: extern "C" fn(*mut c_void, i64, i32) -> u64, +) -> *mut ReadableStream { + let stream = ReadableStream { + userdata, + read_fn, + seek_fn, + }; + Box::into_raw(Box::new(stream)) +} + +/// Destroy a readable stream. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn destroy_readable_stream(stream: *mut ReadableStream) { + if !stream.is_null() { + drop(unsafe { Box::from_raw(stream) }); + } +} + +/// Create a writeable stream. +#[cfg(feature = "encode")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn create_writeable_stream( + userdata: *mut c_void, + write_fn: extern "C" fn(*mut c_void, *const u8, usize) -> usize, + flush_fn: extern "C" fn(*mut c_void) -> i32, + seek_fn: extern "C" fn(*mut c_void, i64, i32) -> u64, +) -> *mut WriteableStream { + let stream = WriteableStream { + userdata, + write_fn, + flush_fn, + seek_fn, + }; + Box::into_raw(Box::new(stream)) +} + +/// Destroy a writeable stream. +#[cfg(feature = "encode")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn destroy_writeable_stream(stream: *mut WriteableStream) { + if !stream.is_null() { + drop(unsafe { Box::from_raw(stream) }); + } +} + +/// Check if it's a valid TLG. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn check_tlg(stream: *mut ReadableStream) -> bool { + if stream.is_null() { + return false; + } + let stream = unsafe { &mut *stream }; + match crate::check_tlg(stream) { + Ok(valid) => valid, + Err(_) => false, + } +} + +/// Check if it's a valid TLG. +/// 11 bytes are needed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn is_valid_tlg(data: *const u8, length: usize) -> bool { + let data = unsafe { std::slice::from_raw_parts(data, length) }; + crate::is_valid_tlg(data) +} + +/// Load a TLG from a readable stream. +/// Returns a pointer to Tlg struct, or null on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn load_tlg(stream: *mut ReadableStream) -> *mut Tlg { + if stream.is_null() { + return std::ptr::null_mut(); + } + let stream = unsafe { &mut *stream }; + match crate::load_tlg(stream) { + Ok(mut tlg) => { + let color_type = match tlg.color { + crate::TlgColorType::Grayscale8 => TlgColorType::Grayscale8, + crate::TlgColorType::Bgr24 => TlgColorType::Bgr24, + crate::TlgColorType::Bgra32 => TlgColorType::Bgra32, + }; + tlg.data.shrink_to_fit(); + let data_ptr = tlg.data.as_mut_ptr(); + let data_len = tlg.data.len(); + std::mem::forget(tlg.data); + let (tag, tag_count) = if tlg.tags.is_empty() { + (std::ptr::null_mut(), 0) + } else { + let mut tags = Vec::with_capacity(tlg.tags.len()); + for (mut name, mut value) in tlg.tags.drain() { + let name_ptr = name.as_mut_ptr(); + let name_len = name.len(); + name.shrink_to_fit(); + std::mem::forget(name); + let value_ptr = value.as_mut_ptr(); + let value_len = value.len(); + value.shrink_to_fit(); + std::mem::forget(value); + tags.push(Tag { + name: name_ptr as *mut c_char, + name_length: name_len, + value: value_ptr as *mut c_char, + value_length: value_len, + }); + } + let tag_ptr = tags.as_mut_ptr(); + let tag_count = tags.len(); + tags.shrink_to_fit(); + std::mem::forget(tags); + (tag_ptr, tag_count) + }; + let tlg_c = Tlg { + version: tlg.version, + width: tlg.width, + height: tlg.height, + color_type, + data: data_ptr, + data_length: data_len, + tags: tag, + tag_count, + }; + Box::into_raw(Box::new(tlg_c)) + } + Err(_) => std::ptr::null_mut(), + } +} + +/// Destroy a Tlg struct. +/// Also frees the image data. +/// WARN: Make sure the Tlg struct is created by `load_tlg`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn destroy_tlg(tlg: *mut Tlg) { + if tlg.is_null() { + return; + } + let tlg = unsafe { Box::from_raw(tlg) }; + let _data = unsafe { Vec::from_raw_parts(tlg.data, tlg.data_length, tlg.data_length) }; + if !tlg.tags.is_null() { + let tags = unsafe { Vec::from_raw_parts(tlg.tags, tlg.tag_count, tlg.tag_count) }; + for tag in tags { + let _name = unsafe { + Vec::from_raw_parts(tag.name as *mut u8, tag.name_length, tag.name_length) + }; + let _value = unsafe { + Vec::from_raw_parts(tag.value as *mut u8, tag.value_length, tag.value_length) + }; + } + } +} + +/// Encode a TLG image to a writeable stream. +/// Returns true on success, false on error. +#[cfg(feature = "encode")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn save_tlg(tlg: Tlg, stream: *mut WriteableStream) -> bool { + if stream.is_null() || tlg.data.is_null() { + return false; + } + let stream = unsafe { &mut *stream }; + let color = match tlg.color_type { + TlgColorType::Grayscale8 => crate::TlgColorType::Grayscale8, + TlgColorType::Bgr24 => crate::TlgColorType::Bgr24, + TlgColorType::Bgra32 => crate::TlgColorType::Bgra32, + }; + let data = unsafe { std::slice::from_raw_parts(tlg.data, tlg.data_length) }.to_vec(); + let mut tags = std::collections::HashMap::new(); + if !tlg.tags.is_null() && tlg.tag_count > 0 { + let tag_slice = unsafe { std::slice::from_raw_parts(tlg.tags, tlg.tag_count) }; + for tag in tag_slice { + let name = + unsafe { std::slice::from_raw_parts(tag.name as *const u8, tag.name_length) } + .to_vec(); + let value = + unsafe { std::slice::from_raw_parts(tag.value as *const u8, tag.value_length) } + .to_vec(); + tags.insert(name, value); + } + } + let tlg_rust = crate::Tlg { + version: tlg.version, + width: tlg.width, + height: tlg.height, + color, + data, + tags, + }; + match crate::save_tlg(&tlg_rust, stream) { + Ok(_) => true, + Err(_) => false, + } +} diff --git a/libtlg-rs/src/lib.rs b/libtlg-rs/src/lib.rs index ca559ee..0ae48c2 100644 --- a/libtlg-rs/src/lib.rs +++ b/libtlg-rs/src/lib.rs @@ -1,5 +1,7 @@ //! A Rust library for processing TLG files. #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "capi")] +pub mod capi; mod load_tlg; #[cfg(feature = "encode")] mod save_tlg;