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}