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