wsi-rs 0.4.0

wsi-rs whole-slide image reader
Documentation
use super::jpeg::read_vms_jpeg_header;
use super::model::invalid_slide;
use super::*;

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;

#[derive(Default)]
pub(super) struct ParsedIni {
    pub(super) groups: HashMap<String, HashMap<String, String>>,
}

pub(super) fn parse_vms_ini(path: &Path) -> Result<ParsedIni, WsiError> {
    let metadata = std::fs::metadata(path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    })?;
    if metadata.len() > KEY_FILE_MAX_SIZE {
        return Err(invalid_slide(path, "VMS key file too large"));
    }
    let text = std::fs::read_to_string(path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    })?;
    let mut parsed = ParsedIni::default();
    let mut current_group: Option<String> = None;
    for raw_line in text.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
            continue;
        }
        if let Some(group) = line
            .strip_prefix('[')
            .and_then(|line| line.strip_suffix(']'))
        {
            current_group = Some(group.to_string());
            parsed.groups.entry(group.to_string()).or_default();
            continue;
        }
        let Some(group) = current_group.as_ref() else {
            continue;
        };
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        parsed
            .groups
            .entry(group.clone())
            .or_default()
            .insert(key.trim().to_string(), value.trim().to_string());
    }
    Ok(parsed)
}

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)
}