wsi-rs 0.5.1

wsi-rs whole-slide image reader
Documentation
use super::jpeg::read_vms_jpeg_header;
use super::model::invalid_slide;
use super::*;
use crate::formats::ini::{parse_ini_file, ParsedIni};

pub(super) const GROUP_VMS: &str = "Virtual Microscope Specimen";
pub(super) const KEY_MAP_FILE: &str = "MapFile";
pub(super) const KEY_IMAGE_FILE: &str = "ImageFile";
pub(super) const KEY_NUM_JPEG_COLS: &str = "NoJpegColumns";
pub(super) const KEY_NUM_JPEG_ROWS: &str = "NoJpegRows";
pub(super) const KEY_OPTIMISATION_FILE: &str = "OptimisationFile";
pub(super) const KEY_MACRO_IMAGE: &str = "MacroImage";
pub(super) const KEY_PHYSICAL_WIDTH: &str = "PhysicalWidth";
pub(super) const KEY_PHYSICAL_HEIGHT: &str = "PhysicalHeight";
pub(super) const KEY_SOURCE_LENS: &str = "SourceLens";
const KEY_FILE_MAX_SIZE: u64 = 64 << 10;

pub(super) fn parse_vms_ini(path: &Path) -> Result<ParsedIni, WsiError> {
    parse_ini_file(
        path,
        KEY_FILE_MAX_SIZE,
        |path| invalid_slide(path, "VMS key file too large"),
        false,
    )
}

pub(super) fn parse_u32(
    path: &Path,
    group: &HashMap<String, String>,
    key: &str,
) -> Result<u32, WsiError> {
    group
        .get(key)
        .ok_or_else(|| invalid_slide(path, format!("missing {key}")))?
        .parse::<u32>()
        .map_err(|_| invalid_slide(path, format!("invalid integer for {key}")))
}

pub(super) struct ImageDims {
    pub(super) layer: u32,
    pub(super) col: u32,
    pub(super) row: u32,
}

pub(super) fn parse_image_key_suffix(path: &Path, key: &str) -> Result<ImageDims, WsiError> {
    let suffix = &key[KEY_IMAGE_FILE.len()..];
    if suffix.is_empty() {
        return Ok(ImageDims {
            layer: 0,
            col: 0,
            row: 0,
        });
    }
    let trimmed = suffix
        .strip_prefix('(')
        .and_then(|value| value.strip_suffix(')'))
        .ok_or_else(|| invalid_slide(path, format!("invalid VMS image key suffix: {suffix}")))?;
    let parts: Vec<&str> = trimmed.split(',').map(str::trim).collect();
    match parts.as_slice() {
        [col, row] => Ok(ImageDims {
            layer: 0,
            col: col
                .parse()
                .map_err(|_| invalid_slide(path, format!("invalid VMS col in {key}")))?,
            row: row
                .parse()
                .map_err(|_| invalid_slide(path, format!("invalid VMS row in {key}")))?,
        }),
        [layer, col, row] => Ok(ImageDims {
            layer: layer
                .parse()
                .map_err(|_| invalid_slide(path, format!("invalid VMS layer in {key}")))?,
            col: col
                .parse()
                .map_err(|_| invalid_slide(path, format!("invalid VMS col in {key}")))?,
            row: row
                .parse()
                .map_err(|_| invalid_slide(path, format!("invalid VMS row in {key}")))?,
        }),
        _ => Err(invalid_slide(
            path,
            format!("unknown VMS image coordinate arity in {key}"),
        )),
    }
}

pub(super) fn parse_vms_opt_offsets(
    opt_path: Option<&Path>,
    image_paths: &[PathBuf],
) -> Result<Vec<Vec<Option<u64>>>, WsiError> {
    let Some(opt_path) = opt_path.filter(|path| path.is_file()) else {
        return Ok(vec![Vec::new(); image_paths.len()]);
    };

    let mut file = File::open(opt_path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: opt_path.to_path_buf(),
    })?;
    let mut per_image = Vec::with_capacity(image_paths.len());
    for image_path in image_paths {
        let geometry = jpeg_geometry_from_file(image_path)?;
        let tiles_down = geometry.height / geometry.tile_height;
        let mut row_starts = Vec::with_capacity(tiles_down as usize);
        let mut block = [0u8; 40];
        for _ in 0..tiles_down {
            match file.read_exact(&mut block) {
                Ok(()) => {
                    let offset = u64::from_le_bytes(block[..8].try_into().unwrap());
                    row_starts.push((offset > 0).then_some(offset));
                }
                Err(_) => {
                    return Ok(vec![Vec::new(); image_paths.len()]);
                }
            }
        }
        per_image.push(row_starts);
    }
    Ok(per_image)
}

fn jpeg_geometry_from_file(path: &Path) -> Result<JpegTileGeometry, WsiError> {
    Ok(read_vms_jpeg_header(path)?.geometry)
}