1use std::sync::Arc;
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
4use ad_core_rs::codec::{Codec, CodecName};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::runtime::{NDPluginProcess, ParamUpdate, ProcessResult};
8
9use lz4_flex::{compress_prepend_size, decompress_size_prepended};
10use rust_hdf5::format::messages::filter::{
11 FILTER_BLOSC, Filter, FilterPipeline, apply_filters, reverse_filters,
12};
13
14const ATTR_ORIGINAL_DATA_TYPE: &str = "CODEC_ORIGINAL_DATA_TYPE";
16
17fn buffer_from_bytes(bytes: &[u8], data_type: NDDataType) -> Option<NDDataBuffer> {
22 let elem_size = data_type.element_size();
23 if bytes.len() % elem_size != 0 {
24 return None;
25 }
26 let count = bytes.len() / elem_size;
27
28 Some(match data_type {
29 NDDataType::Int8 => {
30 let mut v = vec![0i8; count];
31 unsafe {
33 std::ptr::copy_nonoverlapping(
34 bytes.as_ptr(),
35 v.as_mut_ptr() as *mut u8,
36 bytes.len(),
37 );
38 }
39 NDDataBuffer::I8(v)
40 }
41 NDDataType::UInt8 => NDDataBuffer::U8(bytes.to_vec()),
42 NDDataType::Int16 => {
43 let mut v = vec![0i16; count];
44 unsafe {
45 std::ptr::copy_nonoverlapping(
46 bytes.as_ptr(),
47 v.as_mut_ptr() as *mut u8,
48 bytes.len(),
49 );
50 }
51 NDDataBuffer::I16(v)
52 }
53 NDDataType::UInt16 => {
54 let mut v = vec![0u16; count];
55 unsafe {
56 std::ptr::copy_nonoverlapping(
57 bytes.as_ptr(),
58 v.as_mut_ptr() as *mut u8,
59 bytes.len(),
60 );
61 }
62 NDDataBuffer::U16(v)
63 }
64 NDDataType::Int32 => {
65 let mut v = vec![0i32; count];
66 unsafe {
67 std::ptr::copy_nonoverlapping(
68 bytes.as_ptr(),
69 v.as_mut_ptr() as *mut u8,
70 bytes.len(),
71 );
72 }
73 NDDataBuffer::I32(v)
74 }
75 NDDataType::UInt32 => {
76 let mut v = vec![0u32; count];
77 unsafe {
78 std::ptr::copy_nonoverlapping(
79 bytes.as_ptr(),
80 v.as_mut_ptr() as *mut u8,
81 bytes.len(),
82 );
83 }
84 NDDataBuffer::U32(v)
85 }
86 NDDataType::Int64 => {
87 let mut v = vec![0i64; count];
88 unsafe {
89 std::ptr::copy_nonoverlapping(
90 bytes.as_ptr(),
91 v.as_mut_ptr() as *mut u8,
92 bytes.len(),
93 );
94 }
95 NDDataBuffer::I64(v)
96 }
97 NDDataType::UInt64 => {
98 let mut v = vec![0u64; count];
99 unsafe {
100 std::ptr::copy_nonoverlapping(
101 bytes.as_ptr(),
102 v.as_mut_ptr() as *mut u8,
103 bytes.len(),
104 );
105 }
106 NDDataBuffer::U64(v)
107 }
108 NDDataType::Float32 => {
109 let mut v = vec![0f32; count];
110 unsafe {
111 std::ptr::copy_nonoverlapping(
112 bytes.as_ptr(),
113 v.as_mut_ptr() as *mut u8,
114 bytes.len(),
115 );
116 }
117 NDDataBuffer::F32(v)
118 }
119 NDDataType::Float64 => {
120 let mut v = vec![0f64; count];
121 unsafe {
122 std::ptr::copy_nonoverlapping(
123 bytes.as_ptr(),
124 v.as_mut_ptr() as *mut u8,
125 bytes.len(),
126 );
127 }
128 NDDataBuffer::F64(v)
129 }
130 })
131}
132
133pub fn compress_lz4(src: &NDArray) -> NDArray {
139 let raw = src.data.as_u8_slice();
140 let original_data_type = src.data.data_type();
141 let original_size = raw.len();
142 let compressed = compress_prepend_size(raw);
143 let compressed_size = compressed.len();
144
145 let mut arr = src.clone();
146 arr.data = NDDataBuffer::U8(compressed);
147 arr.codec = Some(Codec {
148 name: CodecName::LZ4,
149 compressed_size,
150 });
151
152 arr.attributes.add(NDAttribute {
154 name: ATTR_ORIGINAL_DATA_TYPE.into(),
155 description: "Original NDDataType ordinal before codec compression".into(),
156 source: NDAttrSource::Driver,
157 value: NDAttrValue::UInt8(original_data_type as u8),
158 });
159
160 tracing::debug!(
161 original_size,
162 compressed_size,
163 ratio = original_size as f64 / compressed_size.max(1) as f64,
164 "LZ4 compress"
165 );
166
167 arr
168}
169
170pub fn decompress_lz4(src: &NDArray) -> Option<NDArray> {
175 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::LZ4) {
176 return None;
177 }
178 let compressed = src.data.as_u8_slice();
179 let decompressed = decompress_size_prepended(compressed).ok()?;
180
181 let original_type = src
183 .attributes
184 .get(ATTR_ORIGINAL_DATA_TYPE)
185 .and_then(|a| a.value.as_i64())
186 .and_then(|ord| NDDataType::from_ordinal(ord as u8))
187 .unwrap_or(NDDataType::UInt8);
188
189 let buffer = buffer_from_bytes(&decompressed, original_type)?;
190
191 let mut arr = src.clone();
192 arr.data = buffer;
193 arr.codec = None;
194 arr.attributes.remove(ATTR_ORIGINAL_DATA_TYPE);
195
196 Some(arr)
197}
198
199pub fn compress_jpeg(src: &NDArray, quality: u8) -> Option<NDArray> {
207 if src.data.data_type() != NDDataType::UInt8 {
208 return None;
209 }
210
211 let raw = src.data.as_u8_slice();
212 let info = src.info();
213
214 let (width, height, color_type) = match src.dims.len() {
215 2 => {
216 (
218 info.x_size as u16,
219 info.y_size as u16,
220 jpeg_encoder::ColorType::Luma,
221 )
222 }
223 3 if src.dims[0].size == 3 => {
224 (
226 info.x_size as u16,
227 info.y_size as u16,
228 jpeg_encoder::ColorType::Rgb,
229 )
230 }
231 _ => return None,
232 };
233
234 let mut jpeg_buf = Vec::new();
235 let encoder = jpeg_encoder::Encoder::new(&mut jpeg_buf, quality);
236 if encoder.encode(raw, width, height, color_type).is_err() {
237 return None;
238 }
239
240 let compressed_size = jpeg_buf.len();
241 let original_size = raw.len();
242
243 let mut arr = src.clone();
244 arr.data = NDDataBuffer::U8(jpeg_buf);
245 arr.codec = Some(Codec {
246 name: CodecName::JPEG,
247 compressed_size,
248 });
249
250 tracing::debug!(
251 original_size,
252 compressed_size,
253 ratio = original_size as f64 / compressed_size.max(1) as f64,
254 "JPEG compress (quality={})",
255 quality,
256 );
257
258 Some(arr)
259}
260
261pub fn decompress_jpeg(src: &NDArray) -> Option<NDArray> {
268 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::JPEG) {
269 return None;
270 }
271
272 let compressed = src.data.as_u8_slice();
273 let mut decoder = jpeg_decoder::Decoder::new(compressed);
274 let pixels = decoder.decode().ok()?;
275 let metadata = decoder.info()?;
276
277 let width = metadata.width as usize;
278 let height = metadata.height as usize;
279
280 let dims = match metadata.pixel_format {
281 jpeg_decoder::PixelFormat::L8 => {
282 vec![NDDimension::new(width), NDDimension::new(height)]
284 }
285 jpeg_decoder::PixelFormat::RGB24 => {
286 vec![
288 NDDimension::new(3),
289 NDDimension::new(width),
290 NDDimension::new(height),
291 ]
292 }
293 _ => return None,
294 };
295
296 let mut arr = src.clone();
297 arr.dims = dims;
298 arr.data = NDDataBuffer::U8(pixels);
299 arr.codec = None;
300
301 Some(arr)
302}
303
304#[derive(Debug, Clone, Copy)]
306pub struct BloscConfig {
307 pub compressor: u32,
309 pub clevel: u32,
311 pub shuffle: u32,
313}
314
315impl Default for BloscConfig {
316 fn default() -> Self {
317 Self {
318 compressor: 0,
319 clevel: 3,
320 shuffle: 0,
321 }
322 }
323}
324
325pub fn compress_blosc(src: &NDArray, config: &BloscConfig) -> NDArray {
327 let raw = src.data.as_u8_slice();
328 let element_size = src.data.data_type().element_size();
329
330 let pipeline = FilterPipeline {
331 filters: vec![Filter {
332 id: FILTER_BLOSC,
333 flags: 0,
334 cd_values: vec![
335 2, 2, element_size as u32, raw.len() as u32, config.shuffle, config.compressor, config.clevel, ],
343 }],
344 };
345
346 let compressed = match apply_filters(&pipeline, raw) {
347 Ok(data) => data,
348 Err(_) => return src.clone(),
349 };
350
351 let compressed_size = compressed.len();
352 let mut arr = src.clone();
353 arr.attributes.add(NDAttribute {
354 name: ATTR_ORIGINAL_DATA_TYPE.to_string(),
355 description: String::new(),
356 source: NDAttrSource::Driver,
357 value: NDAttrValue::Int64(src.data.data_type() as u8 as i64),
358 });
359 arr.data = NDDataBuffer::U8(compressed);
360 arr.codec = Some(Codec {
361 name: CodecName::Blosc,
362 compressed_size,
363 });
364 arr
365}
366
367pub fn decompress_blosc(src: &NDArray) -> Option<NDArray> {
369 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::Blosc) {
370 return None;
371 }
372
373 let compressed = src.data.as_u8_slice();
374
375 let pipeline = FilterPipeline {
377 filters: vec![Filter {
378 id: FILTER_BLOSC,
379 flags: 0,
380 cd_values: vec![],
381 }],
382 };
383
384 let decompressed = reverse_filters(&pipeline, compressed).ok()?;
385
386 let original_type = src
387 .attributes
388 .get(ATTR_ORIGINAL_DATA_TYPE)
389 .and_then(|a| a.value.as_i64())
390 .and_then(|ord| NDDataType::from_ordinal(ord as u8))
391 .unwrap_or(NDDataType::UInt8);
392
393 let buffer = buffer_from_bytes(&decompressed, original_type)?;
394
395 let mut arr = src.clone();
396 arr.data = buffer;
397 arr.codec = None;
398 arr.attributes.remove(ATTR_ORIGINAL_DATA_TYPE);
399 Some(arr)
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub enum CodecMode {
405 Compress { codec: CodecName, quality: u8 },
407 Decompress,
409}
410
411#[derive(Default)]
415struct CodecParamIndices {
416 mode: Option<usize>,
417 compressor: Option<usize>,
418 comp_factor: Option<usize>,
419 jpeg_quality: Option<usize>,
420 blosc_compressor: Option<usize>,
421 blosc_clevel: Option<usize>,
422 blosc_shuffle: Option<usize>,
423 blosc_numthreads: Option<usize>,
424 codec_status: Option<usize>,
425 codec_error: Option<usize>,
426}
427
428pub struct CodecProcessor {
429 mode: CodecMode,
430 compression_ratio: f64,
431 jpeg_quality: u8,
432 blosc_config: BloscConfig,
433 params: CodecParamIndices,
434}
435
436impl CodecProcessor {
437 pub fn new(mode: CodecMode) -> Self {
438 let quality = match mode {
439 CodecMode::Compress { quality, .. } => quality,
440 _ => 85,
441 };
442 Self {
443 mode,
444 compression_ratio: 1.0,
445 jpeg_quality: quality,
446 blosc_config: BloscConfig::default(),
447 params: CodecParamIndices::default(),
448 }
449 }
450
451 pub fn compression_ratio(&self) -> f64 {
454 self.compression_ratio
455 }
456}
457
458impl NDPluginProcess for CodecProcessor {
459 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
460 let original_bytes = array.data.as_u8_slice().len();
461
462 let result = match self.mode {
463 CodecMode::Compress {
464 codec: CodecName::LZ4,
465 ..
466 } => Some(compress_lz4(array)),
467 CodecMode::Compress {
468 codec: CodecName::JPEG,
469 ..
470 } => compress_jpeg(array, self.jpeg_quality),
471 CodecMode::Compress {
472 codec: CodecName::Blosc,
473 ..
474 } => Some(compress_blosc(array, &self.blosc_config)),
475 CodecMode::Compress { .. } => None,
476 CodecMode::Decompress => match array.codec.as_ref().map(|c| c.name) {
477 Some(CodecName::LZ4) => decompress_lz4(array),
478 Some(CodecName::JPEG) => decompress_jpeg(array),
479 Some(CodecName::Blosc) => decompress_blosc(array),
480 _ => None,
481 },
482 };
483
484 let mut updates = Vec::new();
485
486 match result {
487 Some(ref out) => {
488 let output_bytes = out.data.as_u8_slice().len();
489 match self.mode {
490 CodecMode::Compress { .. } => {
491 self.compression_ratio = original_bytes as f64 / output_bytes.max(1) as f64;
492 }
493 CodecMode::Decompress => {
494 self.compression_ratio = output_bytes as f64 / original_bytes.max(1) as f64;
495 }
496 }
497 if let Some(idx) = self.params.comp_factor {
498 updates.push(ParamUpdate::float64(idx, self.compression_ratio));
499 }
500 if let Some(idx) = self.params.codec_status {
501 updates.push(ParamUpdate::int32(idx, 0)); }
503 if let Some(idx) = self.params.codec_error {
504 updates.push(ParamUpdate::Octet {
505 reason: idx,
506 addr: 0,
507 value: String::new(),
508 });
509 }
510 let mut r = ProcessResult::arrays(vec![Arc::new(out.clone())]);
511 r.param_updates = updates;
512 r
513 }
514 None => {
515 self.compression_ratio = 1.0;
516 if let Some(idx) = self.params.comp_factor {
517 updates.push(ParamUpdate::float64(idx, 0.0));
518 }
519 if let Some(idx) = self.params.codec_status {
520 updates.push(ParamUpdate::int32(idx, 1)); }
522 if let Some(idx) = self.params.codec_error {
523 updates.push(ParamUpdate::Octet {
524 reason: idx,
525 addr: 0,
526 value: "codec operation failed or unsupported".to_string(),
527 });
528 }
529 ProcessResult::sink(updates)
530 }
531 }
532 }
533
534 fn plugin_type(&self) -> &str {
535 "NDPluginCodec"
536 }
537
538 fn register_params(
539 &mut self,
540 base: &mut asyn_rs::port::PortDriverBase,
541 ) -> asyn_rs::error::AsynResult<()> {
542 use asyn_rs::param::ParamType;
543 base.create_param("MODE", ParamType::Int32)?;
544 base.create_param("COMPRESSOR", ParamType::Int32)?;
545 base.create_param("COMP_FACTOR", ParamType::Float64)?;
546 base.create_param("JPEG_QUALITY", ParamType::Int32)?;
547 base.create_param("BLOSC_COMPRESSOR", ParamType::Int32)?;
548 base.create_param("BLOSC_CLEVEL", ParamType::Int32)?;
549 base.create_param("BLOSC_SHUFFLE", ParamType::Int32)?;
550 base.create_param("BLOSC_NUMTHREADS", ParamType::Int32)?;
551 base.create_param("CODEC_STATUS", ParamType::Int32)?;
552 base.create_param("CODEC_ERROR", ParamType::Octet)?;
553
554 self.params.mode = base.find_param("MODE");
555 self.params.compressor = base.find_param("COMPRESSOR");
556 self.params.comp_factor = base.find_param("COMP_FACTOR");
557 self.params.jpeg_quality = base.find_param("JPEG_QUALITY");
558 self.params.blosc_compressor = base.find_param("BLOSC_COMPRESSOR");
559 self.params.blosc_clevel = base.find_param("BLOSC_CLEVEL");
560 self.params.blosc_shuffle = base.find_param("BLOSC_SHUFFLE");
561 self.params.blosc_numthreads = base.find_param("BLOSC_NUMTHREADS");
562 self.params.codec_status = base.find_param("CODEC_STATUS");
563 self.params.codec_error = base.find_param("CODEC_ERROR");
564 Ok(())
565 }
566
567 fn on_param_change(
568 &mut self,
569 reason: usize,
570 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
571 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
572 if Some(reason) == self.params.mode {
573 let v = params.value.as_i32();
574 if v == 0 {
575 let codec = match self.mode {
577 CodecMode::Compress { codec, .. } => codec,
578 _ => CodecName::LZ4,
579 };
580 self.mode = CodecMode::Compress {
581 codec,
582 quality: self.jpeg_quality,
583 };
584 } else {
585 self.mode = CodecMode::Decompress;
586 }
587 } else if Some(reason) == self.params.compressor {
588 let codec = match params.value.as_i32() {
590 1 => CodecName::LZ4,
591 2 => CodecName::JPEG,
592 3 => CodecName::Blosc,
593 _ => CodecName::LZ4,
594 };
595 if let CodecMode::Compress { .. } = self.mode {
596 self.mode = CodecMode::Compress {
597 codec,
598 quality: self.jpeg_quality,
599 };
600 }
601 } else if Some(reason) == self.params.jpeg_quality {
602 self.jpeg_quality = params.value.as_i32().clamp(1, 100) as u8;
603 if let CodecMode::Compress { codec, .. } = self.mode {
604 self.mode = CodecMode::Compress {
605 codec,
606 quality: self.jpeg_quality,
607 };
608 }
609 } else if Some(reason) == self.params.blosc_compressor {
610 self.blosc_config.compressor = params.value.as_i32().max(0) as u32;
611 } else if Some(reason) == self.params.blosc_clevel {
612 self.blosc_config.clevel = params.value.as_i32().clamp(0, 9) as u32;
613 } else if Some(reason) == self.params.blosc_shuffle {
614 self.blosc_config.shuffle = params.value.as_i32().max(0) as u32;
615 }
616
617 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 fn make_u8_array(width: usize, height: usize) -> NDArray {
626 let mut arr = NDArray::new(
627 vec![NDDimension::new(width), NDDimension::new(height)],
628 NDDataType::UInt8,
629 );
630 if let NDDataBuffer::U8(ref mut v) = arr.data {
631 for i in 0..v.len() {
632 v[i] = (i % 256) as u8;
633 }
634 }
635 arr
636 }
637
638 fn make_rgb_array(width: usize, height: usize) -> NDArray {
639 let mut arr = NDArray::new(
640 vec![
641 NDDimension::new(3),
642 NDDimension::new(width),
643 NDDimension::new(height),
644 ],
645 NDDataType::UInt8,
646 );
647 if let NDDataBuffer::U8(ref mut v) = arr.data {
648 for i in 0..v.len() {
649 v[i] = (i % 256) as u8;
650 }
651 }
652 arr
653 }
654
655 #[test]
658 fn test_lz4_roundtrip_u8() {
659 let arr = make_u8_array(4, 4);
660 let original_data = arr.data.as_u8_slice().to_vec();
661
662 let compressed = compress_lz4(&arr);
663 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
664 assert_ne!(compressed.data.as_u8_slice(), original_data.as_slice());
666
667 let decompressed = decompress_lz4(&compressed).unwrap();
668 assert!(decompressed.codec.is_none());
669 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
670 assert_eq!(decompressed.data.as_u8_slice(), original_data.as_slice());
671 }
672
673 #[test]
674 fn test_lz4_roundtrip_u16() {
675 let mut arr = NDArray::new(
676 vec![NDDimension::new(8), NDDimension::new(8)],
677 NDDataType::UInt16,
678 );
679 if let NDDataBuffer::U16(ref mut v) = arr.data {
680 for i in 0..v.len() {
681 v[i] = (i * 100) as u16;
682 }
683 }
684 let original_bytes = arr.data.as_u8_slice().to_vec();
685
686 let compressed = compress_lz4(&arr);
687 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
688 let dt_attr = compressed.attributes.get(ATTR_ORIGINAL_DATA_TYPE).unwrap();
690 assert_eq!(dt_attr.value, NDAttrValue::UInt8(NDDataType::UInt16 as u8));
691
692 let decompressed = decompress_lz4(&compressed).unwrap();
693 assert!(decompressed.codec.is_none());
694 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
695 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
696 assert!(
698 decompressed
699 .attributes
700 .get(ATTR_ORIGINAL_DATA_TYPE)
701 .is_none()
702 );
703 }
704
705 #[test]
706 fn test_lz4_roundtrip_f64() {
707 let mut arr = NDArray::new(vec![NDDimension::new(16)], NDDataType::Float64);
708 if let NDDataBuffer::F64(ref mut v) = arr.data {
709 for i in 0..v.len() {
710 v[i] = i as f64 * 1.5;
711 }
712 }
713 let original_bytes = arr.data.as_u8_slice().to_vec();
714
715 let compressed = compress_lz4(&arr);
716 let decompressed = decompress_lz4(&compressed).unwrap();
717 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
718 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
719 }
720
721 #[test]
722 fn test_lz4_compresses_repetitive_data() {
723 let mut arr = NDArray::new(
725 vec![NDDimension::new(256), NDDimension::new(256)],
726 NDDataType::UInt8,
727 );
728 if let NDDataBuffer::U8(ref mut v) = arr.data {
730 for x in v.iter_mut() {
731 *x = 0;
732 }
733 }
734 let original_size = arr.data.as_u8_slice().len();
735
736 let compressed = compress_lz4(&arr);
737 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
738 assert!(
739 compressed_size < original_size,
740 "compressed ({}) should be smaller than original ({})",
741 compressed_size,
742 original_size,
743 );
744 }
745
746 #[test]
747 fn test_lz4_preserves_metadata() {
748 let mut arr = make_u8_array(4, 4);
749 arr.unique_id = 42;
750
751 let compressed = compress_lz4(&arr);
752 assert_eq!(compressed.unique_id, 42);
753 assert_eq!(compressed.dims.len(), 2);
754 assert_eq!(compressed.dims[0].size, 4);
755 assert_eq!(compressed.dims[1].size, 4);
756 }
757
758 #[test]
761 fn test_jpeg_compress_mono() {
762 let arr = make_u8_array(16, 16);
763 let compressed = compress_jpeg(&arr, 90).unwrap();
764 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
765 let data = compressed.data.as_u8_slice();
767 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
768 }
769
770 #[test]
771 fn test_jpeg_compress_rgb() {
772 let arr = make_rgb_array(16, 16);
773 let compressed = compress_jpeg(&arr, 90).unwrap();
774 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
775 let data = compressed.data.as_u8_slice();
776 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
777 }
778
779 #[test]
780 fn test_jpeg_roundtrip_mono() {
781 let arr = make_u8_array(16, 16);
782 let compressed = compress_jpeg(&arr, 100).unwrap();
783 let decompressed = decompress_jpeg(&compressed).unwrap();
784 assert!(decompressed.codec.is_none());
785 assert_eq!(decompressed.dims.len(), 2);
786 assert_eq!(decompressed.dims[0].size, 16); assert_eq!(decompressed.dims[1].size, 16); assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
789 assert_eq!(decompressed.data.len(), 16 * 16);
791 }
792
793 #[test]
794 fn test_jpeg_roundtrip_rgb() {
795 let arr = make_rgb_array(16, 16);
796 let compressed = compress_jpeg(&arr, 100).unwrap();
797 let decompressed = decompress_jpeg(&compressed).unwrap();
798 assert!(decompressed.codec.is_none());
799 assert_eq!(decompressed.dims.len(), 3);
800 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);
804 }
805
806 #[test]
807 fn test_jpeg_rejects_non_u8() {
808 let arr = NDArray::new(
809 vec![NDDimension::new(8), NDDimension::new(8)],
810 NDDataType::UInt16,
811 );
812 assert!(compress_jpeg(&arr, 90).is_none());
813 }
814
815 #[test]
816 fn test_jpeg_rejects_1d() {
817 let arr = NDArray::new(vec![NDDimension::new(64)], NDDataType::UInt8);
818 assert!(compress_jpeg(&arr, 90).is_none());
819 }
820
821 #[test]
822 fn test_jpeg_quality_affects_size() {
823 let arr = make_u8_array(64, 64);
824 let high = compress_jpeg(&arr, 95).unwrap();
825 let low = compress_jpeg(&arr, 10).unwrap();
826 let high_size = high.codec.as_ref().unwrap().compressed_size;
827 let low_size = low.codec.as_ref().unwrap().compressed_size;
828 assert!(
829 high_size > low_size,
830 "high quality ({}) should produce larger output than low quality ({})",
831 high_size,
832 low_size,
833 );
834 }
835
836 #[test]
839 fn test_decompress_wrong_codec() {
840 let arr = make_u8_array(4, 4);
841 assert!(decompress_lz4(&arr).is_none());
842 assert!(decompress_jpeg(&arr).is_none());
843 }
844
845 #[test]
848 fn test_processor_lz4_compress() {
849 let pool = NDArrayPool::new(1_000_000);
850 let mut proc = CodecProcessor::new(CodecMode::Compress {
851 codec: CodecName::LZ4,
852 quality: 0,
853 });
854 let arr = make_u8_array(32, 32);
855 let result = proc.process_array(&arr, &pool);
856 assert_eq!(result.output_arrays.len(), 1);
857 assert_eq!(
858 result.output_arrays[0].codec.as_ref().unwrap().name,
859 CodecName::LZ4
860 );
861 assert!(proc.compression_ratio() >= 1.0);
862 }
863
864 #[test]
865 fn test_processor_jpeg_compress() {
866 let pool = NDArrayPool::new(1_000_000);
867 let mut proc = CodecProcessor::new(CodecMode::Compress {
868 codec: CodecName::JPEG,
869 quality: 80,
870 });
871 let arr = make_u8_array(16, 16);
872 let result = proc.process_array(&arr, &pool);
873 assert_eq!(result.output_arrays.len(), 1);
874 assert_eq!(
875 result.output_arrays[0].codec.as_ref().unwrap().name,
876 CodecName::JPEG
877 );
878 }
879
880 #[test]
881 fn test_processor_decompress_auto_lz4() {
882 let pool = NDArrayPool::new(1_000_000);
883 let arr = make_u8_array(16, 16);
884 let compressed = compress_lz4(&arr);
885
886 let mut proc = CodecProcessor::new(CodecMode::Decompress);
887 let result = proc.process_array(&compressed, &pool);
888 assert_eq!(result.output_arrays.len(), 1);
889 assert!(result.output_arrays[0].codec.is_none());
890 assert_eq!(
891 result.output_arrays[0].data.as_u8_slice(),
892 arr.data.as_u8_slice()
893 );
894 assert!(proc.compression_ratio() > 0.0);
895 }
896
897 #[test]
898 fn test_processor_decompress_auto_jpeg() {
899 let pool = NDArrayPool::new(1_000_000);
900 let arr = make_u8_array(16, 16);
901 let compressed = compress_jpeg(&arr, 90).unwrap();
902
903 let mut proc = CodecProcessor::new(CodecMode::Decompress);
904 let result = proc.process_array(&compressed, &pool);
905 assert_eq!(result.output_arrays.len(), 1);
906 assert!(result.output_arrays[0].codec.is_none());
907 }
908
909 #[test]
910 fn test_processor_decompress_no_codec() {
911 let pool = NDArrayPool::new(1_000_000);
912 let arr = make_u8_array(8, 8);
913 let mut proc = CodecProcessor::new(CodecMode::Decompress);
914 let result = proc.process_array(&arr, &pool);
915 assert!(result.output_arrays.is_empty());
916 assert_eq!(proc.compression_ratio(), 1.0);
917 }
918
919 #[test]
920 fn test_processor_compression_ratio() {
921 let pool = NDArrayPool::new(1_000_000);
922 let mut arr = NDArray::new(
924 vec![NDDimension::new(128), NDDimension::new(128)],
925 NDDataType::UInt8,
926 );
927 if let NDDataBuffer::U8(ref mut v) = arr.data {
928 for x in v.iter_mut() {
929 *x = 0;
930 }
931 }
932
933 let mut proc = CodecProcessor::new(CodecMode::Compress {
934 codec: CodecName::LZ4,
935 quality: 0,
936 });
937 let _ = proc.process_array(&arr, &pool);
938 let ratio = proc.compression_ratio();
939 assert!(
940 ratio > 2.0,
941 "all-zeros 128x128 should compress at least 2x, got {}",
942 ratio,
943 );
944 }
945
946 #[test]
947 fn test_processor_plugin_type() {
948 let proc = CodecProcessor::new(CodecMode::Decompress);
949 assert_eq!(proc.plugin_type(), "NDPluginCodec");
950 }
951
952 #[test]
955 fn test_buffer_from_bytes_u8() {
956 let data = vec![1u8, 2, 3, 4];
957 let buf = buffer_from_bytes(&data, NDDataType::UInt8).unwrap();
958 assert_eq!(buf.data_type(), NDDataType::UInt8);
959 assert_eq!(buf.len(), 4);
960 assert_eq!(buf.as_u8_slice(), &[1, 2, 3, 4]);
961 }
962
963 #[test]
964 fn test_buffer_from_bytes_u16() {
965 let original = vec![1000u16, 2000, 3000];
966 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
967 let buf = buffer_from_bytes(&bytes, NDDataType::UInt16).unwrap();
968 assert_eq!(buf.data_type(), NDDataType::UInt16);
969 assert_eq!(buf.len(), 3);
970 if let NDDataBuffer::U16(v) = buf {
971 assert_eq!(v, original);
972 } else {
973 panic!("wrong buffer type");
974 }
975 }
976
977 #[test]
978 fn test_buffer_from_bytes_bad_alignment() {
979 let data = vec![0u8; 3];
981 assert!(buffer_from_bytes(&data, NDDataType::UInt16).is_none());
982 }
983
984 #[test]
985 fn test_buffer_from_bytes_f64_roundtrip() {
986 let original = vec![1.5f64, -2.7, 3.14159];
987 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
988 let buf = buffer_from_bytes(&bytes, NDDataType::Float64).unwrap();
989 if let NDDataBuffer::F64(v) = buf {
990 assert_eq!(v, original);
991 } else {
992 panic!("wrong buffer type");
993 }
994 }
995}