Skip to main content

flake/misc/
flake_image.rs

1use crate::prelude::{Color, FilterType};
2use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
3
4/// # Image
5///
6/// Contains pixel data for a `width * height` image in RGBA format.
7/// Data is stored as `4 * w * h` 8-bit r, g, b, a values.
8#[derive(Clone, Default, Debug)]
9pub struct Image {
10    pub width: u32,
11    pub height: u32,
12    pub data: Vec<u8>,
13    pub filter_type: FilterType,
14}
15
16impl Image {
17    /// Creates a new black/transparent image filled with zeros (RGBA 8-bit).
18    pub fn new(width: u32, height: u32, filter_type: FilterType) -> Self {
19        Self {
20            width,
21            height,
22            data: vec![0; 4 * width as usize * height as usize],
23            filter_type,
24        }
25    }
26
27    /// Wraps an existing Vec<u8> RGBA buffer.
28    pub fn from_data(width: u32, height: u32, filter_type: FilterType, data: Vec<u8>) -> Self {
29        debug_assert_eq!(
30            data.len(),
31            4 * width as usize * height as usize,
32            "Data buffer size does not match width * height * 4"
33        );
34
35        Self {
36            width,
37            height,
38            data,
39            filter_type,
40        }
41    }
42
43    /// Creates an image from a collection of Colors.
44    ///
45    /// Color channels are expected to be normalized to [0.0, 1.0]
46    /// and are converted to 8-bit [0, 255] values.
47    pub fn from_pixels(
48        width: u32,
49        height: u32,
50        filter_type: FilterType,
51        pixels: Vec<Color>,
52    ) -> Self {
53        debug_assert_eq!(
54            pixels.len(),
55            width as usize * height as usize,
56            "Pixel buffer size does not match width * height"
57        );
58
59        let mut data = Vec::with_capacity(pixels.len() * 4);
60
61        for color in pixels {
62            let [r, g, b, a] = color.to_array();
63
64            data.extend_from_slice(&[
65                (r.clamp(0.0, 1.0) * 255.0) as u8,
66                (g.clamp(0.0, 1.0) * 255.0) as u8,
67                (b.clamp(0.0, 1.0) * 255.0) as u8,
68                (a.clamp(0.0, 1.0) * 255.0) as u8,
69            ]);
70        }
71
72        Self {
73            width,
74            height,
75            data,
76            filter_type,
77        }
78    }
79
80    /// Returns image with filter type.
81    pub fn with_filter_type(mut self, filter_type: FilterType) -> Self {
82        self.filter_type = filter_type;
83        self
84    }
85
86    /// Calculates the starting 1D array index for pixel (x, y).
87    #[inline]
88    fn pixel_index(&self, x: u32, y: u32) -> usize {
89        ((y * self.width + x) * 4) as usize
90    }
91
92    /// Sets the RGBA color [r, g, b, a] for pixel at (x, y).
93    ///
94    /// Color channels are expected to be normalized to [0.0, 1.0].
95    pub fn set_pixel(&mut self, x: u32, y: u32, color: Color) {
96        if x < self.width && y < self.height {
97            let idx = self.pixel_index(x, y);
98            let [r, g, b, a] = color.to_array();
99
100            self.data[idx] = (r.clamp(0.0, 1.0) * 255.0) as u8;
101            self.data[idx + 1] = (g.clamp(0.0, 1.0) * 255.0) as u8;
102            self.data[idx + 2] = (b.clamp(0.0, 1.0) * 255.0) as u8;
103            self.data[idx + 3] = (a.clamp(0.0, 1.0) * 255.0) as u8;
104        }
105    }
106
107    /// Gets the RGBA color [r, g, b, a] at (x, y).
108    /// Returns None if out of bounds.
109    ///
110    /// Color channels are returned normalized to [0.0, 1.0].
111    pub fn get_pixel(&self, x: u32, y: u32) -> Option<Color> {
112        if x < self.width && y < self.height {
113            let idx = self.pixel_index(x, y);
114
115            Some(Color::new(
116                self.data[idx] as f32 / 255.0,
117                self.data[idx + 1] as f32 / 255.0,
118                self.data[idx + 2] as f32 / 255.0,
119                self.data[idx + 3] as f32 / 255.0,
120            ))
121        } else {
122            None
123        }
124    }
125
126    /// Clears the image using a single RGBA color.
127    ///
128    /// Color channels are expected to be normalized to [0.0, 1.0].
129    pub fn clear(&mut self, color: [f32; 4]) {
130        let color = [
131            (color[0].clamp(0.0, 1.0) * 255.0) as u8,
132            (color[1].clamp(0.0, 1.0) * 255.0) as u8,
133            (color[2].clamp(0.0, 1.0) * 255.0) as u8,
134            (color[3].clamp(0.0, 1.0) * 255.0) as u8,
135        ];
136
137        for chunk in self.data.chunks_exact_mut(4) {
138            chunk.copy_from_slice(&color);
139        }
140    }
141
142    /// Converts this struct directly into `image::DynamicImage`.
143    pub fn to_dynamic(&self) -> DynamicImage {
144        let buffer = ImageBuffer::<Rgba<u8>, Vec<u8>>::from_raw(
145            self.width,
146            self.height,
147            self.data.clone(),
148        )
149            .expect("Failed to create ImageBuffer from Image data");
150
151        DynamicImage::ImageRgba8(buffer)
152    }
153
154    /// Creates an `Image` from an `image::DynamicImage`.
155    ///
156    /// Pixel data is stored directly as normalized 8-bit RGBA.
157    pub fn from_dynamic(dynamic: &DynamicImage, filter_type: FilterType) -> Self {
158        let rgba = dynamic.to_rgba8();
159        let width = rgba.width();
160        let height = rgba.height();
161        let data = rgba.into_raw();
162
163        Self {
164            width,
165            height,
166            data,
167            filter_type,
168        }
169    }
170
171    /// Converts this image to an 8-bit RGBA ImageBuffer.
172    ///
173    /// This clones the underlying data because the returned ImageBuffer owns it.
174    pub fn to_rgba8_buffer(&self) -> RgbaImage {
175        ImageBuffer::from_raw(self.width, self.height, self.data.clone())
176            .expect("Failed to create RGBA8 ImageBuffer")
177    }
178}