Skip to main content

ad_plugins_rs/
file_magick.rs

1use std::path::{Path, PathBuf};
2
3use ad_core_rs::color::{NDColorMode, convert_rgb_layout};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10    NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
11};
12
13use image::codecs::png::{CompressionType as PngCompression, FilterType as PngFilter};
14use image::{DynamicImage, ImageEncoder, ImageFormat};
15
16/// GraphicsMagick `CompressionType` ordinals as used by C++ NDFileMagick.cpp:20
17/// (`compressionTypes[]`). The `MAGICK_COMPRESS_TYPE` param indexes this list.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum MagickCompression {
20    None = 0,
21    BZip = 1,
22    Fax = 2,
23    Group4 = 3,
24    Jpeg = 4,
25    Lzw = 5,
26    Rle = 6,
27    Zip = 7,
28}
29
30impl MagickCompression {
31    fn from_index(idx: i32) -> Self {
32        match idx {
33            1 => Self::BZip,
34            2 => Self::Fax,
35            3 => Self::Group4,
36            4 => Self::Jpeg,
37            5 => Self::Lzw,
38            6 => Self::Rle,
39            7 => Self::Zip,
40            _ => Self::None,
41        }
42    }
43}
44
45/// NDFileMagick: file writer using the `image` crate.
46///
47/// Format is determined by the file extension (PNG, BMP, GIF, TIFF, etc.).
48/// Supports UInt8 and UInt16 data in mono and RGB color modes.
49pub struct MagickWriter {
50    current_path: Option<PathBuf>,
51    quality: u8,
52    bit_depth: u32,
53    compress_type: MagickCompression,
54}
55
56impl MagickWriter {
57    pub fn new() -> Self {
58        Self {
59            current_path: None,
60            quality: 100,
61            // 0 = keep the native depth of the NDArray data type. GraphicsMagick
62            // `image.depth(0)` is likewise a no-op; an explicit 8/16/32 forces
63            // the output sample depth.
64            bit_depth: 0,
65            compress_type: MagickCompression::None,
66        }
67    }
68
69    pub fn set_quality(&mut self, q: u8) {
70        self.quality = q;
71    }
72
73    pub fn set_bit_depth(&mut self, depth: u32) {
74        self.bit_depth = depth;
75    }
76
77    pub fn set_compress_type(&mut self, idx: i32) {
78        self.compress_type = MagickCompression::from_index(idx);
79    }
80
81    fn color_mode(array: &NDArray) -> NDColorMode {
82        array
83            .attributes
84            .get("ColorMode")
85            .and_then(|attr| attr.value.as_i64())
86            .map(|v| NDColorMode::from_i32(v as i32))
87            .unwrap_or_else(|| match array.dims.as_slice() {
88                [a, _, _] if a.size == 3 => NDColorMode::RGB1,
89                [_, b, _] if b.size == 3 => NDColorMode::RGB2,
90                [_, _, c] if c.size == 3 => NDColorMode::RGB3,
91                _ => NDColorMode::Mono,
92            })
93    }
94
95    /// Convert NDArray to DynamicImage for encoding.
96    ///
97    /// `bit_depth` selects the output sample depth (C++ `image.depth(depth)`):
98    /// `0` keeps the native NDArray depth, `<= 8` produces an 8-bit image,
99    /// anything larger a 16-bit image.
100    fn array_to_image(array: &NDArray, bit_depth: u32) -> ADResult<DynamicImage> {
101        let img = Self::array_to_image_native(array)?;
102        Ok(Self::apply_bit_depth(img, bit_depth))
103    }
104
105    /// Apply the requested output bit depth by converting the DynamicImage.
106    fn apply_bit_depth(img: DynamicImage, bit_depth: u32) -> DynamicImage {
107        if bit_depth == 0 {
108            // Keep native depth.
109            return img;
110        }
111        let is_rgb = matches!(
112            img,
113            DynamicImage::ImageRgb8(_) | DynamicImage::ImageRgb16(_)
114        );
115        if bit_depth <= 8 {
116            if is_rgb {
117                DynamicImage::ImageRgb8(img.to_rgb8())
118            } else {
119                DynamicImage::ImageLuma8(img.to_luma8())
120            }
121        } else {
122            if is_rgb {
123                DynamicImage::ImageRgb16(img.to_rgb16())
124            } else {
125                DynamicImage::ImageLuma16(img.to_luma16())
126            }
127        }
128    }
129
130    /// Convert NDArray to DynamicImage at the native depth of the data type.
131    fn array_to_image_native(array: &NDArray) -> ADResult<DynamicImage> {
132        let info = array.info();
133        let width = info.x_size as u32;
134        let height = info.y_size as u32;
135        let color = Self::color_mode(array);
136        let is_rgb = matches!(
137            color,
138            NDColorMode::RGB1 | NDColorMode::RGB2 | NDColorMode::RGB3
139        );
140
141        // Convert to RGB1 layout if needed (image crate expects interleaved RGB)
142        let src = if is_rgb && color != NDColorMode::RGB1 {
143            &convert_rgb_layout(array, color, NDColorMode::RGB1)?
144        } else {
145            array
146        };
147
148        match &src.data {
149            NDDataBuffer::U8(v) => {
150                if is_rgb {
151                    image::RgbImage::from_raw(width, height, v.clone())
152                        .map(DynamicImage::ImageRgb8)
153                        .ok_or_else(|| {
154                            ADError::UnsupportedConversion("RGB8 buffer size mismatch".into())
155                        })
156                } else {
157                    image::GrayImage::from_raw(width, height, v.clone())
158                        .map(DynamicImage::ImageLuma8)
159                        .ok_or_else(|| {
160                            ADError::UnsupportedConversion("Gray8 buffer size mismatch".into())
161                        })
162                }
163            }
164            NDDataBuffer::I8(v) => {
165                let u8_data: Vec<u8> = v.iter().map(|&b| b as u8).collect();
166                if is_rgb {
167                    image::RgbImage::from_raw(width, height, u8_data)
168                        .map(DynamicImage::ImageRgb8)
169                        .ok_or_else(|| {
170                            ADError::UnsupportedConversion("RGB8 buffer size mismatch".into())
171                        })
172                } else {
173                    image::GrayImage::from_raw(width, height, u8_data)
174                        .map(DynamicImage::ImageLuma8)
175                        .ok_or_else(|| {
176                            ADError::UnsupportedConversion("Gray8 buffer size mismatch".into())
177                        })
178                }
179            }
180            NDDataBuffer::U16(v) => {
181                if is_rgb {
182                    image::ImageBuffer::<image::Rgb<u16>, Vec<u16>>::from_raw(
183                        width,
184                        height,
185                        v.clone(),
186                    )
187                    .map(DynamicImage::ImageRgb16)
188                    .ok_or_else(|| {
189                        ADError::UnsupportedConversion("RGB16 buffer size mismatch".into())
190                    })
191                } else {
192                    image::ImageBuffer::<image::Luma<u16>, Vec<u16>>::from_raw(
193                        width,
194                        height,
195                        v.clone(),
196                    )
197                    .map(DynamicImage::ImageLuma16)
198                    .ok_or_else(|| {
199                        ADError::UnsupportedConversion("Gray16 buffer size mismatch".into())
200                    })
201                }
202            }
203            NDDataBuffer::I16(v) => {
204                let u16_data: Vec<u16> = v.iter().map(|&b| b as u16).collect();
205                if is_rgb {
206                    image::ImageBuffer::<image::Rgb<u16>, Vec<u16>>::from_raw(
207                        width, height, u16_data,
208                    )
209                    .map(DynamicImage::ImageRgb16)
210                    .ok_or_else(|| {
211                        ADError::UnsupportedConversion("RGB16 buffer size mismatch".into())
212                    })
213                } else {
214                    image::ImageBuffer::<image::Luma<u16>, Vec<u16>>::from_raw(
215                        width, height, u16_data,
216                    )
217                    .map(DynamicImage::ImageLuma16)
218                    .ok_or_else(|| {
219                        ADError::UnsupportedConversion("Gray16 buffer size mismatch".into())
220                    })
221                }
222            }
223            NDDataBuffer::F32(v) => {
224                // Scale by the actual data range, not a fixed [0,1] clamp
225                // (C++ NDFileMagick scales by the image's min/max range).
226                let mut min = f32::INFINITY;
227                let mut max = f32::NEG_INFINITY;
228                for &f in v {
229                    if f.is_finite() {
230                        min = min.min(f);
231                        max = max.max(f);
232                    }
233                }
234                let range = if min.is_finite() && max > min {
235                    max - min
236                } else {
237                    1.0
238                };
239                let offset = if min.is_finite() { min } else { 0.0 };
240                let u16_data: Vec<u16> = v
241                    .iter()
242                    .map(|&f| {
243                        let norm = ((f - offset) / range).clamp(0.0, 1.0);
244                        (norm * 65535.0).round() as u16
245                    })
246                    .collect();
247                if is_rgb {
248                    image::ImageBuffer::<image::Rgb<u16>, Vec<u16>>::from_raw(
249                        width, height, u16_data,
250                    )
251                    .map(DynamicImage::ImageRgb16)
252                    .ok_or_else(|| {
253                        ADError::UnsupportedConversion("RGB16 buffer size mismatch".into())
254                    })
255                } else {
256                    image::ImageBuffer::<image::Luma<u16>, Vec<u16>>::from_raw(
257                        width, height, u16_data,
258                    )
259                    .map(DynamicImage::ImageLuma16)
260                    .ok_or_else(|| {
261                        ADError::UnsupportedConversion("Gray16 buffer size mismatch".into())
262                    })
263                }
264            }
265            _ => Err(ADError::UnsupportedConversion(format!(
266                "NDFileMagick: unsupported data type {:?}, use UInt8, Int8, UInt16, Int16, or Float32",
267                src.data.data_type()
268            ))),
269        }
270    }
271}
272
273impl NDFileWriter for MagickWriter {
274    fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
275        self.current_path = Some(path.to_path_buf());
276        Ok(())
277    }
278
279    fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
280        let path = self
281            .current_path
282            .as_ref()
283            .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
284
285        let img = Self::array_to_image(array, self.bit_depth)?;
286
287        // Determine format from extension, default to PNG
288        let format = ImageFormat::from_path(path).unwrap_or(ImageFormat::Png);
289
290        match format {
291            ImageFormat::Jpeg => {
292                // JPEG: use the quality setting.
293                let mut buf = Vec::new();
294                let encoder =
295                    image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, self.quality);
296                img.write_with_encoder(encoder).map_err(|e| {
297                    ADError::UnsupportedConversion(format!("Magick encode error: {e}"))
298                })?;
299                std::fs::write(path, &buf)?;
300            }
301            ImageFormat::Png => {
302                // PNG: map the GraphicsMagick compression type onto the PNG
303                // deflate compression level. Zip/BZip → best, None → uncompressed,
304                // everything else → the encoder default.
305                let compression = match self.compress_type {
306                    MagickCompression::None => PngCompression::Uncompressed,
307                    MagickCompression::Zip | MagickCompression::BZip => PngCompression::Best,
308                    _ => PngCompression::default(),
309                };
310                let mut buf = Vec::new();
311                let encoder = image::codecs::png::PngEncoder::new_with_quality(
312                    &mut buf,
313                    compression,
314                    PngFilter::Adaptive,
315                );
316                let rgb = img.color();
317                encoder
318                    .write_image(img.as_bytes(), img.width(), img.height(), rgb.into())
319                    .map_err(|e| {
320                        ADError::UnsupportedConversion(format!("Magick PNG encode error: {e}"))
321                    })?;
322                std::fs::write(path, &buf)?;
323            }
324            _ => {
325                // Other formats: the `image` crate's high-level save() has no
326                // compression knob; GraphicsMagick's CompressionType does not
327                // map onto these codecs, so the compress-type param has no
328                // effect for them (matches `image` crate capability).
329                img.save(path).map_err(|e| {
330                    ADError::UnsupportedConversion(format!("Magick save error: {e}"))
331                })?;
332            }
333        }
334
335        Ok(())
336    }
337
338    fn read_file(&mut self) -> ADResult<NDArray> {
339        let path = self
340            .current_path
341            .as_ref()
342            .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
343
344        let img = image::open(path)
345            .map_err(|e| ADError::UnsupportedConversion(format!("Magick read error: {e}")))?;
346
347        let width = img.width() as usize;
348        let height = img.height() as usize;
349
350        match img {
351            DynamicImage::ImageLuma8(buf) => {
352                let mut arr = NDArray::new(
353                    vec![NDDimension::new(width), NDDimension::new(height)],
354                    NDDataType::UInt8,
355                );
356                arr.data = NDDataBuffer::U8(buf.into_raw());
357                Ok(arr)
358            }
359            DynamicImage::ImageRgb8(buf) => {
360                let mut arr = NDArray::new(
361                    vec![
362                        NDDimension::new(3),
363                        NDDimension::new(width),
364                        NDDimension::new(height),
365                    ],
366                    NDDataType::UInt8,
367                );
368                arr.data = NDDataBuffer::U8(buf.into_raw());
369                Ok(arr)
370            }
371            DynamicImage::ImageLuma16(buf) => {
372                let mut arr = NDArray::new(
373                    vec![NDDimension::new(width), NDDimension::new(height)],
374                    NDDataType::UInt16,
375                );
376                arr.data = NDDataBuffer::U16(buf.into_raw());
377                Ok(arr)
378            }
379            DynamicImage::ImageRgb16(buf) => {
380                let mut arr = NDArray::new(
381                    vec![
382                        NDDimension::new(3),
383                        NDDimension::new(width),
384                        NDDimension::new(height),
385                    ],
386                    NDDataType::UInt16,
387                );
388                arr.data = NDDataBuffer::U16(buf.into_raw());
389                Ok(arr)
390            }
391            other => {
392                // Convert anything else to RGB8
393                let rgb = other.to_rgb8();
394                let mut arr = NDArray::new(
395                    vec![
396                        NDDimension::new(3),
397                        NDDimension::new(width),
398                        NDDimension::new(height),
399                    ],
400                    NDDataType::UInt8,
401                );
402                arr.data = NDDataBuffer::U8(rgb.into_raw());
403                Ok(arr)
404            }
405        }
406    }
407
408    fn close_file(&mut self) -> ADResult<()> {
409        self.current_path = None;
410        Ok(())
411    }
412
413    fn supports_multiple_arrays(&self) -> bool {
414        false
415    }
416}
417
418/// Magick file processor wrapping FilePluginController<MagickWriter>.
419pub struct MagickFileProcessor {
420    ctrl: FilePluginController<MagickWriter>,
421    quality_idx: Option<usize>,
422    bit_depth_idx: Option<usize>,
423    compress_type_idx: Option<usize>,
424}
425
426impl MagickFileProcessor {
427    pub fn new() -> Self {
428        Self {
429            ctrl: FilePluginController::new(MagickWriter::new()),
430            quality_idx: None,
431            bit_depth_idx: None,
432            compress_type_idx: None,
433        }
434    }
435}
436
437impl NDPluginProcess for MagickFileProcessor {
438    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
439        self.ctrl.process_array(array)
440    }
441
442    fn plugin_type(&self) -> &str {
443        "NDFileMagick"
444    }
445
446    /// C `NDPluginFile.cpp:948` (base of every file writer) sets
447    /// `NDArrayCallbacks = 0`: file plugins write to disk, not downstream.
448    fn does_array_callbacks(&self) -> bool {
449        false
450    }
451
452    fn register_params(
453        &mut self,
454        base: &mut asyn_rs::port::PortDriverBase,
455    ) -> asyn_rs::error::AsynResult<()> {
456        self.ctrl.register_params(base)?;
457        use asyn_rs::param::ParamType;
458        self.quality_idx = Some(base.create_param("MAGICK_QUALITY", ParamType::Int32)?);
459        self.bit_depth_idx = Some(base.create_param("MAGICK_BIT_DEPTH", ParamType::Int32)?);
460        self.compress_type_idx = Some(base.create_param("MAGICK_COMPRESS_TYPE", ParamType::Int32)?);
461        // Set defaults
462        base.set_int32_param(self.quality_idx.unwrap(), 0, 100)?;
463        base.set_int32_param(self.bit_depth_idx.unwrap(), 0, 8)?;
464        base.set_int32_param(self.compress_type_idx.unwrap(), 0, 0)?;
465        Ok(())
466    }
467
468    fn on_param_change(
469        &mut self,
470        reason: usize,
471        params: &PluginParamSnapshot,
472    ) -> ParamChangeResult {
473        if Some(reason) == self.quality_idx {
474            let q = params.value.as_i32().clamp(1, 100) as u8;
475            self.ctrl.writer.set_quality(q);
476            return ParamChangeResult::empty();
477        }
478        if Some(reason) == self.bit_depth_idx {
479            let d = params.value.as_i32() as u32;
480            self.ctrl.writer.set_bit_depth(d);
481            return ParamChangeResult::empty();
482        }
483        if Some(reason) == self.compress_type_idx {
484            self.ctrl.writer.set_compress_type(params.value.as_i32());
485            return ParamChangeResult::empty();
486        }
487        self.ctrl.on_param_change(reason, params)
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use std::sync::atomic::{AtomicU32, Ordering};
495
496    static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
497
498    fn temp_path(ext: &str) -> PathBuf {
499        let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
500        std::env::temp_dir().join(format!("adcore_test_magick_{n}.{ext}"))
501    }
502
503    #[test]
504    fn test_write_read_png_u8() {
505        let path = temp_path("png");
506        let mut writer = MagickWriter::new();
507
508        let mut arr = NDArray::new(
509            vec![NDDimension::new(8), NDDimension::new(8)],
510            NDDataType::UInt8,
511        );
512        if let NDDataBuffer::U8(ref mut v) = arr.data {
513            for i in 0..64 {
514                v[i] = (i * 4) as u8;
515            }
516        }
517
518        writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
519        writer.write_file(&arr).unwrap();
520
521        let read_back = writer.read_file().unwrap();
522        assert_eq!(read_back.data.data_type(), NDDataType::UInt8);
523        if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
524            assert_eq!(orig, read);
525        }
526
527        writer.close_file().unwrap();
528        std::fs::remove_file(&path).ok();
529    }
530
531    #[test]
532    fn test_write_read_png_u16() {
533        let path = temp_path("png");
534        let mut writer = MagickWriter::new();
535
536        let mut arr = NDArray::new(
537            vec![NDDimension::new(8), NDDimension::new(8)],
538            NDDataType::UInt16,
539        );
540        if let NDDataBuffer::U16(ref mut v) = arr.data {
541            for i in 0..64 {
542                v[i] = (i * 1000) as u16;
543            }
544        }
545
546        writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
547        writer.write_file(&arr).unwrap();
548
549        let read_back = writer.read_file().unwrap();
550        assert_eq!(read_back.data.data_type(), NDDataType::UInt16);
551        if let (NDDataBuffer::U16(orig), NDDataBuffer::U16(read)) = (&arr.data, &read_back.data) {
552            assert_eq!(orig, read);
553        }
554
555        writer.close_file().unwrap();
556        std::fs::remove_file(&path).ok();
557    }
558
559    #[test]
560    fn test_write_read_bmp_rgb() {
561        use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
562
563        let path = temp_path("bmp");
564        let mut writer = MagickWriter::new();
565
566        let mut arr = NDArray::new(
567            vec![
568                NDDimension::new(3),
569                NDDimension::new(4),
570                NDDimension::new(4),
571            ],
572            NDDataType::UInt8,
573        );
574        arr.attributes.add(NDAttribute::new_static(
575            "ColorMode",
576            "Color Mode",
577            NDAttrSource::Driver,
578            NDAttrValue::Int32(2), // RGB1
579        ));
580        if let NDDataBuffer::U8(ref mut v) = arr.data {
581            for i in 0..48 {
582                v[i] = (i * 5) as u8;
583            }
584        }
585
586        writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
587        writer.write_file(&arr).unwrap();
588
589        let read_back = writer.read_file().unwrap();
590        assert_eq!(read_back.dims.len(), 3);
591        assert_eq!(read_back.dims[0].size, 3);
592
593        writer.close_file().unwrap();
594        std::fs::remove_file(&path).ok();
595    }
596
597    #[test]
598    fn test_rejects_unsupported_type() {
599        // F32 is now supported (normalized to U16). Use Float64 as unsupported.
600        let arr = NDArray::new(
601            vec![NDDimension::new(4), NDDimension::new(4)],
602            NDDataType::Float64,
603        );
604        assert!(MagickWriter::array_to_image(&arr, 8).is_err());
605    }
606
607    #[test]
608    fn test_bit_depth_controls_output_depth() {
609        // u16 input with bit_depth 8 → 8-bit output image.
610        let mut arr = NDArray::new(
611            vec![NDDimension::new(4), NDDimension::new(4)],
612            NDDataType::UInt16,
613        );
614        if let NDDataBuffer::U16(ref mut v) = arr.data {
615            for (i, x) in v.iter_mut().enumerate() {
616                *x = (i * 4000) as u16;
617            }
618        }
619        let img8 = MagickWriter::array_to_image(&arr, 8).unwrap();
620        assert!(matches!(img8, DynamicImage::ImageLuma8(_)));
621        let img16 = MagickWriter::array_to_image(&arr, 16).unwrap();
622        assert!(matches!(img16, DynamicImage::ImageLuma16(_)));
623    }
624
625    #[test]
626    fn test_f32_scales_by_actual_range() {
627        // Values well outside [0,1] must not all saturate to white.
628        let mut arr = NDArray::new(
629            vec![NDDimension::new(2), NDDimension::new(2)],
630            NDDataType::Float32,
631        );
632        if let NDDataBuffer::F32(ref mut v) = arr.data {
633            v[0] = 100.0;
634            v[1] = 200.0;
635            v[2] = 300.0;
636            v[3] = 400.0;
637        }
638        let img = MagickWriter::array_to_image(&arr, 16).unwrap();
639        if let DynamicImage::ImageLuma16(buf) = img {
640            let raw = buf.into_raw();
641            // min maps to 0, max maps to 65535, intermediate values spread out.
642            assert_eq!(raw[0], 0);
643            assert_eq!(raw[3], 65535);
644            assert!(raw[1] > 0 && raw[1] < raw[2]);
645        } else {
646            panic!("expected 16-bit luma image");
647        }
648    }
649
650    #[test]
651    fn test_compress_type_applied_to_png() {
652        // None vs Best compression must produce different PNG file sizes for
653        // compressible data — proving the param is not discarded.
654        let mut arr = NDArray::new(
655            vec![NDDimension::new(64), NDDimension::new(64)],
656            NDDataType::UInt8,
657        );
658        if let NDDataBuffer::U8(ref mut v) = arr.data {
659            for x in v.iter_mut() {
660                *x = 128; // uniform → highly compressible
661            }
662        }
663
664        let path_none = temp_path("png");
665        let mut w_none = MagickWriter::new();
666        w_none.set_compress_type(0); // None
667        w_none
668            .open_file(&path_none, NDFileMode::Single, &arr)
669            .unwrap();
670        w_none.write_file(&arr).unwrap();
671        w_none.close_file().unwrap();
672
673        let path_zip = temp_path("png");
674        let mut w_zip = MagickWriter::new();
675        w_zip.set_compress_type(7); // Zip
676        w_zip
677            .open_file(&path_zip, NDFileMode::Single, &arr)
678            .unwrap();
679        w_zip.write_file(&arr).unwrap();
680        w_zip.close_file().unwrap();
681
682        let size_none = std::fs::metadata(&path_none).unwrap().len();
683        let size_zip = std::fs::metadata(&path_zip).unwrap().len();
684        assert!(
685            size_zip < size_none,
686            "Zip ({size_zip}) should be smaller than None ({size_none})"
687        );
688
689        std::fs::remove_file(&path_none).ok();
690        std::fs::remove_file(&path_zip).ok();
691    }
692}