use super::*;
impl TiffPixelReader {
#[cfg(any(feature = "metal", feature = "cuda"))]
pub(super) fn ndpi_jpeg_batchable(&self, reqs: &[TileRequest]) -> Result<bool, WsiError> {
if reqs.is_empty() {
return Ok(false);
}
for req in reqs {
if !matches!(self.tile_source_for(req)?, TileSource::NdpiJpeg { .. }) {
return Ok(false);
}
}
Ok(true)
}
#[cfg(any(feature = "metal", feature = "cuda"))]
pub(super) fn decode_ndpi_jpeg_pixels(
&self,
reqs: &[TileRequest],
backend: BackendRequest,
require_device: bool,
metal_sessions: MetalBackendSessionsRef<'_>,
cuda_sessions: CudaBackendSessionsRef<'_>,
) -> Result<Vec<TilePixels>, WsiError> {
let mut jobs = Vec::with_capacity(reqs.len());
for req in reqs {
let source = self.tile_source_for(req)?;
let TileSource::NdpiJpeg {
ifd_id,
jpeg_header,
mcu_starts_tag,
tiles_across,
tiles_down,
strip_offset,
strip_byte_count,
..
} = source
else {
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "NDPI JPEG device batch received a non-NDPI tile source".into(),
});
};
jobs.push(self.ndpi_jpeg_decode_job(
req,
*ifd_id,
jpeg_header,
*mcu_starts_tag,
*tiles_across,
*tiles_down,
*strip_offset,
*strip_byte_count,
)?);
}
decode_batch_jpeg_pixels(
&jobs,
backend,
require_device,
metal_sessions,
cuda_sessions,
)
.into_iter()
.zip(reqs.iter())
.map(|(result, req)| {
result.map_err(|err| WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: err.to_string(),
})
})
.collect()
}
fn crop_ndpi_full_image_tile(
full_image: &CpuTile,
req: &TileRequest,
src_x: u32,
src_y: u32,
tile_w: u32,
tile_h: u32,
) -> Result<CpuTile, WsiError> {
let full_w = full_image.width as usize;
let channels = full_image.channels as usize;
let src_data = full_image.data.as_u8().ok_or_else(|| WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "expected U8 data in full decode cache".into(),
})?;
let tile_len = crate::core::limits::checked_product_to_usize(
&[u64::from(tile_w), u64::from(tile_h), channels as u64],
crate::core::limits::MAX_DECODED_IMAGE_BYTES,
"NDPI decoded tile",
)
.map_err(|reason| WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason,
})?;
let mut tile_data = Vec::with_capacity(tile_len);
for y in 0..tile_h {
let row_start = ((src_y + y) as usize * full_w + src_x as usize) * channels;
let row_end = row_start + tile_w as usize * channels;
if row_end > src_data.len() {
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "decoded image smaller than expected".into(),
});
}
tile_data.extend_from_slice(&src_data[row_start..row_end]);
}
if tile_data.len() != tile_len {
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "decoded NDPI tile length mismatch".into(),
});
}
Ok(CpuTile {
width: tile_w,
height: tile_h,
channels: full_image.channels,
color_space: ColorSpace::Rgb,
layout: CpuTileLayout::Interleaved,
data: CpuTileData::u8(tile_data),
})
}
pub(super) fn read_ndpi_full_decode_tile(
&self,
req: &TileRequest,
ifd_id: IfdId,
_jpeg_header: &[u8],
strip_offset: u64,
strip_byte_count: u64,
) -> Result<CpuTile, WsiError> {
let full_image =
self.get_or_decode_ndpi_full_image(req, ifd_id, strip_offset, strip_byte_count)?;
let level = &self.layout.dataset.scenes[req.scene.get()].series[req.series.get()].levels
[req.level.get() as usize];
let (level_w, level_h) = level.dimensions;
let (vtw, vth) = match &level.tile_layout {
TileLayout::WholeLevel {
virtual_tile_width,
virtual_tile_height,
..
} => (*virtual_tile_width, *virtual_tile_height),
_ => {
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "NdpiFullDecode expects WholeLevel tile layout".into(),
});
}
};
let (col_u32, row_u32) = validate_tile_coords(req.col, req.row, req.level.get())?;
let src_x = col_u32 * vtw;
let src_y = row_u32 * vth;
let tile_w = vtw.min((level_w as u32).saturating_sub(src_x));
let tile_h = vth.min((level_h as u32).saturating_sub(src_y));
if tile_w == 0 || tile_h == 0 {
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "tile has zero dimensions".into(),
});
}
Self::crop_ndpi_full_image_tile(full_image.as_ref(), req, src_x, src_y, tile_w, tile_h)
}
pub(super) fn read_ndpi_full_display_tile(
&self,
req: &TileViewRequest,
ifd_id: IfdId,
strip_offset: u64,
strip_byte_count: u64,
) -> Result<CpuTile, WsiError> {
let tile_req = TileRequest {
scene: req.scene.get().into(),
series: req.series.get().into(),
level: req.level.get().into(),
plane: req.plane,
col: req.col,
row: req.row,
};
let full_image =
self.get_or_decode_ndpi_full_image(&tile_req, ifd_id, strip_offset, strip_byte_count)?;
let level = &self.layout.dataset.scenes[req.scene.get()].series[req.series.get()].levels
[req.level.get() as usize];
let (level_w, level_h) = (level.dimensions.0 as i64, level.dimensions.1 as i64);
let tile_origin_x = req.col.saturating_mul(i64::from(req.tile_width));
let tile_origin_y = req.row.saturating_mul(i64::from(req.tile_height));
if tile_origin_x < 0
|| tile_origin_y < 0
|| tile_origin_x >= level_w
|| tile_origin_y >= level_h
{
return Err(WsiError::TileRead {
col: req.col,
row: req.row,
level: req.level.get(),
reason: "display tile origin out of bounds".into(),
});
}
let tile_w = req.tile_width.min((level_w - tile_origin_x) as u32);
let tile_h = req.tile_height.min((level_h - tile_origin_y) as u32);
Self::crop_ndpi_full_image_tile(
full_image.as_ref(),
&tile_req,
tile_origin_x as u32,
tile_origin_y as u32,
tile_w,
tile_h,
)
}
}