1use crate::{BlendMode, Color, Size};
4use std::hash::{Hash, Hasher};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7use thiserror::Error;
8
9static NEXT_IMAGE_BITMAP_ID: AtomicU64 = AtomicU64::new(1);
10
11#[derive(Debug, Clone, PartialEq, Eq, Error)]
13pub enum ImageBitmapError {
14 #[error("image dimensions must be greater than zero")]
15 InvalidDimensions,
16 #[error("image dimensions are too large")]
17 DimensionsTooLarge,
18 #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
19 PixelDataLengthMismatch { expected: usize, actual: usize },
20}
21
22#[derive(Clone, Debug)]
24pub struct ImageBitmap {
25 id: u64,
26 width: u32,
27 height: u32,
28 pixels: Arc<[u8]>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
33pub enum ColorFilter {
34 Tint(Color),
36 Modulate(Color),
38 Matrix([f32; 20]),
43}
44
45impl ColorFilter {
46 pub fn tint(color: Color) -> Self {
48 Self::Tint(color)
49 }
50
51 pub fn modulate(color: Color) -> Self {
53 Self::Modulate(color)
54 }
55
56 pub fn matrix(matrix: [f32; 20]) -> Self {
58 Self::Matrix(matrix)
59 }
60
61 pub fn compose(self, next: ColorFilter) -> ColorFilter {
62 ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
63 }
64
65 pub fn as_matrix(self) -> [f32; 20] {
66 match self {
67 Self::Tint(tint) => [
68 0.0,
69 0.0,
70 0.0,
71 tint.r(),
72 0.0, 0.0,
74 0.0,
75 0.0,
76 tint.g(),
77 0.0, 0.0,
79 0.0,
80 0.0,
81 tint.b(),
82 0.0, 0.0,
84 0.0,
85 0.0,
86 tint.a(),
87 0.0, ],
89 Self::Modulate(modulate) => [
90 modulate.r(),
91 0.0,
92 0.0,
93 0.0,
94 0.0, 0.0,
96 modulate.g(),
97 0.0,
98 0.0,
99 0.0, 0.0,
101 0.0,
102 modulate.b(),
103 0.0,
104 0.0, 0.0,
106 0.0,
107 0.0,
108 modulate.a(),
109 0.0, ],
111 Self::Matrix(matrix) => matrix,
112 }
113 }
114
115 pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
116 apply_color_matrix(self.as_matrix(), rgba)
117 }
118
119 pub fn supports_gpu_vertex_modulation(self) -> bool {
120 matches!(self, Self::Modulate(_))
121 }
122
123 pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
124 match self {
125 Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
126 _ => None,
127 }
128 }
129
130 pub fn blend_mode(self) -> BlendMode {
131 match self {
132 Self::Tint(_) => BlendMode::SrcIn,
133 Self::Modulate(_) => BlendMode::Modulate,
134 Self::Matrix(_) => BlendMode::SrcOver,
135 }
136 }
137}
138
139fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
140 let r = rgba[0];
141 let g = rgba[1];
142 let b = rgba[2];
143 let a = rgba[3];
144 [
145 (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
146 (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
147 (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
148 .clamp(0.0, 1.0),
149 (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
150 .clamp(0.0, 1.0),
151 ]
152}
153
154fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
155 let mut composed = [0.0f32; 20];
156 for row in 0..4 {
157 let row_base = row * 5;
158 let s0 = second[row_base];
159 let s1 = second[row_base + 1];
160 let s2 = second[row_base + 2];
161 let s3 = second[row_base + 3];
162 let s4 = second[row_base + 4];
163
164 composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
165 composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
166 composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
167 composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
168 composed[row_base + 4] =
169 s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
170 }
171 composed
172}
173
174impl ImageBitmap {
175 pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
177 Self::from_rgba8_slice(width, height, &pixels)
178 }
179
180 pub fn from_rgba8_slice(
182 width: u32,
183 height: u32,
184 pixels: &[u8],
185 ) -> Result<Self, ImageBitmapError> {
186 if width == 0 || height == 0 {
187 return Err(ImageBitmapError::InvalidDimensions);
188 }
189 let expected = (width as usize)
190 .checked_mul(height as usize)
191 .and_then(|value| value.checked_mul(4))
192 .ok_or(ImageBitmapError::DimensionsTooLarge)?;
193
194 if pixels.len() != expected {
195 return Err(ImageBitmapError::PixelDataLengthMismatch {
196 expected,
197 actual: pixels.len(),
198 });
199 }
200
201 Ok(Self {
202 id: NEXT_IMAGE_BITMAP_ID.fetch_add(1, Ordering::Relaxed),
203 width,
204 height,
205 pixels: Arc::from(pixels),
206 })
207 }
208
209 pub fn id(&self) -> u64 {
211 self.id
212 }
213
214 pub fn width(&self) -> u32 {
216 self.width
217 }
218
219 pub fn height(&self) -> u32 {
221 self.height
222 }
223
224 pub fn pixels(&self) -> &[u8] {
226 &self.pixels
227 }
228
229 pub fn intrinsic_size(&self) -> Size {
231 Size {
232 width: self.width as f32,
233 height: self.height as f32,
234 }
235 }
236}
237
238impl PartialEq for ImageBitmap {
239 fn eq(&self, other: &Self) -> bool {
240 self.id == other.id
241 }
242}
243
244impl Eq for ImageBitmap {}
245
246impl Hash for ImageBitmap {
247 fn hash<H: Hasher>(&self, state: &mut H) {
248 self.id.hash(state);
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn from_rgba8_accepts_valid_data() {
258 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
259 .expect("valid bitmap");
260
261 assert_eq!(bitmap.width(), 2);
262 assert_eq!(bitmap.height(), 1);
263 assert_eq!(bitmap.pixels().len(), 8);
264 }
265
266 #[test]
267 fn from_rgba8_rejects_zero_dimensions() {
268 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
269 assert_eq!(err, ImageBitmapError::InvalidDimensions);
270 }
271
272 #[test]
273 fn from_rgba8_rejects_wrong_pixel_length() {
274 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
275 assert_eq!(
276 err,
277 ImageBitmapError::PixelDataLengthMismatch {
278 expected: 16,
279 actual: 15,
280 }
281 );
282 }
283
284 #[test]
285 fn from_rgba8_slice_accepts_valid_data() {
286 let pixels = [255u8, 0, 0, 255];
287 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
288 assert_eq!(bitmap.pixels(), &pixels);
289 }
290
291 #[test]
292 fn ids_are_unique() {
293 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
294 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
295 assert_ne!(a.id(), b.id());
296 }
297
298 #[test]
299 fn intrinsic_size_matches_dimensions() {
300 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
301 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
302 }
303
304 #[test]
305 fn tint_filter_multiplies_channels() {
306 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
307 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
308 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
309 assert!((tinted[1] - 0.5).abs() < 1e-5);
310 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
311 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
312 }
313
314 #[test]
315 fn tint_constructor_matches_variant() {
316 let color = Color::from_rgba_u8(10, 20, 30, 40);
317 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
318 }
319
320 #[test]
321 fn tint_filter_uses_src_in_behavior() {
322 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
323 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
324 assert!((tinted[0] - 0.25).abs() < 1e-5);
325 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
326 assert!(tinted[2].abs() < 1e-5);
327 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
328 }
329
330 #[test]
331 fn matrix_filter_transforms_channels() {
332 let matrix = [
333 1.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ];
338 let filter = ColorFilter::matrix(matrix);
339 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
340 assert!((transformed[0] - 0.3).abs() < 1e-5);
341 assert!((transformed[1] - 0.3).abs() < 1e-5);
342 assert!((transformed[2] - 0.4).abs() < 1e-5);
343 assert!((transformed[3] - 0.4).abs() < 1e-5);
344 }
345
346 #[test]
347 fn filter_compose_applies_in_order() {
348 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
349 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
350 let chained = first.compose(second);
351 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
352 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
353 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
354 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
355 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
356 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
357 }
358}