diff --git a/Cargo.toml b/Cargo.toml index a20504a..b10f740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptonica-plumbing" -version = "0.6.0" +version = "1.0.0" authors = ["Chris Couzens "] edition = "2018" description = "Safe wrapper of `leptonica-sys`" diff --git a/README.md b/README.md index b726f75..afdb240 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,26 @@ their memory safety. Having a safety layer that stays simple improves the correctness and maintainability of the above libraries. + +## Testing + +To test for memory leaks, test with `valgrind`. + +```bash +cargo test --release && valgrind --leak-check=yes --error-exitcode=1 --leak-check=full --show-leak-kinds=all "$(find target/*/deps/ -executable -name 'leptonica_plumbing-*')" +``` + +You may find that leptonica always leaks 16B of memory. + +To test with a manually compiled Leptonica, test with additional environment +variables + +```bash +LD_LIBRARY_PATH="$(pwd)/../../DanBloomberg/leptonica/local/lib" PKG_CONFIG_PATH="$(pwd)/../../DanBloomberg/leptonica/local/lib/pkgconfig" cargo test +``` + +The two can be combined + +```bash +LD_LIBRARY_PATH="$(pwd)/../../DanBloomberg/leptonica/local/lib" PKG_CONFIG_PATH="$(pwd)/../../DanBloomberg/leptonica/local/lib/pkgconfig" bash -c 'cargo test --release && valgrind --leak-check=yes --error-exitcode=1 --leak-check=full --show-leak-kinds=all "$(find target/*/deps/ -executable -name 'leptonica_plumbing-*')"' +``` diff --git a/src/borrowed_box.rs b/src/borrowed_box.rs deleted file mode 100644 index 06c98d8..0000000 --- a/src/borrowed_box.rs +++ /dev/null @@ -1,21 +0,0 @@ -/// Borrowed wrapper around Leptonica's [`Box`](https://tpgit.github.io/Leptonica/struct_box.html) structure -#[derive(Debug, PartialEq)] -pub struct BorrowedBox<'a>(&'a *mut leptonica_sys::Box); - -impl<'a> AsRef for BorrowedBox<'a> { - fn as_ref(&self) -> &leptonica_sys::Box { - unsafe { &**self.0 } - } -} - -impl<'a> BorrowedBox<'a> { - /// Create a new BorrowedBox from a pointer - /// - /// # Safety - /// - /// The pointer must be to a valid Box struct. - /// The box must not be mutated whilst the BorrowedBox exists. - pub unsafe fn new(b: &'a *mut leptonica_sys::Box) -> Self { - Self(b) - } -} diff --git a/src/borrowed_pix.rs b/src/borrowed_pix.rs deleted file mode 100644 index 106fc2d..0000000 --- a/src/borrowed_pix.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::marker::PhantomData; - -/// Borrowed wrapper around Leptonica's [`Pix`](https://tpgit.github.io/Leptonica/struct_pix.html) structure -#[derive(Debug, PartialEq)] -pub struct BorrowedPix<'a> { - raw: *mut leptonica_sys::Pix, - phantom: PhantomData<&'a *mut leptonica_sys::Pix>, -} - -impl<'a> AsRef for BorrowedPix<'a> { - fn as_ref(&self) -> &leptonica_sys::Pix { - unsafe { &*self.raw } - } -} - -impl<'a> BorrowedPix<'a> { - /// Create a new BorrowedPix from a pointer - /// - /// # Safety - /// - /// The pointer must be to a valid Pix struct. - /// The pix must not be mutated whilst the BorrowedPix exists. - pub unsafe fn new(p: *mut leptonica_sys::Pix) -> Self { - Self { - raw: p, - phantom: PhantomData, - } - } -} diff --git a/src/box.rs b/src/box.rs index 79c8f6a..0326618 100644 --- a/src/box.rs +++ b/src/box.rs @@ -1,8 +1,7 @@ -extern crate leptonica_sys; -extern crate thiserror; +use crate::memory::{LeptonicaDestroy, RefCountedExclusive}; -use self::thiserror::Error; -use leptonica_sys::{boxCreateValid, boxDestroy, l_int32}; +use leptonica_sys::{boxCreateValid, boxDestroy, boxGetGeometry, l_int32, l_ok}; +use thiserror::Error; /// Wrapper around Leptonica's [`Box`](https://tpgit.github.io/Leptonica/struct_box.html) structure #[derive(Debug, PartialEq)] @@ -13,29 +12,30 @@ pub struct Box(*mut leptonica_sys::Box); #[error("Box::create_valid returned null")] pub struct BoxCreateValidError(); -impl Drop for Box { - fn drop(&mut self) { - unsafe { - boxDestroy(&mut self.0); - } - } -} - impl AsRef for Box { fn as_ref(&self) -> &leptonica_sys::Box { unsafe { &*self.0 } } } +impl AsMut for Box { + fn as_mut(&mut self) -> &mut leptonica_sys::Box { + unsafe { &mut *self.0 } + } +} + impl Box { - /// Convinience wrapper for [Self::create_valid] - pub fn new( - x: l_int32, - y: l_int32, - w: l_int32, - h: l_int32, - ) -> Result { - Self::create_valid(x, y, w, h) + /// Create an owned Box from a box pointer + /// + /// # Safety + /// + /// The pointer must be to a valid Box struct. + /// The data pointed at may not be mutated while held by + /// this struct except by this struct. + /// On drop, the destroy method will be called (decrements + /// the ref counter). + pub unsafe fn new_from_pointer(b: *mut leptonica_sys::Box) -> Self { + Self(b) } /// Wrapper for [`boxCreateValid`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#a435610d86a8562dc60bfd75fe0a15420) @@ -46,21 +46,59 @@ impl Box { y: l_int32, w: l_int32, h: l_int32, - ) -> Result { + ) -> Result, BoxCreateValidError> { let ptr = unsafe { boxCreateValid(x, y, w, h) }; if ptr.is_null() { Err(BoxCreateValidError()) } else { - Ok(Self(ptr)) + Ok(unsafe { RefCountedExclusive::new(Self(ptr)) }) + } + } + + /// Wrapper for [`boxGetGeometry`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#aaf754e00c062c3f0f726bea73a17e646) + pub fn get_geometry( + &self, + px: Option<&mut l_int32>, + py: Option<&mut l_int32>, + pw: Option<&mut l_int32>, + ph: Option<&mut l_int32>, + ) -> l_ok { + unsafe { + boxGetGeometry( + self.0, + match px { + None => std::ptr::null_mut(), + Some(px) => px, + }, + match py { + None => std::ptr::null_mut(), + Some(py) => py, + }, + match pw { + None => std::ptr::null_mut(), + Some(pw) => pw, + }, + match ph { + None => std::ptr::null_mut(), + Some(ph) => ph, + }, + ) } } } +impl LeptonicaDestroy for Box { + unsafe fn destroy(&mut self) { + boxDestroy(&mut self.0); + } +} + #[test] fn create_valid_test() { let r#box = Box::create_valid(1, 2, 3, 4).unwrap(); - let lbox: &leptonica_sys::Box = r#box.as_ref(); - assert_eq!(lbox.w, 3); + let mut pw = 0; + r#box.get_geometry(None, None, Some(&mut pw), None); + assert_eq!(pw, 3); } #[test] diff --git a/src/boxa.rs b/src/boxa.rs index c78d270..3b9fa65 100644 --- a/src/boxa.rs +++ b/src/boxa.rs @@ -1,26 +1,28 @@ -extern crate leptonica_sys; -extern crate thiserror; +use std::convert::TryInto; -use leptonica_sys::{boxaCreate, boxaDestroy, l_int32}; +use leptonica_sys::{boxaCreate, boxaDestroy, boxaGetBox, boxaGetCount, l_int32, L_CLONE, L_COPY}; + +use crate::{ + memory::{LeptonicaDestroy, RefCounted, RefCountedExclusive}, + Box, +}; /// Wrapper around Leptonica's [`Boxa`](https://tpgit.github.io/Leptonica/struct_boxa.html) structure #[derive(Debug, PartialEq)] pub struct Boxa(*mut leptonica_sys::Boxa); -impl Drop for Boxa { - fn drop(&mut self) { - unsafe { - boxaDestroy(&mut self.0); - } - } -} - impl AsRef for Boxa { fn as_ref(&self) -> &leptonica_sys::Boxa { unsafe { &*self.0 } } } +impl AsMut for Boxa { + fn as_mut(&mut self) -> &mut leptonica_sys::Boxa { + unsafe { &mut *self.0 } + } +} + impl Boxa { /// Create a new Boxa from a pointer /// @@ -35,30 +37,76 @@ impl Boxa { /// Wrapper for [`boxaCreate`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#ae59916b7506831be9bf2119dea063253) /// /// Input: n (initial number of ptrs) Return: boxa, or null on error - pub fn create(n: l_int32) -> Option { + pub fn create(n: l_int32) -> Option> { let ptr = unsafe { boxaCreate(n) }; if ptr.is_null() { None } else { - Some(Self(ptr)) + Some(unsafe { RefCountedExclusive::new(Self(ptr)) }) } } - /// Safely borrow the nth item - pub fn get(&self, i: isize) -> Option { - let lboxa: &leptonica_sys::Boxa = self.as_ref(); - if lboxa.n <= std::convert::TryFrom::try_from(i).ok()? { - None - } else { - unsafe { Some(crate::BorrowedBox::new(&*lboxa.box_.offset(i))) } + /// Wrapper for [`boxaGetCount`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#a82555cab9ef5578c4728ef5109264723) + pub fn get_count(&self) -> l_int32 { + unsafe { boxaGetCount(self.0) } + } + + /// Wrapper for [`boxaGetBox`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#ac7c6fcfadf130bfa738ce6aab51318e5) with copied `accessflag`: `L_COPY` + pub fn get_box_copied(&self, index: l_int32) -> Option> { + unsafe { + boxaGetBox(self.0, index, L_COPY.try_into().unwrap()) + .as_mut() + .map(|raw| RefCountedExclusive::new(Box::new_from_pointer(raw))) + } + } + + /// Wrapper for [`boxaGetBox`](https://tpgit.github.io/Leptonica/boxbasic_8c.html#ac7c6fcfadf130bfa738ce6aab51318e5) with copied `accessflag`: `L_CLONE` + pub fn get_box_cloned(&self, index: l_int32) -> Option> { + unsafe { + boxaGetBox(self.0, index, L_CLONE.try_into().unwrap()) + .as_mut() + .map(|raw| RefCounted::new(Box::new_from_pointer(raw))) } } } +impl LeptonicaDestroy for Boxa { + unsafe fn destroy(&mut self) { + boxaDestroy(&mut self.0); + } +} + #[test] -fn create_valid_test() { - let boxa = Boxa::create(4).unwrap(); - let lboxa: &leptonica_sys::Boxa = boxa.as_ref(); - assert_eq!(lboxa.nalloc, 4); - assert_eq!(lboxa.n, 0); +fn get_test() { + use leptonica_sys::boxaAddBox; + + let mut boxa = Boxa::create(4).unwrap(); + assert_eq!(boxa.get_count(), 0); + let mut box_1 = Box::create_valid(1, 2, 3, 4).unwrap(); + let mut box_2 = Box::create_valid(5, 6, 7, 8).unwrap(); + unsafe { + boxaAddBox(boxa.as_mut(), box_1.as_mut(), L_CLONE.try_into().unwrap()); + boxaAddBox(boxa.as_mut(), box_2.as_mut(), L_CLONE.try_into().unwrap()); + } + assert_eq!(boxa.get_count(), 2); + + let box_1_cloned = boxa.get_box_cloned(0).unwrap(); + let box_2_copied = boxa.get_box_copied(1).unwrap(); + + let (mut px, mut py, mut pw, mut ph) = (-1, -1, -1, -1); + box_1_cloned.get_geometry(Some(&mut px), Some(&mut py), Some(&mut pw), Some(&mut ph)); + assert_eq!((px, py, pw, ph), (1, 2, 3, 4)); + // Because Cloned reuses and reference counts the same pointer + assert_eq!( + box_1.as_ref() as *const leptonica_sys::Box, + box_1_cloned.as_ref() as *const leptonica_sys::Box + ); + + box_2_copied.get_geometry(Some(&mut px), Some(&mut py), Some(&mut pw), Some(&mut ph)); + assert_eq!((px, py, pw, ph), (5, 6, 7, 8)); + // Because Copied creates a new instance + assert!( + box_2.as_ref() as *const leptonica_sys::Box + != box_2_copied.as_ref() as *const leptonica_sys::Box + ); } diff --git a/src/lib.rs b/src/lib.rs index 172559f..f632dcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,64 +1,53 @@ -mod borrowed_box; -mod borrowed_pix; mod r#box; mod boxa; +pub mod memory; mod pix; mod pixa; +mod str; -use self::leptonica_sys::{free, getImagelibVersions, getLeptonicaVersion}; +use self::leptonica_sys::{getImagelibVersions, getLeptonicaVersion}; pub use leptonica_sys; -use libc::c_char; -use std::ffi::{c_void, CStr}; -pub use borrowed_box::BorrowedBox; -pub use borrowed_pix::BorrowedPix; +pub use crate::str::Str; pub use boxa::Boxa; pub use pix::{Pix, PixReadError, PixReadMemError}; pub use pixa::Pixa; pub use r#box::{Box, BoxCreateValidError}; -/// Wrapper for leptonica strings -#[derive(Debug)] -pub struct LeptonicaString { - value: *mut c_char, +/// Wrapper for [`getLeptonicaVersion`](https://github.com/DanBloomberg/leptonica/blob/1.82.0/src/utils1.c#L970-L982) +/// +/// Returns the version identifier as a LeptonicaString. +pub fn get_version() -> Str { + unsafe { Str::new_from_pointer(getLeptonicaVersion()) } } -impl AsRef for LeptonicaString { - fn as_ref(&self) -> &str { - unsafe { - CStr::from_ptr(self.value) - .to_str() - .expect("failed to get leptonica string") +/// Wrapper for [`getImagelibVersions`](https://github.com/DanBloomberg/leptonica/blob/1.82.0/src/libversions.c#L82-L102) +/// +/// Returns the image lib version identifiers as a LeptonicaString. +pub fn get_imagelib_versions() -> Option { + unsafe { + let pointer = getImagelibVersions(); + if pointer.is_null() { + None + } else { + Some(Str::new_from_pointer(pointer)) } } } -impl AsRef for LeptonicaString { - fn as_ref(&self) -> &CStr { - unsafe { CStr::from_ptr(self.value) } - } -} - -impl Drop for LeptonicaString { - fn drop(&mut self) { - unsafe { free(self.value as *mut c_void) } - } -} +#[cfg(test)] +mod tests { + use super::*; -/// Wrapper for [`getLeptonicaVersion`](https://github.com/DanBloomberg/leptonica/blob/1.82.0/src/utils1.c#L970-L982) -/// -/// Returns the version identifier as a LeptonicaString. -pub fn get_version() -> LeptonicaString { - LeptonicaString { - value: unsafe { getLeptonicaVersion() }, + #[test] + fn test_get_version() { + assert_eq!(&get_version().to_str().unwrap()[0..10], "leptonica-"); } -} -/// Wrapper for [`getImagelibVersions`](https://github.com/DanBloomberg/leptonica/blob/1.82.0/src/libversions.c#L82-L102) -/// -/// Returns the image lib version identifiers as a LeptonicaString. -pub fn get_imagelib_versions() -> LeptonicaString { - LeptonicaString { - value: unsafe { getImagelibVersions() }, + #[test] + fn test_get_imagelib_versions() { + // No assertions as there's not much we can guarantee given different compile time options. + // Instead used when testing with valgrind to check for leaks. + get_imagelib_versions(); } } diff --git a/src/memory/borrowed_from.rs b/src/memory/borrowed_from.rs new file mode 100644 index 0000000..db990d7 --- /dev/null +++ b/src/memory/borrowed_from.rs @@ -0,0 +1,29 @@ +use std::{marker::PhantomData, ops::Deref}; + +/// A non-ref counted wrapper that's valid for the lifetime of a parent struct +pub struct BorrowedFrom<'a, T: 'a> { + inner: T, + phantom: PhantomData<&'a T>, +} + +impl<'a, T: 'a> BorrowedFrom<'a, T> { + /// Creates a new borrowed-from wrapper + /// + /// # Safety + /// + /// The pointer must not be mutated whilst this wrapper exists. + pub unsafe fn new(inner: T) -> Self { + Self { + inner, + phantom: PhantomData, + } + } +} + +impl<'a, T: 'a> Deref for BorrowedFrom<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/src/memory/leptonica_clone.rs b/src/memory/leptonica_clone.rs new file mode 100644 index 0000000..ab7202c --- /dev/null +++ b/src/memory/leptonica_clone.rs @@ -0,0 +1,13 @@ +/// Trait to define how leptonica clones this memory. +/// +/// Usually this would be to call a `clone` method that increments a reference count. +pub trait LeptonicaClone { + /// Call to leptonica's internal structure-clone method. + /// + /// # Safety + /// + /// This must eventually be accompanied by a destroy call to decrement the reference count and possibly free the memory. + /// + /// The memory will be shared, so must not be mutated. + unsafe fn clone(&mut self) -> Self; +} diff --git a/src/memory/leptonica_destroy.rs b/src/memory/leptonica_destroy.rs new file mode 100644 index 0000000..c4015e1 --- /dev/null +++ b/src/memory/leptonica_destroy.rs @@ -0,0 +1,12 @@ +/// Trait to define how leptonica frees this memory. +/// +/// Usually this would be to call a `destroy` method that decrements a reference count. +/// Sometimes a reference count isn't used, so this isn't called (eg `BorrowedFrom`). +pub trait LeptonicaDestroy { + /// Call to leptonica's internal structure-destroy method. + /// + /// # Safety + /// + /// The reference to the pointer must not be used after destroy is called. + unsafe fn destroy(&mut self); +} diff --git a/src/memory/mod.rs b/src/memory/mod.rs new file mode 100644 index 0000000..21784be --- /dev/null +++ b/src/memory/mod.rs @@ -0,0 +1,11 @@ +mod borrowed_from; +mod leptonica_clone; +mod leptonica_destroy; +mod ref_counted; +mod ref_counted_exclusive; + +pub use self::borrowed_from::BorrowedFrom; +pub use self::leptonica_clone::LeptonicaClone; +pub use self::leptonica_destroy::LeptonicaDestroy; +pub use self::ref_counted::RefCounted; +pub use self::ref_counted_exclusive::RefCountedExclusive; diff --git a/src/memory/ref_counted.rs b/src/memory/ref_counted.rs new file mode 100644 index 0000000..4cc3c63 --- /dev/null +++ b/src/memory/ref_counted.rs @@ -0,0 +1,34 @@ +use super::leptonica_destroy::LeptonicaDestroy; +use std::ops::Deref; + +/// A wrapper for ref counted leptonica pointers. +pub struct RefCounted { + inner: T, +} + +impl RefCounted { + /// Creates a new ref counted wrapper + /// + /// # Safety + /// + /// It must be safe for this wrapper to destroy (decrement the ref count). + /// The ref count must have already been incremented before being passed to `new`. + /// The pointer must not be mutated whilst this wrapper exists. + pub unsafe fn new(inner: T) -> Self { + Self { inner } + } +} + +impl Drop for RefCounted { + fn drop(&mut self) { + unsafe { self.inner.destroy() } + } +} + +impl Deref for RefCounted { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/src/memory/ref_counted_exclusive.rs b/src/memory/ref_counted_exclusive.rs new file mode 100644 index 0000000..0cf197f --- /dev/null +++ b/src/memory/ref_counted_exclusive.rs @@ -0,0 +1,49 @@ +use super::{leptonica_destroy::LeptonicaDestroy, LeptonicaClone, RefCounted}; +use std::ops::{Deref, DerefMut}; + +/// A wrapper for ref counted leptonica pointers that can be safely mutated. +/// +/// For example if it is the only reference. +pub struct RefCountedExclusive { + inner: T, +} + +impl RefCountedExclusive { + /// Creates a new ref counted exclusive wrapper + + /// # Safety + + /// It must be safe for this wrapper to destroy (decrement the ref count). + /// The ref count must have already been incremented before being passed to `new`. + /// The pointer must not be mutated whilst this wrapper exists, except via this wrapper. + pub unsafe fn new(inner: T) -> Self { + Self { inner } + } +} + +impl RefCountedExclusive { + /// Convert to a ref counted wrapper + pub fn to_ref_counted(mut self) -> RefCounted { + unsafe { RefCounted::new(self.inner.clone()) } + } +} + +impl Drop for RefCountedExclusive { + fn drop(&mut self) { + unsafe { self.inner.destroy() } + } +} + +impl Deref for RefCountedExclusive { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for RefCountedExclusive { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/src/pix.rs b/src/pix.rs index 8a135a6..fb9bb17 100644 --- a/src/pix.rs +++ b/src/pix.rs @@ -1,10 +1,11 @@ -extern crate leptonica_sys; -extern crate thiserror; +use leptonica_sys::{ + l_int32, pixClone, pixDestroy, pixGetHeight, pixGetWidth, pixRead, pixReadMem, +}; -use self::leptonica_sys::{pixDestroy, pixRead, pixReadMem}; -use self::thiserror::Error; +use crate::memory::{LeptonicaClone, LeptonicaDestroy, RefCountedExclusive}; use std::convert::{AsRef, TryInto}; use std::{ffi::CStr, num::TryFromIntError}; +use thiserror::Error; /// Wrapper around Leptonica's [`Pix`](https://tpgit.github.io/Leptonica/struct_pix.html) structure #[derive(Debug)] @@ -24,14 +25,6 @@ pub enum PixReadMemError { #[error("Pix::read returned null")] pub struct PixReadError(); -impl Drop for Pix { - fn drop(&mut self) { - unsafe { - pixDestroy(&mut self.0); - } - } -} - impl AsRef<*mut leptonica_sys::Pix> for Pix { fn as_ref(&self) -> &*mut leptonica_sys::Pix { &self.0 @@ -51,9 +44,7 @@ impl Pix { /// /// The pointer must be to a valid `Pix` struct. /// - /// The structure must not be mutated or freed outside of the Rust code. - /// - /// It must be safe for Rust to free the pointer. If this is not the case consider using [super::BorrowedPix::new]. + /// The structure must not be mutated or freed outside of the Rust code whilst this instance exists. pub unsafe fn new_from_pointer(ptr: *mut leptonica_sys::Pix) -> Self { Self(ptr) } @@ -61,51 +52,82 @@ impl Pix { /// Wrapper for [`pixRead`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#a84634846cbb5e01df667d6e9241dfc53) /// /// Read an image from a filename - pub fn read(filename: &CStr) -> Result { + pub fn read(filename: &CStr) -> Result, PixReadError> { let ptr = unsafe { pixRead(filename.as_ptr()) }; if ptr.is_null() { Err(PixReadError()) } else { - Ok(Self(ptr)) + Ok(unsafe { RefCountedExclusive::new(Self(ptr)) }) } } /// Wrapper for [`pixReadMem`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#a027a927dc3438192e3bdae8c219d7f6a) /// /// Read an image from memory - pub fn read_mem(img: &[u8]) -> Result { + pub fn read_mem(img: &[u8]) -> Result, PixReadMemError> { let ptr = unsafe { pixReadMem(img.as_ptr(), img.len().try_into()?) }; if ptr.is_null() { Err(PixReadMemError::NullPtr) } else { - Ok(Self(ptr)) + Ok(unsafe { RefCountedExclusive::new(Self(ptr)) }) } } -} -#[test] -fn read_error_test() { - let path = std::ffi::CString::new("fail").unwrap(); - assert!(Pix::read(&path).is_err()); + /// Wrapper for [`pixGetHeight`](https://tpgit.github.io/Leptonica/pix1_8c.html#ae40704b3acbd343639e9aed696da531f) + pub fn get_height(&self) -> l_int32 { + unsafe { pixGetHeight(self.0) } + } + + /// Wrapper for [`pixGetWidth`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#aa71e0b02548a56e723c76996ab145257) + pub fn get_width(&self) -> l_int32 { + unsafe { pixGetWidth(self.0) } + } } -#[test] -fn read_mem_error_test() { - assert_eq!(Pix::read_mem(&[]).err(), Some(PixReadMemError::NullPtr)); +impl LeptonicaDestroy for Pix { + unsafe fn destroy(&mut self) { + pixDestroy(&mut self.0); + } } -#[test] -fn read_test() { - let path = std::ffi::CString::new("image.png").unwrap(); - let pix = Pix::read(&path).unwrap(); - let lpix: &leptonica_sys::Pix = pix.as_ref(); - assert_eq!(lpix.w, 200); +impl LeptonicaClone for Pix { + unsafe fn clone(&mut self) -> Self { + Self::new_from_pointer(pixClone(self.0)) + } } -#[test] -fn read_memory_test() -> Result<(), Box> { - let pix = Pix::read_mem(include_bytes!("../image.png"))?; - let lpix: &leptonica_sys::Pix = pix.as_ref(); - assert_eq!(lpix.h, 23); - Ok(()) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_error_test() { + let path = std::ffi::CString::new("fail").unwrap(); + assert!(Pix::read(&path).is_err()); + } + + #[test] + fn read_mem_error_test() { + assert_eq!(Pix::read_mem(&[]).err(), Some(PixReadMemError::NullPtr)); + } + + #[test] + fn read_test() { + let path = std::ffi::CString::new("image.png").unwrap(); + let pix = Pix::read(&path).unwrap(); + assert_eq!(pix.get_width(), 200); + } + + #[test] + fn read_memory_test() { + let pix = Pix::read_mem(include_bytes!("../image.png")).unwrap(); + assert_eq!(pix.get_height(), 23); + } + + #[test] + fn clone_test() { + let pix = Pix::read_mem(include_bytes!("../image.png")).unwrap(); + let pix = pix.to_ref_counted(); + assert_eq!(pix.get_height(), 23); + } } diff --git a/src/pixa.rs b/src/pixa.rs index cf323ab..6e88f03 100644 --- a/src/pixa.rs +++ b/src/pixa.rs @@ -1,21 +1,16 @@ -extern crate leptonica_sys; -extern crate thiserror; - -use leptonica_sys::{pixaDestroy, pixaReadMultipageTiff}; -use std::ffi::CStr; +use crate::{ + memory::{LeptonicaDestroy, RefCounted, RefCountedExclusive}, + Pix, +}; +use leptonica_sys::{ + pixaDestroy, pixaGetCount, pixaGetPix, pixaReadMultipageTiff, L_CLONE, L_COPY, +}; +use std::{convert::TryInto, ffi::CStr}; /// Wrapper around Leptonica's [`Pixa`](https://tpgit.github.io/Leptonica/struct_pixa.html) structure #[derive(Debug, PartialEq)] pub struct Pixa(*mut leptonica_sys::Pixa); -impl Drop for Pixa { - fn drop(&mut self) { - unsafe { - pixaDestroy(&mut self.0); - } - } -} - impl AsRef for Pixa { fn as_ref(&self) -> &leptonica_sys::Pixa { unsafe { &*self.0 } @@ -34,34 +29,62 @@ impl Pixa { } /// Wrapper for [`pixaReadMultipageTiff`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#a4a52e686cf67f0e5bfda661fc3a3fb7b) - pub fn read_multipage_tiff(filename: &CStr) -> Option { + pub fn read_multipage_tiff(filename: &CStr) -> Option> { let ptr = unsafe { pixaReadMultipageTiff(filename.as_ptr()) }; if ptr.is_null() { None } else { - Some(Self(ptr)) + Some(unsafe { RefCountedExclusive::new(Self(ptr)) }) } } - /// Safely borrow the nth item - pub fn get_pix(&self, i: isize) -> Option { - let lpixa: &leptonica_sys::Pixa = self.as_ref(); - if lpixa.n <= std::convert::TryFrom::try_from(i).ok()? { - None - } else { - unsafe { Some(crate::BorrowedPix::new(*lpixa.pix.offset(i))) } + /// Wrapper for [`pixaGetCount`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#a098d32e94acc16c5d6e91a930a4b45ff) + pub fn get_count(&self) -> leptonica_sys::l_int32 { + unsafe { pixaGetCount(self.0) } + } + + /// Wrapper for [`pixaGetPix`](https://tpgit.github.io/Leptonica/leptprotos_8h.html#a3f62a77cf11114981267a6cf9918fc45) with copied `accessflag`: `L_COPY` + pub fn get_pix_copied( + &self, + index: leptonica_sys::l_int32, + ) -> Option> { + unsafe { + pixaGetPix(self.0, index, L_COPY.try_into().unwrap()) + .as_mut() + .map(|raw| RefCountedExclusive::new(Pix::new_from_pointer(raw))) + } + } + + pub fn get_pix_cloned(&self, index: leptonica_sys::l_int32) -> Option> { + unsafe { + pixaGetPix(self.0, index, L_CLONE.try_into().unwrap()) + .as_mut() + .map(|raw| RefCounted::new(Pix::new_from_pointer(raw))) } } } -#[test] -fn read_multipage_tiff_test() { - let pixa = - Pixa::read_multipage_tiff(CStr::from_bytes_with_nul(b"multipage.tiff\0").unwrap()).unwrap(); - assert_eq!(pixa.as_ref().n, 2); - assert_eq!(pixa.get_pix(0).unwrap().as_ref().w, 165); - assert_eq!(pixa.get_pix(0).unwrap().as_ref().h, 67); - assert_eq!(pixa.get_pix(1).unwrap().as_ref().w, 165); - assert_eq!(pixa.get_pix(1).unwrap().as_ref().h, 67); - assert_eq!(pixa.get_pix(2), None); +impl LeptonicaDestroy for Pixa { + unsafe fn destroy(&mut self) { + pixaDestroy(&mut self.0); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_multipage_tiff_test() { + let pixa = + Pixa::read_multipage_tiff(CStr::from_bytes_with_nul(b"multipage.tiff\0").unwrap()) + .unwrap(); + assert_eq!(pixa.get_count(), 2); + assert_eq!(pixa.get_pix_copied(0).unwrap().get_width(), 165); + assert_eq!(pixa.get_pix_cloned(0).unwrap().get_height(), 67); + assert_eq!(pixa.get_pix_copied(1).unwrap().get_width(), 165); + assert_eq!(pixa.get_pix_cloned(1).unwrap().get_height(), 67); + assert!(pixa.get_pix_copied(2).is_none()); + assert!(pixa.get_pix_cloned(2).is_none()); + } } diff --git a/src/str.rs b/src/str.rs new file mode 100644 index 0000000..9a65d43 --- /dev/null +++ b/src/str.rs @@ -0,0 +1,40 @@ +use self::leptonica_sys::free; +pub use leptonica_sys; +use libc::c_char; +use std::{ffi::CStr, str::Utf8Error}; + +/// Wrapper for heap allocated leptonica strings +#[derive(Debug)] +pub struct Str { + value: *mut c_char, +} + +impl Str { + /// Create a new Str from a heap allocated c-string pointer + /// + /// # Safety + /// + /// The pointer must be to a valid heap allocated c-string. + /// The data pointed at may not be mutated while held by + /// this struct except by this struct. + /// On drop, the pointer will be freed. + pub unsafe fn new_from_pointer(pointer: *mut c_char) -> Self { + Self { value: pointer } + } + + pub fn to_str(&self) -> Result<&str, Utf8Error> { + AsRef::::as_ref(self).to_str() + } +} + +impl AsRef for Str { + fn as_ref(&self) -> &CStr { + unsafe { CStr::from_ptr(self.value) } + } +} + +impl Drop for Str { + fn drop(&mut self) { + unsafe { free(self.value.cast()) } + } +}