use image::DynamicImage;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek};
use std::path::Path;
use tiff::decoder::Decoder;
use tiff::decoder::ifd::Value;
use tiff::tags::Tag;
use crate::Level;
use crate::error::WsiError;
use crate::filter::TissueMask;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Photometric {
WhiteIsZero,
BlackIsZero,
RGB,
Palette,
TransparencyMask,
CMYK,
YCbCr,
CIELab,
}
impl Photometric {
pub fn jpeg_color_transform(self) -> jpeg_decoder::ColorTransform {
match self {
Self::RGB => jpeg_decoder::ColorTransform::RGB,
Self::YCbCr => jpeg_decoder::ColorTransform::YCbCr,
_ => jpeg_decoder::ColorTransform::RGB,
}
}
}
impl TryFrom<&Value> for Photometric {
type Error = WsiError;
fn try_from(value: &Value) -> Result<Self, Self::Error> {
let code = match value {
Value::Short(v) => *v,
_ => return Err(WsiError::UnsupportedFormat),
};
match code {
0 => Ok(Self::WhiteIsZero),
1 => Ok(Self::BlackIsZero),
2 => Ok(Self::RGB),
3 => Ok(Self::Palette),
4 => Ok(Self::TransparencyMask),
5 => Ok(Self::CMYK),
6 => Ok(Self::YCbCr),
8 => Ok(Self::CIELab),
_ => Err(WsiError::UnsupportedFormat),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TileDirectory {
offsets: Vec<u64>,
byte_counts: Vec<u64>,
jpeg_tables: Vec<u8>,
photometric: Photometric,
}
impl TileDirectory {
pub fn read_tile_directory(
decoder: &mut Decoder<BufReader<File>>,
) -> Result<TileDirectory, WsiError> {
let tile_offsets = decoder
.find_tag(Tag::TileOffsets)?
.ok_or(WsiError::UnsupportedFormat)?;
let byte_counts = decoder
.find_tag(Tag::TileByteCounts)?
.ok_or(WsiError::UnsupportedFormat)?;
let jpeg_tables = decoder
.find_tag(Tag::JPEGTables)?
.ok_or(WsiError::UnsupportedFormat)?;
let photometric = decoder
.find_tag(Tag::PhotometricInterpretation)?
.ok_or(WsiError::UnsupportedFormat)?;
let photometric = Photometric::try_from(&photometric)?;
let tile_offsets = read_typed_list(&tile_offsets, |v| match v {
Value::Unsigned(v) => Some(*v as u64),
_ => None,
})?;
let byte_counts = read_typed_list(&byte_counts, |v| match v {
Value::Unsigned(v) => Some(*v as u64),
_ => None,
})?;
let jpeg_tables = read_typed_list(&jpeg_tables, |v| match v {
Value::Byte(v) => Some(*v),
_ => None,
})?;
if tile_offsets.len() != byte_counts.len() {
return Err(WsiError::InvalidMetadata);
}
Ok(TileDirectory {
offsets: tile_offsets,
byte_counts,
jpeg_tables,
photometric,
})
}
pub fn jpeg_tables(&self) -> &[u8] {
&self.jpeg_tables
}
pub fn jpeg_reader<'a>(&'a self, tile: &'a [u8]) -> impl Read + 'a {
Cursor::new(&self.jpeg_tables[..self.jpeg_tables.len() - 2]).chain(Cursor::new(&tile[2..]))
}
pub fn reconstruct_jpeg(&self, tile: &[u8]) -> Result<Vec<u8>, WsiError> {
let mut jpeg = Vec::new();
self.jpeg_reader(tile).read_to_end(&mut jpeg)?;
Ok(jpeg)
}
pub fn get_tile_location_by_index(&self, index: usize) -> Option<(u64, u64)> {
Some((*self.offsets.get(index)?, *self.byte_counts.get(index)?))
}
pub fn photometric(&self) -> &Photometric {
&self.photometric
}
pub fn get_tile_location_by_coord(
&self,
tile_x: u32,
tile_y: u32,
level: &Level,
) -> Result<(u64, u64), WsiError> {
let index = self.tile_index(tile_x, tile_y, level)?;
self.get_tile_location_by_index(index)
.ok_or(WsiError::TileIndexOutOfBounds)
}
fn tile_index(&self, tile_x: u32, tile_y: u32, level: &Level) -> Result<usize, WsiError> {
if tile_x >= level.tiles_x() || tile_y >= level.tiles_y() {
return Err(WsiError::TileIndexOutOfBounds);
}
Ok((tile_y * level.tiles_x() + tile_x) as usize)
}
pub fn tile_count(&self) -> usize {
self.offsets.len()
}
}
pub struct Tile {
image: DynamicImage,
level: usize,
tile_x: u32,
tile_y: u32,
}
impl Tile {
pub fn new(image: DynamicImage, level: usize, tile_x: u32, tile_y: u32) -> Tile {
Tile {
image,
level,
tile_x,
tile_y,
}
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), WsiError> {
self.image
.save(path)
.map_err(|_| WsiError::UnsupportedFormat)
}
pub fn tile_x(&self) -> u32 {
self.tile_x
}
pub fn tile_y(&self) -> u32 {
self.tile_y
}
pub fn image(&self) -> &DynamicImage {
&self.image
}
pub fn level(&self) -> usize {
self.level
}
pub fn tissue_mask(&self) -> TissueMask {
TissueMask::compute(&self.image)
}
pub fn tissue_mask_with_threshold(&self, threshold: u8) -> TissueMask {
TissueMask::compute_with_threshold(&self.image, threshold)
}
pub fn normalize_stain(&self) -> Tile {
Tile::new(
crate::stain::normalize_reinhard(&self.image),
self.level,
self.tile_x,
self.tile_y,
)
}
pub fn resize(&self, width: u32, height: u32) -> Tile {
Tile::new(
self.image
.resize_exact(width, height, image::imageops::FilterType::Triangle),
self.level,
self.tile_x,
self.tile_y,
)
}
pub fn encode_jpeg(&self) -> Result<Vec<u8>, WsiError> {
let mut buf = Vec::new();
self.image
.write_to(
&mut std::io::Cursor::new(&mut buf),
image::ImageFormat::Jpeg,
)
.map_err(|_| WsiError::UnsupportedFormat)?;
Ok(buf)
}
}
fn read_typed_list<T>(
value: &Value,
extract: impl Fn(&Value) -> Option<T>,
) -> Result<Vec<T>, WsiError> {
match value {
Value::List(values) => values
.iter()
.map(|v| extract(v).ok_or(WsiError::UnsupportedFormat))
.collect(),
_ => Err(WsiError::UnsupportedFormat),
}
}
pub fn read_tile_bytes(
reader: &mut BufReader<File>,
offset: u64,
byte_count: u64,
) -> Result<Vec<u8>, WsiError> {
let delta = offset as i64 - reader.stream_position()? as i64;
reader.seek_relative(delta)?;
let mut buffer = vec![0; byte_count as usize];
reader.read_exact(&mut buffer[..])?;
Ok(buffer)
}
#[cfg(unix)]
pub fn read_tile_bytes_at(file: &File, offset: u64, byte_count: u64) -> Result<Vec<u8>, WsiError> {
use std::os::unix::fs::FileExt;
let mut buffer = vec![0; byte_count as usize];
file.read_exact_at(&mut buffer, offset)?;
Ok(buffer)
}
#[cfg(windows)]
pub fn read_tile_bytes_at(file: &File, offset: u64, byte_count: u64) -> Result<Vec<u8>, WsiError> {
use std::os::windows::fs::FileExt;
let mut buffer = vec![0; byte_count as usize];
let mut read = 0;
while read < buffer.len() {
let n = file.seek_read(&mut buffer[read..], offset + read as u64)?;
if n == 0 {
return Err(WsiError::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"unexpected end of file while reading tile",
)));
}
read += n;
}
Ok(buffer)
}