Skip to main content

cranpose_ui_graphics/
image.rs

1//! Image bitmap primitives used by render backends.
2
3use std::{
4    hash::{BuildHasher, Hash, Hasher},
5    sync::Arc,
6};
7
8use thiserror::Error;
9
10use crate::{BlendMode, Color, Size};
11
12/// Errors returned while constructing an [`ImageBitmap`].
13#[derive(Debug, Clone, PartialEq, Eq, Error)]
14pub enum ImageBitmapError {
15    #[error("image dimensions must be greater than zero")]
16    InvalidDimensions,
17    #[error("image dimensions are too large")]
18    DimensionsTooLarge,
19    #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
20    PixelDataLengthMismatch { expected: usize, actual: usize },
21}
22
23/// Immutable RGBA image data used by UI primitives and render backends.
24#[derive(Clone, Debug)]
25pub struct ImageBitmap {
26    width: u32,
27    height: u32,
28    id: u64,
29    opaque: bool,
30    pixels: Arc<[u8]>,
31}
32
33/// Texture sampling mode for image primitives.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum ImageSampling {
36    /// Preserve source texels exactly. Use this for atlases, pixel art, and UI skins.
37    #[default]
38    Nearest,
39    /// Interpolate adjacent texels. Use this for photographic or continuously scaled images.
40    Linear,
41}
42
43/// Simple image color filter model.
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum ColorFilter {
46    /// Compose-style tint using `BlendMode::SrcIn`.
47    Tint(Color),
48    /// Explicit per-channel modulation (multiply behavior).
49    Modulate(Color),
50    /// 4x5 color matrix in row-major order.
51    ///
52    /// Rows map output RGBA channels, columns map input RGBA plus constant term:
53    /// `out = M * [r, g, b, a, 1]`.
54    Matrix([f32; 20]),
55}
56
57impl ColorFilter {
58    /// Creates a Compose-style tint filter (`SrcIn`).
59    pub fn tint(color: Color) -> Self {
60        Self::Tint(color)
61    }
62
63    /// Creates an explicit modulation filter that multiplies channels by `color`.
64    pub fn modulate(color: Color) -> Self {
65        Self::Modulate(color)
66    }
67
68    /// Creates a filter from a 4x5 color matrix.
69    pub fn matrix(matrix: [f32; 20]) -> Self {
70        Self::Matrix(matrix)
71    }
72
73    pub fn compose(self, next: ColorFilter) -> ColorFilter {
74        ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
75    }
76
77    pub fn as_matrix(self) -> [f32; 20] {
78        match self {
79            Self::Tint(tint) => [
80                0.0,
81                0.0,
82                0.0,
83                tint.r(),
84                0.0,
85                0.0,
86                0.0,
87                0.0,
88                tint.g(),
89                0.0,
90                0.0,
91                0.0,
92                0.0,
93                tint.b(),
94                0.0,
95                0.0,
96                0.0,
97                0.0,
98                tint.a(),
99                0.0,
100            ],
101            Self::Modulate(modulate) => [
102                modulate.r(),
103                0.0,
104                0.0,
105                0.0,
106                0.0,
107                0.0,
108                modulate.g(),
109                0.0,
110                0.0,
111                0.0,
112                0.0,
113                0.0,
114                modulate.b(),
115                0.0,
116                0.0,
117                0.0,
118                0.0,
119                0.0,
120                modulate.a(),
121                0.0,
122            ],
123            Self::Matrix(matrix) => matrix,
124        }
125    }
126
127    pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
128        apply_color_matrix(self.as_matrix(), rgba)
129    }
130
131    pub fn supports_gpu_vertex_modulation(self) -> bool {
132        matches!(self, Self::Modulate(_))
133    }
134
135    pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
136        match self {
137            Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
138            _ => None,
139        }
140    }
141
142    pub fn blend_mode(self) -> BlendMode {
143        match self {
144            Self::Tint(_) => BlendMode::SrcIn,
145            Self::Modulate(_) => BlendMode::Modulate,
146            Self::Matrix(_) => BlendMode::SrcOver,
147        }
148    }
149}
150
151fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
152    let r = rgba[0];
153    let g = rgba[1];
154    let b = rgba[2];
155    let a = rgba[3];
156    [
157        (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
158        (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
159        (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
160            .clamp(0.0, 1.0),
161        (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
162            .clamp(0.0, 1.0),
163    ]
164}
165
166fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
167    let mut composed = [0.0f32; 20];
168    for row in 0..4 {
169        let row_base = row * 5;
170        let s0 = second[row_base];
171        let s1 = second[row_base + 1];
172        let s2 = second[row_base + 2];
173        let s3 = second[row_base + 3];
174        let s4 = second[row_base + 4];
175
176        composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
177        composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
178        composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
179        composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
180        composed[row_base + 4] =
181            s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
182    }
183    composed
184}
185
186impl ImageBitmap {
187    /// Creates a bitmap from tightly packed RGBA8 pixels.
188    pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
189        Self::from_rgba8_slice(width, height, &pixels)
190    }
191
192    /// Creates a bitmap from tightly packed RGBA8 pixels.
193    pub fn from_rgba8_slice(
194        width: u32,
195        height: u32,
196        pixels: &[u8],
197    ) -> Result<Self, ImageBitmapError> {
198        if width == 0 || height == 0 {
199            return Err(ImageBitmapError::InvalidDimensions);
200        }
201        let expected = (width as usize)
202            .checked_mul(height as usize)
203            .and_then(|value| value.checked_mul(4))
204            .ok_or(ImageBitmapError::DimensionsTooLarge)?;
205
206        if pixels.len() != expected {
207            return Err(ImageBitmapError::PixelDataLengthMismatch {
208                expected,
209                actual: pixels.len(),
210            });
211        }
212
213        let id = bitmap_content_id(width, height, pixels);
214        let opaque = pixels
215            .as_chunks::<4>()
216            .0
217            .iter()
218            .all(|pixel| pixel[3] == u8::MAX);
219        Ok(Self {
220            width,
221            height,
222            id,
223            opaque,
224            pixels: Arc::from(pixels),
225        })
226    }
227
228    /// Content-derived bitmap identity used by renderer caches.
229    pub fn id(&self) -> u64 {
230        self.id
231    }
232
233    /// Width in pixels.
234    pub fn width(&self) -> u32 {
235        self.width
236    }
237
238    /// Height in pixels.
239    pub fn height(&self) -> u32 {
240        self.height
241    }
242
243    /// Returns the raw RGBA8 pixel data.
244    pub fn pixels(&self) -> &[u8] {
245        &self.pixels
246    }
247
248    /// Returns true when every source pixel has full alpha.
249    pub fn is_opaque(&self) -> bool {
250        self.opaque
251    }
252
253    /// Returns intrinsic size in logical units.
254    pub fn intrinsic_size(&self) -> Size {
255        Size {
256            width: self.width as f32,
257            height: self.height as f32,
258        }
259    }
260}
261
262impl PartialEq for ImageBitmap {
263    fn eq(&self, other: &Self) -> bool {
264        self.id() == other.id()
265    }
266}
267
268impl Eq for ImageBitmap {}
269
270impl Hash for ImageBitmap {
271    fn hash<H: Hasher>(&self, state: &mut H) {
272        self.id().hash(state);
273    }
274}
275
276fn bitmap_content_id(width: u32, height: u32, pixels: &[u8]) -> u64 {
277    let mut hasher = foldhash::quality::FixedState::default().build_hasher();
278    width.hash(&mut hasher);
279    height.hash(&mut hasher);
280    pixels.hash(&mut hasher);
281    hasher.finish()
282}
283
284#[cfg(test)]
285#[path = "tests/image_tests.rs"]
286mod tests;