Skip to content

Commit

Permalink
feat: Add support for Openslide 4.x (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
AzHicham authored Dec 9, 2023
1 parent 89bfe54 commit 1911455
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# will have compiled files and executables
/target/

/tests/assets

# These are backup files generated by rustfmt
**/*.rs.bk

Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 23 additions & 1 deletion benches/bench_read.rs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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<OpenSlide, _> =
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<OpenSlide, _> =
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<OpenSlide, _> = DeepZoomGenerator::new(slide, 257, 0, false).unwrap();
Expand Down Expand Up @@ -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
);
Expand Down
99 changes: 98 additions & 1 deletion src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> {
let version = unsafe { sys::openslide_get_version() };
if !version.is_null() {
Expand Down Expand Up @@ -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<i64> {
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<Vec<u8>> {
let size = get_icc_profile_size(osr)?;
let mut buffer: Vec<u8> = 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<i64> {
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<Vec<u8>> {
let c_name = ffi::CString::new(name)?;
let size = get_associated_image_icc_profile_size(osr, name)?;
let mut buffer: Vec<u8> = 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) };
}
18 changes: 18 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
@@ -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<Cache> {
let cache = bindings::CacheWrapper(bindings::cache_create(capacity)?);
Ok(Cache(cache))
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use {
};

mod bindings;
#[cfg(feature = "openslide4")]
mod cache;
#[cfg(feature = "deepzoom")]
pub mod deepzoom;
pub mod errors;
Expand Down
3 changes: 3 additions & 0 deletions src/properties/openslide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -52,6 +53,7 @@ pub struct OpenSlide {
pub bounds_y: Option<u32>,
pub bounds_width: Option<u32>,
pub bounds_height: Option<u32>,
pub icc_profile_size: Option<u32>,
pub background_color: Option<String>,
pub levels: Vec<LevelProperties>,
}
Expand Down Expand Up @@ -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 => {
Expand Down
28 changes: 27 additions & 1 deletion src/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<OpenSlide> {
pub fn new<T: AsRef<Path>>(path: T) -> Result<OpenSlide> {
let path = path.as_ref();
if !path.exists() {
return Err(OpenSlideError::MissingFile(path.display().to_string()));
}
Expand All @@ -53,6 +57,18 @@ impl OpenSlide {
})
}

#[cfg(feature = "openslide4")]
pub fn new_with_cache<T: AsRef<Path>>(path: T, capacity: usize) -> Result<OpenSlide> {
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<String> {
if !path.exists() {
Expand Down Expand Up @@ -298,6 +314,16 @@ impl OpenSlide {

Ok(image)
}

#[cfg(feature = "openslide4")]
pub fn icc_profile(&self) -> Result<Vec<u8>> {
bindings::read_icc_profile(*self.osr)
}

#[cfg(feature = "openslide4")]
pub fn associated_image_icc_profile(&self, name: &str) -> Result<Vec<u8>> {
bindings::read_associated_image_icc_profile(*self.osr, name)
}
}

#[cfg(feature = "deepzoom")]
Expand Down
27 changes: 27 additions & 0 deletions tests/openslide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())]
Expand Down Expand Up @@ -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);
}

1 comment on commit 1911455

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openslide-rs Benchmark

Benchmark suite Current: 1911455 Previous: 58a89ab Ratio
deepzoom_read_image_256 1174741 ns/iter (± 8226) 1174243 ns/iter (± 5732) 1.00
deepzoom_read_image_256_arc 1175096 ns/iter (± 9223) 1174068 ns/iter (± 6883) 1.00
deepzoom_read_image_256_recreate_dz 1176436 ns/iter (± 12406)
deepzoom_read_image_512 4728602 ns/iter (± 28274) 4709524 ns/iter (± 41606) 1.00
deepzoom_read_image_512_arc 4725596 ns/iter (± 50711) 4711445 ns/iter (± 51159) 1.00
deepzoom_read_image_512_recreate_dz 4724551 ns/iter (± 56379)
openslide_read_image_256 1139618 ns/iter (± 11657) 1136848 ns/iter (± 9235) 1.00
openslide_read_image_512 4738644 ns/iter (± 86330) 4726958 ns/iter (± 56626) 1.00
openslide_read_region_256 1087743 ns/iter (± 8899) 1088449 ns/iter (± 6957) 1.00
openslide_read_region_512 4531293 ns/iter (± 60344) 4525222 ns/iter (± 89830) 1.00

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.