1use std::borrow::Cow;
9use std::io::{Read, Write};
10use std::sync::Arc;
11
12use ad_core_rs::codec::{Codec, CodecName, CodecStatus};
13use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
14use ad_core_rs::ndarray_pool::NDArrayPool;
15use ad_core_rs::plugin::runtime::{NDPluginProcess, ParamUpdate, ProcessResult};
16
17use flate2::Compression;
18use flate2::read::ZlibDecoder;
19use flate2::write::ZlibEncoder;
20use lz4_flex::block::{compress, decompress};
21use parking_lot::Mutex;
22use rust_hdf5::format::messages::filter::{
23 FILTER_BLOSC, Filter, FilterPipeline, apply_filters, reverse_filters,
24};
25
26pub fn original_data_type(array: &NDArray) -> NDDataType {
38 match &array.codec {
39 Some(c) => c.original_data_type,
40 None => array.data.data_type(),
41 }
42}
43
44pub(crate) fn buffer_from_bytes(bytes: &[u8], data_type: NDDataType) -> Option<NDDataBuffer> {
49 let elem_size = data_type.element_size();
50 if bytes.len() % elem_size != 0 {
51 return None;
52 }
53 let count = bytes.len() / elem_size;
54
55 Some(match data_type {
56 NDDataType::Int8 => {
57 let mut v = vec![0i8; count];
58 unsafe {
60 std::ptr::copy_nonoverlapping(
61 bytes.as_ptr(),
62 v.as_mut_ptr() as *mut u8,
63 bytes.len(),
64 );
65 }
66 NDDataBuffer::I8(v)
67 }
68 NDDataType::UInt8 => NDDataBuffer::U8(bytes.to_vec()),
69 NDDataType::Int16 => {
70 let mut v = vec![0i16; count];
71 unsafe {
72 std::ptr::copy_nonoverlapping(
73 bytes.as_ptr(),
74 v.as_mut_ptr() as *mut u8,
75 bytes.len(),
76 );
77 }
78 NDDataBuffer::I16(v)
79 }
80 NDDataType::UInt16 => {
81 let mut v = vec![0u16; count];
82 unsafe {
83 std::ptr::copy_nonoverlapping(
84 bytes.as_ptr(),
85 v.as_mut_ptr() as *mut u8,
86 bytes.len(),
87 );
88 }
89 NDDataBuffer::U16(v)
90 }
91 NDDataType::Int32 => {
92 let mut v = vec![0i32; count];
93 unsafe {
94 std::ptr::copy_nonoverlapping(
95 bytes.as_ptr(),
96 v.as_mut_ptr() as *mut u8,
97 bytes.len(),
98 );
99 }
100 NDDataBuffer::I32(v)
101 }
102 NDDataType::UInt32 => {
103 let mut v = vec![0u32; count];
104 unsafe {
105 std::ptr::copy_nonoverlapping(
106 bytes.as_ptr(),
107 v.as_mut_ptr() as *mut u8,
108 bytes.len(),
109 );
110 }
111 NDDataBuffer::U32(v)
112 }
113 NDDataType::Int64 => {
114 let mut v = vec![0i64; count];
115 unsafe {
116 std::ptr::copy_nonoverlapping(
117 bytes.as_ptr(),
118 v.as_mut_ptr() as *mut u8,
119 bytes.len(),
120 );
121 }
122 NDDataBuffer::I64(v)
123 }
124 NDDataType::UInt64 => {
125 let mut v = vec![0u64; count];
126 unsafe {
127 std::ptr::copy_nonoverlapping(
128 bytes.as_ptr(),
129 v.as_mut_ptr() as *mut u8,
130 bytes.len(),
131 );
132 }
133 NDDataBuffer::U64(v)
134 }
135 NDDataType::Float32 => {
136 let mut v = vec![0f32; count];
137 unsafe {
138 std::ptr::copy_nonoverlapping(
139 bytes.as_ptr(),
140 v.as_mut_ptr() as *mut u8,
141 bytes.len(),
142 );
143 }
144 NDDataBuffer::F32(v)
145 }
146 NDDataType::Float64 => {
147 let mut v = vec![0f64; count];
148 unsafe {
149 std::ptr::copy_nonoverlapping(
150 bytes.as_ptr(),
151 v.as_mut_ptr() as *mut u8,
152 bytes.len(),
153 );
154 }
155 NDDataBuffer::F64(v)
156 }
157 })
158}
159
160pub fn compress_lz4(src: &NDArray) -> NDArray {
166 let raw = src.data.as_u8_slice();
167 let original_data_type = src.data.data_type();
168 let original_size = raw.len();
169 let compressed = compress(raw);
171 let compressed_size = compressed.len();
172
173 let mut arr = src.clone();
174 arr.data = NDDataBuffer::U8(compressed);
175 arr.codec = Some(Codec {
176 name: CodecName::LZ4,
177 compressed_size,
178 level: 0,
179 shuffle: 0,
180 compressor: 0,
181 original_data_type,
184 });
185
186 tracing::debug!(
187 original_size,
188 compressed_size,
189 ratio = original_size as f64 / compressed_size.max(1) as f64,
190 "LZ4 compress"
191 );
192
193 arr
194}
195
196pub fn decompress_lz4(src: &NDArray) -> Option<NDArray> {
201 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::LZ4) {
202 return None;
203 }
204 let compressed = src.data.as_u8_slice();
205 let original_type = original_data_type(src);
208 let num_elements: usize = src.dims.iter().map(|d| d.size).product();
209 let uncompressed_size = num_elements * original_type.element_size();
210 let decompressed = decompress(compressed, uncompressed_size).ok()?;
211
212 let buffer = buffer_from_bytes(&decompressed, original_type)?;
213
214 let mut arr = src.clone();
215 arr.data = buffer;
216 arr.codec = None;
217
218 Some(arr)
219}
220
221const ZLIB_DEFAULT_LEVEL: u32 = 6;
232
233pub fn compress_zlib(src: &NDArray) -> NDArray {
239 let raw = src.data.as_u8_slice();
240 let original_data_type = src.data.data_type();
241 let original_size = raw.len();
242
243 let mut encoder = ZlibEncoder::new(Vec::<u8>::new(), Compression::new(ZLIB_DEFAULT_LEVEL));
244 if encoder.write_all(raw).is_err() {
246 return src.clone();
247 }
248 let compressed = match encoder.finish() {
249 Ok(buf) => buf,
250 Err(_) => return src.clone(),
251 };
252 let compressed_size = compressed.len();
253
254 let mut arr = src.clone();
255 arr.data = NDDataBuffer::U8(compressed);
256 arr.codec = Some(Codec {
257 name: CodecName::Zlib,
258 compressed_size,
259 level: ZLIB_DEFAULT_LEVEL as i32,
260 shuffle: 0,
261 compressor: 0,
262 original_data_type,
263 });
264
265 tracing::debug!(
266 original_size,
267 compressed_size,
268 ratio = original_size as f64 / compressed_size.max(1) as f64,
269 "Zlib compress"
270 );
271 arr
272}
273
274pub fn decompress_zlib(src: &NDArray) -> Option<NDArray> {
279 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::Zlib) {
280 return None;
281 }
282 let compressed = src.data.as_u8_slice();
283
284 let original_type = original_data_type(src);
285 let num_elements: usize = src.dims.iter().map(|d| d.size).product();
286 let uncompressed_size = num_elements * original_type.element_size();
287
288 let mut decoder = ZlibDecoder::new(compressed);
289 let mut decompressed = Vec::with_capacity(uncompressed_size);
290 decoder.read_to_end(&mut decompressed).ok()?;
291
292 let buffer = buffer_from_bytes(&decompressed, original_type)?;
293
294 let mut arr = src.clone();
295 arr.data = buffer;
296 arr.codec = None;
297 Some(arr)
298}
299
300const LZ4HDF5_DEFAULT_BLOCK_SIZE: usize = 1 << 20;
320
321pub fn compress_lz4hdf5(src: &NDArray) -> NDArray {
328 let raw = src.data.as_u8_slice();
329 let data_type = src.data.data_type();
330 let original_size = raw.len();
331 let block_size = LZ4HDF5_DEFAULT_BLOCK_SIZE;
332
333 let mut out: Vec<u8> = Vec::with_capacity(original_size / 2 + 12);
335 out.extend_from_slice(&(original_size as u64).to_be_bytes());
336 out.extend_from_slice(&(block_size as u32).to_be_bytes());
337
338 let mut pos = 0usize;
339 while pos < raw.len() {
340 let n = block_size.min(raw.len() - pos);
341 let block = &raw[pos..pos + n];
342 let comp = compress(block);
343 if comp.len() < n {
346 out.extend_from_slice(&(comp.len() as u32).to_be_bytes());
347 out.extend_from_slice(&comp);
348 } else {
349 out.extend_from_slice(&(n as u32).to_be_bytes());
350 out.extend_from_slice(block);
351 }
352 pos += n;
353 }
354
355 let compressed_size = out.len();
356 let mut arr = src.clone();
357 arr.data = NDDataBuffer::U8(out);
358 arr.codec = Some(Codec {
359 name: CodecName::LZ4HDF5,
360 compressed_size,
361 level: 0,
362 shuffle: 0,
363 compressor: 0,
364 original_data_type: data_type,
365 });
366
367 tracing::debug!(
368 original_size,
369 compressed_size,
370 ratio = original_size as f64 / compressed_size.max(1) as f64,
371 "LZ4HDF5 compress"
372 );
373 arr
374}
375
376pub fn decompress_lz4hdf5(src: &NDArray) -> Option<NDArray> {
380 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::LZ4HDF5) {
381 return None;
382 }
383 let buf = src.data.as_u8_slice();
384 if buf.len() < 12 {
385 return None;
386 }
387 let total_bytes = u64::from_be_bytes(buf[0..8].try_into().ok()?) as usize;
388 let block_size = u32::from_be_bytes(buf[8..12].try_into().ok()?) as usize;
389 if block_size == 0 {
390 return None;
391 }
392
393 let original_type = original_data_type(src);
394
395 let mut out: Vec<u8> = Vec::with_capacity(total_bytes);
396 let mut pos = 12usize;
397 while out.len() < total_bytes {
398 let n = block_size.min(total_bytes - out.len());
399 if pos + 4 > buf.len() {
400 return None;
401 }
402 let clen = u32::from_be_bytes(buf[pos..pos + 4].try_into().ok()?) as usize;
403 pos += 4;
404 if pos + clen > buf.len() {
405 return None;
406 }
407 let block_payload = &buf[pos..pos + clen];
408 if clen == n {
409 out.extend_from_slice(block_payload);
411 } else {
412 let block = decompress(block_payload, n).ok()?;
413 if block.len() != n {
414 return None;
415 }
416 out.extend_from_slice(&block);
417 }
418 pos += clen;
419 }
420 if out.len() != total_bytes {
421 return None;
422 }
423
424 let buffer = buffer_from_bytes(&out, original_type)?;
425 let mut arr = src.clone();
426 arr.data = buffer;
427 arr.codec = None;
428 Some(arr)
429}
430
431const BSHUF_TARGET_BLOCK_SIZE_B: usize = 8192;
454const BSHUF_BLOCKED_MULT: usize = 8;
456const BSHUF_MIN_RECOMMEND_BLOCK: usize = 128;
458
459pub(crate) fn bshuf_default_block_size(elem_size: usize) -> usize {
466 let bs = BSHUF_TARGET_BLOCK_SIZE_B / elem_size.max(1);
467 let bs = (bs / BSHUF_BLOCKED_MULT) * BSHUF_BLOCKED_MULT;
468 bs.max(BSHUF_MIN_RECOMMEND_BLOCK)
469}
470
471#[inline]
474fn trans_bit_8x8(mut x: u64) -> u64 {
475 let t = (x ^ (x >> 7)) & 0x00AA_00AA_00AA_00AA;
476 x = x ^ t ^ (t << 7);
477 let t = (x ^ (x >> 14)) & 0x0000_CCCC_0000_CCCC;
478 x = x ^ t ^ (t << 14);
479 let t = (x ^ (x >> 28)) & 0x0000_0000_F0F0_F0F0;
480 x = x ^ t ^ (t << 28);
481 x
482}
483
484#[inline]
486fn read_u64_le(b: &[u8], off: usize) -> u64 {
487 u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
488}
489
490fn bshuf_trans_byte_elem(input: &[u8], out: &mut [u8], size: usize, elem_size: usize) {
493 let mut ii = 0;
494 while ii + 7 < size {
495 for jj in 0..elem_size {
496 for kk in 0..8 {
497 out[jj * size + ii + kk] = input[ii * elem_size + kk * elem_size + jj];
498 }
499 }
500 ii += 8;
501 }
502 let mut ii = size - size % 8;
504 while ii < size {
505 for jj in 0..elem_size {
506 out[jj * size + ii] = input[ii * elem_size + jj];
507 }
508 ii += 1;
509 }
510}
511
512fn bshuf_trans_bit_byte(input: &[u8], out: &mut [u8], size: usize, elem_size: usize) {
515 let nbyte = elem_size * size;
516 let nbyte_bitrow = nbyte / 8;
517 for ii in 0..nbyte_bitrow {
518 let mut x = trans_bit_8x8(read_u64_le(input, ii * 8));
519 for kk in 0..8 {
520 out[kk * nbyte_bitrow + ii] = x as u8;
521 x >>= 8;
522 }
523 }
524}
525
526fn bshuf_trans_bitrow_eight(input: &[u8], out: &mut [u8], size: usize, elem_size: usize) {
529 let nbyte_bitrow = size / 8;
530 for ii in 0..8 {
531 for jj in 0..elem_size {
532 let src = (ii * elem_size + jj) * nbyte_bitrow;
533 let dst = (jj * 8 + ii) * nbyte_bitrow;
534 out[dst..dst + nbyte_bitrow].copy_from_slice(&input[src..src + nbyte_bitrow]);
535 }
536 }
537}
538
539fn bshuf_trans_bit_elem(input: &[u8], size: usize, elem_size: usize) -> Vec<u8> {
543 debug_assert_eq!(size % 8, 0);
544 let nbyte = size * elem_size;
545 let mut a = vec![0u8; nbyte];
546 bshuf_trans_byte_elem(input, &mut a, size, elem_size);
547 let mut b = vec![0u8; nbyte];
548 bshuf_trans_bit_byte(&a, &mut b, size, elem_size);
549 let mut out = vec![0u8; nbyte];
550 bshuf_trans_bitrow_eight(&b, &mut out, size, elem_size);
551 out
552}
553
554fn bshuf_trans_byte_bitrow(input: &[u8], out: &mut [u8], size: usize, elem_size: usize) {
557 let nbyte_row = size / 8;
558 for jj in 0..elem_size {
559 for ii in 0..nbyte_row {
560 for kk in 0..8 {
561 out[ii * 8 * elem_size + jj * 8 + kk] = input[(jj * 8 + kk) * nbyte_row + ii];
562 }
563 }
564 }
565}
566
567fn bshuf_shuffle_bit_eightelem(input: &[u8], out: &mut [u8], size: usize, elem_size: usize) {
570 let nbyte = elem_size * size;
571 let mut jj = 0;
572 while jj < 8 * elem_size {
573 let mut ii = 0;
574 while ii + 8 * elem_size - 1 < nbyte {
575 let mut x = trans_bit_8x8(read_u64_le(input, ii + jj));
576 for kk in 0..8 {
577 out[ii + jj / 8 + kk * elem_size] = x as u8;
578 x >>= 8;
579 }
580 ii += 8 * elem_size;
581 }
582 jj += 8;
583 }
584}
585
586fn bshuf_untrans_bit_elem(input: &[u8], size: usize, elem_size: usize) -> Vec<u8> {
589 debug_assert_eq!(size % 8, 0);
590 let nbyte = size * elem_size;
591 let mut tmp = vec![0u8; nbyte];
592 bshuf_trans_byte_bitrow(input, &mut tmp, size, elem_size);
593 let mut out = vec![0u8; nbyte];
594 bshuf_shuffle_bit_eightelem(&tmp, &mut out, size, elem_size);
595 out
596}
597
598fn bshuf_compress_lz4_block(
602 out: &mut Vec<u8>,
603 raw: &[u8],
604 elem_start: usize,
605 size: usize,
606 elem_size: usize,
607) {
608 let off = elem_start * elem_size;
609 let shuffled = bshuf_trans_bit_elem(&raw[off..off + size * elem_size], size, elem_size);
610 let comp = compress(&shuffled);
611 out.extend_from_slice(&(comp.len() as u32).to_be_bytes());
612 out.extend_from_slice(&comp);
613}
614
615fn bshuf_decompress_lz4_block(
619 buf: &[u8],
620 pos: usize,
621 size: usize,
622 elem_size: usize,
623) -> Option<(Vec<u8>, usize)> {
624 if pos + 4 > buf.len() {
625 return None;
626 }
627 let clen = u32::from_be_bytes(buf[pos..pos + 4].try_into().ok()?) as usize;
628 let dstart = pos + 4;
629 if dstart + clen > buf.len() {
630 return None;
631 }
632 let shuffled = decompress(&buf[dstart..dstart + clen], size * elem_size).ok()?;
633 if shuffled.len() != size * elem_size {
634 return None;
635 }
636 Some((
637 bshuf_untrans_bit_elem(&shuffled, size, elem_size),
638 dstart + clen,
639 ))
640}
641
642pub fn compress_bslz4(src: &NDArray) -> NDArray {
655 let raw = src.data.as_u8_slice();
656 let data_type = src.data.data_type();
657 let elem_size = data_type.element_size();
658 let total_elems = if elem_size > 0 {
659 raw.len() / elem_size
660 } else {
661 0
662 };
663 let block_size = bshuf_default_block_size(elem_size);
664
665 let mut out: Vec<u8> = Vec::with_capacity(raw.len() / 2 + 16);
666
667 let n_full = total_elems / block_size;
668 let mut elem = 0usize;
669 for _ in 0..n_full {
670 bshuf_compress_lz4_block(&mut out, raw, elem, block_size, elem_size);
671 elem += block_size;
672 }
673 let mut last_block = total_elems % block_size;
675 last_block -= last_block % BSHUF_BLOCKED_MULT;
676 if last_block > 0 {
677 bshuf_compress_lz4_block(&mut out, raw, elem, last_block, elem_size);
678 elem += last_block;
679 }
680 if elem < total_elems {
682 out.extend_from_slice(&raw[elem * elem_size..total_elems * elem_size]);
683 }
684
685 let compressed_size = out.len();
686 let mut arr = src.clone();
687 arr.data = NDDataBuffer::U8(out);
688 arr.codec = Some(Codec {
689 name: CodecName::BSLZ4,
690 compressed_size,
691 level: 0,
692 shuffle: 0,
693 compressor: 0,
694 original_data_type: data_type,
695 });
696
697 tracing::debug!(
698 original_size = raw.len(),
699 compressed_size,
700 ratio = raw.len() as f64 / compressed_size.max(1) as f64,
701 "BSLZ4 compress"
702 );
703 arr
704}
705
706pub fn decompress_bslz4(src: &NDArray) -> Option<NDArray> {
714 let codec = src.codec.as_ref()?;
715 if codec.name != CodecName::BSLZ4 {
716 return None;
717 }
718 let buf = src.data.as_u8_slice();
719 let original_type = original_data_type(src);
720 let elem_size = original_type.element_size();
721 if elem_size == 0 {
722 return None;
723 }
724 let total_elems: usize = src.dims.iter().map(|d| d.size).product();
725 let total_bytes = total_elems * elem_size;
726 let block_size = bshuf_default_block_size(elem_size);
727
728 let mut out: Vec<u8> = Vec::with_capacity(total_bytes);
729 let mut pos = 0usize;
730
731 let n_full = total_elems / block_size;
732 for _ in 0..n_full {
733 let (block, next) = bshuf_decompress_lz4_block(buf, pos, block_size, elem_size)?;
734 out.extend_from_slice(&block);
735 pos = next;
736 }
737 let mut last_block = total_elems % block_size;
739 last_block -= last_block % BSHUF_BLOCKED_MULT;
740 if last_block > 0 {
741 let (block, next) = bshuf_decompress_lz4_block(buf, pos, last_block, elem_size)?;
742 out.extend_from_slice(&block);
743 pos = next;
744 }
745 let leftover_bytes = (total_elems % BSHUF_BLOCKED_MULT) * elem_size;
747 if leftover_bytes > 0 {
748 if pos + leftover_bytes > buf.len() {
749 return None;
750 }
751 out.extend_from_slice(&buf[pos..pos + leftover_bytes]);
752 }
753 if out.len() != total_bytes {
754 return None;
755 }
756
757 let buffer = buffer_from_bytes(&out, original_type)?;
758 let mut arr = src.clone();
759 arr.data = buffer;
760 arr.codec = None;
761 Some(arr)
762}
763
764pub fn compress_jpeg(src: &NDArray, quality: u8) -> Result<NDArray, JpegCompressError> {
783 use ad_core_rs::color::{NDColorMode, convert_rgb_layout};
784
785 match src.data.data_type() {
787 NDDataType::UInt8 | NDDataType::Int8 => {}
788 _ => return Err(JpegCompressError::NotEightBit),
789 }
790
791 let info = src.info();
792
793 if !matches!(src.dims.len(), 2 | 3) {
796 return Err(JpegCompressError::UnsupportedArrayStructure);
797 }
798
799 match info.color_mode {
804 NDColorMode::Mono | NDColorMode::RGB1 | NDColorMode::RGB2 | NDColorMode::RGB3 => {}
805 mode => return Err(JpegCompressError::UnknownColorMode(mode as i32)),
806 }
807
808 if info.x_size > u16::MAX as usize || info.y_size > u16::MAX as usize {
810 return Err(JpegCompressError::EncodeFailed);
811 }
812
813 let (color_type, interleaved) = match (src.dims.len(), info.color_mode) {
816 (2, NDColorMode::Mono | NDColorMode::RGB1) => (jpeg_encoder::ColorType::Luma, None),
817 (3, NDColorMode::RGB1) if info.color_size == 3 => (jpeg_encoder::ColorType::Rgb, None),
818 (3, mode @ (NDColorMode::RGB2 | NDColorMode::RGB3)) if info.color_size == 3 => (
819 jpeg_encoder::ColorType::Rgb,
820 Some(
821 convert_rgb_layout(src, mode, NDColorMode::RGB1)
822 .map_err(|_| JpegCompressError::EncodeFailed)?,
823 ),
824 ),
825 _ => return Err(JpegCompressError::EncodeFailed),
828 };
829
830 let width = info.x_size as u16;
831 let height = info.y_size as u16;
832 let pixels = interleaved
833 .as_ref()
834 .map_or_else(|| src.data.as_u8_slice(), |a| a.data.as_u8_slice());
835
836 let mut jpeg_buf = Vec::new();
837 let encoder = jpeg_encoder::Encoder::new(&mut jpeg_buf, quality);
838 if encoder.encode(pixels, width, height, color_type).is_err() {
839 return Err(JpegCompressError::EncodeFailed);
840 }
841
842 let compressed_size = jpeg_buf.len();
843 let original_size = src.data.as_u8_slice().len();
844
845 let mut arr = src.clone();
846 arr.data = NDDataBuffer::U8(jpeg_buf);
847 arr.codec = Some(Codec {
848 name: CodecName::JPEG,
849 compressed_size,
850 level: 0,
851 shuffle: 0,
852 compressor: 0,
853 original_data_type: src.data.data_type(),
856 });
857
858 tracing::debug!(
859 original_size,
860 compressed_size,
861 ratio = original_size as f64 / compressed_size.max(1) as f64,
862 "JPEG compress (quality={})",
863 quality,
864 );
865
866 Ok(arr)
867}
868
869#[derive(Debug, Clone, Copy, PartialEq, Eq)]
878pub enum JpegCompressError {
879 NotEightBit,
881 UnsupportedArrayStructure,
883 UnknownColorMode(i32),
886 EncodeFailed,
896}
897
898impl JpegCompressError {
899 pub fn message(&self) -> Cow<'static, str> {
901 match self {
902 Self::NotEightBit => "JPEG only supports 8-bit data".into(),
903 Self::UnsupportedArrayStructure => "Unsupported array structure".into(),
904 Self::UnknownColorMode(mode) => format!("Unknown color mode {}", mode).into(),
907 Self::EncodeFailed => "Error writing JPEG data".into(),
908 }
909 }
910}
911
912pub fn decompress_jpeg(src: &NDArray) -> Option<NDArray> {
926 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
927 use ad_core_rs::color::NDColorMode;
928
929 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::JPEG) {
930 return None;
931 }
932
933 let compressed = src.data.as_u8_slice();
934 let mut decoder = jpeg_decoder::Decoder::new(compressed);
935 let pixels = decoder.decode().ok()?;
936 let metadata = decoder.info()?;
937
938 let width = metadata.width as usize;
939 let height = metadata.height as usize;
940
941 let (dims, color_mode) = match metadata.pixel_format {
942 jpeg_decoder::PixelFormat::L8 => (
943 vec![NDDimension::new(width), NDDimension::new(height)],
944 NDColorMode::Mono,
945 ),
946 jpeg_decoder::PixelFormat::RGB24 => (
947 vec![
948 NDDimension::new(3),
949 NDDimension::new(width),
950 NDDimension::new(height),
951 ],
952 NDColorMode::RGB1,
953 ),
954 _ => return None,
955 };
956
957 let mut arr = src.clone();
958 arr.dims = dims;
959 arr.data = NDDataBuffer::U8(pixels);
960 arr.codec = None;
961 arr.attributes.add(NDAttribute::new_static(
962 "ColorMode",
963 "Color Mode",
964 NDAttrSource::Driver,
965 NDAttrValue::Int32(color_mode as i32),
966 ));
967
968 Some(arr)
969}
970
971#[derive(Debug, Clone, Copy)]
973pub struct BloscConfig {
974 pub compressor: u32,
976 pub clevel: u32,
978 pub shuffle: u32,
980}
981
982impl Default for BloscConfig {
983 fn default() -> Self {
984 Self {
985 compressor: 0,
986 clevel: 5,
991 shuffle: 0,
992 }
993 }
994}
995
996pub fn compress_blosc(src: &NDArray, config: &BloscConfig) -> NDArray {
998 let raw = src.data.as_u8_slice();
999 let element_size = src.data.data_type().element_size();
1000
1001 let pipeline = FilterPipeline {
1008 filters: vec![Filter {
1009 id: FILTER_BLOSC,
1010 flags: 0,
1011 cd_values: vec![
1012 2, 2, element_size as u32, raw.len() as u32, config.clevel, config.shuffle, config.compressor, ],
1020 }],
1021 };
1022
1023 let compressed = match apply_filters(&pipeline, raw) {
1024 Ok(data) => data,
1025 Err(_) => return src.clone(),
1026 };
1027
1028 let compressed_size = compressed.len();
1029 let original_data_type = src.data.data_type();
1030 let mut arr = src.clone();
1031 arr.data = NDDataBuffer::U8(compressed);
1032 arr.codec = Some(Codec {
1033 name: CodecName::Blosc,
1034 compressed_size,
1035 level: config.clevel as i32,
1038 shuffle: config.shuffle as i32,
1039 compressor: config.compressor as i32,
1040 original_data_type,
1041 });
1042 arr
1043}
1044
1045pub fn decompress_blosc(src: &NDArray) -> Option<NDArray> {
1047 let codec = src.codec.as_ref()?;
1048 if codec.name != CodecName::Blosc {
1049 return None;
1050 }
1051
1052 let compressed = src.data.as_u8_slice();
1053 let original_type = original_data_type(src);
1054 let element_size = original_type.element_size();
1055
1056 let pipeline = FilterPipeline {
1061 filters: vec![Filter {
1062 id: FILTER_BLOSC,
1063 flags: 0,
1064 cd_values: vec![
1065 2,
1066 2,
1067 element_size as u32,
1068 0,
1069 codec.level as u32,
1070 codec.shuffle as u32,
1071 codec.compressor as u32,
1072 ],
1073 }],
1074 };
1075
1076 let decompressed = reverse_filters(&pipeline, compressed).ok()?;
1077
1078 let buffer = buffer_from_bytes(&decompressed, original_type)?;
1079
1080 let mut arr = src.clone();
1081 arr.data = buffer;
1082 arr.codec = None;
1083 Some(arr)
1084}
1085
1086#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1088pub enum CodecMode {
1089 Compress { codec: CodecName, quality: u8 },
1091 Decompress,
1093}
1094
1095#[derive(Default)]
1099struct CodecParamIndices {
1100 mode: Option<usize>,
1101 compressor: Option<usize>,
1102 comp_factor: Option<usize>,
1103 jpeg_quality: Option<usize>,
1104 blosc_compressor: Option<usize>,
1105 blosc_clevel: Option<usize>,
1106 blosc_shuffle: Option<usize>,
1107 blosc_numthreads: Option<usize>,
1108 codec_status: Option<usize>,
1109 codec_error: Option<usize>,
1110}
1111
1112struct CodecState {
1116 mode: CodecMode,
1117 compression_ratio: f64,
1118 jpeg_quality: u8,
1119 blosc_config: BloscConfig,
1120}
1121
1122pub struct CodecProcessor {
1123 state: Mutex<CodecState>,
1124 params: CodecParamIndices,
1125}
1126
1127impl CodecProcessor {
1128 pub fn new(mode: CodecMode) -> Self {
1129 let quality = match mode {
1130 CodecMode::Compress { quality, .. } => quality,
1131 _ => 85,
1132 };
1133 Self {
1134 state: Mutex::new(CodecState {
1135 mode,
1136 compression_ratio: 1.0,
1137 jpeg_quality: quality,
1138 blosc_config: BloscConfig::default(),
1139 }),
1140 params: CodecParamIndices::default(),
1141 }
1142 }
1143
1144 pub fn compression_ratio(&self) -> f64 {
1147 self.state.lock().compression_ratio
1148 }
1149}
1150
1151enum CodecOutcome {
1167 PassThrough,
1170 Skipped(&'static str),
1174 Converted(NDArray),
1177 Failed(Cow<'static, str>),
1184}
1185
1186impl CodecOutcome {
1187 fn status(&self) -> CodecStatus {
1189 match self {
1190 Self::PassThrough | Self::Converted(_) => CodecStatus::Success,
1191 Self::Skipped(_) => CodecStatus::Warning,
1192 Self::Failed(_) => CodecStatus::Error,
1193 }
1194 }
1195
1196 fn error_message(&self) -> &str {
1199 match self {
1200 Self::PassThrough | Self::Converted(_) => "",
1201 Self::Skipped(message) => message,
1202 Self::Failed(message) => message,
1203 }
1204 }
1205}
1206
1207impl NDPluginProcess for CodecProcessor {
1208 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
1209 let (mode, jpeg_quality, blosc_config) = {
1215 let state = self.state.lock();
1216 (state.mode, state.jpeg_quality, state.blosc_config)
1217 };
1218 let original_bytes = array.data.as_u8_slice().len();
1219
1220 let mut compressor: Option<i32> = None;
1225
1226 let outcome = match mode {
1227 CodecMode::Compress {
1232 codec: CodecName::None,
1233 ..
1234 } => CodecOutcome::PassThrough,
1235 CodecMode::Compress { .. } if array.codec.is_some() => {
1236 CodecOutcome::Skipped("Array already compressed")
1239 }
1240 CodecMode::Compress { codec, .. } => match codec {
1241 CodecName::LZ4 => CodecOutcome::Converted(compress_lz4(array)),
1242 CodecName::JPEG => match compress_jpeg(array, jpeg_quality) {
1246 Ok(out) => CodecOutcome::Converted(out),
1247 Err(e) => CodecOutcome::Failed(e.message()),
1248 },
1249 CodecName::Zlib => CodecOutcome::Converted(compress_zlib(array)),
1250 CodecName::Blosc => CodecOutcome::Converted(compress_blosc(array, &blosc_config)),
1251 CodecName::LZ4HDF5 => CodecOutcome::Converted(compress_lz4hdf5(array)),
1252 CodecName::BSLZ4 => CodecOutcome::Converted(compress_bslz4(array)),
1253 CodecName::None => CodecOutcome::PassThrough,
1255 },
1256 CodecMode::Decompress => {
1257 let name = array
1262 .codec
1263 .as_ref()
1264 .map(|c| c.name)
1265 .unwrap_or(CodecName::None);
1266 compressor = Some(name.ordinal());
1267 match name {
1268 CodecName::None => CodecOutcome::PassThrough,
1271 CodecName::LZ4 => match decompress_lz4(array) {
1272 Some(out) => CodecOutcome::Converted(out),
1273 None => CodecOutcome::Failed("Failed to LZ4 decompress".into()),
1274 },
1275 CodecName::JPEG => match decompress_jpeg(array) {
1276 Some(out) => CodecOutcome::Converted(out),
1277 None => CodecOutcome::Failed("Error decoding JPEG".into()),
1278 },
1279 CodecName::Zlib => match decompress_zlib(array) {
1280 Some(out) => CodecOutcome::Converted(out),
1281 None => CodecOutcome::Failed("Failed to Zlib decompress".into()),
1282 },
1283 CodecName::Blosc => match decompress_blosc(array) {
1284 Some(out) => CodecOutcome::Converted(out),
1285 None => CodecOutcome::Failed("Failed to Blosc decompress".into()),
1286 },
1287 CodecName::LZ4HDF5 => match decompress_lz4hdf5(array) {
1288 Some(out) => CodecOutcome::Converted(out),
1289 None => CodecOutcome::Failed("Failed to LZ4 decompress".into()),
1290 },
1291 CodecName::BSLZ4 => match decompress_bslz4(array) {
1296 Some(out) => CodecOutcome::Converted(out),
1297 None => CodecOutcome::Failed("Failed to Blosc decompress".into()),
1298 },
1299 }
1300 }
1301 };
1302
1303 let status = outcome.status();
1304 let error = outcome.error_message().to_string();
1305
1306 let mut compression_ratio = 1.0;
1310 let output = match outcome {
1311 CodecOutcome::Converted(out) => {
1312 let output_bytes = out.data.as_u8_slice().len();
1313 compression_ratio = match mode {
1314 CodecMode::Compress { .. } => {
1315 original_bytes as f64 / output_bytes.max(1) as f64
1316 }
1317 CodecMode::Decompress => output_bytes as f64 / original_bytes.max(1) as f64,
1318 };
1319 out
1320 }
1321 CodecOutcome::PassThrough | CodecOutcome::Skipped(_) | CodecOutcome::Failed(_) => {
1322 array.clone()
1323 }
1324 };
1325 self.state.lock().compression_ratio = compression_ratio;
1326
1327 let mut updates = Vec::new();
1328 if let Some(idx) = self.params.comp_factor {
1329 updates.push(ParamUpdate::float64(idx, compression_ratio));
1330 }
1331 if let (Some(idx), Some(value)) = (self.params.compressor, compressor) {
1332 updates.push(ParamUpdate::int32(idx, value));
1333 }
1334 if let Some(idx) = self.params.codec_status {
1335 updates.push(ParamUpdate::int32(idx, status.as_i32()));
1336 }
1337 if let Some(idx) = self.params.codec_error {
1338 updates.push(ParamUpdate::Octet {
1339 reason: idx,
1340 addr: 0,
1341 value: error,
1342 });
1343 }
1344
1345 let mut r = ProcessResult::arrays(vec![Arc::new(output)]);
1346 r.param_updates = updates;
1347 r
1348 }
1349
1350 fn plugin_type(&self) -> &str {
1351 "NDPluginCodec"
1352 }
1353
1354 fn compression_aware(&self) -> bool {
1363 true
1364 }
1365
1366 fn register_params(
1367 &mut self,
1368 base: &mut asyn_rs::port::PortDriverBase,
1369 ) -> asyn_rs::error::AsynResult<()> {
1370 use asyn_rs::param::ParamType;
1371 base.create_param("MODE", ParamType::Int32)?;
1372 base.create_param("COMPRESSOR", ParamType::Int32)?;
1373 base.create_param("COMP_FACTOR", ParamType::Float64)?;
1374 base.create_param("JPEG_QUALITY", ParamType::Int32)?;
1375 base.create_param("BLOSC_COMPRESSOR", ParamType::Int32)?;
1376 base.create_param("BLOSC_CLEVEL", ParamType::Int32)?;
1377 base.create_param("BLOSC_SHUFFLE", ParamType::Int32)?;
1378 base.create_param("BLOSC_NUMTHREADS", ParamType::Int32)?;
1379 base.create_param("CODEC_STATUS", ParamType::Int32)?;
1380 base.create_param("CODEC_ERROR", ParamType::Octet)?;
1381
1382 self.params.mode = base.find_param("MODE");
1383 self.params.compressor = base.find_param("COMPRESSOR");
1384 self.params.comp_factor = base.find_param("COMP_FACTOR");
1385 self.params.jpeg_quality = base.find_param("JPEG_QUALITY");
1386 self.params.blosc_compressor = base.find_param("BLOSC_COMPRESSOR");
1387 self.params.blosc_clevel = base.find_param("BLOSC_CLEVEL");
1388 self.params.blosc_shuffle = base.find_param("BLOSC_SHUFFLE");
1389 self.params.blosc_numthreads = base.find_param("BLOSC_NUMTHREADS");
1390 self.params.codec_status = base.find_param("CODEC_STATUS");
1391 self.params.codec_error = base.find_param("CODEC_ERROR");
1392 Ok(())
1393 }
1394
1395 fn on_param_change(
1396 &self,
1397 reason: usize,
1398 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
1399 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
1400 let mut state = self.state.lock();
1401 if Some(reason) == self.params.mode {
1402 let v = params.value.as_i32();
1403 if v == 0 {
1404 let codec = match state.mode {
1406 CodecMode::Compress { codec, .. } => codec,
1407 _ => CodecName::LZ4,
1408 };
1409 state.mode = CodecMode::Compress {
1410 codec,
1411 quality: state.jpeg_quality,
1412 };
1413 } else {
1414 state.mode = CodecMode::Decompress;
1415 }
1416 } else if Some(reason) == self.params.compressor {
1417 let codec = CodecName::from_ordinal(params.value.as_i32());
1421 if let CodecMode::Compress { .. } = state.mode {
1422 state.mode = CodecMode::Compress {
1423 codec,
1424 quality: state.jpeg_quality,
1425 };
1426 }
1427 } else if Some(reason) == self.params.jpeg_quality {
1428 state.jpeg_quality = params.value.as_i32().clamp(1, 100) as u8;
1429 if let CodecMode::Compress { codec, .. } = state.mode {
1430 state.mode = CodecMode::Compress {
1431 codec,
1432 quality: state.jpeg_quality,
1433 };
1434 }
1435 } else if Some(reason) == self.params.blosc_compressor {
1436 state.blosc_config.compressor = params.value.as_i32().max(0) as u32;
1437 } else if Some(reason) == self.params.blosc_clevel {
1438 state.blosc_config.clevel = params.value.as_i32().clamp(0, 9) as u32;
1439 } else if Some(reason) == self.params.blosc_shuffle {
1440 state.blosc_config.shuffle = params.value.as_i32().max(0) as u32;
1441 }
1442
1443 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
1444 }
1445}
1446
1447#[cfg(test)]
1448mod tests {
1449 use super::*;
1450
1451 fn make_u8_array(width: usize, height: usize) -> NDArray {
1452 let mut arr = NDArray::new(
1453 vec![NDDimension::new(width), NDDimension::new(height)],
1454 NDDataType::UInt8,
1455 );
1456 if let NDDataBuffer::U8(ref mut v) = arr.data {
1457 for i in 0..v.len() {
1458 v[i] = (i % 256) as u8;
1459 }
1460 }
1461 arr
1462 }
1463
1464 fn make_rgb_array(width: usize, height: usize) -> NDArray {
1465 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
1466 let mut arr = NDArray::new(
1467 vec![
1468 NDDimension::new(3),
1469 NDDimension::new(width),
1470 NDDimension::new(height),
1471 ],
1472 NDDataType::UInt8,
1473 );
1474 arr.attributes.add(NDAttribute::new_static(
1476 "ColorMode",
1477 "Color Mode",
1478 NDAttrSource::Driver,
1479 NDAttrValue::Int32(2), ));
1481 if let NDDataBuffer::U8(ref mut v) = arr.data {
1482 for i in 0..v.len() {
1483 v[i] = (i % 256) as u8;
1484 }
1485 }
1486 arr
1487 }
1488
1489 #[test]
1495 fn compressors_record_type_in_codec_not_an_attribute() {
1496 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::UInt16);
1497 if let NDDataBuffer::U16(ref mut v) = arr.data {
1498 for (i, x) in v.iter_mut().enumerate() {
1499 *x = (i * 7) as u16;
1500 }
1501 }
1502 for compressed in [
1503 compress_lz4(&arr),
1504 compress_zlib(&arr),
1505 compress_lz4hdf5(&arr),
1506 compress_bslz4(&arr),
1507 compress_blosc(&arr, &BloscConfig::default()),
1508 ] {
1509 assert_eq!(
1510 compressed.codec.as_ref().unwrap().original_data_type,
1511 NDDataType::UInt16,
1512 "the original element type must travel in the codec"
1513 );
1514 assert!(
1515 compressed
1516 .attributes
1517 .get("CODEC_ORIGINAL_DATA_TYPE")
1518 .is_none(),
1519 "no codec carrier attribute may be attached to a compressed frame"
1520 );
1521 }
1522 }
1523
1524 #[test]
1525 fn test_adp29_blosc_default_clevel_and_codec_params() {
1526 assert_eq!(
1529 BloscConfig::default().clevel,
1530 5,
1531 "default Blosc clevel must be 5 (C parity)"
1532 );
1533
1534 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::UInt16);
1537 if let NDDataBuffer::U16(ref mut v) = arr.data {
1538 for (i, x) in v.iter_mut().enumerate() {
1539 *x = (i * 7) as u16;
1540 }
1541 }
1542 let out = compress_blosc(&arr, &BloscConfig::default());
1543 let codec = out.codec.as_ref().expect("blosc codec metadata");
1544 assert_eq!(codec.level, 5, "codec.level records the default clevel 5");
1547 assert_eq!(codec.shuffle, 0, "codec.shuffle records shuffle");
1548 assert_eq!(codec.compressor, 0, "codec.compressor records compressor");
1549 }
1550
1551 #[test]
1552 fn test_blosc_roundtrip_u16_default_compressor() {
1553 let mut arr = NDArray::new(
1559 vec![NDDimension::new(100), NDDimension::new(20)],
1560 NDDataType::UInt16,
1561 );
1562 if let NDDataBuffer::U16(ref mut v) = arr.data {
1563 for (i, x) in v.iter_mut().enumerate() {
1564 *x = (i * 37 % 65521) as u16;
1565 }
1566 }
1567 let original = arr.data.as_u8_slice().to_vec();
1568
1569 let compressed = compress_blosc(&arr, &BloscConfig::default());
1570 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::Blosc);
1571 assert_ne!(
1572 compressed.data.as_u8_slice(),
1573 original.as_slice(),
1574 "blosc must actually compress (not fall back to the raw clone)"
1575 );
1576
1577 let decompressed = decompress_blosc(&compressed).expect("blosc round-trip");
1578 assert!(decompressed.codec.is_none());
1579 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
1580 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1581 }
1582
1583 #[test]
1584 fn test_blosc_roundtrip_u16_lz4_subcompressor() {
1585 let cfg = BloscConfig {
1588 compressor: 1,
1589 clevel: 5,
1590 shuffle: 1,
1591 };
1592 let mut arr = NDArray::new(vec![NDDimension::new(256)], NDDataType::UInt16);
1593 if let NDDataBuffer::U16(ref mut v) = arr.data {
1594 for (i, x) in v.iter_mut().enumerate() {
1595 *x = (i * 13 % 65521) as u16;
1596 }
1597 }
1598 let original = arr.data.as_u8_slice().to_vec();
1599
1600 let compressed = compress_blosc(&arr, &cfg);
1601 let codec = compressed.codec.as_ref().unwrap();
1602 assert_eq!(codec.compressor, 1, "records the LZ4 sub-compressor");
1603 assert_eq!(codec.shuffle, 1, "records byte shuffle");
1604
1605 let decompressed = decompress_blosc(&compressed).expect("blosc lz4 round-trip");
1606 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1607 }
1608
1609 #[test]
1612 fn test_lz4_roundtrip_u8() {
1613 let arr = make_u8_array(4, 4);
1614 let original_data = arr.data.as_u8_slice().to_vec();
1615
1616 let compressed = compress_lz4(&arr);
1617 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
1618 assert_ne!(compressed.data.as_u8_slice(), original_data.as_slice());
1620
1621 let decompressed = decompress_lz4(&compressed).unwrap();
1622 assert!(decompressed.codec.is_none());
1623 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
1624 assert_eq!(decompressed.data.as_u8_slice(), original_data.as_slice());
1625 }
1626
1627 #[test]
1628 fn test_decompress_runtime_does_not_drop_compressed_input() {
1629 use ad_core_rs::plugin::channel::{NDArrayOutput, ndarray_channel};
1636 use ad_core_rs::plugin::runtime::create_plugin_runtime_with_output;
1637 use ad_core_rs::plugin::wiring::WiringRegistry;
1638 use std::sync::atomic::Ordering;
1639
1640 let mut raw = make_u8_array(4, 4);
1642 raw.unique_id = 1;
1643 let original_data = raw.data.as_u8_slice().to_vec();
1644 let compressed = compress_lz4(&raw);
1645 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
1646 assert_eq!(compressed.unique_id, 1);
1647
1648 let mut sentinel = make_u8_array(4, 4);
1652 sentinel.unique_id = 2;
1653
1654 let pool = Arc::new(NDArrayPool::new(1_000_000));
1655 let (ds_sender, mut ds_rx) = ndarray_channel("DS", 10);
1656 let mut output = NDArrayOutput::new();
1657 output.add(ds_sender);
1658 let (handle, _jh) = create_plugin_runtime_with_output(
1659 "CODEC_DECOMP",
1660 CodecProcessor::new(CodecMode::Decompress),
1661 pool,
1662 10,
1663 output,
1664 "",
1665 Arc::new(WiringRegistry::new()),
1666 );
1667 let dropped = handle.array_sender().dropped_arrays_counter().clone();
1668 handle
1669 .port_runtime()
1670 .port_handle()
1671 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
1672 .unwrap();
1673 assert!(
1675 handle.wait_params_applied(std::time::Duration::from_secs(10)),
1676 "data thread did not apply EnableCallbacks"
1677 );
1678
1679 let rt = tokio::runtime::Builder::new_current_thread()
1680 .enable_all()
1681 .build()
1682 .unwrap();
1683 rt.block_on(handle.array_sender().publish(Arc::new(compressed)));
1684 rt.block_on(handle.array_sender().publish(Arc::new(sentinel)));
1685
1686 let first = ds_rx.blocking_recv().expect("downstream array");
1687 assert_eq!(
1688 first.unique_id, 1,
1689 "compressed input must be decompressed and delivered, not dropped"
1690 );
1691 assert!(
1692 first.codec.is_none(),
1693 "delivered array must be decompressed (codec cleared)"
1694 );
1695 assert_eq!(first.data.as_u8_slice(), original_data.as_slice());
1696 assert_eq!(
1697 dropped.load(Ordering::Acquire),
1698 0,
1699 "compression-aware Codec must not count its compressed input as dropped"
1700 );
1701 }
1702
1703 #[test]
1704 fn test_lz4_roundtrip_u16() {
1705 let mut arr = NDArray::new(
1706 vec![NDDimension::new(8), NDDimension::new(8)],
1707 NDDataType::UInt16,
1708 );
1709 if let NDDataBuffer::U16(ref mut v) = arr.data {
1710 for i in 0..v.len() {
1711 v[i] = (i * 100) as u16;
1712 }
1713 }
1714 let original_bytes = arr.data.as_u8_slice().to_vec();
1715
1716 let compressed = compress_lz4(&arr);
1717 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
1718 assert_eq!(
1720 compressed.codec.as_ref().unwrap().original_data_type,
1721 NDDataType::UInt16
1722 );
1723 assert!(
1725 compressed
1726 .attributes
1727 .get("CODEC_ORIGINAL_DATA_TYPE")
1728 .is_none()
1729 );
1730
1731 let decompressed = decompress_lz4(&compressed).unwrap();
1732 assert!(decompressed.codec.is_none());
1733 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
1734 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
1735 }
1736
1737 #[test]
1738 fn test_lz4_roundtrip_f64() {
1739 let mut arr = NDArray::new(vec![NDDimension::new(16)], NDDataType::Float64);
1740 if let NDDataBuffer::F64(ref mut v) = arr.data {
1741 for i in 0..v.len() {
1742 v[i] = i as f64 * 1.5;
1743 }
1744 }
1745 let original_bytes = arr.data.as_u8_slice().to_vec();
1746
1747 let compressed = compress_lz4(&arr);
1748 let decompressed = decompress_lz4(&compressed).unwrap();
1749 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
1750 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
1751 }
1752
1753 #[test]
1754 fn test_lz4_compresses_repetitive_data() {
1755 let mut arr = NDArray::new(
1757 vec![NDDimension::new(256), NDDimension::new(256)],
1758 NDDataType::UInt8,
1759 );
1760 if let NDDataBuffer::U8(ref mut v) = arr.data {
1762 for x in v.iter_mut() {
1763 *x = 0;
1764 }
1765 }
1766 let original_size = arr.data.as_u8_slice().len();
1767
1768 let compressed = compress_lz4(&arr);
1769 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
1770 assert!(
1771 compressed_size < original_size,
1772 "compressed ({}) should be smaller than original ({})",
1773 compressed_size,
1774 original_size,
1775 );
1776 }
1777
1778 #[test]
1779 fn test_lz4_preserves_metadata() {
1780 let mut arr = make_u8_array(4, 4);
1781 arr.unique_id = 42;
1782
1783 let compressed = compress_lz4(&arr);
1784 assert_eq!(compressed.unique_id, 42);
1785 assert_eq!(compressed.dims.len(), 2);
1786 assert_eq!(compressed.dims[0].size, 4);
1787 assert_eq!(compressed.dims[1].size, 4);
1788 }
1789
1790 #[test]
1793 fn test_bitshuffle_block_transpose_roundtrip() {
1794 for &(n, elem_size) in &[(16usize, 4usize), (8, 2), (256, 8), (128, 1)] {
1797 let input: Vec<u8> = (0..n * elem_size).map(|i| (i * 7 + 3) as u8).collect();
1798 let shuffled = bshuf_trans_bit_elem(&input, n, elem_size);
1799 assert_eq!(shuffled.len(), input.len());
1800 let restored = bshuf_untrans_bit_elem(&shuffled, n, elem_size);
1801 assert_eq!(restored, input, "elem_size {elem_size}, n {n}");
1802 }
1803 }
1804
1805 #[test]
1806 fn test_bitshuffle_matches_c_reference_vector() {
1807 let input: Vec<u8> = (0..16u16).flat_map(|v| v.to_le_bytes()).collect();
1815 let shuffled = bshuf_trans_bit_elem(&input, 16, 2);
1816 let mut expected = vec![0u8; 32];
1817 expected[..8].copy_from_slice(&[170, 170, 204, 204, 240, 240, 0, 255]);
1818 assert_eq!(
1819 shuffled, expected,
1820 "canonical bitshuffle transpose must match the C library bytes"
1821 );
1822 }
1823
1824 #[test]
1825 fn test_bslz4_roundtrip_u8() {
1826 let mut arr = NDArray::new(
1827 vec![NDDimension::new(64), NDDimension::new(64)],
1828 NDDataType::UInt8,
1829 );
1830 if let NDDataBuffer::U8(ref mut v) = arr.data {
1831 for (i, x) in v.iter_mut().enumerate() {
1832 *x = (i % 251) as u8;
1833 }
1834 }
1835 let original = arr.data.as_u8_slice().to_vec();
1836
1837 let compressed = compress_bslz4(&arr);
1838 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::BSLZ4);
1839 assert_ne!(compressed.data.as_u8_slice(), original.as_slice());
1840
1841 let decompressed = decompress_bslz4(&compressed).unwrap();
1842 assert!(decompressed.codec.is_none());
1843 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
1844 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1845 }
1846
1847 #[test]
1848 fn test_bslz4_roundtrip_u16() {
1849 let mut arr = NDArray::new(
1850 vec![NDDimension::new(100), NDDimension::new(20)],
1851 NDDataType::UInt16,
1852 );
1853 if let NDDataBuffer::U16(ref mut v) = arr.data {
1854 for (i, x) in v.iter_mut().enumerate() {
1855 *x = (i * 37 % 65521) as u16;
1856 }
1857 }
1858 let original = arr.data.as_u8_slice().to_vec();
1859
1860 let compressed = compress_bslz4(&arr);
1861 assert_eq!(
1862 compressed.codec.as_ref().unwrap().original_data_type,
1863 NDDataType::UInt16
1864 );
1865 let decompressed = decompress_bslz4(&compressed).unwrap();
1866 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
1867 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1868 }
1869
1870 #[test]
1871 fn test_bslz4_roundtrip_f64_with_negatives() {
1872 let mut arr = NDArray::new(vec![NDDimension::new(73)], NDDataType::Float64);
1873 if let NDDataBuffer::F64(ref mut v) = arr.data {
1874 for (i, x) in v.iter_mut().enumerate() {
1875 *x = (i as f64 - 36.0) * 2.5;
1876 }
1877 }
1878 let original = arr.data.as_u8_slice().to_vec();
1879
1880 let compressed = compress_bslz4(&arr);
1881 let decompressed = decompress_bslz4(&compressed).unwrap();
1882 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
1883 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1884 }
1885
1886 #[test]
1887 fn test_bslz4_roundtrip_multi_block() {
1888 let elem_size = 4usize;
1891 let block = bshuf_default_block_size(elem_size);
1892 let count = block * 2 + block / 2 + 3;
1894 let mut arr = NDArray::new(vec![NDDimension::new(count)], NDDataType::Int32);
1895 if let NDDataBuffer::I32(ref mut v) = arr.data {
1896 for (i, x) in v.iter_mut().enumerate() {
1897 *x = (i as i32).wrapping_mul(2_654_435_761u32 as i32);
1898 }
1899 }
1900 let original = arr.data.as_u8_slice().to_vec();
1901
1902 let compressed = compress_bslz4(&arr);
1903 let decompressed = decompress_bslz4(&compressed).unwrap();
1904 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
1905 }
1906
1907 #[test]
1908 fn test_bslz4_compresses_repetitive_data() {
1909 let arr = NDArray::new(
1911 vec![NDDimension::new(256), NDDimension::new(256)],
1912 NDDataType::UInt16,
1913 );
1914 let original_size = arr.data.as_u8_slice().len();
1915 let compressed = compress_bslz4(&arr);
1916 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
1917 assert!(
1918 compressed_size < original_size,
1919 "bslz4 compressed ({compressed_size}) should be < original ({original_size})"
1920 );
1921 }
1922
1923 #[test]
1924 fn test_r9_71_corrupt_bslz4_reports_cs_blosc_text() {
1925 use ad_core_rs::plugin::runtime::ParamUpdate;
1930
1931 let mut arr = NDArray::new(
1932 vec![NDDimension::new(32), NDDimension::new(32)],
1933 NDDataType::UInt16,
1934 );
1935 if let NDDataBuffer::U16(ref mut v) = arr.data {
1936 for (i, x) in v.iter_mut().enumerate() {
1937 *x = (i * 11) as u16;
1938 }
1939 }
1940 let pool = NDArrayPool::new(10_000_000);
1941
1942 let mut compressed = compress_bslz4(&arr);
1944 if let NDDataBuffer::U8(ref mut v) = compressed.data {
1945 for b in v.iter_mut() {
1946 *b = 0xFF;
1947 }
1948 }
1949 assert!(
1950 decompress_bslz4(&compressed).is_none(),
1951 "the corrupted frame must fail to decompress"
1952 );
1953
1954 let mut decomp = CodecProcessor::new(CodecMode::Decompress);
1955 decomp.params.codec_error = Some(13);
1956 let result = decomp.process_array(&compressed, &pool);
1957 let text = result
1958 .param_updates
1959 .iter()
1960 .find_map(|u| match u {
1961 ParamUpdate::Octet {
1962 reason: 13, value, ..
1963 } => Some(value.clone()),
1964 _ => None,
1965 })
1966 .expect("CodecError posted");
1967 assert_eq!(text, "Failed to Blosc decompress");
1968 }
1969
1970 #[test]
1971 fn test_bslz4_via_processor() {
1972 let mut arr = NDArray::new(
1974 vec![NDDimension::new(32), NDDimension::new(32)],
1975 NDDataType::UInt16,
1976 );
1977 if let NDDataBuffer::U16(ref mut v) = arr.data {
1978 for (i, x) in v.iter_mut().enumerate() {
1979 *x = (i * 11) as u16;
1980 }
1981 }
1982 let original = arr.data.as_u8_slice().to_vec();
1983 let pool = NDArrayPool::new(10_000_000);
1984
1985 let comp = CodecProcessor::new(CodecMode::Compress {
1986 codec: CodecName::BSLZ4,
1987 quality: 0,
1988 });
1989 let compressed = comp.process_array(&arr, &pool);
1990 let compressed_arr = &compressed.output_arrays[0];
1991 assert_eq!(
1992 compressed_arr.codec.as_ref().unwrap().name,
1993 CodecName::BSLZ4
1994 );
1995
1996 let decomp = CodecProcessor::new(CodecMode::Decompress);
1997 let result = decomp.process_array(compressed_arr, &pool);
1998 assert_eq!(
1999 result.output_arrays[0].data.as_u8_slice(),
2000 original.as_slice()
2001 );
2002 }
2003
2004 #[test]
2007 fn test_jpeg_compress_mono() {
2008 let arr = make_u8_array(16, 16);
2009 let compressed = compress_jpeg(&arr, 90).unwrap();
2010 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
2011 let data = compressed.data.as_u8_slice();
2013 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
2014 }
2015
2016 #[test]
2017 fn test_jpeg_compress_rgb() {
2018 let arr = make_rgb_array(16, 16);
2019 let compressed = compress_jpeg(&arr, 90).unwrap();
2020 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
2021 let data = compressed.data.as_u8_slice();
2022 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
2023 }
2024
2025 #[test]
2026 fn test_jpeg_roundtrip_mono() {
2027 let arr = make_u8_array(16, 16);
2028 let compressed = compress_jpeg(&arr, 100).unwrap();
2029 let decompressed = decompress_jpeg(&compressed).unwrap();
2030 assert!(decompressed.codec.is_none());
2031 assert_eq!(decompressed.dims.len(), 2);
2032 assert_eq!(decompressed.dims[0].size, 16); assert_eq!(decompressed.dims[1].size, 16); assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
2035 assert_eq!(decompressed.data.len(), 16 * 16);
2037 }
2038
2039 #[test]
2040 fn test_jpeg_roundtrip_rgb() {
2041 let arr = make_rgb_array(16, 16);
2042 let compressed = compress_jpeg(&arr, 100).unwrap();
2043 let decompressed = decompress_jpeg(&compressed).unwrap();
2044 assert!(decompressed.codec.is_none());
2045 assert_eq!(decompressed.dims.len(), 3);
2046 assert_eq!(decompressed.dims[0].size, 3); assert_eq!(decompressed.dims[1].size, 16); assert_eq!(decompressed.dims[2].size, 16); assert_eq!(decompressed.data.len(), 3 * 16 * 16);
2050 }
2051
2052 fn make_rgb_layout(mode: ad_core_rs::color::NDColorMode, w: usize, h: usize) -> NDArray {
2058 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
2059 use ad_core_rs::color::NDColorMode;
2060
2061 let pixel = |x: usize, y: usize, c: usize| ((x * 7 + y * 13 + c * 61) % 256) as u8;
2062 let dims = match mode {
2063 NDColorMode::RGB1 => vec![3, w, h],
2064 NDColorMode::RGB2 => vec![w, 3, h],
2065 NDColorMode::RGB3 => vec![w, h, 3],
2066 other => panic!("not an RGB layout: {other:?}"),
2067 };
2068 let mut arr = NDArray::new(
2069 dims.into_iter().map(NDDimension::new).collect(),
2070 NDDataType::UInt8,
2071 );
2072 arr.attributes.add(NDAttribute::new_static(
2073 "ColorMode",
2074 "Color Mode",
2075 NDAttrSource::Driver,
2076 NDAttrValue::Int32(mode as i32),
2077 ));
2078 if let NDDataBuffer::U8(ref mut v) = arr.data {
2079 for y in 0..h {
2080 for x in 0..w {
2081 for c in 0..3 {
2082 let idx = match mode {
2083 NDColorMode::RGB1 => c + x * 3 + y * w * 3,
2084 NDColorMode::RGB2 => x + c * w + y * w * 3,
2085 NDColorMode::RGB3 => x + y * w + c * w * h,
2086 _ => unreachable!(),
2087 };
2088 v[idx] = pixel(x, y, c);
2089 }
2090 }
2091 }
2092 }
2093 arr
2094 }
2095
2096 #[test]
2097 fn test_r8_62_jpeg_compresses_rgb2_and_rgb3_as_reinterleaved_rgb() {
2098 use ad_core_rs::color::NDColorMode;
2103
2104 let rgb1 = make_rgb_layout(NDColorMode::RGB1, 16, 8);
2105 let reference = compress_jpeg(&rgb1, 90).expect("RGB1 must compress");
2106
2107 for mode in [NDColorMode::RGB2, NDColorMode::RGB3] {
2108 let src = make_rgb_layout(mode, 16, 8);
2109 let out = compress_jpeg(&src, 90)
2110 .unwrap_or_else(|e| panic!("{mode:?} must compress, C encodes it: {e:?}"));
2111 assert_eq!(out.codec.as_ref().unwrap().name, CodecName::JPEG);
2112 assert_eq!(&out.data.as_u8_slice()[0..2], &[0xFF, 0xD8], "SOI marker");
2113 assert_eq!(
2114 out.data.as_u8_slice(),
2115 reference.data.as_u8_slice(),
2116 "{mode:?} must encode the same pixels as the RGB1 image — a wrong \
2117 (or missing) scanline re-interleave changes the JPEG bytes"
2118 );
2119 assert_eq!(out.dims.len(), 3);
2121 }
2122 }
2123
2124 #[test]
2125 fn test_r8_62_decompressed_jpeg_reports_rgb1_colormode() {
2126 use ad_core_rs::color::NDColorMode;
2131
2132 let src = make_rgb_layout(NDColorMode::RGB2, 16, 8);
2133 let compressed = compress_jpeg(&src, 90).expect("rgb2 jpeg");
2134 assert_eq!(
2135 compressed
2136 .attributes
2137 .get("ColorMode")
2138 .unwrap()
2139 .value
2140 .as_i64(),
2141 Some(NDColorMode::RGB2 as i64),
2142 "the compressed frame keeps the source ColorMode"
2143 );
2144
2145 let out = decompress_jpeg(&compressed).expect("jpeg decode");
2146 assert_eq!(
2147 out.attributes.get("ColorMode").unwrap().value.as_i64(),
2148 Some(NDColorMode::RGB1 as i64),
2149 "decompressed JPEG must be reported as RGB1"
2150 );
2151 assert_eq!(out.dims[0].size, 3);
2152 assert_eq!(out.dims[1].size, 16);
2153 assert_eq!(out.dims[2].size, 8);
2154 assert_eq!(out.info().color_mode, NDColorMode::RGB1);
2155
2156 let mono = decompress_jpeg(&compress_jpeg(&make_u8_array(16, 16), 90).unwrap()).unwrap();
2158 assert_eq!(
2159 mono.attributes.get("ColorMode").unwrap().value.as_i64(),
2160 Some(NDColorMode::Mono as i64)
2161 );
2162 }
2163
2164 #[test]
2165 fn test_r8_62_jpeg_accepts_int8_like_c() {
2166 let mut arr = NDArray::new(
2170 vec![NDDimension::new(8), NDDimension::new(8)],
2171 NDDataType::Int8,
2172 );
2173 if let NDDataBuffer::I8(ref mut v) = arr.data {
2174 for (i, x) in v.iter_mut().enumerate() {
2175 *x = (i as i32 - 32) as i8;
2176 }
2177 }
2178 let out = compress_jpeg(&arr, 90).expect("Int8 must compress");
2179 assert_eq!(&out.data.as_u8_slice()[0..2], &[0xFF, 0xD8]);
2180 assert_eq!(
2181 out.codec.as_ref().unwrap().original_data_type,
2182 NDDataType::Int8
2183 );
2184 }
2185
2186 #[test]
2187 fn test_r8_62_jpeg_rejects_3d_without_an_rgb_colormode() {
2188 let arr = NDArray::new(
2197 vec![
2198 NDDimension::new(3),
2199 NDDimension::new(8),
2200 NDDimension::new(8),
2201 ],
2202 NDDataType::UInt8,
2203 );
2204 assert_eq!(
2205 compress_jpeg(&arr, 90).unwrap_err(),
2206 JpegCompressError::EncodeFailed,
2207 "3-D Mono (no ColorMode attribute) is not a JPEG-encodable layout in C"
2208 );
2209 }
2210
2211 #[test]
2212 fn test_jpeg_rejects_non_u8() {
2213 let arr = NDArray::new(
2215 vec![NDDimension::new(8), NDDimension::new(8)],
2216 NDDataType::UInt16,
2217 );
2218 let err = compress_jpeg(&arr, 90).unwrap_err();
2219 assert_eq!(err, JpegCompressError::NotEightBit);
2220 assert_eq!(err.message(), "JPEG only supports 8-bit data");
2221 }
2222
2223 #[test]
2224 fn test_jpeg_rejects_1d() {
2225 let arr = NDArray::new(vec![NDDimension::new(64)], NDDataType::UInt8);
2227 let err = compress_jpeg(&arr, 90).unwrap_err();
2228 assert_eq!(err, JpegCompressError::UnsupportedArrayStructure);
2229 assert_eq!(err.message(), "Unsupported array structure");
2230 }
2231
2232 #[test]
2233 fn test_r8_74_jpeg_compress_failures_carry_the_c_error_texts() {
2234 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
2240 use ad_core_rs::color::NDColorMode;
2241
2242 let arr = NDArray::new(
2244 vec![NDDimension::new(8), NDDimension::new(8)],
2245 NDDataType::Float32,
2246 );
2247 assert_eq!(
2248 compress_jpeg(&arr, 90).unwrap_err().message(),
2249 "JPEG only supports 8-bit data"
2250 );
2251
2252 let arr = NDArray::new(
2254 vec![
2255 NDDimension::new(2),
2256 NDDimension::new(2),
2257 NDDimension::new(2),
2258 NDDimension::new(2),
2259 ],
2260 NDDataType::UInt8,
2261 );
2262 assert_eq!(
2263 compress_jpeg(&arr, 90).unwrap_err().message(),
2264 "Unsupported array structure"
2265 );
2266
2267 for (mode, text) in [
2271 (NDColorMode::Bayer, "Unknown color mode 1"),
2272 (NDColorMode::YUV444, "Unknown color mode 5"),
2273 (NDColorMode::YUV411, "Unknown color mode 7"),
2274 ] {
2275 let mut arr = NDArray::new(
2276 vec![NDDimension::new(8), NDDimension::new(8)],
2277 NDDataType::UInt8,
2278 );
2279 arr.attributes.add(NDAttribute::new_static(
2280 "ColorMode",
2281 "",
2282 NDAttrSource::Driver,
2283 NDAttrValue::Int32(mode as i32),
2284 ));
2285 assert_eq!(compress_jpeg(&arr, 90).unwrap_err().message(), text);
2286 }
2287 }
2288
2289 #[test]
2290 fn test_r8_74_codec_error_pv_carries_the_jpeg_text() {
2291 use ad_core_rs::plugin::runtime::ParamUpdate;
2294
2295 let mut proc = CodecProcessor::new(CodecMode::Compress {
2296 codec: CodecName::JPEG,
2297 quality: 90,
2298 });
2299 proc.params.codec_error = Some(13);
2300 let arr = NDArray::new(
2301 vec![NDDimension::new(8), NDDimension::new(8)],
2302 NDDataType::UInt16,
2303 );
2304 let result = proc.process_array(&arr, &NDArrayPool::new(0));
2305 let text = result
2306 .param_updates
2307 .iter()
2308 .find_map(|u| match u {
2309 ParamUpdate::Octet {
2310 reason: 13, value, ..
2311 } => Some(value.clone()),
2312 _ => None,
2313 })
2314 .expect("CodecError posted");
2315 assert_eq!(text, "JPEG only supports 8-bit data");
2316 }
2317
2318 #[test]
2319 fn test_jpeg_quality_affects_size() {
2320 let arr = make_u8_array(64, 64);
2321 let high = compress_jpeg(&arr, 95).unwrap();
2322 let low = compress_jpeg(&arr, 10).unwrap();
2323 let high_size = high.codec.as_ref().unwrap().compressed_size;
2324 let low_size = low.codec.as_ref().unwrap().compressed_size;
2325 assert!(
2326 high_size > low_size,
2327 "high quality ({}) should produce larger output than low quality ({})",
2328 high_size,
2329 low_size,
2330 );
2331 }
2332
2333 #[test]
2336 fn test_zlib_roundtrip_u8() {
2337 let arr = make_u8_array(8, 8);
2338 let original = arr.data.as_u8_slice().to_vec();
2339
2340 let compressed = compress_zlib(&arr);
2341 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::Zlib);
2342 assert_ne!(compressed.data.as_u8_slice(), original.as_slice());
2343
2344 let decompressed = decompress_zlib(&compressed).unwrap();
2345 assert!(decompressed.codec.is_none());
2346 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
2347 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2348 }
2349
2350 #[test]
2351 fn test_zlib_roundtrip_u16() {
2352 let mut arr = NDArray::new(
2353 vec![NDDimension::new(16), NDDimension::new(16)],
2354 NDDataType::UInt16,
2355 );
2356 if let NDDataBuffer::U16(ref mut v) = arr.data {
2357 for (i, x) in v.iter_mut().enumerate() {
2358 *x = (i * 257 % 65521) as u16;
2359 }
2360 }
2361 let original = arr.data.as_u8_slice().to_vec();
2362
2363 let compressed = compress_zlib(&arr);
2364 assert_eq!(
2365 compressed.codec.as_ref().unwrap().original_data_type,
2366 NDDataType::UInt16
2367 );
2368
2369 let decompressed = decompress_zlib(&compressed).unwrap();
2370 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
2371 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2372 }
2373
2374 #[test]
2375 fn test_zlib_roundtrip_f64_with_negatives() {
2376 let mut arr = NDArray::new(vec![NDDimension::new(64)], NDDataType::Float64);
2377 if let NDDataBuffer::F64(ref mut v) = arr.data {
2378 for (i, x) in v.iter_mut().enumerate() {
2379 *x = (i as f64 - 32.0) * 3.25;
2380 }
2381 }
2382 let original = arr.data.as_u8_slice().to_vec();
2383
2384 let compressed = compress_zlib(&arr);
2385 let decompressed = decompress_zlib(&compressed).unwrap();
2386 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
2387 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2388 }
2389
2390 #[test]
2391 fn test_zlib_compresses_repetitive_data() {
2392 let arr = NDArray::new(
2393 vec![NDDimension::new(256), NDDimension::new(256)],
2394 NDDataType::UInt8,
2395 );
2396 let original_size = arr.data.as_u8_slice().len();
2397 let compressed = compress_zlib(&arr);
2398 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
2399 assert!(
2400 compressed_size < original_size,
2401 "zlib compressed ({compressed_size}) should be < original ({original_size})"
2402 );
2403 }
2404
2405 #[test]
2406 fn test_zlib_via_processor() {
2407 let mut arr = NDArray::new(
2408 vec![NDDimension::new(32), NDDimension::new(32)],
2409 NDDataType::UInt16,
2410 );
2411 if let NDDataBuffer::U16(ref mut v) = arr.data {
2412 for (i, x) in v.iter_mut().enumerate() {
2413 *x = (i * 13) as u16;
2414 }
2415 }
2416 let original = arr.data.as_u8_slice().to_vec();
2417 let pool = NDArrayPool::new(10_000_000);
2418
2419 let comp = CodecProcessor::new(CodecMode::Compress {
2420 codec: CodecName::Zlib,
2421 quality: 0,
2422 });
2423 let compressed = comp.process_array(&arr, &pool);
2424 let compressed_arr = &compressed.output_arrays[0];
2425 assert_eq!(compressed_arr.codec.as_ref().unwrap().name, CodecName::Zlib);
2426
2427 let decomp = CodecProcessor::new(CodecMode::Decompress);
2428 let result = decomp.process_array(compressed_arr, &pool);
2429 assert_eq!(
2430 result.output_arrays[0].data.as_u8_slice(),
2431 original.as_slice()
2432 );
2433 }
2434
2435 #[test]
2438 fn test_lz4hdf5_roundtrip_u8() {
2439 let mut arr = NDArray::new(
2440 vec![NDDimension::new(64), NDDimension::new(64)],
2441 NDDataType::UInt8,
2442 );
2443 if let NDDataBuffer::U8(ref mut v) = arr.data {
2444 for (i, x) in v.iter_mut().enumerate() {
2445 *x = (i % 251) as u8;
2446 }
2447 }
2448 let original = arr.data.as_u8_slice().to_vec();
2449
2450 let compressed = compress_lz4hdf5(&arr);
2451 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4HDF5);
2452 assert_ne!(compressed.data.as_u8_slice(), original.as_slice());
2453
2454 let decompressed = decompress_lz4hdf5(&compressed).unwrap();
2455 assert!(decompressed.codec.is_none());
2456 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
2457 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2458 }
2459
2460 #[test]
2461 fn test_lz4hdf5_roundtrip_u16() {
2462 let mut arr = NDArray::new(
2463 vec![NDDimension::new(80), NDDimension::new(40)],
2464 NDDataType::UInt16,
2465 );
2466 if let NDDataBuffer::U16(ref mut v) = arr.data {
2467 for (i, x) in v.iter_mut().enumerate() {
2468 *x = (i * 37 % 65521) as u16;
2469 }
2470 }
2471 let original = arr.data.as_u8_slice().to_vec();
2472
2473 let compressed = compress_lz4hdf5(&arr);
2474 assert_eq!(
2475 compressed.codec.as_ref().unwrap().original_data_type,
2476 NDDataType::UInt16
2477 );
2478
2479 let decompressed = decompress_lz4hdf5(&compressed).unwrap();
2480 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
2481 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2482 }
2483
2484 #[test]
2485 fn test_lz4hdf5_roundtrip_f64_with_negatives() {
2486 let mut arr = NDArray::new(vec![NDDimension::new(97)], NDDataType::Float64);
2487 if let NDDataBuffer::F64(ref mut v) = arr.data {
2488 for (i, x) in v.iter_mut().enumerate() {
2489 *x = (i as f64 - 48.0) * 1.75;
2490 }
2491 }
2492 let original = arr.data.as_u8_slice().to_vec();
2493
2494 let compressed = compress_lz4hdf5(&arr);
2495 let decompressed = decompress_lz4hdf5(&compressed).unwrap();
2496 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
2497 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2498 }
2499
2500 #[test]
2501 fn test_lz4hdf5_multi_block_roundtrip() {
2502 let block = LZ4HDF5_DEFAULT_BLOCK_SIZE;
2505 let count = block * 2 + block / 3 + 7; let mut arr = NDArray::new(vec![NDDimension::new(count)], NDDataType::UInt8);
2507 if let NDDataBuffer::U8(ref mut v) = arr.data {
2508 for (i, x) in v.iter_mut().enumerate() {
2509 *x = (i.wrapping_mul(2_654_435_761) % 251) as u8;
2510 }
2511 }
2512 let original = arr.data.as_u8_slice().to_vec();
2513
2514 let compressed = compress_lz4hdf5(&arr);
2515 let decompressed = decompress_lz4hdf5(&compressed).unwrap();
2516 assert_eq!(decompressed.data.as_u8_slice(), original.as_slice());
2517 }
2518
2519 #[test]
2520 fn test_lz4hdf5_compresses_repetitive_data() {
2521 let arr = NDArray::new(
2522 vec![NDDimension::new(256), NDDimension::new(256)],
2523 NDDataType::UInt16,
2524 );
2525 let original_size = arr.data.as_u8_slice().len();
2526 let compressed = compress_lz4hdf5(&arr);
2527 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
2528 assert!(
2529 compressed_size < original_size,
2530 "lz4hdf5 compressed ({compressed_size}) should be < original ({original_size})"
2531 );
2532 }
2533
2534 #[test]
2535 fn test_lz4hdf5_via_processor() {
2536 let mut arr = NDArray::new(
2537 vec![NDDimension::new(48), NDDimension::new(48)],
2538 NDDataType::UInt16,
2539 );
2540 if let NDDataBuffer::U16(ref mut v) = arr.data {
2541 for (i, x) in v.iter_mut().enumerate() {
2542 *x = (i * 7) as u16;
2543 }
2544 }
2545 let original = arr.data.as_u8_slice().to_vec();
2546 let pool = NDArrayPool::new(10_000_000);
2547
2548 let comp = CodecProcessor::new(CodecMode::Compress {
2549 codec: CodecName::LZ4HDF5,
2550 quality: 0,
2551 });
2552 let compressed = comp.process_array(&arr, &pool);
2553 let compressed_arr = &compressed.output_arrays[0];
2554 assert_eq!(
2555 compressed_arr.codec.as_ref().unwrap().name,
2556 CodecName::LZ4HDF5
2557 );
2558
2559 let decomp = CodecProcessor::new(CodecMode::Decompress);
2560 let result = decomp.process_array(compressed_arr, &pool);
2561 assert_eq!(
2562 result.output_arrays[0].data.as_u8_slice(),
2563 original.as_slice()
2564 );
2565 }
2566
2567 #[test]
2570 fn test_compressor_ordinal_mapping() {
2571 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
2575
2576 let cases = [
2577 (0i32, CodecName::None),
2578 (1, CodecName::JPEG),
2579 (2, CodecName::Blosc),
2580 (3, CodecName::LZ4),
2581 (4, CodecName::BSLZ4),
2582 (5, CodecName::Zlib),
2583 (6, CodecName::LZ4HDF5),
2584 ];
2585
2586 for (ordinal, expected) in cases {
2587 let mut proc = CodecProcessor::new(CodecMode::Compress {
2588 codec: CodecName::LZ4,
2589 quality: 85,
2590 });
2591 proc.params.compressor = Some(0);
2594 let snapshot = PluginParamSnapshot {
2595 enable_callbacks: true,
2596 reason: 0,
2597 addr: 0,
2598 value: ParamChangeValue::Int32(ordinal),
2599 };
2600 proc.on_param_change(0, &snapshot);
2601 match proc.state.lock().mode {
2602 CodecMode::Compress { codec, .. } => assert_eq!(
2603 codec, expected,
2604 "ordinal {ordinal} should select {expected:?}"
2605 ),
2606 other => panic!("expected Compress mode, got {other:?}"),
2607 }
2608 }
2609 }
2610
2611 #[test]
2614 fn test_decompress_wrong_codec() {
2615 let arr = make_u8_array(4, 4);
2616 assert!(decompress_lz4(&arr).is_none());
2617 assert!(decompress_jpeg(&arr).is_none());
2618 assert!(decompress_zlib(&arr).is_none());
2619 assert!(decompress_lz4hdf5(&arr).is_none());
2620 }
2621
2622 #[test]
2625 fn test_processor_lz4_compress() {
2626 let pool = NDArrayPool::new(1_000_000);
2627 let proc = CodecProcessor::new(CodecMode::Compress {
2628 codec: CodecName::LZ4,
2629 quality: 0,
2630 });
2631 let arr = make_u8_array(32, 32);
2632 let result = proc.process_array(&arr, &pool);
2633 assert_eq!(result.output_arrays.len(), 1);
2634 assert_eq!(
2635 result.output_arrays[0].codec.as_ref().unwrap().name,
2636 CodecName::LZ4
2637 );
2638 assert!(proc.compression_ratio() >= 1.0);
2639 }
2640
2641 #[test]
2642 fn test_processor_jpeg_compress() {
2643 let pool = NDArrayPool::new(1_000_000);
2644 let proc = CodecProcessor::new(CodecMode::Compress {
2645 codec: CodecName::JPEG,
2646 quality: 80,
2647 });
2648 let arr = make_u8_array(16, 16);
2649 let result = proc.process_array(&arr, &pool);
2650 assert_eq!(result.output_arrays.len(), 1);
2651 assert_eq!(
2652 result.output_arrays[0].codec.as_ref().unwrap().name,
2653 CodecName::JPEG
2654 );
2655 }
2656
2657 #[test]
2658 fn test_processor_decompress_auto_lz4() {
2659 let pool = NDArrayPool::new(1_000_000);
2660 let arr = make_u8_array(16, 16);
2661 let compressed = compress_lz4(&arr);
2662
2663 let proc = CodecProcessor::new(CodecMode::Decompress);
2664 let result = proc.process_array(&compressed, &pool);
2665 assert_eq!(result.output_arrays.len(), 1);
2666 assert!(result.output_arrays[0].codec.is_none());
2667 assert_eq!(
2668 result.output_arrays[0].data.as_u8_slice(),
2669 arr.data.as_u8_slice()
2670 );
2671 assert!(proc.compression_ratio() > 0.0);
2672 }
2673
2674 #[test]
2675 fn test_processor_decompress_auto_jpeg() {
2676 let pool = NDArrayPool::new(1_000_000);
2677 let arr = make_u8_array(16, 16);
2678 let compressed = compress_jpeg(&arr, 90).unwrap();
2679
2680 let proc = CodecProcessor::new(CodecMode::Decompress);
2681 let result = proc.process_array(&compressed, &pool);
2682 assert_eq!(result.output_arrays.len(), 1);
2683 assert!(result.output_arrays[0].codec.is_none());
2684 }
2685
2686 #[test]
2687 fn test_processor_decompress_no_codec() {
2688 let pool = NDArrayPool::new(1_000_000);
2689 let arr = make_u8_array(8, 8);
2690 let proc = CodecProcessor::new(CodecMode::Decompress);
2691 let result = proc.process_array(&arr, &pool);
2692 assert_eq!(result.output_arrays.len(), 1);
2694 assert_eq!(proc.compression_ratio(), 1.0);
2695 }
2696
2697 fn processor_with_params(mode: CodecMode) -> CodecProcessor {
2702 let mut proc = CodecProcessor::new(mode);
2703 proc.params.comp_factor = Some(10);
2704 proc.params.compressor = Some(11);
2705 proc.params.codec_status = Some(12);
2706 proc.params.codec_error = Some(13);
2707 proc
2708 }
2709
2710 fn int32_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
2711 updates.iter().find_map(|u| match u {
2712 ParamUpdate::Int32 {
2713 reason: r, value, ..
2714 } if *r == reason => Some(*value),
2715 _ => None,
2716 })
2717 }
2718
2719 fn octet_update(updates: &[ParamUpdate], reason: usize) -> Option<String> {
2720 updates.iter().find_map(|u| match u {
2721 ParamUpdate::Octet {
2722 reason: r, value, ..
2723 } if *r == reason => Some(value.clone()),
2724 _ => None,
2725 })
2726 }
2727
2728 #[test]
2729 fn test_r8_61_decompress_uncompressed_input_is_success_passthrough() {
2730 let pool = NDArrayPool::new(1_000_000);
2735 let arr = make_u8_array(8, 8);
2736 let proc = processor_with_params(CodecMode::Decompress);
2737 let result = proc.process_array(&arr, &pool);
2738
2739 assert_eq!(
2740 int32_update(&result.param_updates, 12),
2741 Some(0),
2742 "CodecStatus must stay SUCCESS on an uncompressed input"
2743 );
2744 assert_eq!(
2745 octet_update(&result.param_updates, 13),
2746 Some(String::new()),
2747 "no error string on a pass-through"
2748 );
2749 assert_eq!(
2750 int32_update(&result.param_updates, 11),
2751 Some(0),
2752 "COMPRESSOR must be set to NDCODEC_NONE"
2753 );
2754 assert_eq!(
2755 result.output_arrays[0].data.as_u8_slice(),
2756 arr.data.as_u8_slice(),
2757 "the input array is passed through unchanged"
2758 );
2759 assert_eq!(proc.compression_ratio(), 1.0);
2760 }
2761
2762 #[test]
2763 fn test_r8_61_decompress_reports_compressor_of_the_input_codec() {
2764 let pool = NDArrayPool::new(1_000_000);
2767 let src = make_u8_array(16, 16);
2768 for (codec, ordinal) in [
2769 (compress_lz4(&src), 3),
2770 (compress_blosc(&src, &BloscConfig::default()), 2),
2771 (compress_bslz4(&src), 4),
2772 (compress_jpeg(&src, 90).expect("jpeg"), 1),
2773 ] {
2774 let proc = processor_with_params(CodecMode::Decompress);
2775 let result = proc.process_array(&codec, &pool);
2776 assert_eq!(
2777 int32_update(&result.param_updates, 11),
2778 Some(ordinal),
2779 "COMPRESSOR must report the input codec's C ordinal"
2780 );
2781 assert_eq!(
2782 int32_update(&result.param_updates, 12),
2783 Some(0),
2784 "a successful decompress is SUCCESS"
2785 );
2786 }
2787 }
2788
2789 #[test]
2790 fn test_r8_61_compress_with_compressor_none_is_success_passthrough() {
2791 let pool = NDArrayPool::new(1_000_000);
2796 let arr = make_u8_array(8, 8);
2797 let proc = processor_with_params(CodecMode::Compress {
2798 codec: CodecName::None,
2799 quality: 85,
2800 });
2801 let result = proc.process_array(&arr, &pool);
2802
2803 assert_eq!(int32_update(&result.param_updates, 12), Some(0));
2804 assert_eq!(octet_update(&result.param_updates, 13), Some(String::new()));
2805 assert_eq!(
2806 int32_update(&result.param_updates, 11),
2807 None,
2808 "compress mode must not overwrite the operator's COMPRESSOR selection"
2809 );
2810 assert!(result.output_arrays[0].codec.is_none());
2811 assert_eq!(
2812 result.output_arrays[0].data.as_u8_slice(),
2813 arr.data.as_u8_slice()
2814 );
2815 }
2816
2817 #[test]
2818 fn test_r8_61_genuine_decompress_failure_still_reports_an_error() {
2819 let pool = NDArrayPool::new(1_000_000);
2823 let arr = make_u8_array(16, 16);
2824 let mut corrupted = compress_lz4(&arr);
2825 if let NDDataBuffer::U8(ref mut v) = corrupted.data {
2826 v.truncate(3);
2827 }
2828 let proc = processor_with_params(CodecMode::Decompress);
2829 let result = proc.process_array(&corrupted, &pool);
2830
2831 assert_ne!(
2832 int32_update(&result.param_updates, 12),
2833 Some(0),
2834 "a failed decompress must not report SUCCESS"
2835 );
2836 assert_eq!(
2837 octet_update(&result.param_updates, 13),
2838 Some("Failed to LZ4 decompress".to_string())
2839 );
2840 assert_eq!(int32_update(&result.param_updates, 11), Some(3));
2841 assert_eq!(
2842 result.output_arrays[0].data.as_u8_slice(),
2843 corrupted.data.as_u8_slice(),
2844 "the input array is republished on failure"
2845 );
2846 }
2847
2848 #[test]
2851 fn test_r8_63_status_levels_match_c() {
2852 assert_eq!(CodecStatus::Success.as_i32(), 0);
2855 assert_eq!(CodecStatus::Warning.as_i32(), 1);
2856 assert_eq!(CodecStatus::Error.as_i32(), 2);
2857 }
2858
2859 #[test]
2860 fn test_r8_63_already_compressed_is_a_warning_not_success() {
2861 let pool = NDArrayPool::new(1_000_000);
2865 let compressed = compress_lz4(&make_u8_array(16, 16));
2866 let proc = processor_with_params(CodecMode::Compress {
2867 codec: CodecName::Zlib,
2868 quality: 85,
2869 });
2870 let result = proc.process_array(&compressed, &pool);
2871
2872 assert_eq!(
2873 int32_update(&result.param_updates, 12),
2874 Some(CodecStatus::Warning.as_i32()),
2875 "already-compressed input must report WARNING(1)"
2876 );
2877 assert_eq!(
2878 octet_update(&result.param_updates, 13),
2879 Some("Array already compressed".to_string())
2880 );
2881 assert_eq!(
2883 result.output_arrays[0].codec.as_ref().unwrap().name,
2884 CodecName::LZ4
2885 );
2886 }
2887
2888 #[test]
2889 fn test_r8_63_genuine_failures_are_error_not_warning() {
2890 let pool = NDArrayPool::new(1_000_000);
2895
2896 let wide = NDArray::new(
2898 vec![NDDimension::new(8), NDDimension::new(8)],
2899 NDDataType::UInt16,
2900 );
2901 let proc = processor_with_params(CodecMode::Compress {
2902 codec: CodecName::JPEG,
2903 quality: 85,
2904 });
2905 let result = proc.process_array(&wide, &pool);
2906 assert_eq!(
2907 int32_update(&result.param_updates, 12),
2908 Some(CodecStatus::Error.as_i32()),
2909 "an unsupported JPEG input is an ERROR"
2910 );
2911
2912 let mut corrupted = compress_lz4(&make_u8_array(16, 16));
2914 if let NDDataBuffer::U8(ref mut v) = corrupted.data {
2915 v.truncate(3);
2916 }
2917 let proc = processor_with_params(CodecMode::Decompress);
2918 let result = proc.process_array(&corrupted, &pool);
2919 assert_eq!(
2920 int32_update(&result.param_updates, 12),
2921 Some(CodecStatus::Error.as_i32()),
2922 "a failed decompress is an ERROR"
2923 );
2924 }
2925
2926 #[test]
2927 fn test_r8_63_successful_and_passthrough_paths_report_success() {
2928 let pool = NDArrayPool::new(1_000_000);
2931 let arr = make_u8_array(16, 16);
2932
2933 let proc = processor_with_params(CodecMode::Compress {
2934 codec: CodecName::LZ4,
2935 quality: 85,
2936 });
2937 let compressed = proc.process_array(&arr, &pool);
2938 assert_eq!(
2939 int32_update(&compressed.param_updates, 12),
2940 Some(CodecStatus::Success.as_i32())
2941 );
2942
2943 let proc = processor_with_params(CodecMode::Decompress);
2944 let passthrough = proc.process_array(&arr, &pool);
2945 assert_eq!(
2946 int32_update(&passthrough.param_updates, 12),
2947 Some(CodecStatus::Success.as_i32())
2948 );
2949 }
2950
2951 #[test]
2952 fn test_processor_compression_ratio() {
2953 let pool = NDArrayPool::new(1_000_000);
2954 let mut arr = NDArray::new(
2956 vec![NDDimension::new(128), NDDimension::new(128)],
2957 NDDataType::UInt8,
2958 );
2959 if let NDDataBuffer::U8(ref mut v) = arr.data {
2960 for x in v.iter_mut() {
2961 *x = 0;
2962 }
2963 }
2964
2965 let proc = CodecProcessor::new(CodecMode::Compress {
2966 codec: CodecName::LZ4,
2967 quality: 0,
2968 });
2969 let _ = proc.process_array(&arr, &pool);
2970 let ratio = proc.compression_ratio();
2971 assert!(
2972 ratio > 2.0,
2973 "all-zeros 128x128 should compress at least 2x, got {}",
2974 ratio,
2975 );
2976 }
2977
2978 #[test]
2979 fn test_processor_plugin_type() {
2980 let proc = CodecProcessor::new(CodecMode::Decompress);
2981 assert_eq!(proc.plugin_type(), "NDPluginCodec");
2982 }
2983
2984 #[test]
2987 fn test_buffer_from_bytes_u8() {
2988 let data = vec![1u8, 2, 3, 4];
2989 let buf = buffer_from_bytes(&data, NDDataType::UInt8).unwrap();
2990 assert_eq!(buf.data_type(), NDDataType::UInt8);
2991 assert_eq!(buf.len(), 4);
2992 assert_eq!(buf.as_u8_slice(), &[1, 2, 3, 4]);
2993 }
2994
2995 #[test]
2996 fn test_buffer_from_bytes_u16() {
2997 let original = vec![1000u16, 2000, 3000];
2998 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
2999 let buf = buffer_from_bytes(&bytes, NDDataType::UInt16).unwrap();
3000 assert_eq!(buf.data_type(), NDDataType::UInt16);
3001 assert_eq!(buf.len(), 3);
3002 if let NDDataBuffer::U16(v) = buf {
3003 assert_eq!(v, original);
3004 } else {
3005 panic!("wrong buffer type");
3006 }
3007 }
3008
3009 #[test]
3010 fn test_buffer_from_bytes_bad_alignment() {
3011 let data = vec![0u8; 3];
3013 assert!(buffer_from_bytes(&data, NDDataType::UInt16).is_none());
3014 }
3015
3016 #[test]
3017 fn test_buffer_from_bytes_f64_roundtrip() {
3018 let original = vec![1.5f64, -2.7, 3.14159];
3019 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
3020 let buf = buffer_from_bytes(&bytes, NDDataType::Float64).unwrap();
3021 if let NDDataBuffer::F64(v) = buf {
3022 assert_eq!(v, original);
3023 } else {
3024 panic!("wrong buffer type");
3025 }
3026 }
3027}