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::UnsupportedVersion {
46                major: version.0,
47                minor: version.1,
48            });
49        }
50
51        // Read chunkmap from end of file
52        let chunkmap = read_chunkmap(&mut reader)?;
53
54        Ok(Self {
55            reader,
56            version,
57            chunkmap,
58            attributes: None,
59            experiment: None,
60            text_info: 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    pub 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    pub 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    /// Get text info (descriptions, author, date, etc.)
121    pub fn text_info(&mut self) -> Result<&TextInfo> {
122        if self.text_info.is_none() {
123            let chunk_name: &[u8] = if self.version.0 >= 3 {
124                b"ImageTextInfoLV!"
125            } else {
126                b"ImageTextInfo!"
127            };
128
129            if !self.chunkmap.contains_key(chunk_name) {
130                self.text_info = Some(TextInfo::default());
131            } else {
132                let data = read_chunk(&mut self.reader, &self.chunkmap, chunk_name)?;
133                let parser = ClxLiteParser::new(false);
134                let clx = parser.parse(&data)?;
135                self.text_info = Some(parse_text_info(clx)?);
136            }
137        }
138        Ok(self.text_info.as_ref().unwrap())
139    }
140
141    /// List all chunk names in the file
142    pub fn chunk_names(&self) -> Vec<String> {
143        self.chunkmap
144            .keys()
145            .filter_map(|k| String::from_utf8(k.clone()).ok())
146            .collect()
147    }
148
149    /// Read raw chunk data by name
150    pub fn read_raw_chunk(&mut self, name: &[u8]) -> Result<Vec<u8>> {
151        read_chunk(&mut self.reader, &self.chunkmap, name)
152    }
153
154    /// Dimensions (P,T,C,Z,Y,X) derived from attributes + experiment.
155    /// When experiment is empty, infers minimal structure from sequence_count.
156    pub fn sizes(&mut self) -> Result<HashMap<String, usize>> {
157        let attrs = self.attributes()?.clone();
158        let exp = self.experiment()?.clone();
159
160        let n_chan = attrs.channel_count.unwrap_or(attrs.component_count);
161        let height = attrs.height_px as usize;
162        let width = attrs.width_px.or(attrs.width_bytes.map(|w| {
163            let bpp = attrs.bits_per_component_in_memory / 8;
164            w / (bpp * attrs.component_count)
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 frame: seq_index -> axis name -> index.
219    /// Order follows experiment loop order (matching nd2-py).
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 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 chunk_name = format!("ImageDataSeq|{}!", index);
246        let chunk_key = chunk_name.as_bytes();
247
248        let h = attrs.height_px as usize;
249        let w = attrs.width_px.unwrap_or(0) as usize;
250        let (n_c, n_comp) = match attrs.channel_count {
251            Some(ch) if ch > 0 => (
252                ch as usize,
253                (attrs.component_count / ch) as usize,
254            ),
255            _ => (attrs.component_count as usize, 1),
256        };
257        let frame_size = h * w * n_c * n_comp;
258        let expected_raw = frame_size * (attrs.bits_per_component_in_memory / 8) as usize;
259
260        let data = self.read_raw_chunk(chunk_key)?;
261
262        let pixel_bytes = if attrs.compression_type == Some(CompressionType::Lossless) {
263            let mut decoder = ZlibDecoder::new(&data[8..]);
264            let mut decompressed = Vec::new();
265            decoder.read_to_end(&mut decompressed)?;
266            decompressed
267        } else if data.len() == expected_raw {
268            data
269        } else if data.len() >= 8 && (data.len() - 8) == expected_raw {
270            data[8..].to_vec()
271        } else {
272            data
273        };
274
275        if pixel_bytes.len() / 2 < frame_size {
276            return Err(Nd2Error::InvalidFormat(format!(
277                "Frame {}: expected {} pixels ({} bytes), got {} bytes",
278                index,
279                frame_size,
280                frame_size * 2,
281                pixel_bytes.len()
282            )));
283        }
284
285        let mut pixels: Vec<u16> = vec![0; pixel_bytes.len() / 2];
286        for (i, chunk) in pixel_bytes.chunks_exact(2).enumerate() {
287            pixels[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
288        }
289
290        if pixels.len() < frame_size {
291            return Err(Nd2Error::InvalidFormat(format!(
292                "Frame {}: pixel count {} < expected {}",
293                index, pixels.len(), frame_size
294            )));
295        }
296
297        let mut out = vec![0u16; frame_size];
298        for y in 0..h {
299            for x in 0..w {
300                for c in 0..n_c {
301                    for comp in 0..n_comp {
302                        let src_idx = (y * w * n_c * n_comp) + (x * n_c * n_comp) + (c * n_comp) + comp;
303                        let dst_idx = (c * n_comp + comp) * (h * w) + y * w + x;
304                        out[dst_idx] = pixels[src_idx];
305                    }
306                }
307            }
308        }
309
310        Ok(out)
311    }
312
313    /// Build axis order and coord shape for seq_index (chunk lookup).
314    /// sequence_count = number of ImageDataSeq chunks. When channels are "in-pixel"
315    /// (stored within each chunk), sequence_count = product(experiment loops) and we
316    /// must NOT include C in axis_order for chunk indexing.
317    fn coord_axis_order(&mut self) -> Result<(Vec<&'static str>, Vec<usize>)> {
318        let attrs = self.attributes()?.clone();
319        let exp = self.experiment()?.clone();
320        let n_chan = attrs.channel_count.unwrap_or(attrs.component_count) as usize;
321        let seq_count = attrs.sequence_count as usize;
322
323        let mut axis_order: Vec<&'static str> = Vec::new();
324        let mut coord_shape: Vec<usize> = Vec::new();
325
326        if exp.is_empty() {
327            // Fallback: P,T,C,Z (matches sizes() fallback)
328            let n_z = 1;
329            let n_pos = 1;
330            let n_time = seq_count / (n_pos * n_chan * n_z).max(1);
331            axis_order.extend([AXIS_P, AXIS_T, AXIS_C, AXIS_Z]);
332            coord_shape.extend([n_pos, n_time, n_chan, n_z]);
333        } else {
334            for loop_ in &exp {
335                match loop_ {
336                    crate::types::ExpLoop::TimeLoop(t) => {
337                        axis_order.push(AXIS_T);
338                        coord_shape.push(t.count as usize);
339                    }
340                    crate::types::ExpLoop::NETimeLoop(n) => {
341                        axis_order.push(AXIS_T);
342                        coord_shape.push(n.count as usize);
343                    }
344                    crate::types::ExpLoop::XYPosLoop(xy) => {
345                        axis_order.push(AXIS_P);
346                        coord_shape.push(xy.count as usize);
347                    }
348                    crate::types::ExpLoop::ZStackLoop(z) => {
349                        axis_order.push(AXIS_Z);
350                        coord_shape.push(z.count as usize);
351                    }
352                    crate::types::ExpLoop::CustomLoop(_) => {}
353                }
354            }
355            // Add missing axes with size 1 (matching sizes())
356            if !axis_order.contains(&AXIS_P) {
357                axis_order.push(AXIS_P);
358                coord_shape.push(1);
359            }
360            if !axis_order.contains(&AXIS_T) {
361                axis_order.push(AXIS_T);
362                coord_shape.push(1);
363            }
364            if !axis_order.contains(&AXIS_Z) {
365                axis_order.push(AXIS_Z);
366                coord_shape.push(1);
367            }
368            // Only add C (and ensure Z) when sequence_count indicates chunks span channel
369            let exp_product: usize = coord_shape.iter().product();
370            if exp_product > 0 && exp_product * n_chan <= seq_count {
371                axis_order.push(AXIS_C);
372                coord_shape.push(n_chan);
373            }
374            if !axis_order.contains(&AXIS_Z) {
375                axis_order.push(AXIS_Z);
376                coord_shape.push(1);
377            }
378        }
379
380        Ok((axis_order, coord_shape))
381    }
382
383    /// Compute sequence index from (p,t,c,z) using experiment loop order (matching nd2-py).
384    fn seq_index_from_coords(
385        &mut self,
386        p: usize,
387        t: usize,
388        c: usize,
389        z: usize,
390    ) -> Result<usize> {
391        let (axis_order, coord_shape) = self.coord_axis_order()?;
392        let coords: Vec<usize> = axis_order
393            .iter()
394            .map(|&ax| match ax {
395                AXIS_P => p,
396                AXIS_T => t,
397                AXIS_C => c,
398                AXIS_Z => z,
399                _ => 0,
400            })
401            .collect();
402
403        if coords.len() != coord_shape.len() {
404            return Err(Nd2Error::InvalidFormat(
405                "Coord/axis length mismatch".to_string(),
406            ));
407        }
408
409        let mut seq = 0usize;
410        let mut stride = 1;
411        for i in (0..coords.len()).rev() {
412            seq += coords[i] * stride;
413            stride *= coord_shape[i];
414        }
415        Ok(seq)
416    }
417
418    /// Read 2D Y×X frame at (p,t,c,z). Returns the Y×X pixels for the requested channel.
419    pub fn read_frame_2d(&mut self, p: usize, t: usize, c: usize, z: usize) -> Result<Vec<u16>> {
420        let sizes = self.sizes()?;
421        let height = *sizes.get(AXIS_Y).unwrap_or(&1);
422        let width = *sizes.get(AXIS_X).unwrap_or(&1);
423        let seq_index = self.seq_index_from_coords(p, t, c, z)?;
424
425        let frame = self.read_frame(seq_index)?;
426        let len = height * width;
427        // Frame is (C,Y,X) planar: channel c is at [c*len..(c+1)*len]
428        let start = (c * len).min(frame.len());
429        let end = ((c + 1) * len).min(frame.len());
430        Ok(frame[start..end].to_vec())
431    }
432
433    fn read_version<R: Read + Seek>(reader: &mut R) -> Result<(u32, u32)> {
434        reader.seek(SeekFrom::Start(0))?;
435
436        let mut header = [0u8; 112]; // 4 + 4 + 8 + 32 + 64
437        reader.read_exact(&mut header).map_err(|e| {
438            Nd2Error::InvalidFormat(format!(
439                "Failed to read file header (expected 112 bytes): {}",
440                e
441            ))
442        })?;
443
444        let magic = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
445
446        if magic == JP2_MAGIC {
447            return Ok((1, 0)); // Legacy format
448        }
449
450        if magic != ND2_CHUNK_MAGIC {
451            return Err(Nd2Error::InvalidMagic {
452                expected: ND2_CHUNK_MAGIC,
453                actual: magic,
454            });
455        }
456
457        let name_length = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
458        let data_length = u64::from_le_bytes([
459            header[8], header[9], header[10], header[11], header[12], header[13], header[14],
460            header[15],
461        ]);
462
463        // Validate header
464        if name_length != 32 || data_length != 64 {
465            return Err(Nd2Error::InvalidFormat(
466                "Corrupt file header".to_string(),
467            ));
468        }
469
470        // Check signature
471        let name = &header[16..48];
472        if name != ND2_FILE_SIGNATURE {
473            return Err(Nd2Error::InvalidFormat(
474                "Invalid file signature".to_string(),
475            ));
476        }
477
478        // Parse version from data (e.g., "Ver3.0")
479        let data = &header[48..112];
480        let major = (data[3] as char).to_digit(10).unwrap_or(0);
481        let minor = (data[5] as char).to_digit(10).unwrap_or(0);
482
483        Ok((major, minor))
484    }
485}
486
487impl Drop for Nd2File {
488    fn drop(&mut self) {
489        // File is automatically closed when BufReader<File> is dropped
490    }
491}