Skip to main content

cranpose_ui_graphics/
image.rs

1//! Image bitmap primitives used by render backends.
2
3use std::{
4    borrow::Cow,
5    hash::{BuildHasher, Hash, Hasher},
6    sync::Arc,
7};
8
9use thiserror::Error;
10
11use crate::{BlendMode, Color, Size};
12
13/// Errors returned while constructing an [`ImageBitmap`].
14#[derive(Debug, Clone, PartialEq, Eq, Error)]
15pub enum ImageBitmapError {
16    #[error("image dimensions must be greater than zero")]
17    InvalidDimensions,
18    #[error("image dimensions are too large")]
19    DimensionsTooLarge,
20    #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
21    PixelDataLengthMismatch { expected: usize, actual: usize },
22}
23
24/// Storage format of an immutable bitmap.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum ImagePixelFormat {
27    /// Four bytes per pixel, ordered red, green, blue, alpha.
28    Rgba8,
29    /// One alpha byte per pixel and a constant, unpremultiplied RGB color.
30    Alpha8 {
31        /// RGB channels shared by every pixel, including transparent pixels.
32        color: [u8; 3],
33    },
34}
35
36/// Immutable image data used by UI primitives and render backends.
37#[derive(Clone, Debug)]
38pub struct ImageBitmap {
39    data: Arc<ImageBitmapData>,
40}
41
42#[derive(Debug)]
43struct ImageBitmapData {
44    width: u32,
45    height: u32,
46    id: u64,
47    opaque: bool,
48    format: ImagePixelFormat,
49    pixels: Box<[u8]>,
50}
51
52/// Texture sampling mode for image primitives.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
54pub enum ImageSampling {
55    /// Preserve source texels exactly. Use this for atlases, pixel art, and UI skins.
56    #[default]
57    Nearest,
58    /// Interpolate adjacent texels. Use this for photographic or continuously scaled images.
59    Linear,
60}
61
62/// Simple image color filter model.
63#[derive(Clone, Copy, Debug, PartialEq)]
64pub enum ColorFilter {
65    /// Compose-style tint using `BlendMode::SrcIn`.
66    Tint(Color),
67    /// Explicit per-channel modulation (multiply behavior).
68    Modulate(Color),
69    /// 4x5 color matrix in row-major order.
70    ///
71    /// Rows map output RGBA channels, columns map input RGBA plus constant term:
72    /// `out = M * [r, g, b, a, 1]`.
73    Matrix([f32; 20]),
74}
75
76impl ColorFilter {
77    /// Creates a Compose-style tint filter (`SrcIn`).
78    pub fn tint(color: Color) -> Self {
79        Self::Tint(color)
80    }
81
82    /// Creates an explicit modulation filter that multiplies channels by `color`.
83    pub fn modulate(color: Color) -> Self {
84        Self::Modulate(color)
85    }
86
87    /// Creates a filter from a 4x5 color matrix.
88    pub fn matrix(matrix: [f32; 20]) -> Self {
89        Self::Matrix(matrix)
90    }
91
92    pub fn compose(self, next: ColorFilter) -> ColorFilter {
93        ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
94    }
95
96    pub fn as_matrix(self) -> [f32; 20] {
97        match self {
98            Self::Tint(tint) => [
99                0.0,
100                0.0,
101                0.0,
102                tint.r(),
103                0.0,
104                0.0,
105                0.0,
106                0.0,
107                tint.g(),
108                0.0,
109                0.0,
110                0.0,
111                0.0,
112                tint.b(),
113                0.0,
114                0.0,
115                0.0,
116                0.0,
117                tint.a(),
118                0.0,
119            ],
120            Self::Modulate(modulate) => [
121                modulate.r(),
122                0.0,
123                0.0,
124                0.0,
125                0.0,
126                0.0,
127                modulate.g(),
128                0.0,
129                0.0,
130                0.0,
131                0.0,
132                0.0,
133                modulate.b(),
134                0.0,
135                0.0,
136                0.0,
137                0.0,
138                0.0,
139                modulate.a(),
140                0.0,
141            ],
142            Self::Matrix(matrix) => matrix,
143        }
144    }
145
146    pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
147        apply_color_matrix(self.as_matrix(), rgba)
148    }
149
150    pub fn supports_gpu_vertex_modulation(self) -> bool {
151        matches!(self, Self::Modulate(_))
152    }
153
154    pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
155        match self {
156            Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
157            _ => None,
158        }
159    }
160
161    pub fn blend_mode(self) -> BlendMode {
162        match self {
163            Self::Tint(_) => BlendMode::SrcIn,
164            Self::Modulate(_) => BlendMode::Modulate,
165            Self::Matrix(_) => BlendMode::SrcOver,
166        }
167    }
168}
169
170fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
171    let r = rgba[0];
172    let g = rgba[1];
173    let b = rgba[2];
174    let a = rgba[3];
175    [
176        (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
177        (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
178        (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
179            .clamp(0.0, 1.0),
180        (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
181            .clamp(0.0, 1.0),
182    ]
183}
184
185fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
186    let mut composed = [0.0f32; 20];
187    for row in 0..4 {
188        let row_base = row * 5;
189        let s0 = second[row_base];
190        let s1 = second[row_base + 1];
191        let s2 = second[row_base + 2];
192        let s3 = second[row_base + 3];
193        let s4 = second[row_base + 4];
194
195        composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
196        composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
197        composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
198        composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
199        composed[row_base + 4] =
200            s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
201    }
202    composed
203}
204
205impl ImageBitmap {
206    /// Creates a bitmap by taking ownership of tightly packed RGBA8 pixels.
207    /// Any spare capacity in the pixel buffer is released.
208    pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
209        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
210    }
211
212    /// Copies tightly packed RGBA8 pixels into a new bitmap.
213    pub fn from_rgba8_slice(
214        width: u32,
215        height: u32,
216        pixels: &[u8],
217    ) -> Result<Self, ImageBitmapError> {
218        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
219    }
220
221    /// Takes ownership of one alpha byte per pixel with a constant RGB color.
222    /// Any spare capacity in the alpha buffer is released.
223    pub fn from_alpha8(
224        width: u32,
225        height: u32,
226        color: [u8; 3],
227        alpha: Vec<u8>,
228    ) -> Result<Self, ImageBitmapError> {
229        Self::from_pixels(width, height, ImagePixelFormat::Alpha8 { color }, alpha)
230    }
231
232    fn from_pixels(
233        width: u32,
234        height: u32,
235        format: ImagePixelFormat,
236        pixels: impl AsRef<[u8]> + Into<Box<[u8]>>,
237    ) -> Result<Self, ImageBitmapError> {
238        if width == 0 || height == 0 {
239            return Err(ImageBitmapError::InvalidDimensions);
240        }
241        let expected = (width as usize)
242            .checked_mul(height as usize)
243            .and_then(|value| value.checked_mul(4))
244            .ok_or(ImageBitmapError::DimensionsTooLarge)?;
245        let expected = match format {
246            ImagePixelFormat::Rgba8 => expected,
247            ImagePixelFormat::Alpha8 { .. } => expected / 4,
248        };
249
250        let bytes = pixels.as_ref();
251        if bytes.len() != expected {
252            return Err(ImageBitmapError::PixelDataLengthMismatch {
253                expected,
254                actual: bytes.len(),
255            });
256        }
257
258        let id = bitmap_content_id(width, height, format, bytes);
259        let opaque = match format {
260            ImagePixelFormat::Rgba8 => bytes
261                .as_chunks::<4>()
262                .0
263                .iter()
264                .all(|pixel| pixel[3] == u8::MAX),
265            ImagePixelFormat::Alpha8 { .. } => bytes.iter().all(|alpha| *alpha == u8::MAX),
266        };
267        Ok(Self {
268            data: Arc::new(ImageBitmapData {
269                width,
270                height,
271                id,
272                opaque,
273                format,
274                pixels: pixels.into(),
275            }),
276        })
277    }
278
279    /// Content-derived bitmap identity used by renderer caches.
280    pub fn id(&self) -> u64 {
281        self.data.id
282    }
283
284    /// Width in pixels.
285    pub fn width(&self) -> u32 {
286        self.data.width
287    }
288
289    /// Height in pixels.
290    pub fn height(&self) -> u32 {
291        self.data.height
292    }
293
294    /// Returns the raw pixel bytes in [`Self::format`].
295    pub fn pixels(&self) -> &[u8] {
296        &self.data.pixels
297    }
298
299    /// Returns the storage format of [`Self::pixels`].
300    pub fn format(&self) -> ImagePixelFormat {
301        self.data.format
302    }
303
304    /// Returns the unpremultiplied RGBA8 pixel at a row-major index.
305    ///
306    /// # Panics
307    /// Panics when `index` is outside the image.
308    pub fn rgba8_pixel(&self, index: usize) -> [u8; 4] {
309        match self.format() {
310            ImagePixelFormat::Rgba8 => self.pixels().as_chunks::<4>().0[index],
311            ImagePixelFormat::Alpha8 { color: [r, g, b] } => [r, g, b, self.pixels()[index]],
312        }
313    }
314
315    /// Returns tightly packed RGBA8 bytes, borrowing RGBA images and expanding alpha masks.
316    pub fn rgba8_pixels(&self) -> Cow<'_, [u8]> {
317        match self.format() {
318            ImagePixelFormat::Rgba8 => Cow::Borrowed(self.pixels()),
319            ImagePixelFormat::Alpha8 { color: [r, g, b] } => {
320                let mut pixels = Vec::with_capacity(self.pixels().len() * 4);
321                for &alpha in self.pixels() {
322                    pixels.extend_from_slice(&[r, g, b, alpha]);
323                }
324                Cow::Owned(pixels)
325            }
326        }
327    }
328
329    /// Returns true when every source pixel has full alpha.
330    pub fn is_opaque(&self) -> bool {
331        self.data.opaque
332    }
333
334    /// Returns intrinsic size in logical units.
335    pub fn intrinsic_size(&self) -> Size {
336        Size {
337            width: self.data.width as f32,
338            height: self.data.height as f32,
339        }
340    }
341}
342
343impl PartialEq for ImageBitmap {
344    fn eq(&self, other: &Self) -> bool {
345        self.id() == other.id()
346    }
347}
348
349impl Eq for ImageBitmap {}
350
351impl Hash for ImageBitmap {
352    fn hash<H: Hasher>(&self, state: &mut H) {
353        self.id().hash(state);
354    }
355}
356
357fn bitmap_content_id(width: u32, height: u32, format: ImagePixelFormat, pixels: &[u8]) -> u64 {
358    let mut hasher = foldhash::quality::FixedState::default().build_hasher();
359    width.hash(&mut hasher);
360    height.hash(&mut hasher);
361    format.hash(&mut hasher);
362    pixels.hash(&mut hasher);
363    hasher.finish()
364}
365
366#[cfg(test)]
367#[path = "tests/image_tests.rs"]
368mod tests;