From 1911455402a3039f56a72cc8befd60da3bc72711 Mon Sep 17 00:00:00 2001 From: Hicham Azimani Date: Sat, 9 Dec 2023 14:14:41 +0100 Subject: [PATCH] feat: Add support for Openslide 4.x (#118) --- .gitignore | 2 + .pre-commit-config.yaml | 4 +- Cargo.toml | 1 + benches/bench_read.rs | 24 ++++++++- src/bindings.rs | 99 ++++++++++++++++++++++++++++++++++++- src/cache.rs | 18 +++++++ src/lib.rs | 2 + src/properties/openslide.rs | 3 ++ src/wrapper.rs | 28 ++++++++++- tests/openslide.rs | 27 ++++++++++ 10 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 src/cache.rs diff --git a/.gitignore b/.gitignore index c7451e8..fcdb609 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # will have compiled files and executables /target/ +/tests/assets + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e13d8f9..42f6367 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.5.0 hooks: - id: check-byte-order-marker - id: check-case-conflict @@ -13,7 +13,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/pre-commit/pre-commit - rev: v2.13.0 + rev: v3.5.0 hooks: - id: validate_manifest - repo: https://github.com/doublify/pre-commit-rust diff --git a/Cargo.toml b/Cargo.toml index 3971ddf..c21c036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "MIT/Apache-2.0" default = ["deepzoom"] deepzoom = ["image"] image = ["dep:image", "dep:fast_image_resize"] +openslide4 = [] [dependencies] libc = "0.2" diff --git a/benches/bench_read.rs b/benches/bench_read.rs index a3e905c..73a67a9 100644 --- a/benches/bench_read.rs +++ b/benches/bench_read.rs @@ -1,5 +1,5 @@ use bencher::{benchmark_group, benchmark_main, Bencher}; -use openslide_rs::{traits::Slide, Address, DeepZoomGenerator, OpenSlide, Region, Size}; +use openslide_rs::{Address, DeepZoomGenerator, OpenSlide, Region, Size}; use std::{path::Path, sync::Arc}; fn openslide_read_region_256(bench: &mut Bencher) { @@ -66,6 +66,26 @@ fn deepzoom_read_image_512(bench: &mut Bencher) { bench.iter(|| dz.get_tile_rgb(12, Address { x: 0, y: 0 })); } +fn deepzoom_read_image_256_recreate_dz(bench: &mut Bencher) { + let slide = OpenSlide::new(Path::new("tests/assets/default.svs")).unwrap(); + + bench.iter(|| { + let dz: DeepZoomGenerator = + DeepZoomGenerator::new(&slide, 257, 0, false).unwrap(); + dz.get_tile_rgb(12, Address { x: 0, y: 0 }) + }); +} + +fn deepzoom_read_image_512_recreate_dz(bench: &mut Bencher) { + let slide = OpenSlide::new(Path::new("tests/assets/default.svs")).unwrap(); + + bench.iter(|| { + let dz: DeepZoomGenerator = + DeepZoomGenerator::new(&slide, 511, 0, false).unwrap(); + dz.get_tile_rgb(12, Address { x: 0, y: 0 }) + }); +} + fn deepzoom_read_image_256_arc(bench: &mut Bencher) { let slide = Arc::new(OpenSlide::new(Path::new("tests/assets/default.svs")).unwrap()); let dz: DeepZoomGenerator = DeepZoomGenerator::new(slide, 257, 0, false).unwrap(); @@ -94,6 +114,8 @@ benchmark_group!( deepzoom_image, deepzoom_read_image_256, deepzoom_read_image_512, + deepzoom_read_image_256_recreate_dz, + deepzoom_read_image_512_recreate_dz, deepzoom_read_image_256_arc, deepzoom_read_image_512_arc ); diff --git a/src/bindings.rs b/src/bindings.rs index 5a4ca42..5fcef80 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -11,7 +11,7 @@ use openslide_sys::sys; /// wrapper around OpenSlideT, this is usefull for implementing Send and Sync #[derive(Debug)] -pub(crate) struct OpenSlideWrapper(pub *mut sys::openslide_t); +pub(crate) struct OpenSlideWrapper(pub(crate) *mut sys::openslide_t); impl Deref for OpenSlideWrapper { type Target = *mut sys::openslide_t; @@ -24,6 +24,22 @@ impl Deref for OpenSlideWrapper { unsafe impl Send for OpenSlideWrapper {} unsafe impl Sync for OpenSlideWrapper {} +#[cfg(feature = "openslide4")] +#[derive(Debug)] +pub(crate) struct CacheWrapper(pub(crate) *mut sys::openslide_cache_t); + +#[cfg(feature = "openslide4")] +impl Deref for CacheWrapper { + type Target = *mut sys::openslide_cache_t; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "openslide4")] +unsafe impl Send for CacheWrapper {} + pub fn get_version() -> Result { let version = unsafe { sys::openslide_get_version() }; if !version.is_null() { @@ -256,3 +272,84 @@ pub fn get_error(osr: *mut sys::openslide_t) -> Result<()> { }; value } + +#[cfg(feature = "openslide4")] +pub fn get_icc_profile_size(osr: *mut sys::openslide_t) -> Result { + let size = unsafe { sys::openslide_get_icc_profile_size(osr) }; + // TODO: check if size == 0 => no ICC profile + if size == -1 { + get_error(osr)?; + return Err(OpenSlideError::CoreError( + "Cannot get ICC profile size".to_string(), + )); + } + Ok(size) +} + +#[cfg(feature = "openslide4")] +pub fn read_icc_profile(osr: *mut sys::openslide_t) -> Result> { + let size = get_icc_profile_size(osr)?; + let mut buffer: Vec = Vec::with_capacity(size as usize); + let p_buffer = buffer.as_mut_ptr() as *mut std::os::raw::c_void; + unsafe { + sys::openslide_read_icc_profile(osr, p_buffer); + get_error(osr)?; + buffer.set_len(size as usize); + } + Ok(buffer) +} + +#[cfg(feature = "openslide4")] +pub fn get_associated_image_icc_profile_size( + osr: *mut sys::openslide_t, + name: &str, +) -> Result { + let c_name = ffi::CString::new(name)?; + let size = + unsafe { sys::openslide_get_associated_image_icc_profile_size(osr, c_name.as_ptr()) }; + // TODO: check if size == 0 => no ICC profile + if size == -1 { + get_error(osr)?; + return Err(OpenSlideError::CoreError( + "Cannot get ICC profile size".to_string(), + )); + } + Ok(size) +} + +#[cfg(feature = "openslide4")] +pub fn read_associated_image_icc_profile( + osr: *mut sys::openslide_t, + name: &str, +) -> Result> { + let c_name = ffi::CString::new(name)?; + let size = get_associated_image_icc_profile_size(osr, name)?; + let mut buffer: Vec = Vec::with_capacity(size as usize); + let p_buffer = buffer.as_mut_ptr() as *mut std::os::raw::c_void; + unsafe { + sys::openslide_read_associated_image_icc_profile(osr, c_name.as_ptr(), p_buffer); + get_error(osr)?; + buffer.set_len(size as usize); + } + Ok(buffer) +} + +#[cfg(feature = "openslide4")] +pub fn cache_create(capacity: usize) -> Result<*mut sys::openslide_cache_t> { + let cache = unsafe { sys::openslide_cache_create(capacity) }; + if cache.is_null() { + Err(OpenSlideError::CoreError("Cannot create cache".to_string())) + } else { + Ok(cache) + } +} + +#[cfg(feature = "openslide4")] +pub fn set_cache(osr: *mut sys::openslide_t, cache: *mut sys::openslide_cache_t) { + unsafe { sys::openslide_set_cache(osr, cache) }; +} + +#[cfg(feature = "openslide4")] +pub fn cache_release(cache: *mut sys::openslide_cache_t) { + unsafe { sys::openslide_cache_release(cache) }; +} diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..6636304 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,18 @@ +use crate::{bindings, Result}; + +#[derive(Debug)] +pub(crate) struct Cache(pub(crate) bindings::CacheWrapper); + +impl Drop for Cache { + fn drop(&mut self) { + bindings::cache_release(*self.0); + } +} + +impl Cache { + /// Create a new cache with the given capacity + pub fn new(capacity: usize) -> Result { + let cache = bindings::CacheWrapper(bindings::cache_create(capacity)?); + Ok(Cache(cache)) + } +} diff --git a/src/lib.rs b/src/lib.rs index be2f7ad..f8ca321 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ use { }; mod bindings; +#[cfg(feature = "openslide4")] +mod cache; #[cfg(feature = "deepzoom")] pub mod deepzoom; pub mod errors; diff --git a/src/properties/openslide.rs b/src/properties/openslide.rs index ca99358..695c1c2 100644 --- a/src/properties/openslide.rs +++ b/src/properties/openslide.rs @@ -20,6 +20,7 @@ pub const OPENSLIDE_PROPERTY_NAME_BOUNDS_Y: &str = "openslide.bounds-y"; pub const OPENSLIDE_PROPERTY_NAME_BOUNDS_WIDTH: &str = "openslide.bounds-width"; pub const OPENSLIDE_PROPERTY_NAME_BOUNDS_HEIGHT: &str = "openslide.bounds-height"; pub const OPENSLIDE_PROPERTY_LEVEL_COUNT: &str = "openslide.level-count"; +pub const OPENSLIDE_PROPERTY_NAME_ICC_SIZE: &str = "openslide.icc-size"; const OPENSLIDE_PROPERTY_LEVEL_DOWNSAMPLE: &str = "downsample"; const OPENSLIDE_PROPERTY_LEVEL_HEIGHT: &str = "height"; @@ -52,6 +53,7 @@ pub struct OpenSlide { pub bounds_y: Option, pub bounds_width: Option, pub bounds_height: Option, + pub icc_profile_size: Option, pub background_color: Option, pub levels: Vec, } @@ -81,6 +83,7 @@ impl OpenSlide { OPENSLIDE_PROPERTY_LEVEL_COUNT => self.level_count = value.parse().ok(), OPENSLIDE_PROPERTY_NAME_BOUNDS_X => self.bounds_x = value.parse().ok(), OPENSLIDE_PROPERTY_NAME_BOUNDS_Y => self.bounds_y = value.parse().ok(), + OPENSLIDE_PROPERTY_NAME_ICC_SIZE => self.icc_profile_size = value.parse().ok(), OPENSLIDE_PROPERTY_NAME_BOUNDS_WIDTH => self.bounds_width = value.parse().ok(), OPENSLIDE_PROPERTY_NAME_BOUNDS_HEIGHT => self.bounds_height = value.parse().ok(), OPENSLIDE_PROPERTY_NAME_BACKGROUND_COLOR => { diff --git a/src/wrapper.rs b/src/wrapper.rs index c1dc020..520dfc6 100644 --- a/src/wrapper.rs +++ b/src/wrapper.rs @@ -10,6 +10,9 @@ use { image::{RgbImage, RgbaImage}, }; +#[cfg(feature = "openslide4")] +use crate::cache::Cache; + impl Drop for OpenSlide { fn drop(&mut self) { bindings::close(*self.osr) @@ -27,7 +30,8 @@ impl OpenSlide { /// This function can be expensive; avoid calling it unnecessarily. For example, a tile server /// should not create a new object on every tile request. Instead, it should maintain a cache /// of OpenSlide objects and reuse them when possible. - pub fn new(path: &Path) -> Result { + pub fn new>(path: T) -> Result { + let path = path.as_ref(); if !path.exists() { return Err(OpenSlideError::MissingFile(path.display().to_string())); } @@ -53,6 +57,18 @@ impl OpenSlide { }) } + #[cfg(feature = "openslide4")] + pub fn new_with_cache>(path: T, capacity: usize) -> Result { + let osr = OpenSlide::new(path)?; + osr.set_cache(Cache::new(capacity)?); + Ok(osr) + } + + #[cfg(feature = "openslide4")] + fn set_cache(&self, cache: Cache) { + bindings::set_cache(*self.osr, *cache.0) + } + /// Quickly determine whether a whole slide image is recognized. pub fn detect_vendor(path: &Path) -> Result { if !path.exists() { @@ -298,6 +314,16 @@ impl OpenSlide { Ok(image) } + + #[cfg(feature = "openslide4")] + pub fn icc_profile(&self) -> Result> { + bindings::read_icc_profile(*self.osr) + } + + #[cfg(feature = "openslide4")] + pub fn associated_image_icc_profile(&self, name: &str) -> Result> { + bindings::read_associated_image_icc_profile(*self.osr, name) + } } #[cfg(feature = "deepzoom")] diff --git a/tests/openslide.rs b/tests/openslide.rs index a203f9a..642d021 100644 --- a/tests/openslide.rs +++ b/tests/openslide.rs @@ -3,9 +3,16 @@ use fixture::{boxes_tiff, missing_file, small_svs, unopenable_tiff, unsupported_ use openslide_rs::{Address, OpenSlide, Region, Size}; use rstest::rstest; use std::path::Path; +use version_compare::Version; mod fixture; +#[rstest] +fn test_version() { + let version = OpenSlide::get_version().expect("Failed to get version"); + Version::from(&version).expect("Failed to parse version"); +} + #[rstest] #[should_panic(expected = "MissingFile(\"missing_file\")")] #[case(missing_file())] @@ -259,3 +266,23 @@ fn test_error_thumbnail(#[case] filename: &Path) { let size = Size { w: 100, h: 0 }; slide.thumbnail_rgb(&size).unwrap(); } + +#[rstest] +#[cfg(feature = "openslide4")] +#[case(small_svs())] +fn test_icc_profile(#[case] filename: &Path) { + let slide = OpenSlide::new(filename).unwrap(); + + let icc_profile = slide.icc_profile().unwrap(); + assert_eq!(icc_profile.len(), 0); +} + +#[rstest] +#[cfg(feature = "openslide4")] +#[case(small_svs())] +fn test_associated_image_icc_profile(#[case] filename: &Path) { + let slide = OpenSlide::new(filename).unwrap(); + + let icc_profile = slide.associated_image_icc_profile("thumbnail").unwrap(); + assert_eq!(icc_profile.len(), 0); +}