wsi-rs 0.5.2

wsi-rs whole-slide image reader
Documentation
use super::model::{RawReadWindow, ZviCompression, ZviPlane, ZviSlide};
use super::*;
use crate::core::limits::{checked_product_to_usize, read_to_end_bounded, MAX_DECODED_IMAGE_BYTES};

impl ZviSlide {
    pub(super) fn read_plane_window(
        &self,
        plane_index: usize,
        x: u32,
        y: u32,
        w: u32,
        h: u32,
    ) -> Result<CpuTile, WsiError> {
        let plane = &self.planes[plane_index];
        if x > plane.width
            || y > plane.height
            || x.saturating_add(w) > plane.width
            || y.saturating_add(h) > plane.height
        {
            return Err(WsiError::TileRead {
                col: 0,
                row: 0,
                level: 0u32,
                reason: "ZVI plane window out of bounds".into(),
            });
        }

        match plane.compression {
            ZviCompression::Raw => self.read_raw_plane_window(plane, x, y, w, h),
            ZviCompression::Zlib => self.read_zlib_plane_window(plane, x, y, w, h),
            ZviCompression::Jpeg => self.read_jpeg_plane_window(plane, x, y, w, h),
        }
    }

    fn read_raw_plane_window(
        &self,
        plane: &ZviPlane,
        x: u32,
        y: u32,
        w: u32,
        h: u32,
    ) -> Result<CpuTile, WsiError> {
        match plane.bytes_per_sample {
            1 => {
                let len = zvi_image_len(w, h, 1)?;
                let mut samples = vec![0u8; len];
                self.read_raw_rows(
                    plane,
                    RawReadWindow {
                        x,
                        y,
                        width: w,
                        height: h,
                        bytes_per_sample: 1,
                    },
                    &mut samples,
                )?;
                CpuTile::new(
                    w,
                    h,
                    1,
                    ColorSpace::Grayscale,
                    CpuTileLayout::Interleaved,
                    CpuTileData::u8(samples),
                )
            }
            2 => {
                let row_len = zvi_image_len(w, 1, 2)?;
                let sample_len = zvi_image_len(w, h, 1)?;
                let mut row_bytes = vec![0u8; row_len];
                let mut samples = vec![0u16; sample_len];
                let mut compound = self.compound.lock().unwrap_or_else(|e| e.into_inner());
                let mut stream = compound.open_stream(&plane.stream_path)?;
                for row in 0..h {
                    let src_offset = plane
                        .payload_offset
                        .checked_add(
                            (u64::from(y + row) * u64::from(plane.width) + u64::from(x)) * 2,
                        )
                        .ok_or_else(|| {
                            WsiError::DisplayConversion("ZVI raw row offset overflow".into())
                        })?;
                    stream.seek(SeekFrom::Start(src_offset))?;
                    stream.read_exact(&mut row_bytes)?;
                    let dst = row as usize * w as usize;
                    for (slot, bytes) in samples[dst..dst + w as usize]
                        .iter_mut()
                        .zip(row_bytes.chunks_exact(2))
                    {
                        *slot = u16::from_le_bytes([bytes[0], bytes[1]]);
                    }
                }
                CpuTile::new(
                    w,
                    h,
                    1,
                    ColorSpace::Grayscale,
                    CpuTileLayout::Interleaved,
                    CpuTileData::u16(samples),
                )
            }
            other => Err(WsiError::Unsupported {
                reason: format!("unsupported ZVI raw sample byte depth {other}"),
            }),
        }
    }

    fn read_raw_rows(
        &self,
        plane: &ZviPlane,
        window: RawReadWindow,
        destination: &mut [u8],
    ) -> Result<(), WsiError> {
        let mut compound = self.compound.lock().unwrap_or_else(|e| e.into_inner());
        let mut stream = compound.open_stream(&plane.stream_path)?;
        let row_bytes = zvi_image_len(window.width, 1, window.bytes_per_sample)?;
        for row in 0..window.height {
            let src_offset = plane
                .payload_offset
                .checked_add(
                    (u64::from(window.y + row) * u64::from(plane.width) + u64::from(window.x))
                        * window.bytes_per_sample,
                )
                .ok_or_else(|| WsiError::DisplayConversion("ZVI raw row offset overflow".into()))?;
            let dst = row as usize * row_bytes;
            stream.seek(SeekFrom::Start(src_offset))?;
            stream.read_exact(&mut destination[dst..dst + row_bytes])?;
        }
        Ok(())
    }

    fn read_zlib_plane_window(
        &self,
        plane: &ZviPlane,
        x: u32,
        y: u32,
        w: u32,
        h: u32,
    ) -> Result<CpuTile, WsiError> {
        let compressed = self.read_plane_payload(plane)?;
        let mut decoder = ZlibDecoder::new(compressed.as_slice());
        let expected_len =
            zvi_image_len(plane.width, plane.height, u64::from(plane.bytes_per_sample))?;
        let decompressed =
            read_to_end_bounded(&mut decoder, expected_len as u64, "ZVI inflated plane")?;
        if decompressed.len() != expected_len {
            return Err(WsiError::InvalidSlide {
                path: PathBuf::from(&plane.stream_path),
                message: format!(
                    "ZVI inflated plane has {} bytes, expected {expected_len}",
                    decompressed.len()
                ),
            });
        }
        crop_decoded_zvi_plane(plane, &decompressed, x, y, w, h)
    }

    fn read_jpeg_plane_window(
        &self,
        plane: &ZviPlane,
        x: u32,
        y: u32,
        w: u32,
        h: u32,
    ) -> Result<CpuTile, WsiError> {
        let jpeg = self.read_plane_payload(plane)?;
        let decoded = crate::core::batch::exactly_one(
            decode_batch_jpeg(&[JpegDecodeJob {
                data: std::borrow::Cow::Borrowed(jpeg.as_slice()),
                tables: None,
                expected_width: plane.width,
                expected_height: plane.height,
                color_transform: j2k_jpeg::ColorTransform::Auto,
                force_dimensions: false,
                requested_size: None,
            }]),
            "ZVI JPEG decode",
        )??;
        crop_interleaved_tile(&decoded, x, y, w, h)
    }

    fn read_plane_payload(&self, plane: &ZviPlane) -> Result<Vec<u8>, WsiError> {
        let mut compound = self.compound.lock().unwrap_or_else(|e| e.into_inner());
        let mut stream = compound.open_stream(&plane.stream_path)?;
        stream.seek(SeekFrom::Start(plane.payload_offset))?;
        let mut payload = vec![
            0;
            usize::try_from(plane.payload_length).map_err(|_| {
                WsiError::DisplayConversion("ZVI payload length exceeds address space".into())
            })?
        ];
        stream.read_exact(&mut payload)?;
        Ok(payload)
    }
}

fn zvi_image_len(width: u32, height: u32, bytes_per_pixel: u64) -> Result<usize, WsiError> {
    checked_product_to_usize(
        &[u64::from(width), u64::from(height), bytes_per_pixel],
        MAX_DECODED_IMAGE_BYTES,
        "ZVI image",
    )
    .map_err(WsiError::DisplayConversion)
}

fn crop_decoded_zvi_plane(
    plane: &ZviPlane,
    data: &[u8],
    x: u32,
    y: u32,
    w: u32,
    h: u32,
) -> Result<CpuTile, WsiError> {
    match plane.bytes_per_sample {
        1 => {
            let mut samples = vec![0u8; zvi_image_len(w, h, 1)?];
            for row in 0..h as usize {
                let src = ((y as usize + row) * plane.width as usize + x as usize)
                    .checked_mul(plane.bytes_per_sample as usize)
                    .ok_or_else(|| {
                        WsiError::DisplayConversion("ZVI decoded offset overflow".into())
                    })?;
                let dst = row * w as usize;
                samples[dst..dst + w as usize].copy_from_slice(&data[src..src + w as usize]);
            }
            CpuTile::new(
                w,
                h,
                1,
                ColorSpace::Grayscale,
                CpuTileLayout::Interleaved,
                CpuTileData::u8(samples),
            )
        }
        2 => {
            let mut samples = vec![0u16; zvi_image_len(w, h, 1)?];
            for row in 0..h as usize {
                let src = ((y as usize + row) * plane.width as usize + x as usize)
                    .checked_mul(2)
                    .ok_or_else(|| {
                        WsiError::DisplayConversion("ZVI decoded offset overflow".into())
                    })?;
                let dst = row * w as usize;
                for (slot, bytes) in samples[dst..dst + w as usize]
                    .iter_mut()
                    .zip(data[src..src + w as usize * 2].chunks_exact(2))
                {
                    *slot = u16::from_le_bytes([bytes[0], bytes[1]]);
                }
            }
            CpuTile::new(
                w,
                h,
                1,
                ColorSpace::Grayscale,
                CpuTileLayout::Interleaved,
                CpuTileData::u16(samples),
            )
        }
        other => Err(WsiError::Unsupported {
            reason: format!("unsupported ZVI decoded sample byte depth {other}"),
        }),
    }
}

fn crop_interleaved_tile(
    src: &CpuTile,
    x: u32,
    y: u32,
    width: u32,
    height: u32,
) -> Result<CpuTile, WsiError> {
    if src.layout != CpuTileLayout::Interleaved {
        return Err(WsiError::DisplayConversion(
            "cannot crop planar ZVI JPEG tile".into(),
        ));
    }
    let channels = src.channels as usize;
    let source = src
        .data
        .as_u8()
        .ok_or_else(|| WsiError::DisplayConversion("ZVI JPEG decoded to non-u8 samples".into()))?;
    let mut out = vec![0u8; zvi_image_len(width, height, channels as u64)?];
    for row in 0..height as usize {
        let src_offset = ((y as usize + row) * src.width as usize + x as usize) * channels;
        let dst_offset = row * width as usize * channels;
        let len = width as usize * channels;
        out[dst_offset..dst_offset + len].copy_from_slice(&source[src_offset..src_offset + len]);
    }
    CpuTile::new(
        width,
        height,
        src.channels,
        src.color_space.clone(),
        CpuTileLayout::Interleaved,
        CpuTileData::u8(out),
    )
}

#[cfg(test)]
mod limit_tests {
    use super::zvi_image_len;

    #[test]
    fn zvi_image_len_rejects_oversized_planes() {
        assert_eq!(zvi_image_len(10, 20, 2).unwrap(), 400);
        assert!(zvi_image_len(u32::MAX, u32::MAX, 4).is_err());
    }
}