Skip to main content

ad_core_rs/
ndarray.rs

1use crate::attributes::NDAttributeList;
2use crate::codec::Codec;
3use crate::error::{ADError, ADResult};
4use crate::timestamp::EpicsTimestamp;
5
6/// Maximum number of NDArray dimensions (C++ `ND_ARRAY_MAX_DIMS`,
7/// NDArray.h:26). The `Dimensions` int32-array param is always posted at this
8/// fixed length, zero-filled beyond the array's actual `ndims`, matching
9/// `doCallbacksInt32Array(dimsPrev_, ND_ARRAY_MAX_DIMS, …)`.
10pub const ND_ARRAY_MAX_DIMS: usize = 10;
11
12/// NDArray data types matching areaDetector NDDataType_t.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum NDDataType {
16    Int8 = 0,
17    UInt8 = 1,
18    Int16 = 2,
19    UInt16 = 3,
20    Int32 = 4,
21    UInt32 = 5,
22    Int64 = 6,
23    UInt64 = 7,
24    Float32 = 8,
25    Float64 = 9,
26}
27
28impl NDDataType {
29    pub fn element_size(&self) -> usize {
30        match self {
31            Self::Int8 | Self::UInt8 => 1,
32            Self::Int16 | Self::UInt16 => 2,
33            Self::Int32 | Self::UInt32 | Self::Float32 => 4,
34            Self::Int64 | Self::UInt64 | Self::Float64 => 8,
35        }
36    }
37
38    pub fn from_ordinal(v: u8) -> Option<Self> {
39        match v {
40            0 => Some(Self::Int8),
41            1 => Some(Self::UInt8),
42            2 => Some(Self::Int16),
43            3 => Some(Self::UInt16),
44            4 => Some(Self::Int32),
45            5 => Some(Self::UInt32),
46            6 => Some(Self::Int64),
47            7 => Some(Self::UInt64),
48            8 => Some(Self::Float32),
49            9 => Some(Self::Float64),
50            _ => None,
51        }
52    }
53}
54
55/// Typed buffer for NDArray data.
56#[derive(Debug, Clone)]
57pub enum NDDataBuffer {
58    I8(Vec<i8>),
59    U8(Vec<u8>),
60    I16(Vec<i16>),
61    U16(Vec<u16>),
62    I32(Vec<i32>),
63    U32(Vec<u32>),
64    I64(Vec<i64>),
65    U64(Vec<u64>),
66    F32(Vec<f32>),
67    F64(Vec<f64>),
68}
69
70impl NDDataBuffer {
71    pub fn zeros(data_type: NDDataType, count: usize) -> Self {
72        match data_type {
73            NDDataType::Int8 => Self::I8(vec![0; count]),
74            NDDataType::UInt8 => Self::U8(vec![0; count]),
75            NDDataType::Int16 => Self::I16(vec![0; count]),
76            NDDataType::UInt16 => Self::U16(vec![0; count]),
77            NDDataType::Int32 => Self::I32(vec![0; count]),
78            NDDataType::UInt32 => Self::U32(vec![0; count]),
79            NDDataType::Int64 => Self::I64(vec![0; count]),
80            NDDataType::UInt64 => Self::U64(vec![0; count]),
81            NDDataType::Float32 => Self::F32(vec![0.0; count]),
82            NDDataType::Float64 => Self::F64(vec![0.0; count]),
83        }
84    }
85
86    pub fn data_type(&self) -> NDDataType {
87        match self {
88            Self::I8(_) => NDDataType::Int8,
89            Self::U8(_) => NDDataType::UInt8,
90            Self::I16(_) => NDDataType::Int16,
91            Self::U16(_) => NDDataType::UInt16,
92            Self::I32(_) => NDDataType::Int32,
93            Self::U32(_) => NDDataType::UInt32,
94            Self::I64(_) => NDDataType::Int64,
95            Self::U64(_) => NDDataType::UInt64,
96            Self::F32(_) => NDDataType::Float32,
97            Self::F64(_) => NDDataType::Float64,
98        }
99    }
100
101    pub fn len(&self) -> usize {
102        match self {
103            Self::I8(v) => v.len(),
104            Self::U8(v) => v.len(),
105            Self::I16(v) => v.len(),
106            Self::U16(v) => v.len(),
107            Self::I32(v) => v.len(),
108            Self::U32(v) => v.len(),
109            Self::I64(v) => v.len(),
110            Self::U64(v) => v.len(),
111            Self::F32(v) => v.len(),
112            Self::F64(v) => v.len(),
113        }
114    }
115
116    pub fn is_empty(&self) -> bool {
117        self.len() == 0
118    }
119
120    pub fn total_bytes(&self) -> usize {
121        self.len() * self.data_type().element_size()
122    }
123
124    /// Capacity of the underlying Vec in bytes.
125    pub fn capacity_bytes(&self) -> usize {
126        let cap = match self {
127            Self::I8(v) => v.capacity(),
128            Self::U8(v) => v.capacity(),
129            Self::I16(v) => v.capacity(),
130            Self::U16(v) => v.capacity(),
131            Self::I32(v) => v.capacity(),
132            Self::U32(v) => v.capacity(),
133            Self::I64(v) => v.capacity(),
134            Self::U64(v) => v.capacity(),
135            Self::F32(v) => v.capacity(),
136            Self::F64(v) => v.capacity(),
137        };
138        cap * self.data_type().element_size()
139    }
140
141    /// Resize the buffer, zeroing new elements if growing.
142    pub fn resize(&mut self, new_len: usize) {
143        match self {
144            Self::I8(v) => v.resize(new_len, 0),
145            Self::U8(v) => v.resize(new_len, 0),
146            Self::I16(v) => v.resize(new_len, 0),
147            Self::U16(v) => v.resize(new_len, 0),
148            Self::I32(v) => v.resize(new_len, 0),
149            Self::U32(v) => v.resize(new_len, 0),
150            Self::I64(v) => v.resize(new_len, 0),
151            Self::U64(v) => v.resize(new_len, 0),
152            Self::F32(v) => v.resize(new_len, 0.0),
153            Self::F64(v) => v.resize(new_len, 0.0),
154        }
155    }
156
157    /// View the underlying data as a byte slice.
158    pub fn as_u8_slice(&self) -> &[u8] {
159        match self {
160            Self::I8(v) => unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len()) },
161            Self::U8(v) => v.as_slice(),
162            Self::I16(v) => unsafe {
163                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 2)
164            },
165            Self::U16(v) => unsafe {
166                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 2)
167            },
168            Self::I32(v) => unsafe {
169                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
170            },
171            Self::U32(v) => unsafe {
172                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
173            },
174            Self::I64(v) => unsafe {
175                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
176            },
177            Self::U64(v) => unsafe {
178                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
179            },
180            Self::F32(v) => unsafe {
181                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
182            },
183            Self::F64(v) => unsafe {
184                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
185            },
186        }
187    }
188
189    /// Get element at index as f64.
190    pub fn get_as_f64(&self, index: usize) -> Option<f64> {
191        match self {
192            Self::I8(v) => v.get(index).map(|&x| x as f64),
193            Self::U8(v) => v.get(index).map(|&x| x as f64),
194            Self::I16(v) => v.get(index).map(|&x| x as f64),
195            Self::U16(v) => v.get(index).map(|&x| x as f64),
196            Self::I32(v) => v.get(index).map(|&x| x as f64),
197            Self::U32(v) => v.get(index).map(|&x| x as f64),
198            Self::I64(v) => v.get(index).map(|&x| x as f64),
199            Self::U64(v) => v.get(index).map(|&x| x as f64),
200            Self::F32(v) => v.get(index).map(|&x| x as f64),
201            Self::F64(v) => v.get(index).copied(),
202        }
203    }
204
205    /// Set element at index from f64 value.
206    pub fn set_from_f64(&mut self, index: usize, value: f64) {
207        match self {
208            Self::I8(v) => {
209                if let Some(e) = v.get_mut(index) {
210                    *e = value as i8;
211                }
212            }
213            Self::U8(v) => {
214                if let Some(e) = v.get_mut(index) {
215                    *e = value as u8;
216                }
217            }
218            Self::I16(v) => {
219                if let Some(e) = v.get_mut(index) {
220                    *e = value as i16;
221                }
222            }
223            Self::U16(v) => {
224                if let Some(e) = v.get_mut(index) {
225                    *e = value as u16;
226                }
227            }
228            Self::I32(v) => {
229                if let Some(e) = v.get_mut(index) {
230                    *e = value as i32;
231                }
232            }
233            Self::U32(v) => {
234                if let Some(e) = v.get_mut(index) {
235                    *e = value as u32;
236                }
237            }
238            Self::I64(v) => {
239                if let Some(e) = v.get_mut(index) {
240                    *e = value as i64;
241                }
242            }
243            Self::U64(v) => {
244                if let Some(e) = v.get_mut(index) {
245                    *e = value as u64;
246                }
247            }
248            Self::F32(v) => {
249                if let Some(e) = v.get_mut(index) {
250                    *e = value as f32;
251                }
252            }
253            Self::F64(v) => {
254                if let Some(e) = v.get_mut(index) {
255                    *e = value;
256                }
257            }
258        }
259    }
260}
261
262/// A single dimension of an NDArray.
263#[derive(Debug, Clone)]
264pub struct NDDimension {
265    pub size: usize,
266    pub offset: usize,
267    pub binning: usize,
268    pub reverse: bool,
269}
270
271impl NDDimension {
272    pub fn new(size: usize) -> Self {
273        Self {
274            size,
275            offset: 0,
276            binning: 1,
277            reverse: false,
278        }
279    }
280}
281
282/// Computed info about an NDArray's layout (matching C++ NDArrayInfo_t).
283#[derive(Debug, Clone)]
284pub struct NDArrayInfo {
285    pub total_bytes: usize,
286    pub bytes_per_element: usize,
287    pub num_elements: usize,
288    pub x_size: usize,
289    pub y_size: usize,
290    pub color_size: usize,
291    /// Which dimension index is X.
292    pub x_dim: usize,
293    /// Which dimension index is Y.
294    pub y_dim: usize,
295    /// Which dimension index is color (0 if mono).
296    pub color_dim: usize,
297    /// Elements between successive X values.
298    pub x_stride: usize,
299    /// Elements between successive Y values.
300    pub y_stride: usize,
301    /// Elements between successive color values.
302    pub color_stride: usize,
303    /// Resolved color mode.
304    pub color_mode: crate::color::NDColorMode,
305}
306
307/// N-dimensional array with typed data buffer.
308#[derive(Debug, Clone)]
309pub struct NDArray {
310    pub unique_id: i32,
311    pub timestamp: EpicsTimestamp,
312    /// Separate double-precision timestamp (C++ `double timeStamp`), independent of `epicsTS`.
313    pub time_stamp: f64,
314    pub dims: Vec<NDDimension>,
315    pub data: NDDataBuffer,
316    pub attributes: NDAttributeList,
317    pub codec: Option<Codec>,
318    /// Identity of the pool that allocated this array (C++ `pNDArrayPool`).
319    /// `0` means the array was not allocated through any pool. `NDArrayPool::release`
320    /// verifies this matches its own id before returning the buffer to the free list.
321    pub pool_id: u64,
322    /// Requested byte count at allocation time (C++ `dataSize`). This is the exact
323    /// `num_elements * element_size` requested, NOT the allocator-rounded Vec capacity.
324    /// Pool memory accounting adds/subtracts this exact value.
325    pub data_size: usize,
326}
327
328impl NDArray {
329    /// Create a new NDArray with zeroed buffer matching dimensions.
330    pub fn new(dims: Vec<NDDimension>, data_type: NDDataType) -> Self {
331        let num_elements: usize = if dims.is_empty() {
332            0
333        } else {
334            dims.iter().map(|d| d.size).product()
335        };
336        Self {
337            unique_id: 0,
338            timestamp: EpicsTimestamp::default(),
339            time_stamp: 0.0,
340            dims,
341            data: NDDataBuffer::zeros(data_type, num_elements),
342            attributes: NDAttributeList::new(),
343            codec: None,
344            pool_id: 0,
345            data_size: num_elements * data_type.element_size(),
346        }
347    }
348
349    /// Create an NDArray wrapping an already-built data buffer.
350    ///
351    /// The array is not pool-allocated (`pool_id == 0`); `data_size` is taken
352    /// from the buffer's element count. Use this when a producer fills its own
353    /// buffer instead of allocating through an [`crate::ndarray_pool::NDArrayPool`].
354    pub fn with_data(dims: Vec<NDDimension>, data: NDDataBuffer) -> Self {
355        let data_size = data.len() * data.data_type().element_size();
356        Self {
357            unique_id: 0,
358            timestamp: EpicsTimestamp::default(),
359            time_stamp: 0.0,
360            dims,
361            data,
362            attributes: NDAttributeList::new(),
363            codec: None,
364            pool_id: 0,
365            data_size,
366        }
367    }
368
369    /// Compute layout info for this array (matching C++ NDArray::getInfo).
370    ///
371    /// For 3D arrays, reads the `ColorMode` attribute to determine which
372    /// dimension is X, Y, and color (RGB1, RGB2, or RGB3 layout).
373    pub fn info(&self) -> NDArrayInfo {
374        use crate::color::NDColorMode;
375
376        let bytes_per_element = self.data.data_type().element_size();
377        let num_elements = self.data.len();
378        let total_bytes = num_elements * bytes_per_element;
379
380        let ndims = self.dims.len();
381
382        // Read ColorMode attribute if present (C++ does this for 3D arrays)
383        let color_mode = self
384            .attributes
385            .get("ColorMode")
386            .and_then(|a| a.value.as_i64())
387            .map(|v| NDColorMode::from_i32(v as i32))
388            .unwrap_or(NDColorMode::Mono);
389
390        let (x_size, y_size, color_size, x_dim, y_dim, color_dim, x_stride, y_stride, color_stride) =
391            match ndims {
392                // C++ getInfo: ySize/colorSize/strides stay 0-initialized for <2-D.
393                // The `colorSize == 0` idiom signals "no color dimension"; keeping it
394                // at 0 (not 1) preserves C parity for that check.
395                0 => (0, 0, 0, 0, 0, 0, 0, 0, 0),
396                1 => (self.dims[0].size, 0, 0, 0, 0, 0, 1, 0, 0),
397                2 => {
398                    let xs = self.dims[0].size;
399                    let ys = self.dims[1].size;
400                    // C++ getInfo for 2-D: yStride = xSize, colorSize/colorStride = 0.
401                    (xs, ys, 0, 0, 1, 0, 1, xs, 0)
402                }
403                3 => {
404                    // 3D: layout depends on ColorMode
405                    match color_mode {
406                        NDColorMode::RGB1 => {
407                            // dim[0]=color, dim[1]=X, dim[2]=Y
408                            let cs = self.dims[0].size;
409                            let xs = self.dims[1].size;
410                            let ys = self.dims[2].size;
411                            (xs, ys, cs, 1, 2, 0, cs, xs * cs, 1)
412                        }
413                        NDColorMode::RGB2 => {
414                            // dim[0]=X, dim[1]=color, dim[2]=Y
415                            let xs = self.dims[0].size;
416                            let cs = self.dims[1].size;
417                            let ys = self.dims[2].size;
418                            (xs, ys, cs, 0, 2, 1, 1, xs * cs, xs)
419                        }
420                        NDColorMode::RGB3 => {
421                            // dim[0]=X, dim[1]=Y, dim[2]=color
422                            let xs = self.dims[0].size;
423                            let ys = self.dims[1].size;
424                            let cs = self.dims[2].size;
425                            (xs, ys, cs, 0, 1, 2, 1, xs, xs * ys)
426                        }
427                        _ => {
428                            // Mono / Bayer / YUV444 / YUV422 / YUV411: treated
429                            // as a plain 3-D array (dim[0]=X, dim[1]=Y,
430                            // dim[2]=Z). G4: C++ NDArray::getInfo has the SAME
431                            // limitation — it only special-cases RGB1/RGB2/RGB3
432                            // and falls through to this generic 3-D layout for
433                            // YUV modes. This is a shared C-parity gap, not a
434                            // Rust regression; YUV layout-awareness would have
435                            // to be added to both implementations together.
436                            let xs = self.dims[0].size;
437                            let ys = self.dims[1].size;
438                            let cs = self.dims[2].size;
439                            (xs, ys, cs, 0, 1, 2, 1, xs, xs * ys)
440                        }
441                    }
442                }
443                // C++ getInfo gates the color-dimension block on
444                // `ndims == 3` exactly. For 4-D and higher arrays it leaves
445                // colorSize/colorDim/colorStride at 0 and only fills xDim/yDim
446                // from the first two dimensions — same as the 2-D case.
447                _ => {
448                    let xs = self.dims[0].size;
449                    let ys = self.dims[1].size;
450                    (xs, ys, 0, 0, 1, 0, 1, xs, 0)
451                }
452            };
453
454        NDArrayInfo {
455            total_bytes,
456            bytes_per_element,
457            num_elements,
458            x_size,
459            y_size,
460            color_size,
461            x_dim,
462            y_dim,
463            color_dim,
464            x_stride,
465            y_stride,
466            color_stride,
467            color_mode,
468        }
469    }
470
471    /// Produce a diagnostic text dump (matching C++ `NDArray::report`).
472    ///
473    /// `details > 5` additionally lists attributes.
474    pub fn report(&self, details: i32) -> String {
475        let mut out = String::new();
476        out.push('\n');
477        out.push_str("NDArray:\n");
478        let dim_sizes: Vec<String> = self.dims.iter().map(|d| d.size.to_string()).collect();
479        out.push_str(&format!(
480            "  ndims={} dims=[{}]\n",
481            self.dims.len(),
482            dim_sizes.join(" ")
483        ));
484        out.push_str(&format!(
485            "  dataType={:?}, dataSize={}, numElements={}\n",
486            self.data.data_type(),
487            self.data_size,
488            self.data.len()
489        ));
490        out.push_str(&format!(
491            "  uniqueId={}, timeStamp={}, epicsTS.secPastEpoch={}, epicsTS.nsec={}\n",
492            self.unique_id, self.time_stamp, self.timestamp.sec, self.timestamp.nsec
493        ));
494        out.push_str(&format!("  poolId={}\n", self.pool_id));
495        match &self.codec {
496            Some(c) => out.push_str(&format!(
497                "  codec={:?}, compressedSize={}\n",
498                c.name, c.compressed_size
499            )),
500            None => out.push_str("  codec=none\n"),
501        }
502        out.push_str(&format!(
503            "  number of attributes={}\n",
504            self.attributes.len()
505        ));
506        if details > 5 {
507            for attr in self.attributes.iter() {
508                out.push_str(&format!(
509                    "    attribute name={}, value={}, source={:?}\n",
510                    attr.name,
511                    attr.value.as_string(),
512                    attr.source
513                ));
514            }
515        }
516        out
517    }
518
519    /// Validate that buffer length matches dimension product.
520    pub fn validate(&self) -> ADResult<()> {
521        let expected: usize = if self.dims.is_empty() {
522            0
523        } else {
524            self.dims.iter().map(|d| d.size).product()
525        };
526        if self.data.len() != expected {
527            return Err(ADError::BufferSizeMismatch {
528                expected,
529                actual: self.data.len(),
530            });
531        }
532        Ok(())
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_element_size_all_types() {
542        assert_eq!(NDDataType::Int8.element_size(), 1);
543        assert_eq!(NDDataType::UInt8.element_size(), 1);
544        assert_eq!(NDDataType::Int16.element_size(), 2);
545        assert_eq!(NDDataType::UInt16.element_size(), 2);
546        assert_eq!(NDDataType::Int32.element_size(), 4);
547        assert_eq!(NDDataType::UInt32.element_size(), 4);
548        assert_eq!(NDDataType::Int64.element_size(), 8);
549        assert_eq!(NDDataType::UInt64.element_size(), 8);
550        assert_eq!(NDDataType::Float32.element_size(), 4);
551        assert_eq!(NDDataType::Float64.element_size(), 8);
552    }
553
554    #[test]
555    fn test_from_ordinal_roundtrip() {
556        for i in 0..10u8 {
557            let dt = NDDataType::from_ordinal(i).unwrap();
558            assert_eq!(dt as u8, i);
559        }
560        assert!(NDDataType::from_ordinal(10).is_none());
561    }
562
563    #[test]
564    fn test_buffer_zeros_type_and_len() {
565        let buf = NDDataBuffer::zeros(NDDataType::UInt16, 100);
566        assert_eq!(buf.data_type(), NDDataType::UInt16);
567        assert_eq!(buf.len(), 100);
568        assert_eq!(buf.total_bytes(), 200);
569    }
570
571    #[test]
572    fn test_buffer_zeros_all_types() {
573        for i in 0..10u8 {
574            let dt = NDDataType::from_ordinal(i).unwrap();
575            let buf = NDDataBuffer::zeros(dt, 10);
576            assert_eq!(buf.data_type(), dt);
577            assert_eq!(buf.len(), 10);
578            assert_eq!(buf.total_bytes(), 10 * dt.element_size());
579        }
580    }
581
582    #[test]
583    fn test_buffer_as_u8_slice() {
584        let buf = NDDataBuffer::U8(vec![1, 2, 3]);
585        assert_eq!(buf.as_u8_slice(), &[1, 2, 3]);
586    }
587
588    #[test]
589    fn test_ndarray_new_allocates() {
590        let dims = vec![NDDimension::new(256), NDDimension::new(256)];
591        let arr = NDArray::new(dims, NDDataType::UInt8);
592        assert_eq!(arr.data.len(), 256 * 256);
593        assert_eq!(arr.data.data_type(), NDDataType::UInt8);
594    }
595
596    #[test]
597    fn test_ndarray_validate_ok() {
598        let dims = vec![NDDimension::new(10), NDDimension::new(20)];
599        let arr = NDArray::new(dims, NDDataType::Float64);
600        arr.validate().unwrap();
601    }
602
603    #[test]
604    fn test_ndarray_validate_mismatch() {
605        let mut arr = NDArray::new(
606            vec![NDDimension::new(10), NDDimension::new(20)],
607            NDDataType::UInt8,
608        );
609        arr.data = NDDataBuffer::U8(vec![0; 100]);
610        assert!(arr.validate().is_err());
611    }
612
613    #[test]
614    fn test_ndarray_info_2d_mono() {
615        let dims = vec![NDDimension::new(640), NDDimension::new(480)];
616        let arr = NDArray::new(dims, NDDataType::UInt16);
617        let info = arr.info();
618        assert_eq!(info.x_size, 640);
619        assert_eq!(info.y_size, 480);
620        // C parity: 2-D arrays have colorSize == 0 (no color dimension).
621        assert_eq!(info.color_size, 0);
622        assert_eq!(info.num_elements, 640 * 480);
623        assert_eq!(info.bytes_per_element, 2);
624        assert_eq!(info.total_bytes, 640 * 480 * 2);
625    }
626
627    #[test]
628    fn test_ndarray_info_3d_rgb() {
629        use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
630        use crate::color::NDColorMode;
631
632        // Without ColorMode attribute: defaults to Mono (x=dim0, y=dim1, color=dim2)
633        let dims = vec![
634            NDDimension::new(3),
635            NDDimension::new(640),
636            NDDimension::new(480),
637        ];
638        let arr = NDArray::new(dims, NDDataType::UInt8);
639        let info = arr.info();
640        assert_eq!(info.x_size, 3);
641        assert_eq!(info.y_size, 640);
642        assert_eq!(info.color_size, 480);
643
644        // With ColorMode=RGB1: dim[0]=color, dim[1]=x, dim[2]=y
645        let dims = vec![
646            NDDimension::new(3),
647            NDDimension::new(640),
648            NDDimension::new(480),
649        ];
650        let mut arr = NDArray::new(dims, NDDataType::UInt8);
651        arr.attributes.add(NDAttribute {
652            name: "ColorMode".into(),
653            description: "Color Mode".into(),
654            source: NDAttrSource::Driver,
655            value: NDAttrValue::Int32(NDColorMode::RGB1 as i32),
656            source_impl: None,
657        });
658        let info = arr.info();
659        assert_eq!(info.color_size, 3);
660        assert_eq!(info.x_size, 640);
661        assert_eq!(info.y_size, 480);
662        assert_eq!(info.x_dim, 1);
663        assert_eq!(info.y_dim, 2);
664        assert_eq!(info.color_dim, 0);
665        assert_eq!(info.num_elements, 3 * 640 * 480);
666    }
667
668    #[test]
669    fn test_ndarray_info_1d() {
670        let dims = vec![NDDimension::new(1024)];
671        let arr = NDArray::new(dims, NDDataType::Float64);
672        let info = arr.info();
673        assert_eq!(info.x_size, 1024);
674        // C parity: 1-D arrays leave ySize / colorSize at 0.
675        assert_eq!(info.y_size, 0);
676        assert_eq!(info.color_size, 0);
677    }
678
679    #[test]
680    fn test_ndarray_info_4d_not_color() {
681        // C++ getInfo gates the color block on `ndims == 3` exactly.
682        // A 4-D array must leave color_size / color_dim / color_stride at 0
683        // and only fill x/y from the first two dimensions.
684        let dims = vec![
685            NDDimension::new(8),
686            NDDimension::new(640),
687            NDDimension::new(480),
688            NDDimension::new(5),
689        ];
690        let arr = NDArray::new(dims, NDDataType::UInt8);
691        let info = arr.info();
692        assert_eq!(info.x_size, 8);
693        assert_eq!(info.y_size, 640);
694        assert_eq!(info.color_size, 0, "4-D array must not get a color size");
695        assert_eq!(info.x_dim, 0);
696        assert_eq!(info.y_dim, 1);
697        assert_eq!(info.color_dim, 0);
698        assert_eq!(info.x_stride, 1);
699        assert_eq!(info.y_stride, 8);
700        assert_eq!(info.color_stride, 0);
701        assert_eq!(info.num_elements, 8 * 640 * 480 * 5);
702    }
703
704    #[test]
705    fn test_buffer_is_empty() {
706        let buf = NDDataBuffer::zeros(NDDataType::UInt8, 0);
707        assert!(buf.is_empty());
708        let buf2 = NDDataBuffer::zeros(NDDataType::UInt8, 1);
709        assert!(!buf2.is_empty());
710    }
711
712    #[test]
713    fn test_codec_field_preserved() {
714        let mut arr = NDArray::new(vec![NDDimension::new(10)], NDDataType::UInt8);
715        arr.codec = Some(Codec {
716            name: crate::codec::CodecName::JPEG,
717            compressed_size: 42,
718            level: 0,
719            shuffle: 0,
720            compressor: 0,
721            original_data_type: NDDataType::UInt8,
722        });
723        let cloned = arr.clone();
724        assert_eq!(cloned.codec.as_ref().unwrap().compressed_size, 42);
725    }
726}