Skip to main content

ebcc/
codec.rs

1//! Safe wrapper functions for EBCC compression and decompression.
2
3use std::{io::Read, num::NonZeroUsize, ptr, slice};
4
5pub use ebcc_sys::EBCC_NDIMS;
6use ebcc_sys::{
7    EBCC_CHUNKING_HEADER_MAGIC, EBCC_CHUNKING_HEADER_VERSION, EBCC_MAX_INTERNAL_IMAGE_DIM,
8    EBCC_MIN_INTERNAL_IMAGE_DIM,
9};
10use ndarray::{ArrayView, ArrayViewMut, Dim, Ix};
11
12use crate::config::EBCCConfig;
13use crate::error::{EBCCError, EBCCResult};
14
15/// EBCC data dimension.
16pub type EbccDim = Dim<[Ix; EBCC_NDIMS]>;
17
18/// EBCC chunk shape.
19pub type EBCCChunkShape = [NonZeroUsize; EBCC_NDIMS];
20
21/// EBCC compat chunk shape.
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub enum EBCCCompatChunkShape {
24    /// EBCC chooses an appropriate chunk shape automatically.
25    Auto,
26    /// EBCC uses the provided explicit chunk shape.
27    Explicit(EBCCChunkShape),
28}
29
30/// Encode a 3D data array using EBCC compression.
31///
32/// # Arguments
33///
34/// - `data`: 3D input data array
35/// - `config`: EBCC configuration
36///
37/// # Returns
38///
39/// The compressed data bytes.
40///
41/// # Errors
42///
43/// - [`EBCCError::InvalidInput`] if the `data` has any zero-size dimension
44/// - [`EBCCError::InvalidInput`] if the size of `data` overflows or would not
45///   fit into memory
46/// - [`EBCCError::InvalidInput`] if the last two dimensions of `data` are too
47///   small or its EBCC internal image dimensions are outside the supported range
48/// - [`EBCCError::InvalidConfig`] if [`config.validate`][EBCCConfig::validate]
49///   fails
50/// - [`EBCCError::InvalidInput`] if the `data` contains any non-finite
51///   (infinite or NaN) values
52/// - [`EBCCError::CompressionError`] if compression with EBCC fails
53///
54/// # Examples
55///
56/// ```rust
57/// use ebcc::{ebcc_encode, EBCCConfig};
58/// use ndarray::Array;
59///
60/// # fn main() -> ebcc::EBCCResult<()> {
61/// // 2D ERA5-like data: 721x1440
62/// let data = Array::from_shape_vec((1, 721, 1440), vec![1.0f32; 721 * 1440]).unwrap();
63/// let config = EBCCConfig::max_absolute_error_bounded(0.01);
64///
65/// let compressed = ebcc_encode(data.view(), &config)?;
66/// println!(
67///     "Compressed {} bytes to {} bytes",
68///     data.len() * 4, compressed.len(),
69/// );
70/// # Ok(())
71/// # }
72/// ```
73pub fn ebcc_encode(data: ArrayView<f32, EbccDim>, config: &EBCCConfig) -> EBCCResult<Vec<u8>> {
74    validate_data_shape(data)?;
75    validate_regular_ebcc_shape(data)?;
76    config.validate()?;
77    validate_only_finite_data(data)?;
78
79    // Convert to FFI types
80    let mut ffi_config = ffi_config(data.dim().into(), config, [0; EBCC_NDIMS]);
81    let mut data_copy: Vec<f32> = data.iter().copied().collect(); // C function may modify the input
82
83    // Call the C function
84    let mut out_buffer: *mut u8 = ptr::null_mut();
85    #[expect(unsafe_code)]
86    let compressed_size = unsafe {
87        ebcc_sys::ebcc_encode(
88            data_copy.as_mut_ptr(),
89            &raw mut ffi_config,
90            &raw mut out_buffer,
91        )
92    };
93
94    // Check for errors
95    if compressed_size == 0 || out_buffer.is_null() {
96        return Err(EBCCError::CompressionError(String::from(
97            "ebcc_encode C function returned null or zero size",
98        )));
99    }
100
101    // Copy the compressed data to a Vec and free the C-allocated memory
102    #[expect(unsafe_code)]
103    let compressed_data = unsafe {
104        let slice = slice::from_raw_parts(out_buffer, compressed_size);
105        let vec = slice.to_vec();
106        ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
107        vec
108    };
109
110    Ok(compressed_data)
111}
112
113/// Encode a 3D data array using EBCC chunked compression.
114///
115/// `chunk_shape` controls the chunk shape used by EBCC.
116/// Chunks may extend beyond the input dimensions.
117/// EBCC pads edge chunks by repeating boundary values.
118///
119/// <div class="warning">
120///
121/// **Warning:** Range-relative error bounds are calculated independently for
122/// each chunk. Use [`ebcc_encode_chunking_compat`] instead to preserve a
123/// range-relative error bound over the entire data.
124///
125/// </div>
126///
127/// # Errors
128///
129/// - [`EBCCError::InvalidInput`] if the `data` has any zero-size dimension
130/// - [`EBCCError::InvalidInput`] if `chunk_shape` has tile dimensions that are
131///   too small or forms EBCC internal image dimensions outside the supported
132///   range
133/// - [`EBCCError::InvalidConfig`] if [`config.validate`][EBCCConfig::validate]
134///   fails
135/// - [`EBCCError::InvalidInput`] if the `data` contains any non-finite
136///   (infinite or NaN) values
137/// - [`EBCCError::CompressionError`] if compression with EBCC fails
138pub fn ebcc_encode_chunking(
139    data: ArrayView<f32, EbccDim>,
140    config: &EBCCConfig,
141    chunk_shape: EBCCChunkShape,
142) -> EBCCResult<Vec<u8>> {
143    validate_data_shape(data)?;
144    let chunk_shape = validate_chunk_shape(chunk_shape)?;
145    config.validate()?;
146    validate_only_finite_data(data)?;
147
148    let mut ffi_config = ffi_config(data.dim().into(), config, chunk_shape);
149    let mut data_copy: Vec<f32> = data.iter().copied().collect(); // C function may modify the input
150
151    let mut out_buffer: *mut u8 = ptr::null_mut();
152    #[expect(unsafe_code)]
153    let compressed_size = unsafe {
154        ebcc_sys::ebcc_encode_chunking(
155            data_copy.as_mut_ptr(),
156            &raw mut ffi_config,
157            &raw mut out_buffer,
158        )
159    };
160
161    if compressed_size == 0 || out_buffer.is_null() {
162        return Err(EBCCError::CompressionError(String::from(
163            "ebcc_encode_chunking C function returned null or zero size",
164        )));
165    }
166
167    #[expect(unsafe_code)]
168    let compressed_data = unsafe {
169        let slice = slice::from_raw_parts(out_buffer, compressed_size);
170        let vec = slice.to_vec();
171        ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
172        vec
173    };
174
175    Ok(compressed_data)
176}
177
178/// Encode a 3D data array using EBCC chunked compression in compatibility mode.
179///
180/// Passing [`Auto`][EBCCCompatChunkShape::Auto] for `chunk_shape` lets EBCC
181/// automatically choose the chunk shape. Passing an
182/// [`Explicit`][EBCCCompatChunkShape::Explicit] `chunk_shape` uses the same
183/// validation rules as [`ebcc_encode_chunking`].
184///
185/// In EBCC compatibility mode, range-relative error bounds are converted to
186/// absolute error bounds using the input data range before chunked encoding.
187///
188/// # Errors
189///
190/// - [`EBCCError::InvalidInput`] if the `data` has any zero-size dimension
191/// - [`EBCCError::InvalidInput`] if explicit `chunk_shape` has tile dimensions
192///   that are too small or forms EBCC internal image dimensions outside the
193///   supported range
194/// - [`EBCCError::InvalidConfig`] if [`config.validate`][EBCCConfig::validate]
195///   fails
196/// - [`EBCCError::InvalidInput`] if the `data` contains any non-finite
197///   (infinite or NaN) values
198/// - [`EBCCError::CompressionError`] if compression with EBCC fails
199pub fn ebcc_encode_chunking_compat(
200    data: ArrayView<f32, EbccDim>,
201    config: &EBCCConfig,
202    chunk_shape: EBCCCompatChunkShape,
203) -> EBCCResult<Vec<u8>> {
204    validate_data_shape(data)?;
205    let chunk_shape = compat_chunk_shape(chunk_shape)?;
206    config.validate()?;
207    validate_only_finite_data(data)?;
208
209    let mut ffi_config = ffi_config(data.dim().into(), config, chunk_shape);
210    let mut data_copy: Vec<f32> = data.iter().copied().collect(); // C function may modify the input
211
212    let mut out_buffer: *mut u8 = ptr::null_mut();
213    #[expect(unsafe_code)]
214    let compressed_size = unsafe {
215        ebcc_sys::ebcc_encode_chunking_compat(
216            data_copy.as_mut_ptr(),
217            &raw mut ffi_config,
218            &raw mut out_buffer,
219        )
220    };
221
222    if compressed_size == 0 || out_buffer.is_null() {
223        return Err(EBCCError::CompressionError(String::from(
224            "ebcc_encode_chunking_compat C function returned null or zero size",
225        )));
226    }
227
228    #[expect(unsafe_code)]
229    let compressed_data = unsafe {
230        let slice = slice::from_raw_parts(out_buffer, compressed_size);
231        let vec = slice.to_vec();
232        ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
233        vec
234    };
235
236    Ok(compressed_data)
237}
238
239/// Decode into a 3D data array using EBCC decompression.
240///
241/// # Arguments
242///
243/// - `compressed_data`: Compressed data bytes produced by [`ebcc_encode`]
244/// - `decompressed_data`: 3D output data array
245///
246/// # Errors
247///
248/// - [`EBCCError::InvalidInput`] if the `compressed_data` is empty
249/// - [`EBCCError::DecompressionError`] if decompression with EBCC fails
250/// - [`EBCCError::InvalidInput`] if the decompressed data does not fit into
251///   `decompressed_data`
252///
253/// # Examples
254///
255/// ```rust
256/// use ebcc::{ebcc_encode, ebcc_decode_into, EBCCConfig};
257/// use ndarray::Array;
258///
259/// # fn main() -> ebcc::EBCCResult<()> {
260/// let data = Array::from_shape_vec((1, 32, 32), vec![1.0f32; 32 * 32]).unwrap();
261/// let config = EBCCConfig::new();
262///
263/// let compressed = ebcc_encode(data.view(), &config)?;
264///
265/// let mut decompressed = Array::zeros(data.dim());
266/// ebcc_decode_into(&compressed, decompressed.view_mut())?;
267/// # Ok(())
268/// # }
269/// ```
270pub fn ebcc_decode_into(
271    compressed_data: &[u8],
272    mut decompressed_data: ArrayViewMut<f32, EbccDim>,
273) -> EBCCResult<()> {
274    if compressed_data.is_empty() {
275        return Err(EBCCError::InvalidInput(String::from(
276            "Compressed data is empty",
277        )));
278    }
279
280    let mut compressed_data_copy = Vec::from(compressed_data); // C function may modify the input
281
282    // Call the C function
283    let mut out_buffer: *mut f32 = ptr::null_mut();
284    #[expect(unsafe_code)]
285    let decompressed_size = unsafe {
286        ebcc_sys::ebcc_decode(
287            compressed_data_copy.as_mut_ptr(),
288            compressed_data.len(),
289            &raw mut out_buffer,
290        )
291    };
292
293    // Check for errors
294    if decompressed_size == 0 || out_buffer.is_null() {
295        return Err(EBCCError::DecompressionError(String::from(
296            "ebcc_decode C function returned null or zero size",
297        )));
298    }
299
300    // Copy the decompressed data to a Vec and free the C-allocated memory
301    #[expect(unsafe_code)]
302    let decompressed_slice = unsafe { slice::from_raw_parts(out_buffer, decompressed_size) };
303
304    let Ok(decompressed_view) = ArrayView::from_shape(decompressed_data.dim(), decompressed_slice)
305    else {
306        #[expect(unsafe_code)]
307        unsafe {
308            ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
309        }
310
311        return Err(EBCCError::InvalidInput(format!(
312            "Decompressed data should be of shape {:?} but decompressed to {} elements",
313            decompressed_data.shape(),
314            decompressed_size
315        )));
316    };
317
318    decompressed_data.assign(&decompressed_view);
319
320    #[expect(unsafe_code)]
321    unsafe {
322        ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
323    }
324
325    Ok(())
326}
327
328/// Decode EBCC chunked compressed data into a 3D data array.
329///
330/// # Errors
331///
332/// - [`EBCCError::InvalidInput`] if the `compressed_data` is empty
333/// - [`EBCCError::DecompressionError`] if decompression with EBCC fails
334/// - [`EBCCError::InvalidInput`] if the decompressed data does not fit into
335///   `decompressed_data`
336pub fn ebcc_decode_chunking_into(
337    compressed_data: &[u8],
338    mut decompressed_data: ArrayViewMut<f32, EbccDim>,
339) -> EBCCResult<()> {
340    if compressed_data.is_empty() {
341        return Err(EBCCError::InvalidInput(String::from(
342            "Compressed data is empty",
343        )));
344    }
345
346    let encoded_dims = read_dims_from_chunking_header(compressed_data)?;
347    let output_dims: [usize; EBCC_NDIMS] = decompressed_data.dim().into();
348    if output_dims != encoded_dims {
349        return Err(EBCCError::InvalidInput(format!(
350            "Chunked EBCC data has shape {encoded_dims:?} but output array has shape {output_dims:?}",
351        )));
352    }
353
354    let mut compressed_data_copy = Vec::from(compressed_data); // C function may modify the input
355
356    let mut out_buffer: *mut f32 = ptr::null_mut();
357    #[expect(unsafe_code)]
358    let decompressed_size = unsafe {
359        ebcc_sys::ebcc_decode_chunking(
360            compressed_data_copy.as_mut_ptr(),
361            compressed_data.len(),
362            &raw mut out_buffer,
363        )
364    };
365
366    if decompressed_size == 0 || out_buffer.is_null() {
367        return Err(EBCCError::DecompressionError(String::from(
368            "ebcc_decode_chunking C function returned null or zero size",
369        )));
370    }
371
372    #[expect(unsafe_code)]
373    let decompressed_slice = unsafe { slice::from_raw_parts(out_buffer, decompressed_size) };
374
375    let Ok(decompressed_view) = ArrayView::from_shape(decompressed_data.dim(), decompressed_slice)
376    else {
377        #[expect(unsafe_code)]
378        unsafe {
379            ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
380        }
381
382        return Err(EBCCError::InvalidInput(format!(
383            "Decompressed data should be of shape {:?} but decompressed to {} elements",
384            decompressed_data.shape(),
385            decompressed_size
386        )));
387    };
388
389    decompressed_data.assign(&decompressed_view);
390
391    #[expect(unsafe_code)]
392    unsafe {
393        ebcc_sys::free_buffer(out_buffer.cast::<core::ffi::c_void>());
394    }
395
396    Ok(())
397}
398
399fn validate_data_shape(data: ArrayView<f32, EbccDim>) -> EBCCResult<usize> {
400    if data.shape().contains(&0) {
401        return Err(EBCCError::InvalidInput(String::from(
402            "All dimensions must be > 0",
403        )));
404    }
405
406    let Some(total_elements) = data
407        .shape()
408        .iter()
409        .try_fold(1usize, |acc, &d| acc.checked_mul(d))
410    else {
411        return Err(EBCCError::InvalidInput(String::from("Dimension overflow")));
412    };
413
414    if total_elements > ((isize::MAX as usize) / std::mem::size_of::<f32>()) {
415        return Err(EBCCError::InvalidInput(String::from("Data too large")));
416    }
417
418    Ok(total_elements)
419}
420
421fn validate_regular_ebcc_shape(data: ArrayView<f32, EbccDim>) -> EBCCResult<()> {
422    // EBCC flattens all dimensions except the last into one internal image height.
423    let (depth, height, width) = data.dim();
424    let Some(image_height) = depth.checked_mul(height) else {
425        return Err(EBCCError::InvalidInput(String::from("Dimension overflow")));
426    };
427
428    if height < EBCC_MIN_INTERNAL_IMAGE_DIM
429        || !(EBCC_MIN_INTERNAL_IMAGE_DIM..=EBCC_MAX_INTERNAL_IMAGE_DIM).contains(&image_height)
430        || !(EBCC_MIN_INTERNAL_IMAGE_DIM..=EBCC_MAX_INTERNAL_IMAGE_DIM).contains(&width)
431    {
432        return Err(EBCCError::InvalidInput(format!(
433            "EBCC requires tile dimensions of at least {EBCC_MIN_INTERNAL_IMAGE_DIM} and internal image dimensions at most {EBCC_MAX_INTERNAL_IMAGE_DIM}, got shape {depth}x{height}x{width}",
434        )));
435    }
436
437    Ok(())
438}
439
440fn validate_chunk_shape(chunk_shape: EBCCChunkShape) -> EBCCResult<[usize; EBCC_NDIMS]> {
441    let [chunk_depth, chunk_height, chunk_width] = chunk_shape;
442    let Some(image_height) = chunk_depth.checked_mul(chunk_height) else {
443        return Err(EBCCError::InvalidInput(String::from(
444            "Chunk dimension overflow",
445        )));
446    };
447    if chunk_height.get() < EBCC_MIN_INTERNAL_IMAGE_DIM
448        || !(EBCC_MIN_INTERNAL_IMAGE_DIM..=EBCC_MAX_INTERNAL_IMAGE_DIM)
449            .contains(&image_height.get())
450        || !(EBCC_MIN_INTERNAL_IMAGE_DIM..=EBCC_MAX_INTERNAL_IMAGE_DIM).contains(&chunk_width.get())
451    {
452        return Err(EBCCError::InvalidInput(format!(
453            "EBCC requires chunk tile dimensions of at least {EBCC_MIN_INTERNAL_IMAGE_DIM} and internal image dimensions at most {EBCC_MAX_INTERNAL_IMAGE_DIM}, got {chunk_depth}x{chunk_height}x{chunk_width}",
454        )));
455    }
456
457    let Some(total_elements) = chunk_shape
458        .iter()
459        .try_fold(1usize, |acc, &d| acc.checked_mul(d.get()))
460    else {
461        return Err(EBCCError::InvalidInput(String::from(
462            "Chunk dimension overflow",
463        )));
464    };
465
466    if total_elements > ((isize::MAX as usize) / std::mem::size_of::<f32>()) {
467        return Err(EBCCError::InvalidInput(String::from(
468            "Chunk dimensions are too large",
469        )));
470    }
471
472    Ok(chunk_shape.map(NonZeroUsize::get))
473}
474
475fn compat_chunk_shape(chunk_shape: EBCCCompatChunkShape) -> EBCCResult<[usize; EBCC_NDIMS]> {
476    match chunk_shape {
477        EBCCCompatChunkShape::Auto => Ok([0; EBCC_NDIMS]),
478        EBCCCompatChunkShape::Explicit(chunk_shape) => validate_chunk_shape(chunk_shape),
479    }
480}
481
482fn validate_only_finite_data(data: ArrayView<f32, EbccDim>) -> EBCCResult<()> {
483    for (i, &value) in data.indexed_iter() {
484        if !value.is_finite() {
485            return Err(EBCCError::InvalidInput(format!(
486                "Non-finite value {value} at index {i:?}"
487            )));
488        }
489    }
490
491    Ok(())
492}
493
494const fn ffi_config(
495    dims: [usize; EBCC_NDIMS],
496    config: &EBCCConfig,
497    chunk_shape: [usize; EBCC_NDIMS],
498) -> ebcc_sys::codec_config_t {
499    ebcc_sys::codec_config_t {
500        dims,
501        base_cr: config.base_cr,
502        residual_compression_type: config.residual_compression_type.as_residual(),
503        residual_cr: 1.0, // Default value for removed field
504        error: config.residual_compression_type.as_error(),
505        chunk_dims: chunk_shape,
506    }
507}
508
509fn read_dims_from_chunking_header(compressed_data: &[u8]) -> EBCCResult<[usize; EBCC_NDIMS]> {
510    let Some(mut compressed_data) = compressed_data.strip_prefix(EBCC_CHUNKING_HEADER_MAGIC) else {
511        return Err(EBCCError::DecompressionError(String::from(
512            "Missing EBCC chunking header",
513        )));
514    };
515
516    let reader = &mut compressed_data;
517
518    let version = read_u32_le(reader)?;
519    if version != EBCC_CHUNKING_HEADER_VERSION {
520        return Err(EBCCError::DecompressionError(format!(
521            "Unsupported EBCC chunking header version: {version}",
522        )));
523    }
524
525    let Ok(ndims) = usize::try_from(read_u32_le(reader)?) else {
526        return Err(EBCCError::DecompressionError(String::from(
527            "EBCC chunking dimensionality does not fit into usize",
528        )));
529    };
530
531    if ndims != EBCC_NDIMS {
532        return Err(EBCCError::InvalidInput(format!(
533            "EBCC chunking streams must be 3D but has {ndims} dimensions"
534        )));
535    }
536
537    let _padding = read_u32_le(reader)?;
538
539    let mut dims = [0; EBCC_NDIMS];
540    for dim in &mut dims {
541        let value = read_u64_le(reader)?;
542        *dim = usize::try_from(value).map_err(|_| {
543            EBCCError::InvalidInput(format!(
544                "Chunked EBCC dimension {value} does not fit into usize",
545            ))
546        })?;
547    }
548
549    Ok(dims)
550}
551
552fn read_u32_le(reader: &mut &[u8]) -> EBCCResult<u32> {
553    let mut array = [0; std::mem::size_of::<u32>()];
554    let Ok(()) = reader.read_exact(&mut array) else {
555        return Err(EBCCError::InvalidInput(String::from(
556            "Chunked EBCC header is truncated",
557        )));
558    };
559    Ok(u32::from_le_bytes(array))
560}
561
562fn read_u64_le(reader: &mut &[u8]) -> EBCCResult<u64> {
563    let mut array = [0; std::mem::size_of::<u64>()];
564    let Ok(()) = reader.read_exact(&mut array) else {
565        return Err(EBCCError::InvalidInput(String::from(
566            "Chunked EBCC header is truncated",
567        )));
568    };
569    Ok(u64::from_le_bytes(array))
570}
571
572#[cfg(test)]
573#[expect(clippy::unwrap_used, clippy::indexing_slicing)]
574mod tests {
575    use ndarray::Array;
576
577    use super::*;
578
579    #[test]
580    fn test_encode_decode_roundtrip() -> EBCCResult<()> {
581        // Create test data for 32x32 minimum size requirement
582        let data = Array::from_shape_vec(
583            (1, EBCC_MIN_INTERNAL_IMAGE_DIM, EBCC_MIN_INTERNAL_IMAGE_DIM),
584            vec![1.0f32; EBCC_MIN_INTERNAL_IMAGE_DIM * EBCC_MIN_INTERNAL_IMAGE_DIM],
585        )
586        .unwrap();
587
588        let config = EBCCConfig::new();
589
590        let compressed = ebcc_encode(data.view(), &config)?;
591
592        let mut decompressed = Array::zeros(data.dim());
593        ebcc_decode_into(&compressed, decompressed.view_mut())?;
594        // Note: Due to lossy compression, values may not be exactly equal
595
596        Ok(())
597    }
598
599    #[test]
600    fn test_invalid_config() {
601        let data = Array::from_shape_vec(
602            (1, EBCC_MIN_INTERNAL_IMAGE_DIM, EBCC_MIN_INTERNAL_IMAGE_DIM),
603            vec![1.0f32; EBCC_MIN_INTERNAL_IMAGE_DIM * EBCC_MIN_INTERNAL_IMAGE_DIM],
604        )
605        .unwrap();
606
607        let mut config = EBCCConfig::new();
608        config.base_cr = -1.0; // Invalid compression ratio
609
610        let result = ebcc_encode(data.view(), &config);
611        assert!(result.is_err());
612    }
613
614    #[test]
615    fn test_nan_input() {
616        let mut data = Array::from_shape_vec(
617            (1, EBCC_MIN_INTERNAL_IMAGE_DIM, EBCC_MIN_INTERNAL_IMAGE_DIM),
618            vec![1.0f32; EBCC_MIN_INTERNAL_IMAGE_DIM * EBCC_MIN_INTERNAL_IMAGE_DIM],
619        )
620        .unwrap();
621        data[(0, 3, 4)] = f32::NAN; // Insert NaN in the middle
622
623        let config = EBCCConfig::new();
624
625        let result = ebcc_encode(data.view(), &config);
626        assert!(result.is_err());
627    }
628
629    #[test]
630    fn test_empty_compressed_data() {
631        let result = ebcc_decode_into(&[], Array::zeros([1, 1, 1]).view_mut());
632        assert!(result.is_err());
633    }
634
635    #[test]
636    #[expect(clippy::cast_precision_loss)]
637    fn test_encode_decode_chunking_roundtrip() -> EBCCResult<()> {
638        let data = Array::from_shape_fn((1, 64, 64), |(_frame, y, x)| (y + x) as f32);
639        let config = EBCCConfig::new();
640
641        let compressed = ebcc_encode_chunking(
642            data.view(),
643            &config,
644            #[expect(clippy::unwrap_used)]
645            [
646                NonZeroUsize::new(1).unwrap(),
647                NonZeroUsize::new(EBCC_MIN_INTERNAL_IMAGE_DIM).unwrap(),
648                NonZeroUsize::new(EBCC_MIN_INTERNAL_IMAGE_DIM).unwrap(),
649            ],
650        )?;
651
652        let mut decompressed = Array::zeros(data.dim());
653        ebcc_decode_chunking_into(&compressed, decompressed.view_mut())?;
654
655        Ok(())
656    }
657}