1use 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
15pub type EbccDim = Dim<[Ix; EBCC_NDIMS]>;
17
18pub type EBCCChunkShape = [NonZeroUsize; EBCC_NDIMS];
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub enum EBCCCompatChunkShape {
24 Auto,
26 Explicit(EBCCChunkShape),
28}
29
30pub 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 let mut ffi_config = ffi_config(data.dim().into(), config, [0; EBCC_NDIMS]);
81 let mut data_copy: Vec<f32> = data.iter().copied().collect(); 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 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 #[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
113pub 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(); 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
178pub 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(); 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
239pub 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); 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 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 #[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
328pub 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); 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 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, 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 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 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; 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; 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}