Skip to main content

czi_rs/
reader.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::BufReader;
4use std::path::{Path, PathBuf};
5
6use crate::error::{CziError, Result};
7use crate::metadata::parse_metadata_xml;
8use crate::parse::{
9    decode_subblock_bitmap, parse_file, read_attachment_blob, read_metadata_xml, read_raw_subblock,
10};
11use crate::types::{
12    AttachmentBlob, AttachmentInfo, Bitmap, Coordinate, Dimension, DirectorySubBlockInfo,
13    FileHeaderInfo, MetadataSummary, PlaneIndex, RawSubBlock, SubBlockStatistics,
14};
15
16pub struct CziFile {
17    path: PathBuf,
18    reader: BufReader<File>,
19    header: FileHeaderInfo,
20    subblocks: Vec<DirectorySubBlockInfo>,
21    attachments: Vec<AttachmentInfo>,
22    statistics: SubBlockStatistics,
23    metadata_xml: Option<String>,
24    metadata: Option<MetadataSummary>,
25}
26
27impl CziFile {
28    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
29        let path = path.as_ref().to_path_buf();
30        let file = File::open(&path)?;
31        let mut reader = BufReader::new(file);
32        let parsed = parse_file(&mut reader)?;
33
34        Ok(Self {
35            path,
36            reader,
37            header: parsed.header,
38            subblocks: parsed.subblocks,
39            attachments: parsed.attachments,
40            statistics: parsed.statistics,
41            metadata_xml: None,
42            metadata: None,
43        })
44    }
45
46    pub fn path(&self) -> &Path {
47        &self.path
48    }
49
50    pub fn version(&self) -> (i32, i32) {
51        (self.header.major, self.header.minor)
52    }
53
54    pub fn file_header(&self) -> &FileHeaderInfo {
55        &self.header
56    }
57
58    pub fn statistics(&self) -> &SubBlockStatistics {
59        &self.statistics
60    }
61
62    pub fn subblocks(&self) -> &[DirectorySubBlockInfo] {
63        &self.subblocks
64    }
65
66    pub fn attachments(&self) -> &[AttachmentInfo] {
67        &self.attachments
68    }
69
70    pub fn metadata_xml(&mut self) -> Result<&str> {
71        if self.metadata_xml.is_none() {
72            let xml = read_metadata_xml(&mut self.reader, self.header.metadata_position)?;
73            self.metadata_xml = Some(xml);
74        }
75
76        Ok(self.metadata_xml.as_deref().unwrap_or_default())
77    }
78
79    pub fn metadata(&mut self) -> Result<&MetadataSummary> {
80        if self.metadata.is_none() {
81            if self.metadata_xml.is_none() {
82                let xml = read_metadata_xml(&mut self.reader, self.header.metadata_position)?;
83                self.metadata_xml = Some(xml);
84            }
85            let parsed = {
86                let xml = self.metadata_xml.as_deref().unwrap_or_default();
87                parse_metadata_xml(xml)?
88            };
89            self.metadata = Some(parsed);
90        }
91
92        Ok(self.metadata.as_ref().unwrap())
93    }
94
95    pub fn sizes(&self) -> Result<HashMap<String, usize>> {
96        let mut sizes = HashMap::new();
97        for dimension in Dimension::FRAME_ORDER {
98            sizes.insert(
99                dimension.as_str().to_owned(),
100                self.statistics
101                    .dim_bounds
102                    .get(dimension)
103                    .map(|interval| interval.size)
104                    .unwrap_or(1),
105            );
106        }
107
108        let rect = self
109            .statistics
110            .bounding_box_layer0
111            .or(self.statistics.bounding_box);
112        sizes.insert(
113            Dimension::X.as_str().to_owned(),
114            rect.map(|value| value.w.max(0) as usize).unwrap_or(0),
115        );
116        sizes.insert(
117            Dimension::Y.as_str().to_owned(),
118            rect.map(|value| value.h.max(0) as usize).unwrap_or(0),
119        );
120
121        Ok(sizes)
122    }
123
124    pub fn loop_indices(&self) -> Result<Vec<HashMap<String, usize>>> {
125        let mut varying_dims = Vec::new();
126        for dimension in Dimension::FRAME_ORDER {
127            let size = self
128                .statistics
129                .dim_bounds
130                .get(dimension)
131                .map(|interval| interval.size)
132                .unwrap_or(1);
133            if size > 1 {
134                varying_dims.push((dimension, size));
135            }
136        }
137
138        if varying_dims.is_empty() {
139            return Ok(vec![HashMap::new()]);
140        }
141
142        let total = varying_dims.iter().map(|(_, size)| *size).product();
143        let mut out = Vec::with_capacity(total);
144        let mut current = HashMap::new();
145        build_loop_indices(&varying_dims, 0, &mut current, &mut out);
146        Ok(out)
147    }
148
149    pub fn channel_pixel_types(&self) -> HashMap<usize, crate::types::PixelType> {
150        let channel_start = self
151            .statistics
152            .dim_bounds
153            .get(Dimension::C)
154            .map(|interval| interval.start)
155            .unwrap_or(0);
156
157        let mut pixel_types = HashMap::new();
158        for subblock in &self.subblocks {
159            let actual_channel = subblock
160                .coordinate
161                .get(Dimension::C)
162                .unwrap_or(channel_start);
163            let relative_channel = actual_channel.saturating_sub(channel_start) as usize;
164            pixel_types
165                .entry(relative_channel)
166                .or_insert(subblock.pixel_type);
167        }
168        pixel_types
169    }
170
171    pub fn read_frame(&mut self, index: usize) -> Result<Bitmap> {
172        let indices = self.loop_indices()?;
173        if index >= indices.len() {
174            return Err(CziError::input_out_of_range(
175                "frame index",
176                index,
177                indices.len(),
178            ));
179        }
180
181        let mut plane = PlaneIndex::new();
182        for (name, value) in &indices[index] {
183            let dimension = Dimension::from_code(name).ok_or_else(|| {
184                CziError::input_argument("frame index", format!("unknown dimension '{name}'"))
185            })?;
186            plane.set(dimension, *value);
187        }
188        self.read_plane(&plane)
189    }
190
191    pub fn read_frame_2d(&mut self, s: usize, t: usize, c: usize, z: usize) -> Result<Bitmap> {
192        let plane = PlaneIndex::new()
193            .with(Dimension::S, s)
194            .with(Dimension::T, t)
195            .with(Dimension::C, c)
196            .with(Dimension::Z, z);
197        self.read_plane(&plane)
198    }
199
200    pub fn read_plane(&mut self, index: &PlaneIndex) -> Result<Bitmap> {
201        let actual = self.resolve_plane_index(index)?;
202        let plane_rect = self
203            .select_plane_rect(actual.get(Dimension::S))
204            .ok_or_else(|| CziError::file_invalid_format("no plane bounding box available"))?;
205        if plane_rect.w <= 0 || plane_rect.h <= 0 {
206            return Err(CziError::file_invalid_format(
207                "plane bounding box has non-positive size",
208            ));
209        }
210
211        let mut matching: Vec<&DirectorySubBlockInfo> = self
212            .subblocks
213            .iter()
214            .filter(|subblock| self.matches_plane(subblock, &actual))
215            .collect();
216        if matching.is_empty() {
217            return Err(CziError::file_invalid_format(
218                "no layer-0 subblocks matched the requested plane",
219            ));
220        }
221
222        matching
223            .sort_by_key(|subblock| (subblock.m_index.unwrap_or(i32::MIN), subblock.file_position));
224
225        let pixel_type = matching[0].pixel_type;
226        if matching
227            .iter()
228            .any(|subblock| subblock.pixel_type != pixel_type)
229        {
230            return Err(CziError::file_invalid_format(
231                "requested plane contains mixed pixel types",
232            ));
233        }
234
235        let mut bitmap = Bitmap::zeros(pixel_type, plane_rect.w as u32, plane_rect.h as u32)?;
236        for subblock in matching {
237            let raw = read_raw_subblock(&mut self.reader, subblock)?;
238            let tile = decode_subblock_bitmap(&raw)?;
239            blit_tile(
240                &mut bitmap,
241                &tile,
242                subblock.rect.x - plane_rect.x,
243                subblock.rect.y - plane_rect.y,
244            )?;
245        }
246
247        Ok(bitmap)
248    }
249
250    pub fn read_subblock(&mut self, index: usize) -> Result<RawSubBlock> {
251        let subblock = self.subblocks.get(index).ok_or_else(|| {
252            CziError::input_out_of_range("subblock index", index, self.subblocks.len())
253        })?;
254        read_raw_subblock(&mut self.reader, subblock)
255    }
256
257    pub fn read_attachment(&mut self, index: usize) -> Result<AttachmentBlob> {
258        let attachment = self.attachments.get(index).ok_or_else(|| {
259            CziError::input_out_of_range("attachment index", index, self.attachments.len())
260        })?;
261        read_attachment_blob(&mut self.reader, attachment)
262    }
263
264    fn resolve_plane_index(&self, index: &PlaneIndex) -> Result<Coordinate> {
265        let mut actual = Coordinate::new();
266
267        for dimension in Dimension::FRAME_ORDER {
268            let requested = index.get(dimension);
269            match self.statistics.dim_bounds.get(dimension) {
270                Some(interval) => {
271                    let relative = match requested {
272                        Some(value) => value,
273                        None if interval.size <= 1 => 0,
274                        None => return Err(CziError::input_missing_dim(dimension.as_str())),
275                    };
276                    if relative >= interval.size {
277                        return Err(CziError::input_out_of_range(
278                            format!("dimension {}", dimension.as_str()),
279                            relative,
280                            interval.size,
281                        ));
282                    }
283                    actual.set(dimension, interval.start + relative as i32);
284                }
285                None => {
286                    if requested.unwrap_or(0) != 0 {
287                        return Err(CziError::input_argument(
288                            dimension.as_str(),
289                            "dimension is not present in this file",
290                        ));
291                    }
292                }
293            }
294        }
295
296        Ok(actual)
297    }
298
299    fn select_plane_rect(&self, scene: Option<i32>) -> Option<crate::types::IntRect> {
300        if let Some(scene) = scene {
301            if let Some(bounding_boxes) = self.statistics.scene_bounding_boxes.get(&scene) {
302                if bounding_boxes.layer0.is_valid() {
303                    return Some(bounding_boxes.layer0);
304                }
305                if bounding_boxes.all.is_valid() {
306                    return Some(bounding_boxes.all);
307                }
308            }
309        }
310
311        self.statistics
312            .bounding_box_layer0
313            .or(self.statistics.bounding_box)
314    }
315
316    fn matches_plane(&self, subblock: &DirectorySubBlockInfo, actual: &Coordinate) -> bool {
317        if !subblock.is_layer0() {
318            return false;
319        }
320
321        for dimension in Dimension::FRAME_ORDER {
322            let Some(requested_value) = actual.get(dimension) else {
323                continue;
324            };
325
326            match subblock.coordinate.get(dimension) {
327                Some(value) if value == requested_value => {}
328                Some(_) => return false,
329                None => {
330                    if self
331                        .statistics
332                        .dim_bounds
333                        .get(dimension)
334                        .map(|interval| interval.size > 1)
335                        .unwrap_or(false)
336                    {
337                        return false;
338                    }
339                }
340            }
341        }
342
343        true
344    }
345}
346
347fn build_loop_indices(
348    dims: &[(Dimension, usize)],
349    depth: usize,
350    current: &mut HashMap<String, usize>,
351    out: &mut Vec<HashMap<String, usize>>,
352) {
353    if depth == dims.len() {
354        out.push(current.clone());
355        return;
356    }
357
358    let (dimension, size) = dims[depth];
359    for value in 0..size {
360        current.insert(dimension.as_str().to_owned(), value);
361        build_loop_indices(dims, depth + 1, current, out);
362    }
363    current.remove(dimension.as_str());
364}
365
366fn blit_tile(
367    destination: &mut Bitmap,
368    source: &Bitmap,
369    offset_x: i32,
370    offset_y: i32,
371) -> Result<()> {
372    if destination.pixel_type != source.pixel_type {
373        return Err(CziError::file_invalid_format(
374            "cannot compose tiles with different pixel types",
375        ));
376    }
377
378    let source_rect = crate::types::IntRect::new(
379        offset_x,
380        offset_y,
381        source.width as i32,
382        source.height as i32,
383    );
384    let destination_rect =
385        crate::types::IntRect::new(0, 0, destination.width as i32, destination.height as i32);
386    let Some(intersection) = source_rect.intersect(destination_rect) else {
387        return Ok(());
388    };
389
390    let bytes_per_pixel = destination.pixel_type.bytes_per_pixel();
391    for row in 0..intersection.h as usize {
392        let src_x = (intersection.x - offset_x) as usize;
393        let src_y = (intersection.y - offset_y) as usize + row;
394        let dst_x = intersection.x as usize;
395        let dst_y = intersection.y as usize + row;
396        let row_bytes = intersection.w as usize * bytes_per_pixel;
397
398        let src_offset = src_y
399            .checked_mul(source.stride)
400            .and_then(|value| value.checked_add(src_x * bytes_per_pixel))
401            .ok_or_else(|| CziError::internal_overflow("source tile offset"))?;
402        let dst_offset = dst_y
403            .checked_mul(destination.stride)
404            .and_then(|value| value.checked_add(dst_x * bytes_per_pixel))
405            .ok_or_else(|| CziError::internal_overflow("destination tile offset"))?;
406
407        destination.data[dst_offset..dst_offset + row_bytes]
408            .copy_from_slice(&source.data[src_offset..src_offset + row_bytes]);
409    }
410
411    Ok(())
412}