Skip to main content

nd2_rs/
reader.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufReader, Read, Seek, SeekFrom};
4use std::path::Path;
5
6use flate2::read::ZlibDecoder;
7
8use crate::chunk::{read_chunk, read_chunkmap, ChunkMap};
9use crate::constants::{JP2_MAGIC, ND2_CHUNK_MAGIC, ND2_FILE_SIGNATURE};
10use crate::error::{Nd2Error, Result};
11use crate::meta_parse::{parse_attributes, parse_experiment, parse_text_info};
12use crate::parse::ClxLiteParser;
13use crate::types::{Attributes, CompressionType, ExpLoop, TextInfo};
14
15/// Axis names matching nd2-py AXIS
16const AXIS_T: &str = "T";
17const AXIS_P: &str = "P";
18const AXIS_C: &str = "C";
19const AXIS_Z: &str = "Z";
20const AXIS_Y: &str = "Y";
21const AXIS_X: &str = "X";
22
23/// Main reader for ND2 files
24pub struct Nd2File {
25    reader: BufReader<File>,
26    version: (u32, u32),
27    chunkmap: ChunkMap,
28    // Cached metadata
29    attributes: Option<Attributes>,
30    experiment: Option<Vec<ExpLoop>>,
31    text_info: Option<TextInfo>,
32}
33
34impl Nd2File {
35    /// Open an ND2 file for reading
36    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
37        let file = File::open(path)?;
38        let mut reader = BufReader::new(file);
39
40        // Read and validate file header
41        let version = Self::read_version(&mut reader)?;
42
43        // Validate version is supported (2.0, 2.1, 3.0)
44        if version.0 < 2 || version.0 > 3 {
45            return Err(Nd2Error::unsupported_version(version.0, version.1));
46        }
47
48        // Read chunkmap from end of file
49        let chunkmap = read_chunkmap(&mut reader)?;
50
51        Ok(Self {
52            reader,
53            version,
54            chunkmap,
55            attributes: None,
56            experiment: None,
57            text_info: None,
58        })
59    }
60
61    /// Get the file format version (major, minor)
62    pub fn version(&self) -> (u32, u32) {
63        self.version
64    }
65
66    /// Get image attributes
67    pub fn attributes(&mut self) -> Result<&Attributes> {
68        if self.attributes.is_none() {
69            let chunk_name: &[u8] = if self.version.0 >= 3 {
70                b"ImageAttributesLV!"
71            } else {
72                b"ImageAttributes!"
73            };
74            let data = read_chunk(&mut self.reader, &self.chunkmap, chunk_name)?;
75            let parser = ClxLiteParser::new(false);
76            let clx = parser.parse(&data)?;
77            self.attributes = Some(parse_attributes(clx)?);
78        }
79        Ok(self.attributes.as_ref().unwrap())
80    }
81
82    /// Get experiment loop definitions
83    pub fn experiment(&mut self) -> Result<&Vec<ExpLoop>> {
84        if self.experiment.is_none() {
85            let chunk_name: &[u8] = if self.version.0 >= 3 {
86                b"ImageMetadataLV!"
87            } else {
88                b"ImageMetadata!"
89            };
90
91            if !self.chunkmap.contains_key(chunk_name) {
92                self.experiment = Some(Vec::new());
93            } else {
94                let data = read_chunk(&mut self.reader, &self.chunkmap, chunk_name)?;
95                let parser = ClxLiteParser::new(false);
96                let clx = parser.parse(&data)?;
97                // v3 wraps in SLxExperiment; unwrap if present and is object
98                let to_parse = if self.version.0 >= 3 {
99                    match clx.as_object().and_then(|o| o.get("SLxExperiment")) {
100                        Some(inner) if inner.as_object().is_some() => inner.clone(),
101                        _ => clx.clone(),
102                    }
103                } else {
104                    clx.clone()
105                };
106                let mut exp = parse_experiment(to_parse).unwrap_or_default();
107                // If unwrapped gave empty, try parsing root directly (some v3 files differ)
108                if exp.is_empty() && self.version.0 >= 3 {
109                    exp = parse_experiment(clx).unwrap_or_default();
110                }
111                self.experiment = Some(exp);
112            }
113        }
114        Ok(self.experiment.as_ref().unwrap())
115    }
116
117    /// Get text info (descriptions, author, date, etc.)
118    pub fn text_info(&mut self) -> Result<&TextInfo> {
119        if self.text_info.is_none() {
120            let chunk_name: &[u8] = if self.version.0 >= 3 {
121                b"ImageTextInfoLV!"
122            } else {
123                b"ImageTextInfo!"
124            };
125
126            if !self.chunkmap.contains_key(chunk_name) {
127                self.text_info = Some(TextInfo::default());
128            } else {
129                let data = read_chunk(&mut self.reader, &self.chunkmap, chunk_name)?;
130                let parser = ClxLiteParser::new(false);
131                let clx = parser.parse(&data)?;
132                self.text_info = Some(parse_text_info(clx)?);
133            }
134        }
135        Ok(self.text_info.as_ref().unwrap())
136    }
137
138    /// List all chunk names in the file
139    pub fn chunk_names(&self) -> Vec<String> {
140        self.chunkmap
141            .keys()
142            .filter_map(|k| String::from_utf8(k.clone()).ok())
143            .collect()
144    }
145
146    /// Read raw chunk data by name
147    pub fn read_raw_chunk(&mut self, name: &[u8]) -> Result<Vec<u8>> {
148        read_chunk(&mut self.reader, &self.chunkmap, name)
149    }
150
151    /// Dimensions (P,T,C,Z,Y,X) derived from attributes + experiment.
152    /// When experiment is empty, infers minimal structure from sequence_count.
153    pub fn sizes(&mut self) -> Result<HashMap<String, usize>> {
154        let attrs = self.attributes()?.clone();
155        let exp = self.experiment()?.clone();
156
157        let n_chan = attrs.channel_count.unwrap_or(attrs.component_count);
158        let height = attrs.height_px as usize;
159        let width = attrs
160            .width_px
161            .or(attrs.width_bytes.map(|w| {
162                let bpp = attrs.bits_per_component_in_memory / 8;
163                w / (bpp * attrs.component_count)
164            }))
165            .unwrap_or(0) as usize;
166
167        let mut sizes: HashMap<String, usize> = HashMap::new();
168
169        if exp.is_empty() {
170            // Fallback: assume P=1, Z=1, infer T from sequence_count
171            let total = attrs.sequence_count as usize;
172            let n_z: usize = 1;
173            let n_pos: usize = 1;
174            let n_chan_usize = n_chan as usize;
175            let n_time = total / (n_pos * n_chan_usize * n_z).max(1);
176            sizes.insert(AXIS_P.to_string(), n_pos);
177            sizes.insert(AXIS_T.to_string(), n_time);
178            sizes.insert(AXIS_C.to_string(), n_chan_usize);
179            sizes.insert(AXIS_Z.to_string(), n_z);
180        } else {
181            for loop_ in exp {
182                match loop_ {
183                    ExpLoop::TimeLoop(t) => {
184                        sizes.insert(AXIS_T.to_string(), t.count as usize);
185                    }
186                    ExpLoop::XYPosLoop(xy) => {
187                        sizes.insert(AXIS_P.to_string(), xy.count as usize);
188                    }
189                    ExpLoop::ZStackLoop(z) => {
190                        sizes.insert(AXIS_Z.to_string(), z.count as usize);
191                    }
192                    ExpLoop::NETimeLoop(n) => {
193                        sizes.insert(AXIS_T.to_string(), n.count as usize);
194                    }
195                    ExpLoop::CustomLoop(_) => {}
196                }
197            }
198            if !sizes.contains_key(AXIS_C) {
199                sizes.insert(AXIS_C.to_string(), n_chan as usize);
200            }
201            if !sizes.contains_key(AXIS_P) {
202                sizes.insert(AXIS_P.to_string(), 1);
203            }
204            if !sizes.contains_key(AXIS_T) {
205                sizes.insert(AXIS_T.to_string(), 1);
206            }
207            if !sizes.contains_key(AXIS_Z) {
208                sizes.insert(AXIS_Z.to_string(), 1);
209            }
210        }
211
212        sizes.insert(AXIS_Y.to_string(), height);
213        sizes.insert(AXIS_X.to_string(), width);
214
215        Ok(sizes)
216    }
217
218    /// Loop indices for each sequence chunk: seq_index -> axis name -> index.
219    /// Channel is omitted when stored in-pixel instead of as separate chunks.
220    pub fn loop_indices(&mut self) -> Result<Vec<HashMap<String, usize>>> {
221        let (axis_order, coord_shape) = self.coord_axis_order()?;
222        let total: usize = coord_shape.iter().product();
223
224        let mut out = Vec::with_capacity(total);
225        let n = axis_order.len();
226
227        for seq in 0..total {
228            let mut idx = seq;
229            let mut m = HashMap::new();
230            // Unravel seq: innermost acquisition axis varies fastest
231            for i in (0..n).rev() {
232                let coord = idx % coord_shape[i];
233                idx /= coord_shape[i];
234                m.insert(axis_order[i].to_string(), coord);
235            }
236            out.push(m);
237        }
238
239        Ok(out)
240    }
241
242    /// Read one frame by sequence index. Returns pixels as (C, Y, X) u16 data.
243    pub fn read_frame(&mut self, index: usize) -> Result<Vec<u16>> {
244        let attrs = self.attributes()?.clone();
245        let max_seq = attrs.sequence_count as usize;
246        let chunk_name = format!("ImageDataSeq|{}!", index);
247        let chunk_key = chunk_name.as_bytes();
248
249        let h = attrs.height_px as usize;
250        let w = attrs.width_px.unwrap_or(0) as usize;
251        let (n_c, n_comp) = match attrs.channel_count {
252            Some(ch) if ch > 0 => (ch as usize, (attrs.component_count / ch) as usize),
253            _ => (attrs.component_count as usize, 1),
254        };
255        let bytes_per_pixel = (attrs.bits_per_component_in_memory / 8) as usize;
256        if bytes_per_pixel == 0 {
257            return Err(Nd2Error::file_invalid_format(
258                "Invalid bits_per_component_in_memory".to_string(),
259            ));
260        }
261        let raw_row_bytes = attrs.width_bytes.map(|w| w as usize).unwrap_or_else(|| {
262            w.saturating_mul(n_c)
263                .saturating_mul(n_comp)
264                .saturating_mul(bytes_per_pixel)
265        });
266        if raw_row_bytes == 0 {
267            return Err(Nd2Error::file_invalid_format(
268                "Invalid frame row stride".to_string(),
269            ));
270        }
271        if raw_row_bytes % bytes_per_pixel != 0 {
272            return Err(Nd2Error::file_invalid_format(format!(
273                "Frame row stride {} is not divisible by bytes per pixel {}",
274                raw_row_bytes, bytes_per_pixel
275            )));
276        }
277        let raw_row_pixels = raw_row_bytes / bytes_per_pixel;
278
279        let frame_size = h
280            .checked_mul(w)
281            .and_then(|v| v.checked_mul(n_c))
282            .and_then(|v| v.checked_mul(n_comp))
283            .ok_or_else(|| {
284                Nd2Error::file_invalid_format("Frame dimensions overflow".to_string())
285            })?;
286        let expected_raw = h
287            .checked_mul(raw_row_bytes)
288            .ok_or_else(|| Nd2Error::file_invalid_format("Frame byte size overflow".to_string()))?;
289        let frame_area = h
290            .checked_mul(w)
291            .ok_or_else(|| Nd2Error::file_invalid_format("Frame area overflow".to_string()))?;
292        let n_c_n_comp = n_c.checked_mul(n_comp).ok_or_else(|| {
293            Nd2Error::file_invalid_format("Frame channel/component overflow".to_string())
294        })?;
295        if raw_row_pixels < n_c_n_comp.saturating_mul(w) {
296            return Err(Nd2Error::file_invalid_format(format!(
297                "Frame row stride {} pixels is smaller than required width {}",
298                raw_row_pixels,
299                n_c_n_comp.saturating_mul(w)
300            )));
301        }
302
303        let pixel_bytes = match attrs.compression_type {
304            Some(CompressionType::Lossless) => {
305                let data = match self.read_raw_chunk(chunk_key) {
306                    Ok(data) => data,
307                    Err(err) => {
308                        if matches!(
309                            err,
310                            Nd2Error::File {
311                                source: crate::error::FileError::ChunkNotFound { .. },
312                            }
313                        ) {
314                            return Err(Nd2Error::input_out_of_range(
315                                "sequence index",
316                                index,
317                                max_seq,
318                            ));
319                        }
320                        return Err(err);
321                    }
322                };
323
324                if data.len() < 8 {
325                    return Err(Nd2Error::file_invalid_format(format!(
326                        "Frame {} compressed chunk too short ({} bytes)",
327                        index,
328                        data.len()
329                    )));
330                }
331                let mut decoder = ZlibDecoder::new(&data[8..]);
332                let mut decompressed = Vec::new();
333                decoder.read_to_end(&mut decompressed)?;
334                decompressed
335            }
336            _ => match self.read_uncompressed_frame_bytes(chunk_key, expected_raw) {
337                Ok(data) => data,
338                Err(err) => {
339                    if matches!(
340                        err,
341                        Nd2Error::File {
342                            source: crate::error::FileError::ChunkNotFound { .. },
343                        }
344                    ) {
345                        return Err(Nd2Error::input_out_of_range(
346                            "sequence index",
347                            index,
348                            max_seq,
349                        ));
350                    }
351                    return Err(err);
352                }
353            },
354        };
355
356        if pixel_bytes.len() % 2 != 0 {
357            return Err(Nd2Error::file_invalid_format(format!(
358                "Frame {}: pixel data length {} is not divisible by 2",
359                index,
360                pixel_bytes.len()
361            )));
362        }
363
364        if pixel_bytes.len() / 2 < frame_size {
365            return Err(Nd2Error::file_invalid_format(format!(
366                "Frame {}: expected {} pixels ({} bytes), got {} bytes",
367                index,
368                frame_size,
369                frame_size * 2,
370                pixel_bytes.len()
371            )));
372        }
373
374        let mut pixels: Vec<u16> = vec![0; pixel_bytes.len() / 2];
375        for (i, chunk) in pixel_bytes.chunks_exact(2).enumerate() {
376            pixels[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
377        }
378
379        if pixels.len() < frame_size {
380            return Err(Nd2Error::file_invalid_format(format!(
381                "Frame {}: pixel count {} < expected {}",
382                index,
383                pixels.len(),
384                frame_size
385            )));
386        }
387
388        let mut out = vec![0u16; frame_size];
389        let row_pixels = raw_row_pixels;
390
391        for y in 0..h {
392            let y_offset = y.checked_mul(row_pixels).ok_or_else(|| {
393                Nd2Error::file_invalid_format("Frame offset overflow".to_string())
394            })?;
395            let y_plane_offset = y.checked_mul(w).ok_or_else(|| {
396                Nd2Error::file_invalid_format("Frame plane offset overflow".to_string())
397            })?;
398            for x in 0..w {
399                let x_offset = x.checked_mul(n_c_n_comp).ok_or_else(|| {
400                    Nd2Error::file_invalid_format("Frame offset overflow".to_string())
401                })?;
402                for c in 0..n_c {
403                    let c_offset = c.checked_mul(n_comp).ok_or_else(|| {
404                        Nd2Error::file_invalid_format("Frame offset overflow".to_string())
405                    })?;
406                    for comp in 0..n_comp {
407                        let src_idx = y_offset
408                            .checked_add(x_offset)
409                            .and_then(|v| v.checked_add(c_offset))
410                            .and_then(|v| v.checked_add(comp))
411                            .ok_or_else(|| {
412                                Nd2Error::file_invalid_format("Frame offset overflow".to_string())
413                            })?;
414                        let dst_x = y_plane_offset.checked_add(x).ok_or_else(|| {
415                            Nd2Error::file_invalid_format("Frame offset overflow".to_string())
416                        })?;
417                        let c_plane = c_offset.checked_add(comp).ok_or_else(|| {
418                            Nd2Error::file_invalid_format("Frame offset overflow".to_string())
419                        })?;
420                        let dst_idx = c_plane
421                            .checked_mul(frame_area)
422                            .and_then(|v| v.checked_add(dst_x))
423                            .ok_or_else(|| {
424                                Nd2Error::file_invalid_format("Frame offset overflow".to_string())
425                            })?;
426                        out[dst_idx] = pixels[src_idx];
427                    }
428                }
429            }
430        }
431
432        Ok(out)
433    }
434
435    fn read_uncompressed_frame_bytes(
436        &mut self,
437        chunk_key: &[u8],
438        expected_raw: usize,
439    ) -> Result<Vec<u8>> {
440        let file_size = self.reader.seek(SeekFrom::End(0))?;
441        let offset = self
442            .chunkmap
443            .get(chunk_key)
444            .map(|(offset, _)| *offset)
445            .ok_or_else(|| Nd2Error::file_chunk_not_found(String::from_utf8_lossy(chunk_key)))?;
446
447        let pixel_offset = match self.read_image_chunk_payload_offset(offset)? {
448            Some(payload_offset) => payload_offset.checked_add(8).ok_or_else(|| {
449                Nd2Error::file_invalid_format("Frame payload offset overflow".to_string())
450            })?,
451            None => offset.checked_add(4096).ok_or_else(|| {
452                Nd2Error::file_invalid_format("Frame fallback offset overflow".to_string())
453            })?,
454        };
455
456        let pixel_end = pixel_offset
457            .checked_add(expected_raw as u64)
458            .ok_or_else(|| Nd2Error::file_invalid_format("Frame bounds overflow".to_string()))?;
459        if pixel_end > file_size {
460            return Err(Nd2Error::file_invalid_format(format!(
461                "Frame chunk '{}' exceeds file bounds",
462                String::from_utf8_lossy(chunk_key)
463            )));
464        }
465
466        self.reader.seek(SeekFrom::Start(pixel_offset))?;
467        let mut pixel_bytes = vec![0u8; expected_raw];
468        self.reader.read_exact(&mut pixel_bytes)?;
469        Ok(pixel_bytes)
470    }
471
472    /// Build axis order and coord shape for seq_index (chunk lookup).
473    /// sequence_count = number of ImageDataSeq chunks. When channels are "in-pixel"
474    /// (stored within each chunk), sequence_count = product(experiment loops) and we
475    /// must NOT include C in axis_order for chunk indexing.
476    fn coord_axis_order(&mut self) -> Result<(Vec<&'static str>, Vec<usize>)> {
477        let attrs = self.attributes()?.clone();
478        let exp = self.experiment()?.clone();
479        let n_chan = attrs.channel_count.unwrap_or(attrs.component_count) as usize;
480        let seq_count = attrs.sequence_count as usize;
481
482        let mut axis_order: Vec<&'static str> = Vec::new();
483        let mut coord_shape: Vec<usize> = Vec::new();
484
485        if exp.is_empty() {
486            // Fallback: P,T,C,Z (matches sizes() fallback)
487            let n_z = 1;
488            let n_pos = 1;
489            let n_time = seq_count / (n_pos * n_chan * n_z).max(1);
490            axis_order.extend([AXIS_P, AXIS_T, AXIS_C, AXIS_Z]);
491            coord_shape.extend([n_pos, n_time, n_chan, n_z]);
492        } else {
493            for loop_ in &exp {
494                match loop_ {
495                    crate::types::ExpLoop::TimeLoop(t) => {
496                        axis_order.push(AXIS_T);
497                        coord_shape.push(t.count as usize);
498                    }
499                    crate::types::ExpLoop::NETimeLoop(n) => {
500                        axis_order.push(AXIS_T);
501                        coord_shape.push(n.count as usize);
502                    }
503                    crate::types::ExpLoop::XYPosLoop(xy) => {
504                        axis_order.push(AXIS_P);
505                        coord_shape.push(xy.count as usize);
506                    }
507                    crate::types::ExpLoop::ZStackLoop(z) => {
508                        axis_order.push(AXIS_Z);
509                        coord_shape.push(z.count as usize);
510                    }
511                    crate::types::ExpLoop::CustomLoop(_) => {}
512                }
513            }
514            // Add missing axes with size 1 (matching sizes())
515            if !axis_order.contains(&AXIS_P) {
516                axis_order.push(AXIS_P);
517                coord_shape.push(1);
518            }
519            if !axis_order.contains(&AXIS_T) {
520                axis_order.push(AXIS_T);
521                coord_shape.push(1);
522            }
523            if !axis_order.contains(&AXIS_Z) {
524                axis_order.push(AXIS_Z);
525                coord_shape.push(1);
526            }
527            // Only add C (and ensure Z) when sequence_count indicates chunks span channel
528            let exp_product: usize = coord_shape.iter().product();
529            if exp_product > 0 && exp_product * n_chan <= seq_count {
530                axis_order.push(AXIS_C);
531                coord_shape.push(n_chan);
532            }
533            if !axis_order.contains(&AXIS_Z) {
534                axis_order.push(AXIS_Z);
535                coord_shape.push(1);
536            }
537        }
538
539        Ok((axis_order, coord_shape))
540    }
541
542    /// Compute sequence index from (p,t,c,z) using experiment loop order (matching nd2-py).
543    fn seq_index_from_coords(&mut self, p: usize, t: usize, c: usize, z: usize) -> Result<usize> {
544        let (axis_order, coord_shape) = self.coord_axis_order()?;
545        let coords: Vec<usize> = axis_order
546            .iter()
547            .map(|&ax| match ax {
548                AXIS_P => p,
549                AXIS_T => t,
550                AXIS_C => c,
551                AXIS_Z => z,
552                _ => 0,
553            })
554            .collect();
555
556        if coords.len() != coord_shape.len() {
557            return Err(Nd2Error::file_invalid_format(
558                "Coord/axis length mismatch".to_string(),
559            ));
560        }
561
562        for (idx, (&coord, &shape)) in coords.iter().zip(coord_shape.iter()).enumerate() {
563            if shape == 0 {
564                return Err(Nd2Error::file_invalid_format(format!(
565                    "Invalid axis length: {} has size 0",
566                    axis_order[idx]
567                )));
568            }
569            if coord >= shape {
570                return Err(Nd2Error::input_out_of_range(
571                    format!("axis {}", axis_order[idx]),
572                    coord,
573                    shape,
574                ));
575            }
576        }
577
578        let mut seq = 0usize;
579        let mut stride = 1;
580        for i in (0..coords.len()).rev() {
581            let next = coords[i]
582                .checked_mul(stride)
583                .ok_or_else(|| Nd2Error::internal_overflow("sequence index multiply"))?;
584            seq = seq
585                .checked_add(next)
586                .ok_or_else(|| Nd2Error::internal_overflow("sequence index add"))?;
587            stride = stride
588                .checked_mul(coord_shape[i])
589                .ok_or_else(|| Nd2Error::internal_overflow("sequence stride multiply"))?;
590        }
591        Ok(seq)
592    }
593
594    fn read_image_chunk_payload_offset(&mut self, offset: u64) -> Result<Option<u64>> {
595        self.reader.seek(SeekFrom::Start(offset))?;
596
597        let header = match crate::chunk::ChunkHeader::read(&mut self.reader) {
598            Ok(header) => header,
599            Err(_) => return Ok(None),
600        };
601
602        if header.magic != ND2_CHUNK_MAGIC {
603            return Ok(None);
604        }
605
606        let payload_offset = offset
607            .checked_add(16)
608            .and_then(|v| v.checked_add(header.name_length as u64))
609            .ok_or_else(|| {
610                Nd2Error::file_invalid_format("Frame payload offset overflow".to_string())
611            })?;
612
613        Ok(Some(payload_offset))
614    }
615
616    /// Read 2D Y×X frame at (p,t,c,z). Returns the Y×X pixels for the requested channel.
617    pub fn read_frame_2d(&mut self, p: usize, t: usize, c: usize, z: usize) -> Result<Vec<u16>> {
618        let sizes = self.sizes()?;
619        let height = *sizes.get(AXIS_Y).ok_or_else(|| {
620            Nd2Error::file_invalid_format("Missing height (Y) dimension".to_string())
621        })?;
622        let width = *sizes.get(AXIS_X).ok_or_else(|| {
623            Nd2Error::file_invalid_format("Missing width (X) dimension".to_string())
624        })?;
625        let n_pos = *sizes.get(AXIS_P).ok_or_else(|| {
626            Nd2Error::file_invalid_format("Missing position (P) dimension".to_string())
627        })?;
628        let n_time = *sizes.get(AXIS_T).ok_or_else(|| {
629            Nd2Error::file_invalid_format("Missing time (T) dimension".to_string())
630        })?;
631        let n_chan = *sizes.get(AXIS_C).ok_or_else(|| {
632            Nd2Error::file_invalid_format("Missing channel (C) dimension".to_string())
633        })?;
634        let n_z = *sizes
635            .get(AXIS_Z)
636            .ok_or_else(|| Nd2Error::file_invalid_format("Missing Z dimension".to_string()))?;
637
638        if p >= n_pos {
639            return Err(Nd2Error::input_out_of_range("position index", p, n_pos));
640        }
641        if t >= n_time {
642            return Err(Nd2Error::input_out_of_range("time index", t, n_time));
643        }
644        if c >= n_chan {
645            return Err(Nd2Error::input_out_of_range("channel index", c, n_chan));
646        }
647        if z >= n_z {
648            return Err(Nd2Error::input_out_of_range("z index", z, n_z));
649        }
650
651        let seq_index = self.seq_index_from_coords(p, t, c, z)?;
652
653        let frame = self.read_frame(seq_index)?;
654        let len = height.checked_mul(width).ok_or_else(|| {
655            Nd2Error::file_invalid_format("Frame dimensions overflow".to_string())
656        })?;
657
658        // Frame is (C,Y,X) planar: channel c is at [c*len..(c+1)*len]
659        let start = c.checked_mul(len).ok_or_else(|| {
660            Nd2Error::file_invalid_format("Frame slice start overflow".to_string())
661        })?;
662        let end = (c + 1)
663            .checked_mul(len)
664            .ok_or_else(|| Nd2Error::file_invalid_format("Frame slice end overflow".to_string()))?;
665        if end > frame.len() {
666            return Err(Nd2Error::file_invalid_format(format!(
667                "Frame data too short for requested channel: frame {} < {}",
668                frame.len(),
669                end
670            )));
671        }
672        Ok(frame[start..end].to_vec())
673    }
674
675    fn read_version<R: Read + Seek>(reader: &mut R) -> Result<(u32, u32)> {
676        reader.seek(SeekFrom::Start(0))?;
677
678        let mut header = [0u8; 112]; // 4 + 4 + 8 + 32 + 64
679        reader.read_exact(&mut header).map_err(|e| {
680            Nd2Error::file_invalid_format(format!(
681                "Failed to read file header (expected 112 bytes): {}",
682                e
683            ))
684        })?;
685
686        let magic = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
687
688        if magic == JP2_MAGIC {
689            return Ok((1, 0)); // Legacy format
690        }
691
692        if magic != ND2_CHUNK_MAGIC {
693            return Err(Nd2Error::file_invalid_magic(ND2_CHUNK_MAGIC, magic));
694        }
695
696        let name_length = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
697        let data_length = u64::from_le_bytes([
698            header[8], header[9], header[10], header[11], header[12], header[13], header[14],
699            header[15],
700        ]);
701
702        // Validate header
703        if name_length != 32 || data_length != 64 {
704            return Err(Nd2Error::file_invalid_format(
705                "Corrupt file header".to_string(),
706            ));
707        }
708
709        // Check signature
710        let name = &header[16..48];
711        if name != ND2_FILE_SIGNATURE {
712            return Err(Nd2Error::file_invalid_format(
713                "Invalid file signature".to_string(),
714            ));
715        }
716
717        // Parse version from data (e.g., "Ver3.0")
718        let data = &header[48..112];
719        let major = (data[3] as char).to_digit(10).unwrap_or(0);
720        let minor = (data[5] as char).to_digit(10).unwrap_or(0);
721
722        Ok((major, minor))
723    }
724}
725
726impl Drop for Nd2File {
727    fn drop(&mut self) {
728        // File is automatically closed when BufReader<File> is dropped
729    }
730}