wsi-rs 0.4.0

wsi-rs whole-slide image reader
Documentation
use super::storage::{dataset_from_metadata, hex_encode, read_svcache};
use super::*;

impl SvcacheBackend {
    pub fn new() -> Self {
        Self
    }
}

impl FormatProbe for SvcacheBackend {
    fn probe(&self, path: &Path) -> Result<ProbeResult, WsiError> {
        let mut file = File::open(path)?;
        let mut magic = [0_u8; 8];
        if file.read_exact(&mut magic).is_err() {
            return Ok(ProbeResult {
                detected: false,
                vendor: "svcache".into(),
                confidence: ProbeConfidence::Likely,
            });
        }
        Ok(ProbeResult {
            detected: &magic == MAGIC,
            vendor: "svcache".into(),
            confidence: ProbeConfidence::Definite,
        })
    }
}

impl DatasetReader for SvcacheBackend {
    fn open(&self, path: &Path) -> Result<Box<dyn SlideReader>, WsiError> {
        let (file, payload_start, metadata) = read_svcache(path)?;
        let dataset = dataset_from_metadata(path, &metadata);
        let associated_index = metadata
            .associated
            .iter()
            .enumerate()
            .map(|(idx, assoc)| (assoc.name.clone(), idx))
            .collect();
        Ok(Box::new(SvcacheReader {
            file: Mutex::new(file),
            payload_start,
            metadata,
            dataset,
            associated_index,
        }))
    }
}

impl SlideReader for SvcacheReader {
    fn dataset(&self) -> &Dataset {
        &self.dataset
    }

    fn read_tile_cpu(&self, req: &TileRequest) -> Result<CpuTile, WsiError> {
        let tile = self.tile_meta(req)?;
        self.read_tile_meta(tile)
    }

    fn read_tiles(
        &self,
        reqs: &[TileRequest],
        output: TileOutputPreference,
    ) -> Result<Vec<TilePixels>, WsiError> {
        if matches!(output, TileOutputPreference::RequireDevice { .. }) {
            return Err(WsiError::Unsupported {
                reason: ".svcache device output is not implemented".into(),
            });
        }
        reqs.iter()
            .map(|req| self.read_tile_cpu(req).map(TilePixels::Cpu))
            .collect()
    }

    fn read_associated(&self, name: &str) -> Result<CpuTile, WsiError> {
        let idx = self
            .associated_index
            .get(name)
            .copied()
            .ok_or_else(|| WsiError::AssociatedImageNotFound(name.into()))?;
        self.read_tile_meta(&self.metadata.associated[idx].tile)
    }
}

impl SvcacheReader {
    fn tile_meta(&self, req: &TileRequest) -> Result<&TileMeta, WsiError> {
        let level = self
            .metadata
            .scenes
            .get(req.scene.get())
            .and_then(|scene| scene.series.get(req.series.get()))
            .and_then(|series| series.levels.get(req.level.get() as usize))
            .ok_or_else(|| WsiError::LevelOutOfRange {
                level: req.level.get(),
                count: 0,
            })?;
        if req.col < 0 || req.row < 0 {
            return Err(WsiError::TileRead {
                col: req.col,
                row: req.row,
                level: req.level.get(),
                reason: "negative .svcache tile coordinate".into(),
            });
        }
        let col = req.col as u64;
        let row = req.row as u64;
        if col >= level.tiles_across || row >= level.tiles_down {
            return Err(WsiError::TileRead {
                col: req.col,
                row: req.row,
                level: req.level.get(),
                reason: ".svcache tile coordinate out of range".into(),
            });
        }
        let idx = row
            .checked_mul(level.tiles_across)
            .and_then(|base| base.checked_add(col))
            .ok_or_else(|| WsiError::TileRead {
                col: req.col,
                row: req.row,
                level: req.level.get(),
                reason: ".svcache tile index overflow".into(),
            })?;
        level
            .tile_meta_for_index(idx)
            .ok_or_else(|| WsiError::TileRead {
                col: req.col,
                row: req.row,
                level: req.level.get(),
                reason: ".svcache tile not populated".into(),
            })
    }

    fn read_tile_meta(&self, tile: &TileMeta) -> Result<CpuTile, WsiError> {
        let mut encoded = vec![0_u8; tile.payload_len as usize];
        {
            let mut file = self.file.lock().unwrap_or_else(|e| e.into_inner());
            file.seek(SeekFrom::Start(self.payload_start + tile.payload_offset))?;
            file.read_exact(&mut encoded)?;
        }
        let actual_hash = hex_encode(&Sha256::digest(&encoded));
        if actual_hash != tile.sha256 {
            return Err(WsiError::InvalidSlide {
                path: PathBuf::from(&self.metadata.source.path),
                message: "svcache tile checksum mismatch".into(),
            });
        }
        let decoded = match tile.codec {
            PayloadCodec::Zstd => {
                zstd::bulk::decompress(&encoded, tile.decoded_len).map_err(|err| {
                    WsiError::Codec {
                        codec: "svcache-zstd",
                        source: Box::new(err),
                    }
                })?
            }
        };
        CpuTile::from_u8_interleaved(
            tile.width,
            tile.height,
            tile.channels,
            tile.color_space.into(),
            decoded,
        )
    }
}