pineapple_core/im/
image.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use std::path::Path;
5
6use fast_image_resize::PixelType;
7use image::{DynamicImage, ImageBuffer, Luma, Rgb, open as open_dynamic};
8use npyz::{self, DType, NpyFile, TypeChar};
9
10use crate::constant;
11use crate::cv::transform;
12use crate::error::PineappleError;
13use crate::im::{MaskingStyle, PineappleBuffer, PineappleMaskView, PineappleView};
14use crate::impl_enum_dispatch;
15use crate::io::write_numpy;
16
17/// A wrapper for representing and storing array-shaped pixels
18///
19/// The enum holds all valid, or potentially valid, image formats in terms
20/// of their subpixel data types. All external image types (e.g `DynamicImage`)
21/// should be converted to a PineappleImage via a method on this enum.
22///
23/// # Examples
24///
25/// ```
26/// use image::{RgbImage, DynamicImage};
27/// use pineapple_core::im::PineappleImage;
28///
29/// let rgb = RgbImage::new(10, 10);
30/// let dynamic = DynamicImage::ImageRgb8(rgb);
31/// let image = PineappleImage::new_from_default(dynamic);
32/// ```
33///
34/// ```no_run
35/// use image::ImageReader;
36/// use pineapple_core::im::PineappleImage;
37///
38/// let dynamic = ImageReader::open("image.png")
39///     .expect("Failed to read image")
40///     .with_guessed_format()
41///     .unwrap()
42///     .decode()
43///     .unwrap();
44///
45/// let image = PineappleImage::new_from_default(dynamic);
46/// ```
47#[derive(Debug, Clone)]
48pub enum PineappleImage {
49    U8(PineappleBuffer<u8, Vec<u8>>),
50    U16(PineappleBuffer<u16, Vec<u16>>),
51    U32(PineappleBuffer<u32, Vec<u32>>),
52    U64(PineappleBuffer<u64, Vec<u64>>),
53    I32(PineappleBuffer<i32, Vec<i32>>),
54    I64(PineappleBuffer<i64, Vec<i64>>),
55    F32(PineappleBuffer<f32, Vec<f32>>),
56    F64(PineappleBuffer<f64, Vec<f64>>),
57}
58
59// >>> I/O METHODS
60
61impl PineappleImage {
62    /// Open a new image from a provided path
63    ///
64    /// # Arguments
65    ///
66    /// * `path` - A path to an image with a valid extension
67    ///
68    /// ```no_run
69    /// use pineapple_core::im::PineappleImage;
70    /// let image = PineappleImage::open("image.png");
71    /// ```
72    pub fn open<P: AsRef<Path>>(path: P) -> Result<PineappleImage, PineappleError> {
73        let extension = path
74            .as_ref()
75            .extension()
76            .and_then(|s| s.to_str())
77            .map(|s| s.to_lowercase());
78
79        if let Some(ext) = extension {
80            if ext == "npy" {
81                if let Ok(bytes) = std::fs::read(&path)
82                    && let Ok(npy) = NpyFile::new(&bytes[..]) {
83                    Self::new_from_numpy(npy.clone()).unwrap();
84                    return Self::new_from_numpy(npy);
85                }
86
87                return Err(PineappleError::ImageReadError);
88            }
89
90            if constant::IMAGE_DYNAMIC_FORMATS.iter().any(|e| e == &ext) {
91                if let Ok(image) = open_dynamic(&path) {
92                    return Self::new_from_default(image);
93                }
94
95                return Err(PineappleError::ImageReadError);
96            }
97        }
98
99        Err(PineappleError::ImageExtensionError)
100    }
101
102    /// Initialize a new image from a DynamicImage
103    ///
104    /// # Arguments
105    ///
106    /// * `image` - An 8 or 16-bit grayscale or rgb DynamicImage
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use image::{GrayImage, DynamicImage};
112    /// use pineapple_core::im::PineappleImage;
113    ///
114    /// let gray = GrayImage::new(10, 10);
115    /// let dynamic = DynamicImage::ImageLuma8(gray);
116    /// let image = PineappleImage::new_from_default(dynamic);
117    /// ```
118    pub fn new_from_default(image: DynamicImage) -> Result<PineappleImage, PineappleError> {
119        let width = image.width();
120        let height = image.height();
121
122        match image {
123            DynamicImage::ImageLuma8(buffer) => Ok(PineappleImage::U8(PineappleBuffer::new(
124                width,
125                height,
126                1,
127                buffer.into_raw(),
128            )?)),
129            DynamicImage::ImageLumaA8(buffer) => Ok(PineappleImage::U8(PineappleBuffer::new(
130                width,
131                height,
132                1,
133                buffer
134                    .into_raw()
135                    .chunks_exact(2)
136                    .map(|pixel| pixel[0])
137                    .collect(),
138            )?)),
139            DynamicImage::ImageLuma16(buffer) => Ok(PineappleImage::U16(PineappleBuffer::new(
140                width,
141                height,
142                1,
143                buffer.into_raw(),
144            )?)),
145            DynamicImage::ImageLumaA16(buffer) => Ok(PineappleImage::U16(PineappleBuffer::new(
146                width,
147                height,
148                1,
149                buffer
150                    .into_raw()
151                    .chunks_exact(2)
152                    .map(|pixel| pixel[0])
153                    .collect(),
154            )?)),
155            DynamicImage::ImageRgb8(buffer) => Ok(PineappleImage::U8(PineappleBuffer::new(
156                width,
157                height,
158                3,
159                buffer.into_raw(),
160            )?)),
161            DynamicImage::ImageRgba8(buffer) => Ok(PineappleImage::U8(PineappleBuffer::new(
162                width,
163                height,
164                3,
165                buffer
166                    .into_raw()
167                    .chunks_exact(4)
168                    .flat_map(|pixel| [pixel[0], pixel[1], pixel[2]])
169                    .collect(),
170            )?)),
171            DynamicImage::ImageRgb16(buffer) => Ok(PineappleImage::U16(PineappleBuffer::new(
172                width,
173                height,
174                3,
175                buffer.into_raw(),
176            )?)),
177            DynamicImage::ImageRgba16(buffer) => Ok(PineappleImage::U16(PineappleBuffer::new(
178                width,
179                height,
180                3,
181                buffer
182                    .into_raw()
183                    .chunks_exact(4)
184                    .flat_map(|pixel| [pixel[0], pixel[1], pixel[2]])
185                    .collect(),
186            )?)),
187            DynamicImage::ImageRgb32F(buffer) => Ok(PineappleImage::F32(PineappleBuffer::new(
188                width,
189                height,
190                3,
191                buffer.into_raw(),
192            )?)),
193            DynamicImage::ImageRgba32F(buffer) => Ok(PineappleImage::F32(PineappleBuffer::new(
194                width,
195                height,
196                3,
197                buffer
198                    .into_raw()
199                    .chunks_exact(4)
200                    .flat_map(|pixel| [pixel[0], pixel[1], pixel[2]])
201                    .collect(),
202            )?)),
203            _ => Err(PineappleError::ImageError(
204                "A dynamic image with a valid data type was not detected.",
205            )),
206        }
207    }
208
209    /// Initialize a new image from a numpy array buffer
210    ///
211    /// # Arguments
212    ///
213    /// * `npy` - A (height, width, channel) shaped numpy array buffer
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// use npyz::NpyFile;
219    /// use pineapple_core::im::PineappleImage;
220    ///
221    /// let bytes = std::fs::read("image.npy").unwrap();
222    /// let npy = NpyFile::new(&bytes[..]).unwrap();
223    /// let image = PineappleImage::new_from_numpy(npy);
224    /// ```
225    pub fn new_from_numpy(npy: NpyFile<&[u8]>) -> Result<PineappleImage, PineappleError> {
226        let shape = npy.shape().to_vec();
227
228        let (h, w, c) = match shape.len() {
229            2 => (shape[0] as u32, shape[1] as u32, 1u32),
230            3 => (shape[0] as u32, shape[1] as u32, shape[2] as u32),
231            _ => {
232                return Err(PineappleError::ImageError(
233                    "Numpy array inputs must have an (H, W) or (H, W, C) shape.",
234                ));
235            }
236        };
237
238        match npy.dtype() {
239            DType::Plain(x) => match (x.type_char(), x.size_field()) {
240                (TypeChar::Uint, 1) => Ok(PineappleImage::U8(PineappleBuffer::new(
241                    w,
242                    h,
243                    c,
244                    npy.into_vec().unwrap(),
245                )?)),
246                (TypeChar::Uint, 2) => Ok(PineappleImage::U16(PineappleBuffer::new(
247                    w,
248                    h,
249                    c,
250                    npy.into_vec().unwrap(),
251                )?)),
252                (TypeChar::Int, 4) => Ok(PineappleImage::I32(PineappleBuffer::new(
253                    w,
254                    h,
255                    c,
256                    npy.into_vec().unwrap(),
257                )?)),
258                (TypeChar::Int, 8) => Ok(PineappleImage::I64(PineappleBuffer::new(
259                    w,
260                    h,
261                    c,
262                    npy.into_vec().unwrap(),
263                )?)),
264                (TypeChar::Float, 4) => Ok(PineappleImage::F32(PineappleBuffer::new(
265                    w,
266                    h,
267                    c,
268                    npy.into_vec().unwrap(),
269                )?)),
270                (TypeChar::Float, 8) => Ok(PineappleImage::F64(PineappleBuffer::new(
271                    w,
272                    h,
273                    c,
274                    npy.into_vec().unwrap(),
275                )?)),
276                _ => Err(PineappleError::ImageError(
277                    "A numpy array with a valid data type was not detected.",
278                )),
279            },
280            _ => Err(PineappleError::ImageError(
281                "Only plain numpy arrays are currentled supported.",
282            )),
283        }
284    }
285
286    /// Save image
287    ///
288    /// # Arguments
289    ///
290    /// * `path` - A path to an image with a valid extension
291    ///
292    /// ```no_run
293    /// use pineapple_core::im::PineappleImage;
294    /// let image = PineappleImage::open("image.png").unwrap();
295    /// image.save("image.npy").unwrap();
296    /// ```
297    pub fn save<P: AsRef<Path>>(self, path: P) -> Result<(), PineappleError> {
298        let extension = path
299            .as_ref()
300            .extension()
301            .and_then(|s| s.to_str())
302            .map(|s| s.to_lowercase());
303
304        if let Some(ext) = extension {
305            if ext == "npy" {
306                return self.save_as_numpy(path);
307            }
308
309            if constant::IMAGE_DYNAMIC_FORMATS.iter().any(|e| e == &ext) {
310                return self.save_as_default(path);
311            }
312        }
313
314        Err(PineappleError::ImageExtensionError)
315    }
316
317    /// Save image as a default image format
318    ///
319    /// # Arguments
320    ///
321    /// * `path` - Path to output image
322    ///
323    /// # Examples
324    ///
325    /// ```no_run
326    /// use image::{GrayImage, DynamicImage};
327    /// use pineapple_core::im::PineappleImage;
328    ///
329    /// let gray = GrayImage::new(10, 10);
330    /// let dynamic = DynamicImage::ImageLuma8(gray);
331    /// let image = PineappleImage::new_from_default(dynamic).unwrap();
332    /// image.save_as_default("new_image.png").unwrap();
333    /// ```
334    pub fn save_as_default<P: AsRef<Path>>(self, path: P) -> Result<(), PineappleError> {
335        let channels = self.channels();
336        match (self, channels) {
337            (PineappleImage::U8(buffer), 1) => {
338                let image_buffer = ImageBuffer::<Luma<u8>, Vec<u8>>::from_raw(
339                    buffer.width(),
340                    buffer.height(),
341                    buffer.into_raw(),
342                )
343                .ok_or(PineappleError::ImageWriteError)?;
344
345                image_buffer
346                    .save(path)
347                    .map_err(|_| PineappleError::ImageWriteError)
348            }
349            (PineappleImage::U16(buffer), 1) => {
350                let image_buffer = ImageBuffer::<Luma<u16>, Vec<u16>>::from_raw(
351                    buffer.width(),
352                    buffer.height(),
353                    buffer.into_raw(),
354                )
355                .ok_or(PineappleError::ImageWriteError)?;
356
357                image_buffer
358                    .save(path)
359                    .map_err(|_| PineappleError::ImageWriteError)
360            }
361            (PineappleImage::U8(buffer), 3) => {
362                let image_buffer = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(
363                    buffer.width(),
364                    buffer.height(),
365                    buffer.into_raw(),
366                )
367                .ok_or(PineappleError::ImageWriteError)?;
368
369                image_buffer
370                    .save(path)
371                    .map_err(|_| PineappleError::ImageWriteError)
372            }
373            (PineappleImage::U16(buffer), 3) => {
374                let image_buffer = ImageBuffer::<Rgb<u16>, Vec<u16>>::from_raw(
375                    buffer.width(),
376                    buffer.height(),
377                    buffer.into_raw(),
378                )
379                .ok_or(PineappleError::ImageWriteError)?;
380
381                image_buffer
382                    .save(path)
383                    .map_err(|_| PineappleError::ImageWriteError)
384            }
385            (PineappleImage::F32(buffer), 3) => {
386                let image_buffer = ImageBuffer::<Rgb<f32>, Vec<f32>>::from_raw(
387                    buffer.width(),
388                    buffer.height(),
389                    buffer.into_raw(),
390                )
391                .ok_or(PineappleError::ImageWriteError)?;
392
393                image_buffer
394                    .save(path)
395                    .map_err(|_| PineappleError::ImageWriteError)
396            }
397            _ => Err(PineappleError::ImageError(
398                "Only 1 or 3 channel RGB/grayscale images can be saved as a default image format (e.g. png).",
399            )),
400        }
401    }
402
403    /// Save image as a numpy format
404    ///
405    /// # Arguments
406    ///
407    /// * `path` - Path to output image
408    ///
409    /// # Examples
410    ///
411    /// ```no_run
412    /// use image::{GrayImage, DynamicImage};
413    /// use pineapple_core::im::PineappleImage;
414    ///
415    /// let gray = GrayImage::new(10, 10);
416    /// let dynamic = DynamicImage::ImageLuma8(gray);
417    /// let image = PineappleImage::new_from_default(dynamic).unwrap();
418    /// image.save_as_numpy("new_image.npy").unwrap();
419    /// ```
420    pub fn save_as_numpy<P: AsRef<Path>>(self, path: P) -> Result<(), PineappleError> {
421        let shape = vec![
422            self.height() as u64,
423            self.width() as u64,
424            self.channels() as u64,
425        ];
426
427        match self {
428            PineappleImage::U8(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
429            PineappleImage::U16(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
430            PineappleImage::U32(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
431            PineappleImage::U64(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
432            PineappleImage::I32(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
433            PineappleImage::I64(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
434            PineappleImage::F32(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
435            PineappleImage::F64(buffer) => write_numpy(path.as_ref(), buffer.into_raw(), shape),
436        }
437    }
438}
439
440// <<< I/O METHODS
441
442// >>> PROPERTY METHODS
443
444impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; width(&self) -> u32);
445impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; height(&self) -> u32);
446impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; channels(&self) -> u32);
447impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; shape(&self) -> (u32, u32, u32));
448impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; len(&self) -> usize);
449impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; is_empty(&self) -> bool);
450
451impl PineappleImage {
452    /// Get the minimum value for the image data type
453    pub fn dtype_min(&self) -> f64 {
454        match self {
455            PineappleImage::U8(_) => u8::MIN as f64,
456            PineappleImage::U16(_) => u16::MIN as f64,
457            PineappleImage::U32(_) => u32::MIN as f64,
458            PineappleImage::U64(_) => u64::MIN as f64,
459            PineappleImage::I32(_) => i32::MIN as f64,
460            PineappleImage::I64(_) => i64::MIN as f64,
461            PineappleImage::F32(_) => f32::MIN as f64,
462            PineappleImage::F64(_) => f64::MIN,
463        }
464    }
465
466    /// Get the maximum value for the image data type
467    pub fn dtype_max(&self) -> f64 {
468        match self {
469            PineappleImage::U8(_) => u8::MAX as f64,
470            PineappleImage::U16(_) => u16::MAX as f64,
471            PineappleImage::U32(_) => u32::MAX as f64,
472            PineappleImage::U64(_) => u64::MAX as f64,
473            PineappleImage::I32(_) => i32::MAX as f64,
474            PineappleImage::I64(_) => i64::MAX as f64,
475            PineappleImage::F32(_) => f32::MAX as f64,
476            PineappleImage::F64(_) => f64::MAX,
477        }
478    }
479}
480
481// <<< PROPERTY METHODS
482
483// >>> CONVERSION METHODS
484
485impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; to_u8(&self) -> Vec<u8>);
486impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; to_u16(&self) -> Vec<u16>);
487impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; to_u32(&self) -> Vec<u32>);
488impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; to_f32(&self) -> Vec<f32>);
489impl_enum_dispatch!(PineappleImage, U8, U16, U32, I32, I64, U64, F32, F64; to_f64(&self) -> Vec<f64>);
490
491// <<< CONVERSION METHODS
492
493// >>> TRANSFORM METHODS
494
495impl PineappleImage {
496    /// Generate a zero-copy crop of an image subregion
497    ///
498    /// # Arguments
499    ///
500    /// * `x` - Minimum x-coordinate (left)
501    /// * `y` - Minimum y-coordinate (bottom)
502    /// * `w` - Width of crop
503    /// * `h` - Height of crop
504    pub fn crop_view(&self, x: u32, y: u32, w: u32, h: u32) -> PineappleView<'_> {
505        match self {
506            PineappleImage::U8(buffer) => PineappleView::U8(buffer.crop_view(x, y, w, h)),
507            PineappleImage::U16(buffer) => PineappleView::U16(buffer.crop_view(x, y, w, h)),
508            PineappleImage::U32(buffer) => PineappleView::U32(buffer.crop_view(x, y, w, h)),
509            PineappleImage::U64(buffer) => PineappleView::U64(buffer.crop_view(x, y, w, h)),
510            PineappleImage::I32(buffer) => PineappleView::I32(buffer.crop_view(x, y, w, h)),
511            PineappleImage::I64(buffer) => PineappleView::I64(buffer.crop_view(x, y, w, h)),
512            PineappleImage::F32(buffer) => PineappleView::F32(buffer.crop_view(x, y, w, h)),
513            PineappleImage::F64(buffer) => PineappleView::F64(buffer.crop_view(x, y, w, h)),
514        }
515    }
516
517    /// Create a new image with copied cropped contents
518    ///
519    /// # Arguments
520    ///
521    /// * `x` - Minimum x-coordinate (left)
522    /// * `y` - Minimum y-coordinate (bottom)
523    /// * `w` - Width of crop
524    /// * `h` - Height of crop
525    pub fn crop(&self, x: u32, y: u32, w: u32, h: u32) -> Result<PineappleImage, PineappleError> {
526        match self {
527            PineappleImage::U8(buffer) => Ok(PineappleImage::U8(buffer.crop(x, y, w, h)?)),
528            PineappleImage::U16(buffer) => Ok(PineappleImage::U16(buffer.crop(x, y, w, h)?)),
529            PineappleImage::U32(buffer) => Ok(PineappleImage::U32(buffer.crop(x, y, w, h)?)),
530            PineappleImage::U64(buffer) => Ok(PineappleImage::U64(buffer.crop(x, y, w, h)?)),
531            PineappleImage::I32(buffer) => Ok(PineappleImage::I32(buffer.crop(x, y, w, h)?)),
532            PineappleImage::I64(buffer) => Ok(PineappleImage::I64(buffer.crop(x, y, w, h)?)),
533            PineappleImage::F32(buffer) => Ok(PineappleImage::F32(buffer.crop(x, y, w, h)?)),
534            PineappleImage::F64(buffer) => Ok(PineappleImage::F64(buffer.crop(x, y, w, h)?)),
535        }
536    }
537
538    /// Crops the image while applying a mask to either foreground or background pixels
539    ///
540    /// # Arguments
541    ///
542    /// * `x` - Minimum x-coordinate (left)
543    /// * `y` - Minimum y-coordinate (bottom)
544    /// * `w` - Width of crop
545    /// * `h` - Height of crop
546    /// * `mask` - A cropped mask view
547    /// * `mask_style` - Foreground or background masking style
548    pub fn crop_masked(
549        &self,
550        x: u32,
551        y: u32,
552        w: u32,
553        h: u32,
554        mask: &PineappleMaskView,
555        mask_style: MaskingStyle,
556    ) -> Result<PineappleImage, PineappleError> {
557        match self {
558            PineappleImage::U8(buffer) => Ok(PineappleImage::U8(
559                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
560            )),
561            PineappleImage::U16(buffer) => Ok(PineappleImage::U16(
562                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
563            )),
564            PineappleImage::U32(buffer) => Ok(PineappleImage::U32(
565                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
566            )),
567            PineappleImage::U64(buffer) => Ok(PineappleImage::U64(
568                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
569            )),
570            PineappleImage::I32(buffer) => Ok(PineappleImage::I32(
571                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
572            )),
573            PineappleImage::I64(buffer) => Ok(PineappleImage::I64(
574                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
575            )),
576            PineappleImage::F32(buffer) => Ok(PineappleImage::F32(
577                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
578            )),
579            PineappleImage::F64(buffer) => Ok(PineappleImage::F64(
580                buffer.crop_masked(x, y, w, h, mask, mask_style)?,
581            )),
582        }
583    }
584
585    /// Resize the image
586    ///
587    /// # Arguments
588    ///
589    /// * `width` - Width of resized image
590    /// * `height` - Height of resized image
591    pub fn resize(&self, width: u32, height: u32) -> Result<PineappleImage, PineappleError> {
592        let channels = self.channels();
593        match (self, channels) {
594            (PineappleImage::U8(buffer), 1) => Ok(PineappleImage::U8(PineappleBuffer::new(
595                width,
596                height,
597                1,
598                transform::resize_bilinear_fast(
599                    &DynamicImage::ImageLuma8(
600                        ImageBuffer::<Luma<u8>, Vec<u8>>::from_raw(
601                            buffer.width(),
602                            buffer.height(),
603                            buffer.as_raw().to_vec(),
604                        )
605                        .ok_or(PineappleError::ImageError("Failed to resize image"))?,
606                    ),
607                    width,
608                    height,
609                    PixelType::U8,
610                ),
611            )?)),
612            (PineappleImage::U8(buffer), 3) => Ok(PineappleImage::U8(
613                PineappleBuffer::new(
614                    width,
615                    height,
616                    3,
617                    transform::resize_bilinear_fast(
618                        &DynamicImage::ImageRgb8(
619                            ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(
620                                buffer.width(),
621                                buffer.height(),
622                                buffer.as_raw().to_vec(),
623                            )
624                            .ok_or(PineappleError::ImageError("Failed to resize image"))?,
625                        ),
626                        width,
627                        height,
628                        PixelType::U8x3,
629                    ),
630                )
631                .map_err(|_| PineappleError::ImageError("Failed to resize image."))?,
632            )),
633            (PineappleImage::U16(buffer), 1) => Ok(PineappleImage::U16(PineappleBuffer::new(
634                width,
635                height,
636                1,
637                transform::resize_bilinear_default(
638                    &ImageBuffer::<Luma<u16>, Vec<u16>>::from_raw(
639                        buffer.width(),
640                        buffer.height(),
641                        buffer.as_raw().to_vec(),
642                    )
643                    .ok_or(PineappleError::ImageError("Failed to resize image"))?,
644                    width,
645                    height,
646                )
647                .into_raw(),
648            )?)),
649            (PineappleImage::U16(buffer), 3) => Ok(PineappleImage::U16(PineappleBuffer::new(
650                width,
651                height,
652                3,
653                transform::resize_bilinear_default(
654                    &ImageBuffer::<Rgb<u16>, Vec<u16>>::from_raw(
655                        buffer.width(),
656                        buffer.height(),
657                        buffer.as_raw().to_vec(),
658                    )
659                    .ok_or(PineappleError::ImageError("Failed to resize image"))?,
660                    width,
661                    height,
662                )
663                .into_raw(),
664            )?)),
665            (PineappleImage::F32(buffer), 3) => Ok(PineappleImage::F32(PineappleBuffer::new(
666                width,
667                height,
668                3,
669                transform::resize_bilinear_default(
670                    &ImageBuffer::<Rgb<f32>, Vec<f32>>::from_raw(
671                        buffer.width(),
672                        buffer.height(),
673                        buffer.as_raw().to_vec(),
674                    )
675                    .ok_or(PineappleError::ImageError("Failed to resize image"))?,
676                    width,
677                    height,
678                )
679                .into_raw(),
680            )?)),
681            (PineappleImage::U8(buffer), _) => Ok(PineappleImage::U8(PineappleBuffer::new(
682                width,
683                height,
684                channels,
685                transform::resize_bilinear_general::<u8>(
686                    buffer.as_raw(),
687                    buffer.width() as usize,
688                    buffer.height() as usize,
689                    channels as usize,
690                    width as usize,
691                    height as usize,
692                    true,
693                ),
694            )?)),
695            (PineappleImage::U16(buffer), _) => Ok(PineappleImage::U16(PineappleBuffer::new(
696                width,
697                height,
698                channels,
699                transform::resize_bilinear_general::<u16>(
700                    buffer.as_raw(),
701                    buffer.width() as usize,
702                    buffer.height() as usize,
703                    channels as usize,
704                    width as usize,
705                    height as usize,
706                    true,
707                ),
708            )?)),
709            (PineappleImage::U32(buffer), _) => Ok(PineappleImage::U32(PineappleBuffer::new(
710                width,
711                height,
712                channels,
713                transform::resize_bilinear_general::<u32>(
714                    buffer.as_raw(),
715                    buffer.width() as usize,
716                    buffer.height() as usize,
717                    channels as usize,
718                    width as usize,
719                    height as usize,
720                    true,
721                ),
722            )?)),
723            (PineappleImage::U64(buffer), _) => Ok(PineappleImage::U64(PineappleBuffer::new(
724                width,
725                height,
726                channels,
727                transform::resize_bilinear_general::<u64>(
728                    buffer.as_raw(),
729                    buffer.width() as usize,
730                    buffer.height() as usize,
731                    channels as usize,
732                    width as usize,
733                    height as usize,
734                    true,
735                ),
736            )?)),
737            (PineappleImage::I32(buffer), _) => Ok(PineappleImage::I32(PineappleBuffer::new(
738                width,
739                height,
740                channels,
741                transform::resize_bilinear_general::<i32>(
742                    buffer.as_raw(),
743                    buffer.width() as usize,
744                    buffer.height() as usize,
745                    channels as usize,
746                    width as usize,
747                    height as usize,
748                    true,
749                ),
750            )?)),
751            (PineappleImage::I64(buffer), _) => Ok(PineappleImage::I64(PineappleBuffer::new(
752                width,
753                height,
754                channels,
755                transform::resize_bilinear_general::<i64>(
756                    buffer.as_raw(),
757                    buffer.width() as usize,
758                    buffer.height() as usize,
759                    channels as usize,
760                    width as usize,
761                    height as usize,
762                    true,
763                ),
764            )?)),
765            (PineappleImage::F32(buffer), _) => Ok(PineappleImage::F32(PineappleBuffer::new(
766                width,
767                height,
768                channels,
769                transform::resize_bilinear_general::<f32>(
770                    buffer.as_raw(),
771                    buffer.width() as usize,
772                    buffer.height() as usize,
773                    channels as usize,
774                    width as usize,
775                    height as usize,
776                    true,
777                ),
778            )?)),
779            (PineappleImage::F64(buffer), _) => Ok(PineappleImage::F64(PineappleBuffer::new(
780                width,
781                height,
782                channels,
783                transform::resize_bilinear_general::<f64>(
784                    buffer.as_raw(),
785                    buffer.width() as usize,
786                    buffer.height() as usize,
787                    channels as usize,
788                    width as usize,
789                    height as usize,
790                    true,
791                ),
792            )?)),
793        }
794    }
795}
796
797// <<< TRANSFORM METHODS
798
799#[cfg(test)]
800mod test {
801
802    use super::*;
803    use image::{
804        DynamicImage, GrayAlphaImage, GrayImage, ImageBuffer, Luma, LumaA, Rgb, Rgb32FImage,
805        RgbImage, Rgba, Rgba32FImage, RgbaImage,
806    };
807
808    const TEST_GRAY: &str = "../data/tests/test_grayscale";
809    const TEST_RGB: &str = "../data/tests/test_rgb";
810
811    #[test]
812    fn test_grayscale_open() {
813        let extensions = [
814            ".jpeg", ".npy", ".pbm", ".png", ".tga", ".tif", "_f32.npy", "_f64.npy", "_i32.npy",
815            "_i64.npy", "_u16.npy",
816        ];
817
818        for ext in extensions.into_iter() {
819            let img = PineappleImage::open(format!("{}{}", TEST_GRAY, ext));
820            assert!(img.is_ok(), "{}", ext);
821
822            let img = img.unwrap();
823            assert_eq!(img.width(), 621);
824            assert_eq!(img.height(), 621);
825            assert_eq!(img.channels(), 1, "{}", ext);
826        }
827
828        // Grayscale .bmp and .webp default to 3-channel
829        // RGB when saved using python Pillow or cv2
830        let extensions = [".bmp", ".webp"];
831        for ext in extensions.into_iter() {
832            let img = PineappleImage::open(format!("{}{}", TEST_GRAY, ext));
833            assert!(img.is_ok(), "{}", ext);
834
835            let img = img.unwrap();
836            assert_eq!(img.width(), 621);
837            assert_eq!(img.height(), 621);
838            assert_eq!(img.channels(), 3, "{}", ext);
839        }
840    }
841
842    #[test]
843    fn test_rgb_open() {
844        let extensions = [
845            ".bmp", ".jpeg", ".npy", ".pbm", ".png", ".tga", ".tif", ".webp", "_f32.npy",
846            "_f64.npy", "_i32.npy", "_i64.npy",
847        ];
848
849        for ext in extensions.into_iter() {
850            let img = PineappleImage::open(format!("{}{}", TEST_RGB, ext));
851            assert!(img.is_ok(), "{}", ext);
852
853            let img = img.unwrap();
854            assert_eq!(img.width(), 621);
855            assert_eq!(img.height(), 621);
856            assert_eq!(img.channels(), 3, "{}", ext);
857        }
858    }
859
860    #[test]
861    fn test_grayscale_save() {
862        const TEST_DEFAULT: &str = "TEST_SAVE_DEFAULT_GRAY.png";
863        const TEST_NUMPY: &str = "TEST_SAVE_NUMPY_GRAY.npy";
864
865        let img =
866            PineappleImage::U8(PineappleBuffer::<u8, Vec<u8>>::new(2, 2, 1, vec![0, 1, 2, 3]).unwrap());
867
868        img.clone().save(TEST_DEFAULT).unwrap();
869        img.clone().save(TEST_NUMPY).unwrap();
870
871        let img_default = PineappleImage::open(TEST_DEFAULT).unwrap();
872        let img_numpy = PineappleImage::open(TEST_NUMPY).unwrap();
873
874        assert_eq!(img.to_u8(), img_default.to_u8());
875        assert_eq!(img.to_u8(), img_numpy.to_u8());
876
877        std::fs::remove_file(TEST_DEFAULT).unwrap();
878        std::fs::remove_file(TEST_NUMPY).unwrap();
879    }
880
881    #[test]
882    fn test_rgb_save() {
883        const TEST_DEFAULT: &str = "TEST_SAVE_DEFAULT_RGB.png";
884        const TEST_NUMPY: &str = "TEST_SAVE_NUMPY_RGB.npy";
885
886        let img = PineappleImage::U8(
887            PineappleBuffer::<u8, Vec<u8>>::new(2, 2, 3, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
888                .unwrap(),
889        );
890
891        img.clone().save(TEST_DEFAULT).unwrap();
892        img.clone().save(TEST_NUMPY).unwrap();
893
894        let img_default = PineappleImage::open(TEST_DEFAULT).unwrap();
895        let img_numpy = PineappleImage::open(TEST_NUMPY).unwrap();
896
897        assert_eq!(img.to_u8(), img_default.to_u8());
898        assert_eq!(img.to_u8(), img_numpy.to_u8());
899
900        std::fs::remove_file(TEST_DEFAULT).unwrap();
901        std::fs::remove_file(TEST_NUMPY).unwrap();
902    }
903
904    #[test]
905    fn test_multichannel_save() {
906        const TEST_DEFAULT: &str = "TEST_SAVE_DEFAULT_MULTI.png";
907        const TEST_NUMPY: &str = "TEST_SAVE_NUMPY_MULTI.npy";
908
909        let img = PineappleImage::U8(
910            PineappleBuffer::<u8, Vec<u8>>::new(
911                2,
912                2,
913                4,
914                vec![0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 8, 9, 10, 11, 11],
915            )
916            .unwrap(),
917        );
918
919        let failure = img.clone().save(TEST_DEFAULT);
920        assert!(failure.is_err());
921
922        img.clone().save(TEST_NUMPY).unwrap();
923
924        let img_numpy = PineappleImage::open(TEST_NUMPY).unwrap();
925        assert_eq!(img.to_u8(), img_numpy.to_u8());
926        std::fs::remove_file(TEST_NUMPY).unwrap();
927    }
928
929    #[test]
930    fn test_dynamic_gray_u8() {
931        let dynamic = DynamicImage::ImageLuma8(GrayImage::from_fn(10, 10, |x, _| Luma([x as u8])));
932
933        let image = PineappleImage::new_from_default(dynamic);
934        assert!(image.is_ok());
935
936        let image = image.unwrap();
937        assert_eq!(image.width(), 10);
938        assert_eq!(image.height(), 10);
939    }
940
941    #[test]
942    fn test_dynamic_gray_alpha_u8() {
943        let dynamic =
944            DynamicImage::ImageLumaA8(GrayAlphaImage::from_fn(10, 10, |x, _| LumaA([x as u8, 10])));
945
946        let image = PineappleImage::new_from_default(dynamic);
947        assert!(image.is_ok());
948
949        let image = image.unwrap();
950        assert_eq!(image.width(), 10);
951        assert_eq!(image.height(), 10);
952
953        let data = image.to_u8();
954        assert_eq!(data[0], 0);
955        assert_eq!(data[1], 1);
956    }
957
958    #[test]
959    fn test_dynamic_gray_u16() {
960        let dynamic = DynamicImage::ImageLuma16(ImageBuffer::<Luma<u16>, Vec<u16>>::from_fn(
961            10,
962            10,
963            |x, _| Luma([x as u16]),
964        ));
965
966        let image = PineappleImage::new_from_default(dynamic);
967        assert!(image.is_ok());
968
969        let image = image.unwrap();
970        assert_eq!(image.width(), 10);
971        assert_eq!(image.height(), 10);
972    }
973
974    #[test]
975    fn test_dynamic_gray_alpha_u16() {
976        let dynamic = DynamicImage::ImageLumaA16(ImageBuffer::<LumaA<u16>, Vec<u16>>::from_fn(
977            10,
978            10,
979            |x, _| LumaA([x as u16, 10]),
980        ));
981
982        let image = PineappleImage::new_from_default(dynamic);
983        assert!(image.is_ok());
984
985        let image = image.unwrap();
986        assert_eq!(image.width(), 10);
987        assert_eq!(image.height(), 10);
988
989        let data = image.to_u16();
990        assert_eq!(data[0], 0);
991        assert_eq!(data[1], 1);
992    }
993
994    #[test]
995    fn test_dynamic_rgb_u8() {
996        let dynamic = DynamicImage::ImageRgb8(RgbImage::from_fn(10, 10, |x, _| {
997            Rgb([x as u8, x as u8, x as u8])
998        }));
999
1000        let image = PineappleImage::new_from_default(dynamic);
1001        assert!(image.is_ok());
1002
1003        let image = image.unwrap();
1004        assert_eq!(image.width(), 10);
1005        assert_eq!(image.height(), 10);
1006
1007        let data = image.to_u8();
1008        assert_eq!((data[0], data[1], data[2]), (0, 0, 0));
1009        assert_eq!((data[3], data[4], data[5]), (1, 1, 1));
1010    }
1011
1012    #[test]
1013    fn test_dynamic_rgb_alpha_u8() {
1014        let dynamic = DynamicImage::ImageRgba8(RgbaImage::from_fn(10, 10, |x, _| {
1015            Rgba([x as u8, x as u8, x as u8, 10])
1016        }));
1017
1018        let image = PineappleImage::new_from_default(dynamic);
1019        assert!(image.is_ok());
1020
1021        let image = image.unwrap();
1022        assert_eq!(image.width(), 10);
1023        assert_eq!(image.height(), 10);
1024
1025        let data = image.to_u8();
1026        assert_eq!((data[0], data[1], data[2]), (0, 0, 0));
1027        assert_eq!((data[3], data[4], data[5]), (1, 1, 1));
1028    }
1029
1030    #[test]
1031    fn test_dynamic_rgb_u16() {
1032        let dynamic = DynamicImage::ImageRgb16(ImageBuffer::<Rgb<u16>, Vec<u16>>::from_fn(
1033            10,
1034            10,
1035            |x, _| Rgb([x as u16, x as u16, x as u16]),
1036        ));
1037
1038        let image = PineappleImage::new_from_default(dynamic);
1039        assert!(image.is_ok());
1040
1041        let image = image.unwrap();
1042        assert_eq!(image.width(), 10);
1043        assert_eq!(image.height(), 10);
1044
1045        let data = image.to_u16();
1046        assert_eq!((data[0], data[1], data[2]), (0, 0, 0));
1047        assert_eq!((data[3], data[4], data[5]), (1, 1, 1));
1048    }
1049
1050    #[test]
1051    fn test_dynamic_rgb_alpha_u16() {
1052        let dynamic = DynamicImage::ImageRgba16(ImageBuffer::<Rgba<u16>, Vec<u16>>::from_fn(
1053            10,
1054            10,
1055            |x, _| Rgba([x as u16, x as u16, x as u16, 10]),
1056        ));
1057
1058        let image = PineappleImage::new_from_default(dynamic);
1059        assert!(image.is_ok());
1060
1061        let image = image.unwrap();
1062        assert_eq!(image.width(), 10);
1063        assert_eq!(image.height(), 10);
1064
1065        let data = image.to_u16();
1066        assert_eq!((data[0], data[1], data[2]), (0, 0, 0));
1067        assert_eq!((data[3], data[4], data[5]), (1, 1, 1));
1068    }
1069
1070    #[test]
1071    fn test_dynamic_rgb_f32() {
1072        let dynamic = DynamicImage::ImageRgb32F(Rgb32FImage::from_fn(10, 10, |x, _| {
1073            Rgb([x as f32, x as f32, x as f32])
1074        }));
1075
1076        let image = PineappleImage::new_from_default(dynamic);
1077        assert!(image.is_ok());
1078
1079        let image = image.unwrap();
1080        assert_eq!(image.width(), 10);
1081        assert_eq!(image.height(), 10);
1082    }
1083
1084    #[test]
1085    fn test_dynamic_rgb_alpha_f32() {
1086        let dynamic = DynamicImage::ImageRgba32F(Rgba32FImage::from_fn(10, 10, |x, _| {
1087            Rgba([x as f32, x as f32, x as f32, 10.0])
1088        }));
1089
1090        let image = PineappleImage::new_from_default(dynamic);
1091        assert!(image.is_ok());
1092
1093        let image = image.unwrap();
1094        assert_eq!(image.width(), 10);
1095        assert_eq!(image.height(), 10);
1096    }
1097
1098    #[test]
1099    fn test_resize_u8() {
1100        let dynamic = DynamicImage::ImageLuma8(GrayImage::from_fn(10, 10, |x, _| Luma([x as u8])));
1101
1102        let one_channel = PineappleImage::new_from_default(dynamic).unwrap();
1103
1104        let downsampled = one_channel.resize(3, 4).unwrap();
1105        assert_eq!(downsampled.width(), 3);
1106        assert_eq!(downsampled.height(), 4);
1107
1108        let upsampled = one_channel.resize(23, 24).unwrap();
1109        assert_eq!(upsampled.width(), 23);
1110        assert_eq!(upsampled.height(), 24);
1111
1112        let dynamic = DynamicImage::ImageRgb8(RgbImage::from_fn(10, 10, |x, _| {
1113            Rgb([x as u8, x as u8, x as u8])
1114        }));
1115
1116        let three_channel = PineappleImage::new_from_default(dynamic).unwrap();
1117
1118        let downsampled = three_channel.resize(3, 4).unwrap();
1119        assert_eq!(downsampled.width(), 3);
1120        assert_eq!(downsampled.height(), 4);
1121
1122        let upsampled = three_channel.resize(23, 24).unwrap();
1123        assert_eq!(upsampled.width(), 23);
1124        assert_eq!(upsampled.height(), 24);
1125
1126        let two_channel = PineappleImage::U8(
1127            PineappleBuffer::<u8, Vec<u8>>::new(2, 2, 2, vec![0, 1, 2, 3, 4, 5, 6, 7]).unwrap(),
1128        );
1129
1130        let downsampled = two_channel.resize(3, 4).unwrap();
1131        assert_eq!(downsampled.width(), 3);
1132        assert_eq!(downsampled.height(), 4);
1133
1134        let upsampled = two_channel.resize(23, 24).unwrap();
1135        assert_eq!(upsampled.width(), 23);
1136        assert_eq!(upsampled.height(), 24);
1137    }
1138
1139    #[test]
1140    fn test_resize_u16() {
1141        let dynamic = DynamicImage::ImageLuma16(ImageBuffer::<Luma<u16>, Vec<u16>>::from_fn(
1142            10,
1143            10,
1144            |x, _| Luma([x as u16]),
1145        ));
1146
1147        let one_channel = PineappleImage::new_from_default(dynamic).unwrap();
1148
1149        let downsampled = one_channel.resize(3, 4).unwrap();
1150        assert_eq!(downsampled.width(), 3);
1151        assert_eq!(downsampled.height(), 4);
1152
1153        let upsampled = one_channel.resize(23, 24).unwrap();
1154        assert_eq!(upsampled.width(), 23);
1155        assert_eq!(upsampled.height(), 24);
1156
1157        let dynamic = DynamicImage::ImageRgb16(ImageBuffer::<Rgb<u16>, Vec<u16>>::from_fn(
1158            10,
1159            10,
1160            |x, _| Rgb([x as u16, x as u16, x as u16]),
1161        ));
1162
1163        let three_channel = PineappleImage::new_from_default(dynamic).unwrap();
1164
1165        let downsampled = three_channel.resize(3, 4).unwrap();
1166        assert_eq!(downsampled.width(), 3);
1167        assert_eq!(downsampled.height(), 4);
1168
1169        let upsampled = three_channel.resize(23, 24).unwrap();
1170        assert_eq!(upsampled.width(), 23);
1171        assert_eq!(upsampled.height(), 24);
1172
1173        let two_channel = PineappleImage::U16(
1174            PineappleBuffer::<u16, Vec<u16>>::new(2, 2, 2, vec![0, 1, 2, 3, 4, 5, 6, 7]).unwrap(),
1175        );
1176
1177        let downsampled = two_channel.resize(3, 4).unwrap();
1178        assert_eq!(downsampled.width(), 3);
1179        assert_eq!(downsampled.height(), 4);
1180
1181        let upsampled = two_channel.resize(23, 24).unwrap();
1182        assert_eq!(upsampled.width(), 23);
1183        assert_eq!(upsampled.height(), 24);
1184    }
1185}