statumen 0.3.0

Statumen whole-slide image reader
Documentation
#![allow(non_camel_case_types)]

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::path::Path;
use std::sync::OnceLock;

use libloading::{Library, Symbol};

#[repr(C)]
pub struct openslide_t {
    _private: [u8; 0],
}

type openslide_open_fn = unsafe extern "C" fn(filename: *const c_char) -> *mut openslide_t;
type openslide_close_fn = unsafe extern "C" fn(osr: *mut openslide_t);
type openslide_get_error_fn = unsafe extern "C" fn(osr: *mut openslide_t) -> *const c_char;
type openslide_read_region_fn = unsafe extern "C" fn(
    osr: *mut openslide_t,
    dest: *mut u32,
    x: i64,
    y: i64,
    level: i32,
    w: i64,
    h: i64,
);
type openslide_get_associated_image_names_fn =
    unsafe extern "C" fn(osr: *mut openslide_t) -> *const *const c_char;
type openslide_get_associated_image_dimensions_fn =
    unsafe extern "C" fn(osr: *mut openslide_t, name: *const c_char, w: *mut i64, h: *mut i64);

struct OpenSlideApi {
    _lib: Library,
    openslide_open: openslide_open_fn,
    openslide_close: openslide_close_fn,
    openslide_get_error: openslide_get_error_fn,
    openslide_read_region: openslide_read_region_fn,
    openslide_get_associated_image_names: openslide_get_associated_image_names_fn,
    openslide_get_associated_image_dimensions: openslide_get_associated_image_dimensions_fn,
}

unsafe fn load_symbol<T: Copy>(lib: &Library, symbol: &[u8]) -> Result<T, String> {
    let name = CStr::from_bytes_with_nul(symbol).expect("symbol name must be NUL terminated");
    let loaded: Symbol<'_, T> = unsafe { lib.get(symbol) }
        .map_err(|err| format!("dlsym({}) failed: {err}", name.to_string_lossy()))?;
    Ok(*loaded)
}

fn load_openslide_api() -> Result<OpenSlideApi, String> {
    let mut candidates = Vec::new();
    if let Some(path) = std::env::var_os("OPENSLIDE_LIB_PATH") {
        candidates.push(path.to_string_lossy().into_owned());
    }
    if let Some(path) = std::env::var_os("STATUMEN_OPENSLIDE_LIBRARY") {
        candidates.push(path.to_string_lossy().into_owned());
    }
    candidates.extend(
        [
            "/opt/homebrew/lib/libopenslide.1.dylib",
            "/opt/homebrew/lib/libopenslide.dylib",
            "/usr/local/lib/libopenslide.1.dylib",
            "/usr/local/lib/libopenslide.dylib",
            "libopenslide.1.dylib",
            "libopenslide.dylib",
            "libopenslide.so.1",
            "libopenslide.so",
            "libopenslide.dll",
            r"C:\Program Files\OpenSlide\bin\libopenslide.dll",
        ]
        .into_iter()
        .map(str::to_owned),
    );

    let mut errors = Vec::new();
    for candidate in candidates {
        let lib = match unsafe { Library::new(&candidate) } {
            Ok(lib) => lib,
            Err(err) => {
                errors.push(format!("{candidate}: {err}"));
                continue;
            }
        };

        let openslide_open = unsafe { load_symbol(&lib, b"openslide_open\0")? };
        let openslide_close = unsafe { load_symbol(&lib, b"openslide_close\0")? };
        let openslide_get_error = unsafe { load_symbol(&lib, b"openslide_get_error\0")? };
        let openslide_read_region = unsafe { load_symbol(&lib, b"openslide_read_region\0")? };
        let openslide_get_associated_image_names =
            unsafe { load_symbol(&lib, b"openslide_get_associated_image_names\0")? };
        let openslide_get_associated_image_dimensions =
            unsafe { load_symbol(&lib, b"openslide_get_associated_image_dimensions\0")? };

        return Ok(OpenSlideApi {
            _lib: lib,
            openslide_open,
            openslide_close,
            openslide_get_error,
            openslide_read_region,
            openslide_get_associated_image_names,
            openslide_get_associated_image_dimensions,
        });
    }

    Err(format!(
        "failed to load libopenslide; tried: {}",
        errors.join(" | ")
    ))
}

fn openslide_api() -> Result<&'static OpenSlideApi, String> {
    static API: OnceLock<Result<OpenSlideApi, String>> = OnceLock::new();
    API.get_or_init(load_openslide_api)
        .as_ref()
        .map_err(|e| e.clone())
}

pub struct OpenSlide {
    raw: *mut openslide_t,
    api: &'static OpenSlideApi,
}

impl OpenSlide {
    pub fn open(path: &Path) -> Result<Self, String> {
        let api = openslide_api()?;
        let cpath = CString::new(path.to_str().ok_or("path is not valid UTF-8")?.as_bytes())
            .map_err(|e| e.to_string())?;
        let raw = unsafe { (api.openslide_open)(cpath.as_ptr()) };
        if raw.is_null() {
            return Err("openslide_open returned NULL".into());
        }
        let err = unsafe { (api.openslide_get_error)(raw) };
        if !err.is_null() {
            let msg = unsafe { CStr::from_ptr(err) }
                .to_string_lossy()
                .into_owned();
            unsafe { (api.openslide_close)(raw) };
            return Err(format!("openslide error: {msg}"));
        }
        Ok(Self { raw, api })
    }

    pub fn associated_names(&self) -> Vec<String> {
        let names_ptr = unsafe { (self.api.openslide_get_associated_image_names)(self.raw) };
        if names_ptr.is_null() {
            return Vec::new();
        }
        let mut out = Vec::new();
        let mut idx = 0usize;
        loop {
            let ptr = unsafe { *names_ptr.add(idx) };
            if ptr.is_null() {
                break;
            }
            out.push(
                unsafe { CStr::from_ptr(ptr) }
                    .to_string_lossy()
                    .into_owned(),
            );
            idx += 1;
        }
        out
    }

    pub fn associated_dimensions(&self, name: &str) -> Result<(u32, u32), String> {
        let cname = CString::new(name).map_err(|e| e.to_string())?;
        let mut width = 0i64;
        let mut height = 0i64;
        unsafe {
            (self.api.openslide_get_associated_image_dimensions)(
                self.raw,
                cname.as_ptr(),
                &mut width,
                &mut height,
            )
        };
        self.check_error()?;
        let width = u32::try_from(width)
            .map_err(|_| format!("associated image width out of range for {name}: {width}"))?;
        let height = u32::try_from(height)
            .map_err(|_| format!("associated image height out of range for {name}: {height}"))?;
        Ok((width, height))
    }

    pub fn read_region_rgba(
        &self,
        x: i64,
        y: i64,
        level: i32,
        width: u32,
        height: u32,
    ) -> Result<Vec<u8>, String> {
        let mut argb = vec![0u32; width as usize * height as usize];
        unsafe {
            (self.api.openslide_read_region)(
                self.raw,
                argb.as_mut_ptr(),
                x,
                y,
                level,
                i64::from(width),
                i64::from(height),
            )
        };
        self.check_error()?;

        let mut rgba = Vec::with_capacity(argb.len() * 4);
        for pixel in argb {
            let a = ((pixel >> 24) & 0xFF) as u8;
            let r = ((pixel >> 16) & 0xFF) as u8;
            let g = ((pixel >> 8) & 0xFF) as u8;
            let b = (pixel & 0xFF) as u8;
            if a == 0 {
                rgba.extend_from_slice(&[0, 0, 0, 0]);
                continue;
            }

            let unpremultiply = |channel: u8| -> u8 {
                ((u16::from(channel) * 255 + u16::from(a) / 2) / u16::from(a)).min(255) as u8
            };
            rgba.push(unpremultiply(r));
            rgba.push(unpremultiply(g));
            rgba.push(unpremultiply(b));
            rgba.push(a);
        }
        Ok(rgba)
    }

    fn check_error(&self) -> Result<(), String> {
        let err = unsafe { (self.api.openslide_get_error)(self.raw) };
        if err.is_null() {
            Ok(())
        } else {
            Err(unsafe { CStr::from_ptr(err) }
                .to_string_lossy()
                .into_owned())
        }
    }
}

impl Drop for OpenSlide {
    fn drop(&mut self) {
        if !self.raw.is_null() {
            unsafe { (self.api.openslide_close)(self.raw) };
        }
    }
}