use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::common::error::{BioFormatsError, Result};
use crate::common::metadata::{DimensionOrder, ImageMetadata, MetadataValue, ModuloAnnotation};
use crate::common::pixel_type::PixelType;
use crate::common::reader::FormatReader;
use crate::common::region::crop_full_plane;
fn czi_pixel_type(code: i32) -> std::io::Result<(PixelType, u32)> {
match code {
0 => Ok((PixelType::Uint8, 1)), 1 => Ok((PixelType::Uint16, 1)), 2 => Ok((PixelType::Float32, 1)), 3 => Ok((PixelType::Uint8, 3)), 4 => Ok((PixelType::Uint16, 3)), 8 => Ok((PixelType::Float32, 3)), 9 => Ok((PixelType::Uint8, 4)), 10 => Ok((PixelType::Float32, 2)), 11 => Ok((PixelType::Float32, 2)), 12 => Ok((PixelType::Uint32, 1)), 13 => Ok((PixelType::Float64, 1)), other => Err(czi_invalid_data(format!(
"CZI unsupported pixel type code {other}"
))),
}
}
const SEG_HEADER: usize = 32;
fn read_seg_type(data: &[u8]) -> String {
let end = data[..16].iter().position(|&b| b == 0).unwrap_or(16);
String::from_utf8_lossy(&data[..end]).into_owned()
}
fn read_i32(data: &[u8], off: usize) -> i32 {
i32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
fn read_i64(data: &[u8], off: usize) -> i64 {
i64::from_le_bytes(data[off..off + 8].try_into().unwrap_or([0; 8]))
}
fn read_u64(data: &[u8], off: usize) -> u64 {
u64::from_le_bytes(data[off..off + 8].try_into().unwrap_or([0; 8]))
}
fn read_seg_sizes(data: &[u8]) -> (u64, u64) {
let allocated = read_u64(data, 16);
let mut used = read_u64(data, 24);
if used == 0 {
used = allocated;
}
(allocated, used)
}
fn valid_segment_position(pos: u64, file_len: u64) -> bool {
pos > 0 && pos.saturating_add(SEG_HEADER as u64) <= file_len
}
#[derive(Debug, Clone)]
struct DirEntry {
pixel_type: i32,
file_position: i64,
compression: i32,
dims: HashMap<String, (i32, i32)>, stored: HashMap<String, i32>, }
impl DirEntry {
fn dim_start(&self, name: &str) -> i32 {
self.dims.get(name).map(|&(start, _)| start).unwrap_or(0)
}
fn dim_size(&self, name: &str) -> i32 {
self.dims.get(name).map(|&(_, size)| size).unwrap_or(1)
}
fn has_dim(&self, name: &str) -> bool {
self.dims.contains_key(name)
}
fn dim_stored_size(&self, name: &str) -> i32 {
match self.stored.get(name) {
Some(&s) if s > 0 => s,
_ => self.dim_size(name),
}
}
fn matches_plane(&self, z: u32, c: u32, t: u32) -> bool {
self.dims
.get("Z")
.map(|&(s, _)| s as u32 == z)
.unwrap_or(z == 0)
&& self
.dims
.get("C")
.map(|&(s, _)| s as u32 == c)
.unwrap_or(c == 0)
&& self
.dims
.get("T")
.map(|&(s, _)| s as u32 == t)
.unwrap_or(t == 0)
}
}
#[derive(Debug, Clone)]
struct CziResolution {
r: i32,
width: u32,
height: u32,
}
#[derive(Debug, Clone)]
struct CziSeries {
scene: Option<i32>,
acquisition: Option<i32>,
angle: Option<i32>,
mosaic: Option<i32>,
pixel_type_index: usize,
palm_size: Option<(u32, u32)>,
resolutions: Vec<CziResolution>,
}
fn parse_dir_entry(data: &[u8]) -> DirEntry {
let pixel_type = read_i32(data, 2);
let file_position = read_i64(data, 6);
let compression = read_i32(data, 18);
let dim_count = read_i32(data, 28) as usize;
let mut dims: HashMap<String, (i32, i32)> = HashMap::new();
let mut stored: HashMap<String, i32> = HashMap::new();
let dim_array_start = 32;
for i in 0..dim_count {
let off = dim_array_start + i * 20;
if off + 20 > data.len() {
break;
}
let dim_name = std::str::from_utf8(&data[off..off + 4])
.unwrap_or("")
.trim_end_matches('\0')
.trim()
.to_string();
let start = read_i32(data, off + 4);
let size = read_i32(data, off + 8);
let stored_size = read_i32(data, off + 16);
if !dim_name.is_empty() {
dims.insert(dim_name.clone(), (start, size));
stored.insert(dim_name, stored_size);
}
}
DirEntry {
pixel_type,
file_position,
compression,
dims,
stored,
}
}
fn czi_invalid_data(message: impl Into<String>) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, message.into())
}
fn parse_directory_entries(data: &[u8], entry_count: usize) -> std::io::Result<Vec<DirEntry>> {
let mut entries = Vec::with_capacity(entry_count);
if entry_count == 0 {
return Ok(entries);
}
let fixed_stride = if entry_count
.checked_mul(256)
.is_some_and(|bytes| data.len() >= bytes)
{
Some(256)
} else {
None
};
let mut off = 0usize;
for entry_index in 0..entry_count {
if off + 32 > data.len() {
return Err(czi_invalid_data(format!(
"CZI directory entry {entry_index} is truncated before its fixed header"
)));
}
let dim_count = read_i32(data, off + 28).max(0) as usize;
let compact_len = 32usize
.checked_add(dim_count.checked_mul(20).ok_or_else(|| {
czi_invalid_data(format!(
"CZI directory entry {entry_index} dimension table size overflows"
))
})?)
.ok_or_else(|| {
czi_invalid_data(format!(
"CZI directory entry {entry_index} dimension table size overflows"
))
})?;
let entry_len = fixed_stride.unwrap_or(compact_len);
let compact_end = off.checked_add(compact_len).ok_or_else(|| {
czi_invalid_data(format!(
"CZI directory entry {entry_index} offset overflows"
))
})?;
if compact_end > data.len() {
return Err(czi_invalid_data(format!(
"CZI directory entry {entry_index} is truncated: need {compact_len} bytes, have {}",
data.len() - off
)));
}
let parse_len = entry_len.min(data.len() - off);
entries.push(parse_dir_entry(&data[off..off + parse_len]));
off = off.checked_add(entry_len).ok_or_else(|| {
czi_invalid_data(format!(
"CZI directory entry {entry_index} offset overflows"
))
})?;
}
Ok(entries)
}
struct CziParsed {
meta_xml: String,
entries: Vec<DirEntry>,
z_count: u32,
c_count: u32,
t_count: u32,
pixel_type: PixelType,
spp: u32,
series: Vec<CziSeries>,
pixel_types: Vec<i32>,
prestitched: bool,
modulo_z: Option<ModuloAnnotation>,
modulo_c: Option<ModuloAnnotation>,
modulo_t: Option<ModuloAnnotation>,
rotations: i32,
illuminations: i32,
phases: i32,
rotation_axis: bool,
palm: bool,
}
#[derive(Default)]
struct DimCounts {
positions: i32, acquisitions: i32, angles: i32, mosaics: i32, rotations: i32, illuminations: i32, phases: i32, rotation_axis: bool,
min_scene: i32,
min_acq: i32,
min_angle: i32,
min_mosaic: i32,
}
fn parse_czi_file(f: &mut BufReader<File>) -> std::io::Result<CziParsed> {
let file_len = f.get_ref().metadata()?.len();
let mut hdr = vec![0u8; SEG_HEADER];
f.read_exact(&mut hdr)?;
let seg_type = read_seg_type(&hdr);
if !seg_type.starts_with("ZISRAWFILE") {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Not a CZI file",
));
}
let mut fh = vec![0u8; 80];
f.read_exact(&mut fh)?;
let dir_position = read_u64(&fh, 36);
let meta_position = read_u64(&fh, 44);
let dir_position = if valid_segment_position(dir_position, file_len) {
dir_position
} else {
0
};
let meta_position = if valid_segment_position(meta_position, file_len) {
meta_position
} else {
0
};
let mut meta_xml = String::new();
if meta_position > 0 {
f.seek(SeekFrom::Start(meta_position))?;
let mut seg_hdr = vec![0u8; SEG_HEADER];
f.read_exact(&mut seg_hdr)?;
let mut meta_body_hdr = vec![0u8; 256];
f.read_exact(&mut meta_body_hdr)?;
let xml_size = read_i32(&meta_body_hdr, 0) as usize;
if xml_size > 0 {
let mut xml_bytes = vec![0u8; xml_size];
f.read_exact(&mut xml_bytes)?;
meta_xml = String::from_utf8_lossy(&xml_bytes).into_owned();
}
}
let mut entries: Vec<DirEntry> = Vec::new();
if dir_position > 0 {
f.seek(SeekFrom::Start(dir_position))?;
let mut seg_hdr = vec![0u8; SEG_HEADER];
f.read_exact(&mut seg_hdr)?;
let (allocated_size, used_size) = read_seg_sizes(&seg_hdr);
let mut dir_hdr = vec![0u8; 128];
f.read_exact(&mut dir_hdr)?;
let entry_count = read_i32(&dir_hdr, 0) as usize;
let body_size = used_size.max(allocated_size).saturating_sub(128);
let remaining = file_len.saturating_sub(f.stream_position()?);
let body_size = body_size.min(remaining);
if body_size > 0 {
let mut entry_bytes = vec![0u8; body_size as usize];
f.read_exact(&mut entry_bytes)?;
entries = parse_directory_entries(&entry_bytes, entry_count)?;
}
}
let parsed = build_dimensions(meta_xml, entries)?;
Ok(parsed)
}
fn build_dimensions(meta_xml: String, entries: Vec<DirEntry>) -> std::io::Result<CziParsed> {
if entries.is_empty() {
return Err(czi_invalid_data("CZI directory contains no subblocks"));
}
let mut max_z = 0i32;
let mut max_c = 0i32;
let mut max_z_size = 0i32;
let mut unique_t: std::collections::HashSet<i32> = std::collections::HashSet::new();
let mut first_pixel_type = 0i32;
let mut pixel_types: Vec<i32> = Vec::new();
let mut c = DimCounts {
positions: 1,
acquisitions: 1,
angles: 1,
mosaics: 1,
rotations: 1,
illuminations: 1,
phases: 1,
rotation_axis: false,
min_scene: 0,
min_acq: 0,
min_angle: 0,
min_mosaic: 0,
};
let (mut min_s, mut max_s) = (i32::MAX, i32::MIN);
let (mut min_b, mut max_b) = (i32::MAX, i32::MIN);
let (mut min_v, mut max_v) = (i32::MAX, i32::MIN);
let (mut min_m, mut max_m) = (i32::MAX, i32::MIN);
let (mut min_r, mut max_r) = (i32::MAX, i32::MIN);
let (mut min_rot, mut max_rot) = (i32::MAX, i32::MIN);
let mut max_r_size = 0i32;
let mut r_level_xsize: HashMap<i32, i32> = HashMap::new();
let (mut min_i, mut max_i) = (i32::MAX, i32::MIN);
let (mut min_h, mut max_h) = (i32::MAX, i32::MIN);
for e in &entries {
if pixel_types.is_empty() {
first_pixel_type = e.pixel_type;
}
if !pixel_types.contains(&e.pixel_type) {
pixel_types.push(e.pixel_type);
}
if let Some(&(start, size)) = e.dims.get("Z") {
if start > 0 && start > max_z {
max_z = start;
} else if size > max_z_size {
max_z_size = size;
}
}
if let Some(&(start, _)) = e.dims.get("C") {
if start > max_c {
max_c = start;
}
}
if let Some(&(start, size)) = e.dims.get("T") {
for i in start..start + size.max(1) {
unique_t.insert(i);
}
}
if let Some(&(start, size)) = e.dims.get("R") {
min_r = min_r.min(start);
max_r = max_r.max(start + 1);
min_rot = min_rot.min(start);
max_rot = max_rot.max(start + size);
max_r_size = max_r_size.max(size);
let x_size = e.dim_size("X").max(0);
let cur = r_level_xsize.entry(start).or_insert(x_size);
*cur = (*cur).max(x_size);
}
if let Some(&(start, _)) = e.dims.get("S") {
min_s = min_s.min(start);
max_s = max_s.max(start);
}
if let Some(&(start, size)) = e.dims.get("I") {
min_i = min_i.min(start);
max_i = max_i.max(start + size);
}
if let Some(&(start, _)) = e.dims.get("B") {
min_b = min_b.min(start);
max_b = max_b.max(start);
}
if let Some(&(start, _)) = e.dims.get("M") {
min_m = min_m.min(start);
max_m = max_m.max(start);
}
if let Some(&(start, size)) = e.dims.get("H") {
min_h = min_h.min(start);
max_h = max_h.max(start + size);
}
if let Some(&(start, _)) = e.dims.get("V") {
min_v = min_v.min(start);
max_v = max_v.max(start);
}
}
if max_s != i32::MIN {
c.positions = (max_s - min_s + 1).max(1);
c.min_scene = min_s;
}
if max_b != i32::MIN {
c.acquisitions = (max_b + 1).max(1);
c.min_acq = min_b;
}
if max_v != i32::MIN {
c.angles = (max_v + 1).max(1);
c.min_angle = min_v;
}
if max_m != i32::MIN {
c.mosaics = (max_m + 1).max(1);
c.min_mosaic = min_m;
}
let distinct_r_levels = r_level_xsize.len();
let all_r_same_xsize = {
let mut sizes = r_level_xsize.values().copied();
match sizes.next() {
Some(first) => sizes.all(|s| s == first),
None => true,
}
};
let rotation_axis = max_r_size > 1 || (distinct_r_levels > 1 && all_r_same_xsize);
if rotation_axis && max_rot != i32::MIN && min_rot != i32::MAX {
c.rotations = (max_rot - min_rot).max(1);
c.rotation_axis = true;
}
let _ = min_r;
if max_i != i32::MIN && min_i != i32::MAX {
c.illuminations = (max_i - min_i).max(1);
}
if max_h != i32::MIN && min_h != i32::MAX {
c.phases = (max_h - min_h).max(1);
}
let mut z_count = ((max_z + 1) as u32).max(max_z_size.max(0) as u32);
let mut c_count = (max_c + 1) as u32;
let mut t_count = (unique_t.len() as u32).max(1);
let (pt, spp) = czi_pixel_type(first_pixel_type)?;
let mut modulo_z = None;
let mut modulo_c = None;
let mut modulo_t = None;
let rotation_labels = parse_modulo_labels(&meta_xml, "Rotations");
let illumination_labels = parse_modulo_labels(&meta_xml, "Illuminations");
let phase_labels = parse_modulo_labels(&meta_xml, "Phases");
if c.rotations > 1 {
let mut end = (z_count as i32 * (c.rotations - 1)) as f64;
if !rotation_labels.is_empty() {
end = 0.0;
}
modulo_z = Some(ModuloAnnotation {
parent_dimension: "Z".into(),
modulo_type: "rotation".into(),
start: 0.0,
step: z_count as f64,
end,
unit: String::new(),
labels: rotation_labels,
});
z_count *= c.rotations as u32;
}
if c.illuminations > 1 {
let mut end = (c_count as i32 * (c.illuminations - 1)) as f64;
if !illumination_labels.is_empty() {
end = 0.0;
}
modulo_c = Some(ModuloAnnotation {
parent_dimension: "C".into(),
modulo_type: "illumination".into(),
start: 0.0,
step: c_count as f64,
end,
unit: String::new(),
labels: illumination_labels,
});
c_count *= c.illuminations as u32;
}
if c.phases > 1 {
let mut end = (t_count as i32 * (c.phases - 1)) as f64;
if !phase_labels.is_empty() {
end = 0.0;
}
modulo_t = Some(ModuloAnnotation {
parent_dimension: "T".into(),
modulo_type: "phase".into(),
start: 0.0,
step: t_count as f64,
end,
unit: String::new(),
labels: phase_labels,
});
t_count *= c.phases as u32;
}
let max_resolution = if !c.rotation_axis && max_r != i32::MIN {
(max_r - 1).max(0)
} else {
0
};
let mut prestitched = false;
let mosaics_as_series = if max_resolution == 0 {
c.mosaics > 1 && c.mosaics_exposed_as_series()
} else {
prestitched = true;
false
};
if c.mosaics > 1 && !mosaics_as_series {
prestitched = true;
}
let image_count_full = z_count * c_count * t_count;
let scan_dim: u32 = 1;
let plane_count = entries.len() as u32;
let mut series_count = (c.positions * c.acquisitions * c.angles).max(1);
if max_resolution == 0 {
series_count *= c.mosaics.max(1);
}
let mut collapse_all = false;
let mut position_collapsed = false;
if max_resolution == 0 && plane_count > 0 {
let lhs = image_count_full as u64 * series_count as u64;
let rhs = plane_count as u64 * scan_dim as u64;
if lhs > rhs {
let sz = z_count.max(1);
if plane_count != image_count_full
&& plane_count != t_count
&& (plane_count % (series_count as u32 * sz)) == 0
{
if c.positions > 1
&& plane_count == (image_count_full * series_count as u32) / c.positions as u32
{
series_count /= c.positions;
c.positions = 1;
position_collapsed = true;
}
} else if plane_count == t_count || plane_count == image_count_full || c.positions > 1 {
c.positions = 1;
c.acquisitions = 1;
c.mosaics = 1;
c.angles = 1;
series_count = 1;
collapse_all = true;
} else if series_count > c.mosaics && c.mosaics > 1 && prestitched {
series_count /= c.mosaics;
c.mosaics = 1;
}
}
}
if prestitched
&& max_resolution == 0
&& series_count == (c.mosaics.max(1) * c.positions.max(1))
&& series_count != c.positions
{
series_count = c.positions.max(1);
c.mosaics = 1;
}
let _ = series_count;
let original_c = c_count;
let pixel_type_count = pixel_types.len().max(1);
if pixel_type_count > 1 {
c_count = (original_c / pixel_type_count as u32).max(1);
}
let mosaic_factor = if mosaics_as_series { c.mosaics } else { 1 };
let mut series: Vec<CziSeries> = Vec::new();
for ptype in 0..pixel_type_count {
for s_idx in 0..c.positions {
for b_idx in 0..c.acquisitions {
for v_idx in 0..c.angles {
for m_idx in 0..mosaic_factor {
let scene = if !collapse_all && !position_collapsed && max_s != i32::MIN {
Some(c.min_scene + s_idx)
} else {
None
};
let acquisition = if !collapse_all && max_b != i32::MIN {
Some(c.min_acq + b_idx)
} else {
None
};
let angle = if !collapse_all && max_v != i32::MIN {
Some(c.min_angle + v_idx)
} else {
None
};
let mosaic = if mosaics_as_series {
Some(c.min_mosaic + m_idx)
} else {
None
};
let resolutions = compute_resolutions(
&entries,
scene,
acquisition,
angle,
mosaic,
prestitched,
c.rotation_axis,
);
series.push(CziSeries {
scene,
acquisition,
angle,
mosaic,
pixel_type_index: ptype,
palm_size: None,
resolutions,
});
}
}
}
}
}
if series.is_empty() {
series.push(CziSeries {
scene: None,
acquisition: None,
angle: None,
mosaic: None,
pixel_type_index: 0,
palm_size: None,
resolutions: vec![CziResolution {
r: 0,
width: 0,
height: 0,
}],
});
}
let image_count = z_count * c_count * t_count;
let mut palm = false;
if entries.len() <= 2 && image_count <= 2 && check_palm(&meta_xml) {
let mut sizes: Vec<(u32, u32)> = Vec::new();
for e in &entries {
let sx = e.dim_stored_size("X").max(0) as u32;
let sy = e.dim_stored_size("Y").max(0) as u32;
if !sizes.contains(&(sx, sy)) {
sizes.push((sx, sy));
}
}
if sizes.len() == 2 {
palm = true;
c_count = 1;
series.clear();
for &(sx, sy) in &sizes {
series.push(CziSeries {
scene: None,
acquisition: None,
angle: None,
mosaic: None,
pixel_type_index: 0,
palm_size: Some((sx, sy)),
resolutions: vec![CziResolution {
r: 0,
width: sx,
height: sy,
}],
});
}
}
}
Ok(CziParsed {
meta_xml,
entries,
z_count: z_count.max(1),
c_count: c_count.max(1),
t_count: t_count.max(1),
pixel_type: pt,
spp,
series,
pixel_types: if pixel_types.is_empty() {
vec![first_pixel_type]
} else {
pixel_types
},
prestitched,
modulo_z,
modulo_c,
modulo_t,
rotations: c.rotations,
illuminations: c.illuminations,
phases: c.phases,
rotation_axis: c.rotation_axis,
palm,
})
}
fn check_palm(xml: &str) -> bool {
if xml.is_empty() {
return false;
}
let lower = xml.to_ascii_lowercase();
let mut search_from = 0usize;
while let Some(rel) = lower[search_from..].find("<lsmtag") {
let tag_start = search_from + rel;
let tag_end = lower[tag_start..]
.find('>')
.map(|e| tag_start + e)
.unwrap_or(lower.len());
let tag = &lower[tag_start..tag_end];
if let Some(npos) = tag.find("name=") {
let rest = &tag[npos + 5..];
let trimmed = rest.trim_start_matches(['"', '\'']);
if trimmed.starts_with("palm") {
return true;
}
}
search_from = tag_end.max(tag_start + 1);
}
if let Some(pos) = lower.find("<palmslider>") {
let value_start = pos + "<palmslider>".len();
if let Some(rel_end) = lower[value_start..].find("</palmslider>") {
let value = lower[value_start..value_start + rel_end].trim();
if value == "true" {
return true;
}
}
}
false
}
fn parse_modulo_labels(xml: &str, name: &str) -> Vec<String> {
if xml.is_empty() {
return Vec::new();
}
let open_needle = format!("<{}>", name);
let close_needle = format!("</{}>", name);
if let Some(start) = xml.find(&open_needle) {
let value_start = start + open_needle.len();
if let Some(rel_end) = xml[value_start..].find(&close_needle) {
let value = &xml[value_start..value_start + rel_end];
let labels: Vec<String> = value.split_whitespace().map(|s| s.to_string()).collect();
if labels.len() > 1 {
return labels;
}
}
}
Vec::new()
}
impl DimCounts {
fn mosaics_exposed_as_series(&self) -> bool {
false
}
}
fn compute_resolutions(
entries: &[DirEntry],
scene: Option<i32>,
acquisition: Option<i32>,
angle: Option<i32>,
mosaic: Option<i32>,
_prestitched: bool,
rotation_axis: bool,
) -> Vec<CziResolution> {
let mut buckets: HashMap<i32, (i64, i64, i64, i64)> = HashMap::new();
for e in entries {
if !entry_in_series(e, scene, acquisition, angle, mosaic) {
continue;
}
let r = if rotation_axis { 0 } else { e.dim_start("R") };
let col = e.dim_start("X").max(0) as i64;
let row = e.dim_start("Y").max(0) as i64;
let x_size = e.dim_size("X").max(0) as i64;
let y_size = e.dim_size("Y").max(0) as i64;
let entry = buckets
.entry(r)
.or_insert((i64::MAX, i64::MAX, i64::MIN, i64::MIN));
entry.0 = entry.0.min(col);
entry.1 = entry.1.min(row);
entry.2 = entry.2.max(col + x_size);
entry.3 = entry.3.max(row + y_size);
}
let mut resolutions: Vec<CziResolution> = buckets
.into_iter()
.map(|(r, (min_c, min_r, max_x, max_y))| CziResolution {
r,
width: (max_x - min_c).max(0) as u32,
height: (max_y - min_r).max(0) as u32,
})
.collect();
resolutions.sort_by_key(|res| res.r);
if resolutions.is_empty() {
resolutions.push(CziResolution {
r: 0,
width: 0,
height: 0,
});
}
resolutions
}
fn entry_in_series(
e: &DirEntry,
scene: Option<i32>,
acquisition: Option<i32>,
angle: Option<i32>,
mosaic: Option<i32>,
) -> bool {
let ok = |sel: Option<i32>, name: &str| -> bool {
match sel {
Some(v) => !e.has_dim(name) || e.dim_start(name) == v,
None => true,
}
};
ok(scene, "S") && ok(acquisition, "B") && ok(angle, "V") && ok(mosaic, "M")
}
fn decompress_subblock(
data: &[u8],
compression: i32,
tile_width: usize,
tile_height: usize,
max_bytes: usize,
) -> Result<Vec<u8>> {
match compression {
0 => Ok(data.to_vec()), 1 => {
let mut dec = jpeg_decoder::Decoder::new(data);
dec.decode()
.map_err(|e| BioFormatsError::Codec(e.to_string()))
}
2 => {
use weezl::{decode::Decoder, BitOrder};
let mut dec = Decoder::with_tiff_size_switch(BitOrder::Msb, 8);
dec.decode(data)
.map_err(|e| BioFormatsError::Codec(e.to_string()))
}
4 => {
crate::common::codec::decompress_jpegxr(data)
}
5 => {
crate::common::codec::zstd_decode_all(data)
}
6 => decompress_zstd_1(data),
104 => {
let mut decoded = decode_12bit_camera(data, max_bytes)?;
reverse_columns_16bit(&mut decoded, tile_width, tile_height);
Ok(decoded)
}
504 => {
decode_12bit_camera(data, max_bytes)
}
_ => Err(BioFormatsError::UnsupportedFormat(format!(
"CZI: unknown compression {}",
compression
))),
}
}
fn decode_12bit_camera(data: &[u8], max_bytes: usize) -> Result<Vec<u8>> {
let mut decoded = vec![0u8; max_bytes];
let four_bits_len = (max_bytes / 2) * 3;
let required_bytes = four_bits_len.div_ceil(2);
if data.len() < required_bytes {
return Err(BioFormatsError::InvalidData(format!(
"CZI 12-bit camera payload is too short: got {}, expected at least {required_bytes}",
data.len()
)));
}
let mut four_bits = vec![0u8; four_bits_len];
let mut bit_pos = 0usize;
for nibble in four_bits.iter_mut() {
let byte_index = bit_pos / 8;
let in_byte_shift = 4 - (bit_pos % 8);
*nibble = (data[byte_index] >> in_byte_shift) & 0x0f;
bit_pos += 4;
}
if four_bits_len > 1 {
for index in 1..four_bits_len - 1 {
if (index as isize - 3) % 6 == 0 {
let middle = four_bits[index];
let last = four_bits[index + 1];
let first = four_bits[index - 1];
four_bits[index + 1] = middle;
four_bits[index] = first;
four_bits[index - 1] = last;
}
}
}
let mut current_byte = 0usize;
let mut index = 0usize;
while index < four_bits_len && current_byte < decoded.len() {
if index % 3 == 0 {
decoded[current_byte] = four_bits[index];
current_byte += 1;
index += 1;
} else {
let hi = four_bits[index];
index += 1;
let lo = if index < four_bits_len {
four_bits[index]
} else {
0
};
index += 1;
decoded[current_byte] = (hi << 4) | lo;
current_byte += 1;
}
}
Ok(decoded)
}
fn reverse_columns_16bit(data: &mut [u8], width: usize, height: usize) {
if width == 0 {
return;
}
for row in 0..height {
for col in 0..width / 2 {
let left = row * width * 2 + col * 2;
let right = row * width * 2 + (width - col - 1) * 2;
if right + 1 >= data.len() {
continue;
}
data.swap(left, right);
data.swap(left + 1, right + 1);
}
}
}
fn read_czi_varint(data: &[u8], offset: &mut usize) -> Result<usize> {
if *offset >= data.len() {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 truncated varint".into(),
));
}
let a = data[*offset];
*offset += 1;
if a & 0x80 == 0 {
return Ok(a as usize);
}
if *offset >= data.len() {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 truncated varint".into(),
));
}
let b = data[*offset];
*offset += 1;
if b & 0x80 == 0 {
return Ok(((b as usize) << 7) | ((a & 0x7f) as usize));
}
if *offset >= data.len() {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 truncated varint".into(),
));
}
let c = data[*offset];
*offset += 1;
Ok(((c as usize) << 14) | (((b & 0x7f) as usize) << 7) | ((a & 0x7f) as usize))
}
fn decompress_zstd_1(data: &[u8]) -> Result<Vec<u8>> {
let mut offset = 0usize;
let header_end = read_czi_varint(data, &mut offset)?;
if header_end > data.len() || header_end < offset {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 invalid header size".into(),
));
}
let mut high_low_unpacking = false;
while offset < header_end {
let chunk_id = read_czi_varint(data, &mut offset)?;
match chunk_id {
1 => {
if offset >= header_end {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 missing chunk payload".into(),
));
}
high_low_unpacking = (data[offset] & 1) == 1;
offset += 1;
}
_ => {
return Err(BioFormatsError::InvalidData(format!(
"CZI ZSTD_1 invalid chunk ID {chunk_id}"
)));
}
}
}
let decoded = crate::common::codec::zstd_decode_all(&data[header_end..])?;
if !high_low_unpacking {
return Ok(decoded);
}
if decoded.len() % 2 != 0 {
return Err(BioFormatsError::InvalidData(
"CZI ZSTD_1 high/low decoded byte count is odd".into(),
));
}
let second_half = decoded.len() / 2;
let mut out = vec![0; decoded.len()];
for i in 0..decoded.len() {
let half_offset = i / 2;
out[i] = if i % 2 == 0 {
decoded[half_offset]
} else {
decoded[second_half + half_offset]
};
}
Ok(out)
}
pub struct CziReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
entries: Vec<DirEntry>,
meta_xml: String,
packed_spp: u32,
series: Vec<CziSeries>,
pixel_types: Vec<i32>,
prestitched: bool,
rotations: u32,
illuminations: u32,
phases: u32,
rotation_axis: bool,
current_series: usize,
current_resolution: usize,
}
impl CziReader {
pub fn new() -> Self {
CziReader {
path: None,
meta: None,
entries: Vec::new(),
meta_xml: String::new(),
packed_spp: 1,
series: Vec::new(),
pixel_types: Vec::new(),
prestitched: false,
rotations: 1,
illuminations: 1,
phases: 1,
rotation_axis: false,
current_series: 0,
current_resolution: 0,
}
}
fn plane_zct(&self, plane_index: u32) -> Option<(u32, u32, u32)> {
let meta = self.meta.as_ref()?;
let sz = meta.size_z;
let sc = meta.size_c;
let z = (plane_index / sc) % sz;
let c = plane_index % sc;
let t = plane_index / (sc * sz);
Some((z, c, t))
}
fn current_resolutions(&self) -> &[CziResolution] {
self.series
.get(self.current_series)
.map(|s| s.resolutions.as_slice())
.unwrap_or(&[])
}
fn matching_entries(&self, plane_index: u32) -> Option<Vec<DirEntry>> {
let (z, c, t) = self.plane_zct(plane_index)?;
let series = self.series.get(self.current_series)?;
let r = self.current_resolutions().get(self.current_resolution)?.r;
let meta = self.meta.as_ref()?;
let orig_z = (meta.size_z / self.rotations.max(1)).max(1);
let orig_c = (meta.size_c / self.illuminations.max(1)).max(1);
let orig_t = (meta.size_t / self.phases.max(1)).max(1);
let rotation = z / orig_z; let z = z % orig_z;
let illum = c / orig_c; let c = c % orig_c;
let phase = t / orig_t; let t = t % orig_t;
let want_pt = self.pixel_types.get(series.pixel_type_index).copied();
let multi_pt = self.pixel_types.len() > 1;
let c_with_offset = c + if multi_pt {
series.pixel_type_index as u32
} else {
0
};
let want_r = if self.rotation_axis {
rotation as i32
} else {
r
};
let match_r = |e: &DirEntry| -> bool {
if !e.has_dim("R") {
want_r == 0
} else {
e.dim_start("R") == want_r
}
};
let match_sub = |e: &DirEntry, name: &str, want: u32| -> bool {
if !e.has_dim(name) {
want == 0
} else {
e.dim_start(name) as u32 == want
}
};
let entries: Vec<DirEntry> = self
.entries
.iter()
.filter(|e| {
entry_in_series(e, series.scene, series.acquisition, series.angle, series.mosaic)
&& match_r(e)
&& (self.illuminations <= 1 || match_sub(e, "I", illum))
&& (self.phases <= 1 || match_sub(e, "H", phase))
&& e.matches_plane(z, c_with_offset, t)
&& (!multi_pt || want_pt == Some(e.pixel_type))
&& series.palm_size.map_or(true, |(sx, sy)| {
e.dim_stored_size("X").max(0) as u32 == sx
&& e.dim_stored_size("Y").max(0) as u32 == sy
})
})
.cloned()
.collect();
(!entries.is_empty()).then_some(entries)
}
fn refresh_meta_dimensions(&mut self) {
let (width, height, res_count) = {
let resolutions = self.current_resolutions();
let res_count = resolutions.len().max(1) as u32;
let res = resolutions.get(self.current_resolution);
(
res.map(|r| r.width).unwrap_or(0),
res.map(|r| r.height).unwrap_or(0),
res_count,
)
};
if let Some(meta) = self.meta.as_mut() {
meta.size_x = width;
meta.size_y = height;
meta.resolution_count = res_count;
}
}
fn read_subblock(path: &Path, entry: &DirEntry, pixel_bytes: usize) -> Result<Vec<u8>> {
let mut f = File::open(path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(entry.file_position as u64))
.map_err(BioFormatsError::Io)?;
let mut seg_hdr = vec![0u8; SEG_HEADER];
f.read_exact(&mut seg_hdr).map_err(BioFormatsError::Io)?;
let mut sb_hdr = vec![0u8; 16];
f.read_exact(&mut sb_hdr).map_err(BioFormatsError::Io)?;
let metadata_size = read_i32(&sb_hdr, 0) as u64;
let data_size = read_u64(&sb_hdr, 8);
let mut de_hdr = vec![0u8; 32];
f.read_exact(&mut de_hdr).map_err(BioFormatsError::Io)?;
let dim_count = read_i32(&de_hdr, 28).max(0) as i64;
let dir_entry_len = 32 + 20 * dim_count;
let skip = 20 * dim_count + (256 - 16 - dir_entry_len).max(0) + metadata_size as i64;
f.seek(SeekFrom::Current(skip)).map_err(BioFormatsError::Io)?;
let mut compressed = vec![0u8; data_size as usize];
f.read_exact(&mut compressed).map_err(BioFormatsError::Io)?;
let tile_w = entry.dim_stored_size("X").max(0) as usize;
let tile_h = entry.dim_stored_size("Y").max(0) as usize;
let max_bytes = tile_w * tile_h * pixel_bytes;
decompress_subblock(&compressed, entry.compression, tile_w, tile_h, max_bytes)
}
#[allow(clippy::too_many_arguments)]
fn assemble_entry(
out: &mut [u8],
out_width: u32,
out_height: u32,
tile: &[u8],
entry: &DirEntry,
pixel_bytes: usize,
off_x: i32,
off_y: i32,
) -> Result<()> {
let tile_x = (entry.dim_start("X").max(0) - off_x).max(0) as u32;
let tile_y = (entry.dim_start("Y").max(0) - off_y).max(0) as u32;
let tile_w = entry.dim_stored_size("X").max(0) as u32;
let tile_h = entry.dim_stored_size("Y").max(0) as u32;
if tile_w > 0 && tile_x >= out_width {
return Err(BioFormatsError::Format(format!(
"CZI tile X bounds exceed output plane: x={tile_x}, width={tile_w}, output width={out_width}"
)));
}
if tile_h > 0 && tile_y >= out_height {
return Err(BioFormatsError::Format(format!(
"CZI tile Y bounds exceed output plane: y={tile_y}, height={tile_h}, output height={out_height}"
)));
}
let copy_w = tile_w.min(out_width.saturating_sub(tile_x));
let copy_h = tile_h.min(out_height.saturating_sub(tile_y));
let src_row_bytes = (tile_w as usize).checked_mul(pixel_bytes).ok_or_else(|| {
BioFormatsError::Format("CZI tile source row byte count overflows".into())
})?;
let dst_row_bytes = (out_width as usize)
.checked_mul(pixel_bytes)
.ok_or_else(|| {
BioFormatsError::Format("CZI tile destination row byte count overflows".into())
})?;
let copy_bytes = (copy_w as usize)
.checked_mul(pixel_bytes)
.ok_or_else(|| BioFormatsError::Format("CZI tile copy byte count overflows".into()))?;
for row in 0..copy_h as usize {
let src_off = row * src_row_bytes;
let dst_off = ((tile_y as usize + row) * dst_row_bytes) + tile_x as usize * pixel_bytes;
if src_off + copy_bytes > tile.len() {
return Err(BioFormatsError::Format(format!(
"CZI tile row {row} exceeds decoded tile buffer: need {} bytes, have {}",
src_off + copy_bytes,
tile.len()
)));
}
if dst_off + copy_bytes > out.len() {
return Err(BioFormatsError::Format(format!(
"CZI tile row {row} exceeds output plane buffer: need {} bytes, have {}",
dst_off + copy_bytes,
out.len()
)));
}
out[dst_off..dst_off + copy_bytes]
.copy_from_slice(&tile[src_off..src_off + copy_bytes]);
}
Ok(())
}
}
impl Default for CziReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for CziReader {
fn is_this_type_by_name(&self, path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("czi"))
.unwrap_or(false)
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.starts_with(b"ZISRAWFILE")
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let f = File::open(path).map_err(BioFormatsError::Io)?;
let mut reader = BufReader::new(f);
let parsed = parse_czi_file(&mut reader).map_err(BioFormatsError::Io)?;
let image_count = parsed.z_count * parsed.c_count * parsed.t_count;
let bps = (parsed.pixel_type.bytes_per_sample() * 8) as u8;
let is_rgb = parsed.spp >= 3;
let mut series_metadata: HashMap<String, MetadataValue> = HashMap::new();
series_metadata.insert(
"czi_subblocks".into(),
MetadataValue::Int(parsed.entries.len() as i64),
);
if parsed.palm {
series_metadata.insert("czi_palm".into(), MetadataValue::Bool(true));
}
let first = parsed.series.first();
let (init_w, init_h, init_res_count) = first
.and_then(|s| {
s.resolutions
.first()
.map(|r| (r.width, r.height, s.resolutions.len()))
})
.unwrap_or((0, 0, 1));
self.meta = Some(ImageMetadata {
size_x: init_w,
size_y: init_h,
size_z: parsed.z_count,
size_c: parsed.c_count,
size_t: parsed.t_count,
pixel_type: parsed.pixel_type,
bits_per_pixel: bps,
image_count,
dimension_order: DimensionOrder::XYCZT,
is_rgb,
is_interleaved: true,
is_indexed: false,
is_little_endian: true,
resolution_count: init_res_count as u32,
series_metadata,
lookup_table: None,
modulo_z: parsed.modulo_z,
modulo_c: parsed.modulo_c,
modulo_t: parsed.modulo_t,
});
self.packed_spp = parsed.spp.max(1);
self.entries = parsed.entries;
self.series = parsed.series;
self.pixel_types = parsed.pixel_types;
self.prestitched = parsed.prestitched;
self.rotations = parsed.rotations.max(1) as u32;
self.illuminations = parsed.illuminations.max(1) as u32;
self.phases = parsed.phases.max(1) as u32;
self.rotation_axis = parsed.rotation_axis;
self.current_series = 0;
self.current_resolution = 0;
self.meta_xml = parsed.meta_xml;
self.path = Some(path.to_path_buf());
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.entries.clear();
self.meta_xml.clear();
self.packed_spp = 1;
self.series.clear();
self.pixel_types.clear();
self.prestitched = false;
self.rotations = 1;
self.illuminations = 1;
self.phases = 1;
self.rotation_axis = false;
self.current_series = 0;
self.current_resolution = 0;
Ok(())
}
fn series_count(&self) -> usize {
if self.meta.is_some() {
self.series.len().max(1)
} else {
0
}
}
fn set_series(&mut self, s: usize) -> Result<()> {
if s >= self.series_count() {
return Err(BioFormatsError::SeriesOutOfRange(s));
}
self.current_series = s;
self.current_resolution = 0;
self.refresh_meta_dimensions();
Ok(())
}
fn series(&self) -> usize {
self.current_series
}
fn metadata(&self) -> &ImageMetadata {
self.meta
.as_ref()
.unwrap_or(crate::common::reader::uninitialized_metadata())
}
fn open_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let entries = self
.matching_entries(plane_index)
.ok_or_else(|| BioFormatsError::PlaneOutOfRange(plane_index))?;
let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let bps = meta.pixel_type.bytes_per_sample();
let expected = meta.size_x as usize * meta.size_y as usize * self.packed_spp as usize * bps;
let pixel_bytes = self.packed_spp as usize * bps;
let mut out = vec![0; expected];
let (min_col, min_row) = if self.prestitched {
entries.iter().fold((i32::MAX, i32::MAX), |(mc, mr), e| {
(mc.min(e.dim_start("X")), mr.min(e.dim_start("Y")))
})
} else {
(0, 0)
};
let (min_col, min_row) = (min_col.max(0), min_row.max(0));
for entry in entries {
let tile_w = entry.dim_stored_size("X").max(0) as usize;
let tile_h = entry.dim_stored_size("Y").max(0) as usize;
let tile_expected = tile_w
.checked_mul(tile_h)
.and_then(|n| n.checked_mul(pixel_bytes))
.ok_or_else(|| BioFormatsError::Format("CZI tile byte count overflows".into()))?;
let tile = Self::read_subblock(path, &entry, pixel_bytes)?;
if tile.len() != tile_expected {
return Err(BioFormatsError::Format(format!(
"CZI decoded tile byte count {} does not match expected {}",
tile.len(),
tile_expected
)));
}
let full_tile =
self.prestitched && tile_w as u32 == meta.size_x && tile_h as u32 == meta.size_y;
let (off_x, off_y) = if full_tile {
(0, 0)
} else {
(min_col, min_row)
};
Self::assemble_entry(
&mut out,
meta.size_x,
meta.size_y,
&tile,
&entry,
pixel_bytes,
off_x,
off_y,
)?;
}
if meta.is_rgb && self.packed_spp >= 3 {
swap_bgr_to_rgb(&mut out, bps, self.packed_spp as usize);
}
Ok(out)
}
fn open_bytes_region(
&mut self,
plane_index: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
let full = self.open_bytes(plane_index)?;
let meta = self.meta.as_ref().unwrap();
crop_full_plane("CZI", &full, meta, self.packed_spp as usize, x, y, w, h)
}
fn open_thumb_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let (tw, th) = (meta.size_x.min(256), meta.size_y.min(256));
let (tx, ty) = ((meta.size_x - tw) / 2, (meta.size_y - th) / 2);
self.open_bytes_region(plane_index, tx, ty, tw, th)
}
fn resolution_count(&self) -> usize {
self.current_resolutions().len().max(1)
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
let count = self.current_resolutions().len();
if level >= count {
return Err(BioFormatsError::Format(format!(
"CZI resolution level {} out of range (max {})",
level,
count.saturating_sub(1)
)));
}
self.current_resolution = level;
self.refresh_meta_dimensions();
Ok(())
}
fn resolution(&self) -> usize {
self.current_resolution
}
fn ome_metadata(&self) -> Option<crate::common::ome_metadata::OmeMetadata> {
if self.meta_xml.is_empty() {
return None;
}
Some(crate::common::ome_metadata::OmeMetadata::from_czi_xml(
&self.meta_xml,
))
}
}
fn swap_bgr_to_rgb(buf: &mut [u8], bytes_per_sample: usize, samples_per_pixel: usize) {
if samples_per_pixel < 3 || bytes_per_sample == 0 {
return;
}
let pixel_bytes = bytes_per_sample * samples_per_pixel;
for pixel in buf.chunks_exact_mut(pixel_bytes) {
for i in 0..bytes_per_sample {
pixel.swap(i, 2 * bytes_per_sample + i);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
#[test]
fn czi_12bit_camera_rejects_truncated_payload() {
let err = decode_12bit_camera(&[0xab, 0xcd], 8).unwrap_err();
assert!(
err.to_string()
.contains("12-bit camera payload is too short"),
"unexpected error: {err}"
);
}
fn put_i32(buf: &mut [u8], off: usize, value: i32) {
buf[off..off + 4].copy_from_slice(&value.to_le_bytes());
}
fn put_i64(buf: &mut [u8], off: usize, value: i64) {
buf[off..off + 8].copy_from_slice(&value.to_le_bytes());
}
fn put_u64(buf: &mut [u8], off: usize, value: u64) {
buf[off..off + 8].copy_from_slice(&value.to_le_bytes());
}
fn segment_header(name: &str, used_size: u64) -> Vec<u8> {
let mut header = vec![0; SEG_HEADER];
header[..name.len()].copy_from_slice(name.as_bytes());
put_u64(&mut header, 16, used_size);
put_u64(&mut header, 24, used_size);
header
}
fn dimension_entry(name: &str, start: i32, size: i32) -> [u8; 20] {
let mut dim = [0; 20];
dim[..name.len()].copy_from_slice(name.as_bytes());
put_i32(&mut dim, 4, start);
put_i32(&mut dim, 8, size);
dim
}
fn directory_entry(pixel_type: i32, file_position: i64, c: i32, x: i32, y: i32) -> Vec<u8> {
directory_entry_dims(pixel_type, file_position, c, 0, 0, x, y, 0)
}
fn directory_entry_dims(
pixel_type: i32,
file_position: i64,
c: i32,
x_start: i32,
y_start: i32,
x_size: i32,
y_size: i32,
r: i32,
) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, pixel_type);
put_i64(&mut entry, 6, file_position);
put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 4);
entry[32..52].copy_from_slice(&dimension_entry("X", x_start, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", y_start, y_size));
entry[72..92].copy_from_slice(&dimension_entry("C", c, 1));
entry[92..112].copy_from_slice(&dimension_entry("R", r, 1));
entry
}
fn directory_entry_extra(
pixel_type: i32,
x_start: i32,
y_start: i32,
x_size: i32,
y_size: i32,
extra_dim: &str,
extra_start: i32,
) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, pixel_type);
put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 5);
entry[32..52].copy_from_slice(&dimension_entry("X", x_start, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", y_start, y_size));
entry[72..92].copy_from_slice(&dimension_entry("C", 0, 1));
entry[92..112].copy_from_slice(&dimension_entry("R", 0, 1));
entry[112..132].copy_from_slice(&dimension_entry(extra_dim, extra_start, 1));
entry
}
fn directory_entry_scene(
pixel_type: i32,
file_position: i64,
scene: i32,
x_size: i32,
y_size: i32,
) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, pixel_type);
put_i64(&mut entry, 6, file_position);
put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 5);
entry[32..52].copy_from_slice(&dimension_entry("X", 0, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", 0, y_size));
entry[72..92].copy_from_slice(&dimension_entry("C", 0, 1));
entry[92..112].copy_from_slice(&dimension_entry("R", 0, 1));
entry[112..132].copy_from_slice(&dimension_entry("S", scene, 1));
entry
}
fn directory_entry_scene_z(
pixel_type: i32,
scene: i32,
z: i32,
x_size: i32,
y_size: i32,
) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, pixel_type);
put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 5);
entry[32..52].copy_from_slice(&dimension_entry("X", 0, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", 0, y_size));
entry[72..92].copy_from_slice(&dimension_entry("Z", z, 1));
entry[92..112].copy_from_slice(&dimension_entry("R", 0, 1));
entry[112..132].copy_from_slice(&dimension_entry("S", scene, 1));
entry
}
fn directory_entry_zc_dims(
pixel_type: i32,
file_position: i64,
z: i32,
c: i32,
x_size: i32,
y_size: i32,
) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, pixel_type);
put_i64(&mut entry, 6, file_position);
put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 5);
entry[32..52].copy_from_slice(&dimension_entry("X", 0, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", 0, y_size));
entry[72..92].copy_from_slice(&dimension_entry("Z", z, 1));
entry[92..112].copy_from_slice(&dimension_entry("C", c, 1));
entry[112..132].copy_from_slice(&dimension_entry("R", 0, 1));
entry
}
fn write_synthetic_bgr_czi(name: &str, pixel_type: i32, planes: &[Vec<u8>]) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"bioformats_czi_{name}_{}_{}.czi",
std::process::id(),
planes.len()
));
let width = 2;
let height = 1;
let file_header_size = SEG_HEADER + 80;
let dir_size = SEG_HEADER + 128 + planes.len() * 256;
let subblock_size = |plane: &Vec<u8>| SEG_HEADER + 256 + plane.len();
let dir_pos = file_header_size as u64;
let mut subblock_pos = (file_header_size + dir_size) as u64;
let mut data = Vec::new();
data.extend_from_slice(&segment_header("ZISRAWFILE", file_header_size as u64));
let mut file_header = vec![0; 80];
put_u64(&mut file_header, 36, dir_pos);
data.extend_from_slice(&file_header);
data.extend_from_slice(&segment_header("ZISRAWDIRECTORY", dir_size as u64));
let mut dir_header = vec![0; 128];
put_i32(&mut dir_header, 0, planes.len() as i32);
data.extend_from_slice(&dir_header);
let mut entries = Vec::new();
for (c, plane) in planes.iter().enumerate() {
entries.push(directory_entry(
pixel_type,
subblock_pos as i64,
c as i32,
width,
height,
));
subblock_pos += subblock_size(plane) as u64;
}
for entry in &entries {
data.extend_from_slice(entry);
}
for (_entry, plane) in entries.iter().zip(planes) {
let used_size = (SEG_HEADER + 256 + plane.len()) as u64;
data.extend_from_slice(&segment_header("ZISRAWSUBBLOCK", used_size));
let mut subblock_body = vec![0; 256];
put_u64(&mut subblock_body, 8, plane.len() as u64);
data.extend_from_slice(&subblock_body);
data.extend_from_slice(plane);
}
let mut file = fs::File::create(&path).unwrap();
file.write_all(&data).unwrap();
path
}
fn write_synthetic_czi_entries(
name: &str,
entries_and_pixels: Vec<(Vec<u8>, Vec<u8>)>,
) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"bioformats_czi_{name}_{}_{}.czi",
std::process::id(),
entries_and_pixels.len()
));
let file_header_size = SEG_HEADER + 80;
let dir_size = SEG_HEADER + 128 + entries_and_pixels.len() * 256;
let dir_pos = file_header_size as u64;
let mut subblock_pos = (file_header_size + dir_size) as u64;
let mut entries = Vec::new();
for (mut entry, pixels) in entries_and_pixels {
put_i64(&mut entry, 6, subblock_pos as i64);
subblock_pos += (SEG_HEADER + 256 + pixels.len()) as u64;
entries.push((entry, pixels));
}
let mut data = Vec::new();
data.extend_from_slice(&segment_header("ZISRAWFILE", file_header_size as u64));
let mut file_header = vec![0; 80];
put_u64(&mut file_header, 36, dir_pos);
data.extend_from_slice(&file_header);
data.extend_from_slice(&segment_header("ZISRAWDIRECTORY", dir_size as u64));
let mut dir_header = vec![0; 128];
put_i32(&mut dir_header, 0, entries.len() as i32);
data.extend_from_slice(&dir_header);
for (entry, _) in &entries {
data.extend_from_slice(entry);
}
for (_entry, pixels) in &entries {
let used_size = (SEG_HEADER + 256 + pixels.len()) as u64;
data.extend_from_slice(&segment_header("ZISRAWSUBBLOCK", used_size));
let mut subblock_body = vec![0; 256];
put_u64(&mut subblock_body, 8, pixels.len() as u64);
data.extend_from_slice(&subblock_body);
data.extend_from_slice(pixels);
}
let mut file = fs::File::create(&path).unwrap();
file.write_all(&data).unwrap();
path
}
fn write_synthetic_czi_with_xml(
name: &str,
entries_and_pixels: Vec<(Vec<u8>, Vec<u8>)>,
xml: &str,
) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"bioformats_czi_{name}_{}_{}.czi",
std::process::id(),
entries_and_pixels.len()
));
let file_header_size = SEG_HEADER + 80;
let dir_size = SEG_HEADER + 128 + entries_and_pixels.len() * 256;
let xml_bytes = xml.as_bytes();
let meta_size = SEG_HEADER + 256 + xml_bytes.len();
let dir_pos = file_header_size as u64;
let meta_pos = (file_header_size + dir_size) as u64;
let mut subblock_pos = (file_header_size + dir_size + meta_size) as u64;
let mut entries = Vec::new();
for (mut entry, pixels) in entries_and_pixels {
put_i64(&mut entry, 6, subblock_pos as i64);
subblock_pos += (SEG_HEADER + 256 + pixels.len()) as u64;
entries.push((entry, pixels));
}
let mut data = Vec::new();
data.extend_from_slice(&segment_header("ZISRAWFILE", file_header_size as u64));
let mut file_header = vec![0; 80];
put_u64(&mut file_header, 36, dir_pos);
put_u64(&mut file_header, 44, meta_pos);
data.extend_from_slice(&file_header);
data.extend_from_slice(&segment_header("ZISRAWDIRECTORY", dir_size as u64));
let mut dir_header = vec![0; 128];
put_i32(&mut dir_header, 0, entries.len() as i32);
data.extend_from_slice(&dir_header);
for (entry, _) in &entries {
data.extend_from_slice(entry);
}
data.extend_from_slice(&segment_header("ZISRAWMETADATA", meta_size as u64));
let mut meta_body = vec![0; 256];
put_i32(&mut meta_body, 0, xml_bytes.len() as i32);
data.extend_from_slice(&meta_body);
data.extend_from_slice(xml_bytes);
for (_entry, pixels) in &entries {
let used_size = (SEG_HEADER + 256 + pixels.len()) as u64;
data.extend_from_slice(&segment_header("ZISRAWSUBBLOCK", used_size));
let mut subblock_body = vec![0; 256];
put_u64(&mut subblock_body, 8, pixels.len() as u64);
data.extend_from_slice(&subblock_body);
data.extend_from_slice(pixels);
}
let mut file = fs::File::create(&path).unwrap();
file.write_all(&data).unwrap();
path
}
#[test]
fn czi_varint_matches_java_encoding() {
let mut offset = 0;
assert_eq!(read_czi_varint(&[0x7f], &mut offset).unwrap(), 0x7f);
assert_eq!(offset, 1);
let mut offset = 0;
assert_eq!(read_czi_varint(&[0x80, 0x01], &mut offset).unwrap(), 0x80);
assert_eq!(offset, 2);
let mut offset = 0;
assert_eq!(
read_czi_varint(&[0x80, 0x80, 0x01], &mut offset).unwrap(),
0x4000
);
assert_eq!(offset, 3);
}
#[test]
fn czi_zstd_1_plain_payload() {
let payload = crate::common::codec::zstd_encode_all(b"\x11\x22\x33\x44", 0).unwrap();
let mut wrapped = vec![3, 1, 0];
wrapped.extend_from_slice(&payload);
assert_eq!(
decompress_zstd_1(&wrapped).unwrap(),
vec![0x11, 0x22, 0x33, 0x44]
);
}
#[test]
fn czi_zstd_1_high_low_unpacking() {
let payload = crate::common::codec::zstd_encode_all(b"\x11\x33\x22\x44", 0).unwrap();
let mut wrapped = vec![3, 1, 1];
wrapped.extend_from_slice(&payload);
assert_eq!(
decompress_zstd_1(&wrapped).unwrap(),
vec![0x11, 0x22, 0x33, 0x44]
);
}
#[test]
fn czi_directory_rejects_truncated_declared_entry() {
let mut entry = directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 0);
entry.truncate(40);
let err = parse_directory_entries(&entry, 1).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("directory entry 0 is truncated"),
"unexpected error: {err}"
);
}
#[test]
fn czi_bgr24_keeps_logical_channels_separate_from_packed_samples() {
let planes = vec![vec![1, 2, 3, 4, 5, 6], vec![7, 8, 9, 10, 11, 12]];
let path = write_synthetic_bgr_czi("bgr24_logical_c", 3, &planes);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!(meta.size_c, 2);
assert_eq!(meta.image_count, 2);
assert!(meta.is_rgb);
assert_eq!(reader.open_bytes(0).unwrap(), vec![3, 2, 1, 6, 5, 4]);
assert_eq!(reader.open_bytes(1).unwrap(), vec![9, 8, 7, 12, 11, 10]);
assert_eq!(
reader.open_bytes_region(1, 1, 0, 1, 1).unwrap(),
vec![12, 11, 10]
);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_bgr48_keeps_logical_channels_separate_from_packed_samples() {
let planes = vec![
vec![1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0],
vec![7, 0, 8, 0, 9, 0, 10, 0, 11, 0, 12, 0],
];
let path = write_synthetic_bgr_czi("bgr48_logical_c", 4, &planes);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!(meta.size_c, 2);
assert_eq!(meta.image_count, 2);
assert_eq!(meta.pixel_type, PixelType::Uint16);
assert!(meta.is_rgb);
assert_eq!(
reader.open_bytes(0).unwrap(),
vec![3, 0, 2, 0, 1, 0, 6, 0, 5, 0, 4, 0]
);
assert_eq!(
reader.open_bytes(1).unwrap(),
vec![9, 0, 8, 0, 7, 0, 12, 0, 11, 0, 10, 0]
);
assert_eq!(
reader.open_bytes_region(1, 1, 0, 1, 1).unwrap(),
vec![12, 0, 11, 0, 10, 0]
);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_assembles_mosaic_tiles_into_single_plane() {
let entries = vec![
(directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 0), vec![1, 2]),
(directory_entry_dims(0, 0, 0, 2, 0, 2, 1, 0), vec![3, 4]),
(directory_entry_dims(0, 0, 0, 0, 1, 2, 1, 0), vec![5, 6]),
(directory_entry_dims(0, 0, 0, 2, 1, 2, 1, 0), vec![7, 8]),
];
let path = write_synthetic_czi_entries("mosaic_tiles", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (4, 2));
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(
reader.open_bytes_region(0, 1, 0, 2, 2).unwrap(),
vec![2, 3, 6, 7]
);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_rejects_short_decoded_tile_instead_of_padding() {
let entries = vec![(directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 0), vec![1])];
let path = write_synthetic_czi_entries("short_tile", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let err = reader.open_bytes(0).unwrap_err();
assert!(
err.to_string()
.contains("decoded tile byte count 1 does not match expected 2"),
"unexpected error: {err}"
);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_rejects_long_decoded_tile_instead_of_truncating() {
let entries = vec![(directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 0), vec![1, 2, 3])];
let path = write_synthetic_czi_entries("long_tile", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let err = reader.open_bytes(0).unwrap_err();
assert!(
err.to_string()
.contains("decoded tile byte count 3 does not match expected 2"),
"unexpected error: {err}"
);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_rejects_tile_outside_output_instead_of_skipping_copy() {
let entry = parse_dir_entry(&directory_entry_dims(0, 0, 0, 1, 0, 1, 1, 0));
let mut out = vec![0u8; 1];
let err = CziReader::assemble_entry(&mut out, 1, 1, &[7], &entry, 1, 0, 0).unwrap_err();
assert!(
err.to_string()
.contains("tile X bounds exceed output plane"),
"unexpected error: {err}"
);
assert_eq!(out, vec![0]);
}
#[test]
fn czi_uses_java_xyczt_plane_order() {
let entries = vec![
(directory_entry_zc_dims(0, 0, 0, 0, 1, 1), vec![10]),
(directory_entry_zc_dims(0, 0, 0, 1, 1, 1), vec![11]),
(directory_entry_zc_dims(0, 0, 1, 0, 1, 1), vec![12]),
(directory_entry_zc_dims(0, 0, 1, 1, 1, 1), vec![13]),
];
let path = write_synthetic_czi_entries("xyczt_order", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!(meta.dimension_order, DimensionOrder::XYCZT);
assert_eq!((meta.size_z, meta.size_c, meta.size_t), (2, 2, 1));
assert_eq!(meta.image_count, 4);
assert_eq!(reader.open_bytes(0).unwrap(), vec![10]);
assert_eq!(reader.open_bytes(1).unwrap(), vec![11]);
assert_eq!(reader.open_bytes(2).unwrap(), vec![12]);
assert_eq!(reader.open_bytes(3).unwrap(), vec![13]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_selects_pyramid_resolution_level() {
let entries = vec![
(
directory_entry_dims(0, 0, 0, 0, 0, 4, 2, 0),
vec![1, 2, 3, 4, 5, 6, 7, 8],
),
(directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 1), vec![9, 10]),
];
let path = write_synthetic_czi_entries("pyramid_levels", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.resolution_count(), 2);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2, 3, 4, 5, 6, 7, 8]);
reader.set_resolution(1).unwrap();
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (2, 1));
assert_eq!(reader.resolution(), 1);
assert_eq!(reader.open_bytes(0).unwrap(), vec![9, 10]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_splits_scenes_into_separate_series() {
let entries = vec![
(directory_entry_scene(0, 0, 0, 2, 1), vec![1, 2]),
(directory_entry_scene(0, 0, 1, 2, 1), vec![3, 4]),
];
let path = write_synthetic_czi_entries("scene_series", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 2);
assert_eq!(reader.series(), 0);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
reader.set_series(1).unwrap();
assert_eq!(reader.series(), 1);
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (2, 1));
assert_eq!(reader.open_bytes(0).unwrap(), vec![3, 4]);
reader.set_series(0).unwrap();
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
assert!(reader.set_series(2).is_err());
fs::remove_file(path).unwrap();
}
#[test]
fn czi_prestitches_mosaic_tiles_into_single_series() {
let entries = vec![
(directory_entry_extra(0, 0, 0, 2, 1, "M", 0), vec![1, 2]),
(directory_entry_extra(0, 2, 0, 2, 1, "M", 1), vec![3, 4]),
(directory_entry_extra(0, 0, 1, 2, 1, "M", 2), vec![5, 6]),
(directory_entry_extra(0, 2, 1, 2, 1, "M", 3), vec![7, 8]),
];
let path = write_synthetic_czi_entries("mosaic_M_prestitch", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 1);
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (4, 2));
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2, 3, 4, 5, 6, 7, 8]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_prestitches_mosaic_with_nonzero_origin() {
let entries = vec![
(directory_entry_extra(0, 10, 20, 2, 1, "M", 0), vec![1, 2]),
(directory_entry_extra(0, 12, 20, 2, 1, "M", 1), vec![3, 4]),
];
let path = write_synthetic_czi_entries("mosaic_M_origin", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 1);
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (4, 1));
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2, 3, 4]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_splits_acquisitions_into_separate_series() {
let entries = vec![
(directory_entry_extra(0, 0, 0, 2, 1, "B", 0), vec![1, 2]),
(directory_entry_extra(0, 0, 0, 2, 1, "B", 1), vec![3, 4]),
];
let path = write_synthetic_czi_entries("acq_series", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 2);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
reader.set_series(1).unwrap();
assert_eq!(reader.open_bytes(0).unwrap(), vec![3, 4]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_splits_angles_into_separate_series() {
let entries = vec![
(directory_entry_extra(0, 0, 0, 2, 1, "V", 0), vec![1, 2]),
(directory_entry_extra(0, 0, 0, 2, 1, "V", 1), vec![3, 4]),
];
let path = write_synthetic_czi_entries("angle_series", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 2);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
reader.set_series(1).unwrap();
assert_eq!(reader.open_bytes(0).unwrap(), vec![3, 4]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_fuses_mosaic_collapses_series_count() {
let entries = vec![
(directory_entry_scene_z(0, 0, 0, 2, 1), vec![1, 2]),
(directory_entry_scene_z(0, 1, 1, 2, 1), vec![3, 4]),
];
let path = write_synthetic_czi_entries("fused_collapse", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 1);
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (2, 1));
assert_eq!((meta.size_z, meta.size_c, meta.size_t), (2, 1, 1));
assert_eq!(meta.image_count, 2);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
assert_eq!(reader.open_bytes(1).unwrap(), vec![3, 4]);
fs::remove_file(path).unwrap();
}
fn directory_entry_rotation(z: i32, r_start: i32, x_size: i32, y_size: i32) -> Vec<u8> {
let mut entry = vec![0; 256];
put_i32(&mut entry, 2, 0); put_i32(&mut entry, 18, 0);
put_i32(&mut entry, 28, 5);
entry[32..52].copy_from_slice(&dimension_entry("X", 0, x_size));
entry[52..72].copy_from_slice(&dimension_entry("Y", 0, y_size));
entry[72..92].copy_from_slice(&dimension_entry("C", 0, 1));
entry[92..112].copy_from_slice(&dimension_entry("Z", z, 1));
entry[112..132].copy_from_slice(&dimension_entry("R", r_start, 1));
entry
}
#[test]
fn czi_rotation_folds_into_modulo_z() {
let entries = vec![
(directory_entry_rotation(0, 0, 2, 1), vec![10, 11]),
(directory_entry_rotation(1, 0, 2, 1), vec![12, 13]),
(directory_entry_rotation(0, 1, 2, 1), vec![20, 21]),
(directory_entry_rotation(1, 1, 2, 1), vec![22, 23]),
];
let path = write_synthetic_czi_entries("rotation_modulo_z", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!(meta.size_z, 4);
assert_eq!(meta.image_count, 4);
assert_eq!(reader.resolution_count(), 1);
let mz = meta.modulo_z.as_ref().expect("moduloZ annotation");
assert_eq!(mz.parent_dimension, "Z");
assert_eq!(mz.modulo_type, "rotation");
assert_eq!(mz.step, 2.0); assert_eq!(mz.end, 2.0);
assert_eq!(reader.open_bytes(0).unwrap(), vec![10, 11]); assert_eq!(reader.open_bytes(1).unwrap(), vec![12, 13]); assert_eq!(reader.open_bytes(2).unwrap(), vec![20, 21]); assert_eq!(reader.open_bytes(3).unwrap(), vec![22, 23]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_r_size_one_stays_pyramid_not_rotation() {
let entries = vec![
(
directory_entry_dims(0, 0, 0, 0, 0, 4, 2, 0),
vec![1, 2, 3, 4, 5, 6, 7, 8],
),
(directory_entry_dims(0, 0, 0, 0, 0, 2, 1, 1), vec![9, 10]),
];
let path = write_synthetic_czi_entries("r_size_one_pyramid", entries);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
let meta = reader.metadata();
assert_eq!(meta.size_z, 1); assert!(meta.modulo_z.is_none());
assert_eq!(reader.resolution_count(), 2);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_check_palm_detects_lsmtag_and_palmslider() {
assert!(check_palm(
r#"<CustomAttributes><LsmTag Name="PALMExperiment">x</LsmTag></CustomAttributes>"#
));
assert!(check_palm(
"<TrackSetup><PalmSlider>true</PalmSlider></TrackSetup>"
));
assert!(!check_palm(
"<TrackSetup><PalmSlider>false</PalmSlider></TrackSetup>"
));
assert!(!check_palm(r#"<LsmTag Name="Gain">3</LsmTag>"#));
assert!(!check_palm(""));
}
#[test]
fn czi_parse_modulo_labels_splits_on_whitespace() {
let xml = "<Rotations>0 90 180 270</Rotations><Phases>a b</Phases>";
assert_eq!(
parse_modulo_labels(xml, "Rotations"),
vec!["0", "90", "180", "270"]
);
assert_eq!(parse_modulo_labels(xml, "Phases"), vec!["a", "b"]);
assert!(parse_modulo_labels("<Rotations>0</Rotations>", "Rotations").is_empty());
assert!(parse_modulo_labels("", "Rotations").is_empty());
}
#[test]
fn czi_palm_splits_two_planes_by_stored_size() {
let palm_xml = "<TrackSetup><PalmSlider>true</PalmSlider></TrackSetup>";
let entries = vec![
(directory_entry(0, 0, 0, 2, 1), vec![1, 2]),
(directory_entry(0, 0, 0, 4, 1), vec![3, 4, 5, 6]),
];
let path = write_synthetic_czi_with_xml("palm_split", entries, palm_xml);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 2);
let meta = reader.metadata();
assert_eq!(meta.size_c, 1);
assert_eq!((meta.size_x, meta.size_y), (2, 1));
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
reader.set_series(1).unwrap();
let meta = reader.metadata();
assert_eq!((meta.size_x, meta.size_y), (4, 1));
assert_eq!(reader.open_bytes(0).unwrap(), vec![3, 4, 5, 6]);
fs::remove_file(path).unwrap();
}
#[test]
fn czi_palm_same_size_pair_is_not_palm() {
let palm_xml = "<TrackSetup><PalmSlider>true</PalmSlider></TrackSetup>";
let entries = vec![
(directory_entry(0, 0, 0, 2, 1), vec![1, 2]),
(directory_entry(0, 0, 1, 2, 1), vec![3, 4]),
];
let path = write_synthetic_czi_with_xml("palm_same_size", entries, palm_xml);
let mut reader = CziReader::new();
reader.set_id(&path).unwrap();
assert_eq!(reader.series_count(), 1);
let meta = reader.metadata();
assert_eq!(meta.size_c, 2);
assert_eq!(reader.open_bytes(0).unwrap(), vec![1, 2]);
assert_eq!(reader.open_bytes(1).unwrap(), vec![3, 4]);
fs::remove_file(path).unwrap();
}
}