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::block::{compress, decompress};
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(raw);
144 let compressed_size = compressed.len();
145
146 let mut arr = src.clone();
147 arr.data = NDDataBuffer::U8(compressed);
148 arr.codec = Some(Codec {
149 name: CodecName::LZ4,
150 compressed_size,
151 level: 0,
152 shuffle: 0,
153 compressor: 0,
154 });
155
156 arr.attributes.add(NDAttribute {
158 name: ATTR_ORIGINAL_DATA_TYPE.into(),
159 description: "Original NDDataType ordinal before codec compression".into(),
160 source: NDAttrSource::Driver,
161 value: NDAttrValue::UInt8(original_data_type as u8),
162 });
163
164 tracing::debug!(
165 original_size,
166 compressed_size,
167 ratio = original_size as f64 / compressed_size.max(1) as f64,
168 "LZ4 compress"
169 );
170
171 arr
172}
173
174pub fn decompress_lz4(src: &NDArray) -> Option<NDArray> {
179 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::LZ4) {
180 return None;
181 }
182 let compressed = src.data.as_u8_slice();
183 let original_type = src
186 .attributes
187 .get(ATTR_ORIGINAL_DATA_TYPE)
188 .and_then(|a| a.value.as_i64())
189 .and_then(|ord| NDDataType::from_ordinal(ord as u8))
190 .unwrap_or(NDDataType::UInt8);
191 let num_elements: usize = src.dims.iter().map(|d| d.size).product();
192 let uncompressed_size = num_elements * original_type.element_size();
193 let decompressed = decompress(compressed, uncompressed_size).ok()?;
194
195 let buffer = buffer_from_bytes(&decompressed, original_type)?;
196
197 let mut arr = src.clone();
198 arr.data = buffer;
199 arr.codec = None;
200 arr.attributes.remove(ATTR_ORIGINAL_DATA_TYPE);
201
202 Some(arr)
203}
204
205pub fn compress_jpeg(src: &NDArray, quality: u8) -> Option<NDArray> {
213 if src.data.data_type() != NDDataType::UInt8 {
214 return None;
215 }
216
217 let raw = src.data.as_u8_slice();
218 let info = src.info();
219
220 if info.x_size > u16::MAX as usize || info.y_size > u16::MAX as usize {
222 return None;
223 }
224
225 let (width, height, color_type) = match src.dims.len() {
226 2 => {
227 (
229 info.x_size as u16,
230 info.y_size as u16,
231 jpeg_encoder::ColorType::Luma,
232 )
233 }
234 3 if src.dims[0].size == 3 => {
235 (
237 info.x_size as u16,
238 info.y_size as u16,
239 jpeg_encoder::ColorType::Rgb,
240 )
241 }
242 _ => return None,
243 };
244
245 let mut jpeg_buf = Vec::new();
246 let encoder = jpeg_encoder::Encoder::new(&mut jpeg_buf, quality);
247 if encoder.encode(raw, width, height, color_type).is_err() {
248 return None;
249 }
250
251 let compressed_size = jpeg_buf.len();
252 let original_size = raw.len();
253
254 let mut arr = src.clone();
255 arr.data = NDDataBuffer::U8(jpeg_buf);
256 arr.codec = Some(Codec {
257 name: CodecName::JPEG,
258 compressed_size,
259 level: 0,
260 shuffle: 0,
261 compressor: 0,
262 });
263
264 tracing::debug!(
265 original_size,
266 compressed_size,
267 ratio = original_size as f64 / compressed_size.max(1) as f64,
268 "JPEG compress (quality={})",
269 quality,
270 );
271
272 Some(arr)
273}
274
275pub fn decompress_jpeg(src: &NDArray) -> Option<NDArray> {
282 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::JPEG) {
283 return None;
284 }
285
286 let compressed = src.data.as_u8_slice();
287 let mut decoder = jpeg_decoder::Decoder::new(compressed);
288 let pixels = decoder.decode().ok()?;
289 let metadata = decoder.info()?;
290
291 let width = metadata.width as usize;
292 let height = metadata.height as usize;
293
294 let dims = match metadata.pixel_format {
295 jpeg_decoder::PixelFormat::L8 => {
296 vec![NDDimension::new(width), NDDimension::new(height)]
298 }
299 jpeg_decoder::PixelFormat::RGB24 => {
300 vec![
302 NDDimension::new(3),
303 NDDimension::new(width),
304 NDDimension::new(height),
305 ]
306 }
307 _ => return None,
308 };
309
310 let mut arr = src.clone();
311 arr.dims = dims;
312 arr.data = NDDataBuffer::U8(pixels);
313 arr.codec = None;
314
315 Some(arr)
316}
317
318#[derive(Debug, Clone, Copy)]
320pub struct BloscConfig {
321 pub compressor: u32,
323 pub clevel: u32,
325 pub shuffle: u32,
327}
328
329impl Default for BloscConfig {
330 fn default() -> Self {
331 Self {
332 compressor: 0,
333 clevel: 3,
334 shuffle: 0,
335 }
336 }
337}
338
339pub fn compress_blosc(src: &NDArray, config: &BloscConfig) -> NDArray {
341 let raw = src.data.as_u8_slice();
342 let element_size = src.data.data_type().element_size();
343
344 let pipeline = FilterPipeline {
345 filters: vec![Filter {
346 id: FILTER_BLOSC,
347 flags: 0,
348 cd_values: vec![
349 2, 2, element_size as u32, raw.len() as u32, config.shuffle, config.compressor, config.clevel, ],
357 }],
358 };
359
360 let compressed = match apply_filters(&pipeline, raw) {
361 Ok(data) => data,
362 Err(_) => return src.clone(),
363 };
364
365 let compressed_size = compressed.len();
366 let mut arr = src.clone();
367 arr.attributes.add(NDAttribute {
368 name: ATTR_ORIGINAL_DATA_TYPE.to_string(),
369 description: String::new(),
370 source: NDAttrSource::Driver,
371 value: NDAttrValue::Int64(src.data.data_type() as u8 as i64),
372 });
373 arr.data = NDDataBuffer::U8(compressed);
374 arr.codec = Some(Codec {
375 name: CodecName::Blosc,
376 compressed_size,
377 level: 0,
378 shuffle: 0,
379 compressor: 0,
380 });
381 arr
382}
383
384pub fn decompress_blosc(src: &NDArray) -> Option<NDArray> {
386 if src.codec.as_ref().map(|c| c.name) != Some(CodecName::Blosc) {
387 return None;
388 }
389
390 let compressed = src.data.as_u8_slice();
391
392 let pipeline = FilterPipeline {
394 filters: vec![Filter {
395 id: FILTER_BLOSC,
396 flags: 0,
397 cd_values: vec![],
398 }],
399 };
400
401 let decompressed = reverse_filters(&pipeline, compressed).ok()?;
402
403 let original_type = src
404 .attributes
405 .get(ATTR_ORIGINAL_DATA_TYPE)
406 .and_then(|a| a.value.as_i64())
407 .and_then(|ord| NDDataType::from_ordinal(ord as u8))
408 .unwrap_or(NDDataType::UInt8);
409
410 let buffer = buffer_from_bytes(&decompressed, original_type)?;
411
412 let mut arr = src.clone();
413 arr.data = buffer;
414 arr.codec = None;
415 arr.attributes.remove(ATTR_ORIGINAL_DATA_TYPE);
416 Some(arr)
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum CodecMode {
422 Compress { codec: CodecName, quality: u8 },
424 Decompress,
426}
427
428#[derive(Default)]
432struct CodecParamIndices {
433 mode: Option<usize>,
434 compressor: Option<usize>,
435 comp_factor: Option<usize>,
436 jpeg_quality: Option<usize>,
437 blosc_compressor: Option<usize>,
438 blosc_clevel: Option<usize>,
439 blosc_shuffle: Option<usize>,
440 blosc_numthreads: Option<usize>,
441 codec_status: Option<usize>,
442 codec_error: Option<usize>,
443}
444
445pub struct CodecProcessor {
446 mode: CodecMode,
447 compression_ratio: f64,
448 jpeg_quality: u8,
449 blosc_config: BloscConfig,
450 params: CodecParamIndices,
451}
452
453impl CodecProcessor {
454 pub fn new(mode: CodecMode) -> Self {
455 let quality = match mode {
456 CodecMode::Compress { quality, .. } => quality,
457 _ => 85,
458 };
459 Self {
460 mode,
461 compression_ratio: 1.0,
462 jpeg_quality: quality,
463 blosc_config: BloscConfig::default(),
464 params: CodecParamIndices::default(),
465 }
466 }
467
468 pub fn compression_ratio(&self) -> f64 {
471 self.compression_ratio
472 }
473}
474
475impl NDPluginProcess for CodecProcessor {
476 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
477 let original_bytes = array.data.as_u8_slice().len();
478
479 let result = match self.mode {
480 CodecMode::Compress { .. } if array.codec.is_some() => {
481 Some(array.clone())
483 }
484 CodecMode::Compress {
485 codec: CodecName::LZ4,
486 ..
487 } => Some(compress_lz4(array)),
488 CodecMode::Compress {
489 codec: CodecName::JPEG,
490 ..
491 } => compress_jpeg(array, self.jpeg_quality),
492 CodecMode::Compress {
493 codec: CodecName::Blosc,
494 ..
495 } => Some(compress_blosc(array, &self.blosc_config)),
496 CodecMode::Compress { .. } => None,
497 CodecMode::Decompress => match array.codec.as_ref().map(|c| c.name) {
498 Some(CodecName::LZ4) => decompress_lz4(array),
499 Some(CodecName::JPEG) => decompress_jpeg(array),
500 Some(CodecName::Blosc) => decompress_blosc(array),
501 _ => None,
502 },
503 };
504
505 let mut updates = Vec::new();
506
507 match result {
508 Some(ref out) => {
509 let output_bytes = out.data.as_u8_slice().len();
510 match self.mode {
511 CodecMode::Compress { .. } => {
512 self.compression_ratio = original_bytes as f64 / output_bytes.max(1) as f64;
513 }
514 CodecMode::Decompress => {
515 self.compression_ratio = output_bytes as f64 / original_bytes.max(1) as f64;
516 }
517 }
518 if let Some(idx) = self.params.comp_factor {
519 updates.push(ParamUpdate::float64(idx, self.compression_ratio));
520 }
521 if let Some(idx) = self.params.codec_status {
522 updates.push(ParamUpdate::int32(idx, 0)); }
524 if let Some(idx) = self.params.codec_error {
525 updates.push(ParamUpdate::Octet {
526 reason: idx,
527 addr: 0,
528 value: String::new(),
529 });
530 }
531 let mut r = ProcessResult::arrays(vec![Arc::new(out.clone())]);
532 r.param_updates = updates;
533 r
534 }
535 None => {
536 self.compression_ratio = 1.0;
538 if let Some(idx) = self.params.comp_factor {
539 updates.push(ParamUpdate::float64(idx, 1.0));
540 }
541 if let Some(idx) = self.params.codec_status {
542 updates.push(ParamUpdate::int32(idx, 1)); }
544 if let Some(idx) = self.params.codec_error {
545 updates.push(ParamUpdate::Octet {
546 reason: idx,
547 addr: 0,
548 value: "codec operation failed or unsupported".to_string(),
549 });
550 }
551 let mut r = ProcessResult::arrays(vec![Arc::new(array.clone())]);
552 r.param_updates = updates;
553 r
554 }
555 }
556 }
557
558 fn plugin_type(&self) -> &str {
559 "NDPluginCodec"
560 }
561
562 fn register_params(
563 &mut self,
564 base: &mut asyn_rs::port::PortDriverBase,
565 ) -> asyn_rs::error::AsynResult<()> {
566 use asyn_rs::param::ParamType;
567 base.create_param("MODE", ParamType::Int32)?;
568 base.create_param("COMPRESSOR", ParamType::Int32)?;
569 base.create_param("COMP_FACTOR", ParamType::Float64)?;
570 base.create_param("JPEG_QUALITY", ParamType::Int32)?;
571 base.create_param("BLOSC_COMPRESSOR", ParamType::Int32)?;
572 base.create_param("BLOSC_CLEVEL", ParamType::Int32)?;
573 base.create_param("BLOSC_SHUFFLE", ParamType::Int32)?;
574 base.create_param("BLOSC_NUMTHREADS", ParamType::Int32)?;
575 base.create_param("CODEC_STATUS", ParamType::Int32)?;
576 base.create_param("CODEC_ERROR", ParamType::Octet)?;
577
578 self.params.mode = base.find_param("MODE");
579 self.params.compressor = base.find_param("COMPRESSOR");
580 self.params.comp_factor = base.find_param("COMP_FACTOR");
581 self.params.jpeg_quality = base.find_param("JPEG_QUALITY");
582 self.params.blosc_compressor = base.find_param("BLOSC_COMPRESSOR");
583 self.params.blosc_clevel = base.find_param("BLOSC_CLEVEL");
584 self.params.blosc_shuffle = base.find_param("BLOSC_SHUFFLE");
585 self.params.blosc_numthreads = base.find_param("BLOSC_NUMTHREADS");
586 self.params.codec_status = base.find_param("CODEC_STATUS");
587 self.params.codec_error = base.find_param("CODEC_ERROR");
588 Ok(())
589 }
590
591 fn on_param_change(
592 &mut self,
593 reason: usize,
594 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
595 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
596 if Some(reason) == self.params.mode {
597 let v = params.value.as_i32();
598 if v == 0 {
599 let codec = match self.mode {
601 CodecMode::Compress { codec, .. } => codec,
602 _ => CodecName::LZ4,
603 };
604 self.mode = CodecMode::Compress {
605 codec,
606 quality: self.jpeg_quality,
607 };
608 } else {
609 self.mode = CodecMode::Decompress;
610 }
611 } else if Some(reason) == self.params.compressor {
612 let codec = match params.value.as_i32() {
614 1 => CodecName::JPEG,
615 2 => CodecName::Blosc,
616 3 => CodecName::LZ4,
617 _ => CodecName::LZ4,
618 };
619 if let CodecMode::Compress { .. } = self.mode {
620 self.mode = CodecMode::Compress {
621 codec,
622 quality: self.jpeg_quality,
623 };
624 }
625 } else if Some(reason) == self.params.jpeg_quality {
626 self.jpeg_quality = params.value.as_i32().clamp(1, 100) as u8;
627 if let CodecMode::Compress { codec, .. } = self.mode {
628 self.mode = CodecMode::Compress {
629 codec,
630 quality: self.jpeg_quality,
631 };
632 }
633 } else if Some(reason) == self.params.blosc_compressor {
634 self.blosc_config.compressor = params.value.as_i32().max(0) as u32;
635 } else if Some(reason) == self.params.blosc_clevel {
636 self.blosc_config.clevel = params.value.as_i32().clamp(0, 9) as u32;
637 } else if Some(reason) == self.params.blosc_shuffle {
638 self.blosc_config.shuffle = params.value.as_i32().max(0) as u32;
639 }
640
641 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 fn make_u8_array(width: usize, height: usize) -> NDArray {
650 let mut arr = NDArray::new(
651 vec![NDDimension::new(width), NDDimension::new(height)],
652 NDDataType::UInt8,
653 );
654 if let NDDataBuffer::U8(ref mut v) = arr.data {
655 for i in 0..v.len() {
656 v[i] = (i % 256) as u8;
657 }
658 }
659 arr
660 }
661
662 fn make_rgb_array(width: usize, height: usize) -> NDArray {
663 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
664 let mut arr = NDArray::new(
665 vec![
666 NDDimension::new(3),
667 NDDimension::new(width),
668 NDDimension::new(height),
669 ],
670 NDDataType::UInt8,
671 );
672 arr.attributes.add(NDAttribute {
674 name: "ColorMode".into(),
675 description: "Color Mode".into(),
676 source: NDAttrSource::Driver,
677 value: NDAttrValue::Int32(2), });
679 if let NDDataBuffer::U8(ref mut v) = arr.data {
680 for i in 0..v.len() {
681 v[i] = (i % 256) as u8;
682 }
683 }
684 arr
685 }
686
687 #[test]
690 fn test_lz4_roundtrip_u8() {
691 let arr = make_u8_array(4, 4);
692 let original_data = arr.data.as_u8_slice().to_vec();
693
694 let compressed = compress_lz4(&arr);
695 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
696 assert_ne!(compressed.data.as_u8_slice(), original_data.as_slice());
698
699 let decompressed = decompress_lz4(&compressed).unwrap();
700 assert!(decompressed.codec.is_none());
701 assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
702 assert_eq!(decompressed.data.as_u8_slice(), original_data.as_slice());
703 }
704
705 #[test]
706 fn test_lz4_roundtrip_u16() {
707 let mut arr = NDArray::new(
708 vec![NDDimension::new(8), NDDimension::new(8)],
709 NDDataType::UInt16,
710 );
711 if let NDDataBuffer::U16(ref mut v) = arr.data {
712 for i in 0..v.len() {
713 v[i] = (i * 100) as u16;
714 }
715 }
716 let original_bytes = arr.data.as_u8_slice().to_vec();
717
718 let compressed = compress_lz4(&arr);
719 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::LZ4);
720 let dt_attr = compressed.attributes.get(ATTR_ORIGINAL_DATA_TYPE).unwrap();
722 assert_eq!(dt_attr.value, NDAttrValue::UInt8(NDDataType::UInt16 as u8));
723
724 let decompressed = decompress_lz4(&compressed).unwrap();
725 assert!(decompressed.codec.is_none());
726 assert_eq!(decompressed.data.data_type(), NDDataType::UInt16);
727 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
728 assert!(
730 decompressed
731 .attributes
732 .get(ATTR_ORIGINAL_DATA_TYPE)
733 .is_none()
734 );
735 }
736
737 #[test]
738 fn test_lz4_roundtrip_f64() {
739 let mut arr = NDArray::new(vec![NDDimension::new(16)], NDDataType::Float64);
740 if let NDDataBuffer::F64(ref mut v) = arr.data {
741 for i in 0..v.len() {
742 v[i] = i as f64 * 1.5;
743 }
744 }
745 let original_bytes = arr.data.as_u8_slice().to_vec();
746
747 let compressed = compress_lz4(&arr);
748 let decompressed = decompress_lz4(&compressed).unwrap();
749 assert_eq!(decompressed.data.data_type(), NDDataType::Float64);
750 assert_eq!(decompressed.data.as_u8_slice(), original_bytes.as_slice());
751 }
752
753 #[test]
754 fn test_lz4_compresses_repetitive_data() {
755 let mut arr = NDArray::new(
757 vec![NDDimension::new(256), NDDimension::new(256)],
758 NDDataType::UInt8,
759 );
760 if let NDDataBuffer::U8(ref mut v) = arr.data {
762 for x in v.iter_mut() {
763 *x = 0;
764 }
765 }
766 let original_size = arr.data.as_u8_slice().len();
767
768 let compressed = compress_lz4(&arr);
769 let compressed_size = compressed.codec.as_ref().unwrap().compressed_size;
770 assert!(
771 compressed_size < original_size,
772 "compressed ({}) should be smaller than original ({})",
773 compressed_size,
774 original_size,
775 );
776 }
777
778 #[test]
779 fn test_lz4_preserves_metadata() {
780 let mut arr = make_u8_array(4, 4);
781 arr.unique_id = 42;
782
783 let compressed = compress_lz4(&arr);
784 assert_eq!(compressed.unique_id, 42);
785 assert_eq!(compressed.dims.len(), 2);
786 assert_eq!(compressed.dims[0].size, 4);
787 assert_eq!(compressed.dims[1].size, 4);
788 }
789
790 #[test]
793 fn test_jpeg_compress_mono() {
794 let arr = make_u8_array(16, 16);
795 let compressed = compress_jpeg(&arr, 90).unwrap();
796 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
797 let data = compressed.data.as_u8_slice();
799 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
800 }
801
802 #[test]
803 fn test_jpeg_compress_rgb() {
804 let arr = make_rgb_array(16, 16);
805 let compressed = compress_jpeg(&arr, 90).unwrap();
806 assert_eq!(compressed.codec.as_ref().unwrap().name, CodecName::JPEG);
807 let data = compressed.data.as_u8_slice();
808 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
809 }
810
811 #[test]
812 fn test_jpeg_roundtrip_mono() {
813 let arr = make_u8_array(16, 16);
814 let compressed = compress_jpeg(&arr, 100).unwrap();
815 let decompressed = decompress_jpeg(&compressed).unwrap();
816 assert!(decompressed.codec.is_none());
817 assert_eq!(decompressed.dims.len(), 2);
818 assert_eq!(decompressed.dims[0].size, 16); assert_eq!(decompressed.dims[1].size, 16); assert_eq!(decompressed.data.data_type(), NDDataType::UInt8);
821 assert_eq!(decompressed.data.len(), 16 * 16);
823 }
824
825 #[test]
826 fn test_jpeg_roundtrip_rgb() {
827 let arr = make_rgb_array(16, 16);
828 let compressed = compress_jpeg(&arr, 100).unwrap();
829 let decompressed = decompress_jpeg(&compressed).unwrap();
830 assert!(decompressed.codec.is_none());
831 assert_eq!(decompressed.dims.len(), 3);
832 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);
836 }
837
838 #[test]
839 fn test_jpeg_rejects_non_u8() {
840 let arr = NDArray::new(
841 vec![NDDimension::new(8), NDDimension::new(8)],
842 NDDataType::UInt16,
843 );
844 assert!(compress_jpeg(&arr, 90).is_none());
845 }
846
847 #[test]
848 fn test_jpeg_rejects_1d() {
849 let arr = NDArray::new(vec![NDDimension::new(64)], NDDataType::UInt8);
850 assert!(compress_jpeg(&arr, 90).is_none());
851 }
852
853 #[test]
854 fn test_jpeg_quality_affects_size() {
855 let arr = make_u8_array(64, 64);
856 let high = compress_jpeg(&arr, 95).unwrap();
857 let low = compress_jpeg(&arr, 10).unwrap();
858 let high_size = high.codec.as_ref().unwrap().compressed_size;
859 let low_size = low.codec.as_ref().unwrap().compressed_size;
860 assert!(
861 high_size > low_size,
862 "high quality ({}) should produce larger output than low quality ({})",
863 high_size,
864 low_size,
865 );
866 }
867
868 #[test]
871 fn test_decompress_wrong_codec() {
872 let arr = make_u8_array(4, 4);
873 assert!(decompress_lz4(&arr).is_none());
874 assert!(decompress_jpeg(&arr).is_none());
875 }
876
877 #[test]
880 fn test_processor_lz4_compress() {
881 let pool = NDArrayPool::new(1_000_000);
882 let mut proc = CodecProcessor::new(CodecMode::Compress {
883 codec: CodecName::LZ4,
884 quality: 0,
885 });
886 let arr = make_u8_array(32, 32);
887 let result = proc.process_array(&arr, &pool);
888 assert_eq!(result.output_arrays.len(), 1);
889 assert_eq!(
890 result.output_arrays[0].codec.as_ref().unwrap().name,
891 CodecName::LZ4
892 );
893 assert!(proc.compression_ratio() >= 1.0);
894 }
895
896 #[test]
897 fn test_processor_jpeg_compress() {
898 let pool = NDArrayPool::new(1_000_000);
899 let mut proc = CodecProcessor::new(CodecMode::Compress {
900 codec: CodecName::JPEG,
901 quality: 80,
902 });
903 let arr = make_u8_array(16, 16);
904 let result = proc.process_array(&arr, &pool);
905 assert_eq!(result.output_arrays.len(), 1);
906 assert_eq!(
907 result.output_arrays[0].codec.as_ref().unwrap().name,
908 CodecName::JPEG
909 );
910 }
911
912 #[test]
913 fn test_processor_decompress_auto_lz4() {
914 let pool = NDArrayPool::new(1_000_000);
915 let arr = make_u8_array(16, 16);
916 let compressed = compress_lz4(&arr);
917
918 let mut proc = CodecProcessor::new(CodecMode::Decompress);
919 let result = proc.process_array(&compressed, &pool);
920 assert_eq!(result.output_arrays.len(), 1);
921 assert!(result.output_arrays[0].codec.is_none());
922 assert_eq!(
923 result.output_arrays[0].data.as_u8_slice(),
924 arr.data.as_u8_slice()
925 );
926 assert!(proc.compression_ratio() > 0.0);
927 }
928
929 #[test]
930 fn test_processor_decompress_auto_jpeg() {
931 let pool = NDArrayPool::new(1_000_000);
932 let arr = make_u8_array(16, 16);
933 let compressed = compress_jpeg(&arr, 90).unwrap();
934
935 let mut proc = CodecProcessor::new(CodecMode::Decompress);
936 let result = proc.process_array(&compressed, &pool);
937 assert_eq!(result.output_arrays.len(), 1);
938 assert!(result.output_arrays[0].codec.is_none());
939 }
940
941 #[test]
942 fn test_processor_decompress_no_codec() {
943 let pool = NDArrayPool::new(1_000_000);
944 let arr = make_u8_array(8, 8);
945 let mut proc = CodecProcessor::new(CodecMode::Decompress);
946 let result = proc.process_array(&arr, &pool);
947 assert_eq!(result.output_arrays.len(), 1);
949 assert_eq!(proc.compression_ratio(), 1.0);
950 }
951
952 #[test]
953 fn test_processor_compression_ratio() {
954 let pool = NDArrayPool::new(1_000_000);
955 let mut arr = NDArray::new(
957 vec![NDDimension::new(128), NDDimension::new(128)],
958 NDDataType::UInt8,
959 );
960 if let NDDataBuffer::U8(ref mut v) = arr.data {
961 for x in v.iter_mut() {
962 *x = 0;
963 }
964 }
965
966 let mut proc = CodecProcessor::new(CodecMode::Compress {
967 codec: CodecName::LZ4,
968 quality: 0,
969 });
970 let _ = proc.process_array(&arr, &pool);
971 let ratio = proc.compression_ratio();
972 assert!(
973 ratio > 2.0,
974 "all-zeros 128x128 should compress at least 2x, got {}",
975 ratio,
976 );
977 }
978
979 #[test]
980 fn test_processor_plugin_type() {
981 let proc = CodecProcessor::new(CodecMode::Decompress);
982 assert_eq!(proc.plugin_type(), "NDPluginCodec");
983 }
984
985 #[test]
988 fn test_buffer_from_bytes_u8() {
989 let data = vec![1u8, 2, 3, 4];
990 let buf = buffer_from_bytes(&data, NDDataType::UInt8).unwrap();
991 assert_eq!(buf.data_type(), NDDataType::UInt8);
992 assert_eq!(buf.len(), 4);
993 assert_eq!(buf.as_u8_slice(), &[1, 2, 3, 4]);
994 }
995
996 #[test]
997 fn test_buffer_from_bytes_u16() {
998 let original = vec![1000u16, 2000, 3000];
999 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
1000 let buf = buffer_from_bytes(&bytes, NDDataType::UInt16).unwrap();
1001 assert_eq!(buf.data_type(), NDDataType::UInt16);
1002 assert_eq!(buf.len(), 3);
1003 if let NDDataBuffer::U16(v) = buf {
1004 assert_eq!(v, original);
1005 } else {
1006 panic!("wrong buffer type");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_buffer_from_bytes_bad_alignment() {
1012 let data = vec![0u8; 3];
1014 assert!(buffer_from_bytes(&data, NDDataType::UInt16).is_none());
1015 }
1016
1017 #[test]
1018 fn test_buffer_from_bytes_f64_roundtrip() {
1019 let original = vec![1.5f64, -2.7, 3.14159];
1020 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_ne_bytes()).collect();
1021 let buf = buffer_from_bytes(&bytes, NDDataType::Float64).unwrap();
1022 if let NDDataBuffer::F64(v) = buf {
1023 assert_eq!(v, original);
1024 } else {
1025 panic!("wrong buffer type");
1026 }
1027 }
1028}