auto_palette/image/
data.rs

1use std::borrow::Cow;
2#[cfg(feature = "image")]
3use std::path::Path;
4
5#[cfg(feature = "image")]
6use image::{DynamicImage, RgbImage, RgbaImage};
7
8use crate::{
9    color::{rgb_to_xyz, xyz_to_lab, Lab, D65},
10    image::{error::ImageError, Pixel, RGBA_CHANNELS},
11    math::normalize,
12    Filter,
13    FloatNumber,
14    ImageResult,
15};
16
17/// The image data representing the pixel data of an image.
18///
19/// Each pixel is represented by 4 bytes in RGBA (Red, Green, Blue, Alpha) format.
20/// The pixel data is stored in a linear array of bytes, where each pixel is represented by 4 bytes.
21///
22/// # Example
23/// ```
24/// #[cfg(feature = "image")]
25/// {
26///     use auto_palette::ImageData;
27///
28///     let pixels = [
29///         255, 0, 0, 255, // Red
30///         0, 255, 0, 255, // Green
31///         0, 0, 255, 255, // Blue
32///         0, 0, 0, 255, // Black
33///     ];
34///     let image_data = ImageData::new(2, 2, &pixels).unwrap();
35///     assert_eq!(image_data.width(), 2);
36///     assert_eq!(image_data.height(), 2);
37///     assert_eq!(image_data.data(), &pixels);
38/// }
39/// ```
40#[derive(Debug)]
41pub struct ImageData<'a> {
42    width: u32,
43    height: u32,
44    data: Cow<'a, [u8]>,
45}
46
47impl<'a> ImageData<'a> {
48    /// Creates a new `ImageData` with the given width, height, and pixel data.
49    ///
50    /// # Arguments
51    /// * `width` - The width of the image data.
52    /// * `height` - The height of the image data.
53    /// * `data` - The pixel data of the image data.
54    ///
55    /// # Returns
56    /// The `ImageData` with the given width, height, and pixel data.
57    ///
58    /// # Errors
59    /// Returns an error if the length of the pixel data is not equal to `width * height * 4`.
60    pub fn new(width: u32, height: u32, data: &'a [u8]) -> ImageResult<Self> {
61        let expected_length = (width * height) as usize * RGBA_CHANNELS;
62        if data.len() != expected_length {
63            return Err(ImageError::UnexpectedLength {
64                expected: expected_length,
65                actual: data.len(),
66            });
67        }
68
69        Ok(Self {
70            width,
71            height,
72            data: Cow::Borrowed(data),
73        })
74    }
75
76    /// Loads the image data from the given path.
77    /// The image data is loaded using the `image` crate.
78    ///
79    /// # Arguments
80    /// * `path` - The path to the image file.
81    ///
82    /// # Returns
83    /// The image data loaded from the given path.
84    ///
85    /// # Errors
86    /// Returns an error if the image loading process fails.
87    /// Returns an error if the color type of the image is not supported.
88    #[cfg(feature = "image")]
89    pub fn load<P>(path: P) -> ImageResult<Self>
90    where
91        P: AsRef<Path>,
92    {
93        let image = image::open(path).map_err(ImageError::from)?;
94        Self::try_from(&image)
95    }
96
97    /// Checks whether the image data is empty.
98    ///
99    /// # Returns
100    /// `true` if the image data is empty, `false` otherwise.
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.data.is_empty()
104    }
105
106    /// Returns the width of the image data.
107    ///
108    /// # Returns
109    /// The width of the image data.
110    #[inline]
111    #[must_use]
112    pub fn width(&self) -> u32 {
113        self.width
114    }
115
116    /// Returns the height of the image data.
117    ///
118    /// # Returns
119    /// The height of the image data.
120    #[inline]
121    #[must_use]
122    pub fn height(&self) -> u32 {
123        self.height
124    }
125
126    /// Returns the area of the image data.
127    ///
128    /// # Returns
129    /// The area of the image data.
130    #[must_use]
131    pub fn area(&self) -> usize {
132        self.width as usize * self.height as usize
133    }
134
135    /// Returns the pixel data of the image data.
136    ///
137    /// Each pixel is represented by 4 bytes in RGBA (Red, Green, Blue, Alpha) format.
138    ///
139    /// # Returns
140    /// The pixel data of the image data.
141    #[must_use]
142    pub fn data(&self) -> &[u8] {
143        &self.data
144    }
145
146    /// Returns an iterator over the pixels of the image data.
147    ///
148    /// # Returns
149    /// An iterator over the pixels of the image data.
150    #[allow(dead_code)]
151    pub(crate) fn pixels<'b, T>(&'b self) -> impl Iterator<Item = Pixel<T>> + 'b
152    where
153        T: FloatNumber + 'b,
154    {
155        self.data
156            .chunks_exact(RGBA_CHANNELS)
157            .enumerate()
158            .map(move |(index, rgba)| self.chunk_to_pixel(index, rgba))
159    }
160
161    /// Returns an iterator over the pixels of the image data together with the result of applying the `filter`.
162    ///
163    /// # Type Parameters
164    /// * `T` - The floating point type.
165    /// * `F` - The filter type.
166    ///
167    /// # Arguments
168    /// * `filter` - The filter to apply to the pixels.
169    ///
170    /// # Returns
171    /// An iterator over the pixels of the image data and the result of applying the filter.
172    pub(crate) fn pixels_with_filter<'b, T, F>(
173        &'b self,
174        filter: &'b F,
175    ) -> impl Iterator<Item = (Pixel<T>, bool)> + 'b
176    where
177        T: FloatNumber + 'b,
178        F: Filter,
179    {
180        self.data
181            .chunks_exact(RGBA_CHANNELS)
182            .enumerate()
183            .map(move |(index, chunk)| {
184                (
185                    self.chunk_to_pixel::<T>(index, chunk),
186                    filter.test(&[chunk[0], chunk[1], chunk[2], chunk[3]]),
187                )
188            })
189    }
190
191    /// Converts a chunk of pixel data to a pixel representation.
192    ///
193    /// # Type Parameters
194    /// * `T` - The floating point type.
195    ///
196    /// # Arguments
197    /// * `index` - The index of the pixel in the image data.
198    /// * `chunk` - The chunk of pixel data.
199    ///
200    /// # Returns
201    /// The pixel representation of the chunk of pixel data.
202    #[inline(always)]
203    #[must_use]
204    fn chunk_to_pixel<T>(&self, index: usize, chunk: &[u8]) -> Pixel<T>
205    where
206        T: FloatNumber,
207    {
208        let (x, y, z) = rgb_to_xyz::<T>(chunk[0], chunk[1], chunk[2]);
209        let (l, a, b) = xyz_to_lab::<T, D65>(x, y, z);
210
211        let coord_x = T::from_usize((index % self.width as usize) + 1);
212        let coord_y = T::from_usize((index / self.width as usize) + 1);
213
214        let width_f = T::from_u32(self.width);
215        let height_f = T::from_u32(self.height);
216
217        [
218            Lab::<T>::normalize_l(l),
219            Lab::<T>::normalize_a(a),
220            Lab::<T>::normalize_b(b),
221            normalize(coord_x, T::zero(), width_f),
222            normalize(coord_y, T::zero(), height_f),
223        ]
224    }
225}
226
227#[cfg(feature = "image")]
228impl TryFrom<&DynamicImage> for ImageData<'_> {
229    type Error = ImageError;
230
231    fn try_from(image: &DynamicImage) -> Result<Self, Self::Error> {
232        match image {
233            DynamicImage::ImageRgb8(image) => Ok(Self::from(image)),
234            DynamicImage::ImageRgba8(image) => Ok(Self::from(image)),
235            _ => Err(ImageError::UnsupportedFormat),
236        }
237    }
238}
239
240#[cfg(feature = "image")]
241impl From<&RgbImage> for ImageData<'_> {
242    fn from(image: &RgbImage) -> Self {
243        let (width, height) = image.dimensions();
244        let size = (width * height) as usize;
245        let data = image.pixels().fold(
246            Vec::with_capacity(size * RGBA_CHANNELS),
247            |mut pixels, pixel| {
248                pixels.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]);
249                pixels
250            },
251        );
252        Self {
253            width,
254            height,
255            data: data.into(),
256        }
257    }
258}
259
260#[cfg(feature = "image")]
261impl From<&RgbaImage> for ImageData<'_> {
262    fn from(image: &RgbaImage) -> Self {
263        let (width, height) = image.dimensions();
264        let data = image.to_vec();
265        Self {
266            width,
267            height,
268            data: data.into(),
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::{assert_approx_eq, Rgba};
277
278    #[test]
279    fn test_new() {
280        // Arrange
281        let pixels = [
282            255, 255, 255, 255, // White
283            255, 255, 255, 255, // White
284            255, 255, 255, 255, // White
285            255, 255, 255, 255, // White
286        ];
287
288        // Act
289        let actual = ImageData::new(2, 2, &pixels);
290
291        // Assert
292        assert!(actual.is_ok());
293
294        let image_data = actual.unwrap();
295        assert!(!image_data.is_empty());
296        assert_eq!(image_data.width(), 2);
297        assert_eq!(image_data.height(), 2);
298        assert_eq!(image_data.area(), 4);
299        assert_eq!(image_data.data(), &pixels);
300    }
301
302    #[test]
303    fn test_new_empty_data() {
304        // Arrange
305        let pixels = [];
306
307        // Act
308        let actual = ImageData::new(0, 0, &pixels);
309
310        // Assert
311        assert!(actual.is_ok());
312
313        let image_data = actual.unwrap();
314        assert!(image_data.is_empty());
315        assert_eq!(image_data.width(), 0);
316        assert_eq!(image_data.height(), 0);
317        assert_eq!(image_data.area(), 0);
318        assert_eq!(image_data.data(), &pixels);
319    }
320
321    #[test]
322    fn test_new_unexpected_length() {
323        // Arrange
324        let pixels = [255, 255, 255, 255];
325
326        // Act
327        let actual = ImageData::new(2, 2, &pixels);
328
329        // Assert
330        assert!(actual.is_err());
331
332        let error = actual.unwrap_err();
333        assert_eq!(
334            error.to_string(),
335            "Unexpected data length - expected 16, got 4"
336        );
337    }
338
339    #[cfg(feature = "image")]
340    #[test]
341    fn test_load_supported_format() {
342        // Act
343        let actual = ImageData::load("../../gfx/parrots_rgba8.png");
344
345        // Assert
346        assert!(actual.is_ok());
347
348        let image_data = actual.unwrap();
349        assert!(!image_data.is_empty());
350        assert_eq!(image_data.width(), 150);
351        assert_eq!(image_data.height(), 150);
352        assert_eq!(image_data.area(), 150 * 150);
353        assert_eq!(image_data.data().len(), 150 * 150 * 4);
354    }
355
356    #[cfg(feature = "image")]
357    #[test]
358    fn test_load_unsupported_format() {
359        // Act
360        let actual = ImageData::load("../../gfx/parrots_la16.png");
361
362        // Assert
363        assert!(actual.is_err());
364
365        let error = actual.unwrap_err();
366        assert_eq!(error.to_string(), "Unsupported image format or color type");
367    }
368
369    #[cfg(all(feature = "image", not(target_os = "windows")))]
370    #[test]
371    fn test_load_unknown_path() {
372        // Act
373        let actual = ImageData::load("../../gfx/unknown.png");
374
375        // Assert
376        assert!(actual.is_err());
377
378        let error = actual.unwrap_err();
379        assert_eq!(
380            error.to_string(),
381            "Failed to load image from file: No such file or directory (os error 2)"
382        );
383    }
384
385    #[cfg(all(feature = "image", target_os = "windows"))]
386    #[test]
387    fn test_load_unknown_path_windows() {
388        // Act
389        let actual = ImageData::load("../../gfx/unknown.png");
390
391        // Assert
392        assert!(actual.is_err());
393
394        let error = actual.unwrap_err();
395        assert_eq!(
396            error.to_string(),
397            "Failed to load image from file: The system cannot find the file specified. (os error 2)"
398        );
399    }
400
401    #[cfg(all(feature = "image", not(target_os = "windows")))]
402    #[test]
403    fn test_load_invalid_file() {
404        // Act
405        let actual = ImageData::load("../../gfx/colors/invalid.jpg");
406
407        // Assert
408        assert!(actual.is_err());
409
410        let error = actual.unwrap_err();
411        assert_eq!(
412            error.to_string(),
413            "Failed to load image from file: No such file or directory (os error 2)"
414        );
415    }
416
417    #[cfg(all(feature = "image", target_os = "windows"))]
418    #[test]
419    fn test_load_invalid_file_windows() {
420        // Act
421        let actual = ImageData::load("../../gfx/colors/invalid.jpg");
422
423        // Assert
424        assert!(actual.is_err());
425
426        let error = actual.unwrap_err();
427        assert_eq!(
428            error.to_string(),
429            "Failed to load image from file: The system cannot find the file specified. (os error 2)"
430        );
431    }
432
433    #[test]
434    fn test_pixels_iter() {
435        // Arrange
436        let pixels = [
437            255, 0, 0, 255, // Red
438            0, 0, 0, 0, // Transparent
439            255, 255, 0, 255, // Yellow
440            0, 0, 0, 0, // Transparent
441        ];
442        let image_data = ImageData::new(2, 2, &pixels).unwrap();
443
444        // Act
445        let actual: Vec<_> = image_data.pixels::<f64>().collect();
446
447        // Assert
448        assert_eq!(actual.len(), 4);
449
450        let pixel = actual[0];
451        assert_approx_eq!(pixel[0], 0.532371);
452        assert_approx_eq!(pixel[1], 0.816032);
453        assert_approx_eq!(pixel[2], 0.765488);
454        assert_approx_eq!(pixel[3], 0.5);
455        assert_approx_eq!(pixel[4], 0.5);
456
457        let pixel = actual[1];
458        assert_approx_eq!(pixel[0], 0.0);
459        assert_approx_eq!(pixel[1], 0.501960);
460        assert_approx_eq!(pixel[2], 0.501960);
461        assert_approx_eq!(pixel[3], 1.0);
462        assert_approx_eq!(pixel[4], 0.5);
463
464        let pixel = actual[2];
465        assert_approx_eq!(pixel[0], 0.971385);
466        assert_approx_eq!(pixel[1], 0.417402);
467        assert_approx_eq!(pixel[2], 0.872457);
468        assert_approx_eq!(pixel[3], 0.5);
469        assert_approx_eq!(pixel[4], 1.0);
470
471        let pixel = actual[3];
472        assert_approx_eq!(pixel[0], 0.0);
473        assert_approx_eq!(pixel[1], 0.501960);
474        assert_approx_eq!(pixel[2], 0.501960);
475        assert_approx_eq!(pixel[3], 1.0);
476        assert_approx_eq!(pixel[4], 1.0);
477    }
478
479    #[test]
480    fn test_pixels_with_filter() {
481        // Arrange
482        let data = [
483            255, 0, 0, 255, // Red
484            0, 0, 0, 0, // Transparent
485            255, 255, 0, 255, // Yellow
486            0, 0, 0, 0, // Transparent
487        ];
488        let image_data = ImageData::new(2, 2, &data).unwrap();
489
490        // Act
491        let (pixels, mask) = image_data
492            .pixels_with_filter::<f64, _>(&|rgba: &Rgba| rgba[3] != 0)
493            .fold(
494                (Vec::new(), Vec::new()),
495                |(mut pixels, mut mask), (pixel, m)| {
496                    pixels.push(pixel);
497                    mask.push(m);
498                    (pixels, mask)
499                },
500            );
501
502        // Assert
503        assert_eq!(pixels.len(), 4);
504        assert_eq!(mask.len(), 4);
505        assert_eq!(mask, vec![true, false, true, false]);
506    }
507}