use crate::error::Error; use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape}; use ndarray::Array2; use ome_metadata::{Ome, ome}; use serde::{Deserialize, Serialize}; use std::cell::{RefCell, RefMut}; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use thread_local::ThreadLocal; use tiff::decoder::{Decoder, DecodingResult}; use tiff::tags::Tag; #[derive(Debug, Serialize, Deserialize)] pub struct TiffReader { #[serde(skip)] reader: ThreadLocal>>, path: PathBuf, series: usize, position: usize, shape: Shape, pixel_type: PixelType, n_samples: usize, p_ndim: u8, planar: bool, interval_t: f64, pixel_size: Option, } impl From for DynReader { fn from(value: TiffReader) -> Self { DynReader::Tiff(value) } } impl Hash for TiffReader { fn hash(&self, state: &mut H) { self.path.hash(state); self.series.hash(state); self.position.hash(state); } } impl Clone for TiffReader { fn clone(&self) -> Self { TiffReader { reader: ThreadLocal::default(), path: self.path.clone(), series: self.series, position: self.position, shape: self.shape.clone(), pixel_type: self.pixel_type, n_samples: self.n_samples, p_ndim: self.p_ndim, planar: self.planar, interval_t: self.interval_t, pixel_size: self.pixel_size, } } } impl PartialEq for TiffReader { fn eq(&self, other: &Self) -> bool { self.path == other.path && self.series == other.series && self.position == other.position } } impl Eq for TiffReader {} impl TiffReader { fn get_reader(&self) -> Result>, Error> { self.reader .get_or_try(|| { let file = File::open(&self.path)?; Ok(RefCell::new(Decoder::new(file)?)) }) .map(|i| i.borrow_mut()) } } fn pixel_type_from_bits_sample(bits: u16, fmt: u16) -> Result { match (fmt, bits) { (1, 8) => Ok(PixelType::U8), (1, 16) => Ok(PixelType::U16), (1, 32) => Ok(PixelType::U32), (2, 8) => Ok(PixelType::I8), (2, 16) => Ok(PixelType::I16), (2, 32) => Ok(PixelType::I32), (3, 32) => Ok(PixelType::F32), (3, 64) => Ok(PixelType::F64), _ => Err(Error::Parse(format!( "unsupported pixel type: SampleFormat={}, BitsPerSample={}", fmt, bits ))), } } fn parse_imagej_metadata(s: &str) -> HashMap { let mut map = HashMap::new(); for line in s.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } if let Some(pos) = line.find('=') { let key = line[..pos].trim().to_string(); let val = line[pos + 1..].trim().to_string(); map.insert(key, val); } } map } impl Reader for TiffReader { fn new

(path: P, series: usize, position: usize) -> Result where P: AsRef, { let mut new = Self { reader: ThreadLocal::default(), path: path.as_ref().to_path_buf(), series, position, shape: Shape::default(), pixel_type: PixelType::U16, n_samples: 0, p_ndim: 0, planar: true, interval_t: 0.0, pixel_size: None, }; let mut reader = new.get_reader()?; let width = reader.get_tag(Tag::ImageWidth)?.into_u32()? as usize; let height = reader.get_tag(Tag::ImageLength)?.into_u32()? as usize; let n_samples = reader .get_tag(Tag::SamplesPerPixel) .ok() .and_then(|t| t.into_u16().ok()) .unwrap_or(1) as usize; let bits = tag_first_u16(&mut reader, Tag::BitsPerSample).unwrap_or(8); let sfmt = tag_first_u16(&mut reader, Tag::SampleFormat).unwrap_or(1); let pixtype = pixel_type_from_bits_sample(bits, sfmt)?; let planar = reader .get_tag(Tag::PlanarConfiguration) .ok() .and_then(|t| t.into_u16().ok()) .unwrap_or(1) == 2; let imagej_bytes = reader .get_tag(Tag::Unknown(50839)) .or_else(|_| reader.get_tag(Tag::ImageDescription))?; let imagej_bytes = match imagej_bytes { tiff::decoder::ifd::Value::Ascii(s) => s.into_bytes(), other => other.into_u8_vec()?, }; let metadata_str = String::from_utf8_lossy(&imagej_bytes); let metadata_map = parse_imagej_metadata(&metadata_str); let p_ndim = if n_samples > 1 { 3u8 } else { 2u8 }; let size_c = if p_ndim == 3 { n_samples } else { metadata_map .get("channels") .and_then(|v| v.parse::().ok()) .unwrap_or(1) }; let size_z = metadata_map .get("slices") .and_then(|v| v.parse::().ok()) .unwrap_or(1); let size_t = metadata_map .get("frames") .and_then(|v| v.parse::().ok()) .unwrap_or(1); let interval_t = metadata_map .get("interval") .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let ru = reader .get_tag(Tag::ResolutionUnit) .ok() .and_then(|i| i.into_u16().ok()) .unwrap_or(2); let pixel_size = reader .get_tag(Tag::XResolution) .ok() .and_then(|i| i.into_f64().ok()) .and_then(|i| { if i <= 0.0 { None } else if ru == 3 { Some(1e4 / i) } else if ru == 2 { Some(25400.0 / i) } else { Some(1.0 / i) } }); let mult = if p_ndim == 3 { n_samples } else { 1 }; let expected = size_c * size_z * size_t / mult; let mut n_ifds = 1; while reader.more_images() { reader.next_image()?; n_ifds += 1; } let size_z = if n_ifds != expected && n_ifds > 0 { n_ifds / (size_c * size_t / mult).max(1) } else { size_z }; drop(reader); new.shape = Shape { c: size_c, z: size_z, t: size_t, y: height, x: width, ..Default::default() }; new.pixel_type = pixtype; new.n_samples = n_samples; new.p_ndim = p_ndim; new.interval_t = interval_t; new.pixel_size = pixel_size; new.planar = planar; // test reading first frame, if error another reader (bioformats) can take over new.get_frame(0, 0, 0)?; Ok(new) } fn metadata(&self) -> Result { let ome_pixel_type = match self.pixel_type { PixelType::I8 => ome::PixelType::Int8, PixelType::U8 => ome::PixelType::Uint8, PixelType::I16 => ome::PixelType::Int16, PixelType::U16 => ome::PixelType::Uint16, PixelType::I32 => ome::PixelType::Int32, PixelType::U32 => ome::PixelType::Uint32, PixelType::F32 => ome::PixelType::Float, PixelType::F64 => ome::PixelType::Double, _ => ome::PixelType::Uint16, }; let mut pixels = ome::Pixels { id: "Pixels:0".to_string(), size_x: self.shape.x as i32, size_y: self.shape.y as i32, size_z: self.shape.z as i32, size_c: self.shape.c as i32, size_t: self.shape.t as i32, dimension_order: ome::PixelsDimensionOrderType::Xyczt, r#type: ome_pixel_type, physical_size_x: self.pixel_size.map(|v| v as f32), physical_size_x_unit: ome::UnitsLength::um, physical_size_y: self.pixel_size.map(|v| v as f32), physical_size_y_unit: ome::UnitsLength::um, physical_size_z: None, physical_size_z_unit: ome::UnitsLength::um, time_increment: None, time_increment_unit: ome::UnitsTime::s, significant_bits: None, interleaved: None, big_endian: None, channel: Vec::new(), bin_data: Vec::new(), tiff_data: Vec::new(), metadata_only: None, plane: Vec::new(), }; for c in 0..self.shape.c { pixels.channel.push(ome::Channel { id: format!("Channel:{}", c), name: Some(format!("Channel {}", c)), ..Default::default() }); } for c in 0..self.shape.c { for z in 0..self.shape.z { for t in 0..self.shape.t { pixels.plane.push(ome::Plane { the_c: Some(c as i32), the_z: Some(z as i32), the_t: Some(t as i32), delta_t: if self.interval_t > 0.0 { Some(t as f32 * self.interval_t as f32) } else { None }, ..Default::default() }); } } } let mut ome = Ome::default(); ome.image.push(ome::Image { id: "Image:0".to_string(), name: self .path .file_name() .map(|n| n.to_string_lossy().to_string()), pixels, instrument_ref: None, objective_settings: None, acquisition_date: None, description: None, experimenter_ref: None, experiment_ref: None, experimenter_group_ref: None, imaging_environment: None, stage_label: None, roi_ref: Vec::new(), microbeam_manipulation_ref: Vec::new(), annotation_ref: Vec::new(), }); Ok(ome) } fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { let (page_idx, offset, stride) = if self.p_ndim == 3 { (z * self.shape.t + t, c, self.n_samples) } else { (c + z * self.shape.c + t * self.shape.c * self.shape.z, 0, 1) }; let mut reader = self.get_reader()?; reader.seek_to_image(page_idx)?; let data: DecodingResult = if self.planar { let mut result = DecodingResult::U16(vec![]); reader.read_image_to_buffer(&mut result)?; result } else { reader.read_image()? }; macro_rules! to_frame { ($var:ident, $ty:ty) => {{ let v: Vec<$ty> = match data { DecodingResult::$var(v) => v, _ => { return Err(Error::Parse(format!( "type mismatch: expected {}", stringify!($var) ))); } }; let yx = self.shape.y * self.shape.x; if self.planar { let chan: Vec<$ty> = v.into_iter().skip(c * yx).take(yx).collect(); Ok(ArrayT::$var(Array2::from_shape_vec( (self.shape.y, self.shape.x), chan, )?)) } else if stride > 1 { let chan: Vec<$ty> = v .into_iter() .skip(offset) .step_by(stride) .take(yx) .collect(); Ok(ArrayT::$var(Array2::from_shape_vec( (self.shape.y, self.shape.x), chan, )?)) } else { Ok(ArrayT::$var(Array2::from_shape_vec( (self.shape.y, self.shape.x), v, )?)) } }}; } match self.pixel_type { PixelType::U8 => to_frame!(U8, u8), PixelType::U16 => to_frame!(U16, u16), PixelType::U32 => to_frame!(U32, u32), PixelType::U64 => to_frame!(U64, u64), PixelType::I8 => to_frame!(I8, i8), PixelType::I16 => to_frame!(I16, i16), PixelType::I32 => to_frame!(I32, i32), PixelType::I64 => to_frame!(I64, i64), PixelType::F32 => to_frame!(F32, f32), PixelType::F64 => to_frame!(F64, f64), _ => Err(Error::NotImplemented(format!( "unsupported TIFF pixel type: {:?}", self.pixel_type ))), } } fn path(&self) -> &Path { &self.path } fn series(&self) -> usize { self.series } fn position(&self) -> usize { self.position } fn shape(&self) -> &Shape { &self.shape } fn pixel_type(&self) -> &PixelType { &self.pixel_type } fn get_available_positions

(_path: P, _series: usize) -> Result, Error> where P: AsRef, { Ok(HashSet::from([0])) } fn get_available_series

(_path: P) -> Result, Error> where P: AsRef, { Ok(HashSet::from([0])) } } fn tag_first_u16(reader: &mut RefMut>, tag: Tag) -> Option { let val = reader.get_tag(tag).ok()?; use tiff::decoder::ifd::Value; match val { Value::Short(v) => Some(v), Value::List(list) => list.first().and_then(|v| match v { Value::Short(v) => Some(*v), _ => None, }), _ => None, } } #[cfg(test)] mod tests { use super::*; fn open(file: &str) -> Result { let path = std::env::current_dir()? .join("tests") .join("files") .join(file); TiffReader::new(&path, 0, 0) } macro_rules! test_metadata { ($($name:ident: $file:expr $(,)?)*) => { $( #[test] fn $name() -> Result<(), Error> { let ts = open($file)?; println!("{}", ts.view().squeeze()?.summary()?); Ok(()) } )* }; } test_metadata! { // metadata_a: "tiff/20251014_20-Pos_000_000_mask.tif", metadata_b: "tiff/20251014_20-Pos_000_000_max.tif", metadata_c: "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif", metadata_e: "tiff/1xp53-01-AP1.tiff", metadata_f: "tiff/test.tif", // metadata_g: "tiff/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif", } }