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