1use crate::{BlendMode, Color, Size};
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6use std::sync::Arc;
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, Error)]
11pub enum ImageBitmapError {
12 #[error("image dimensions must be greater than zero")]
13 InvalidDimensions,
14 #[error("image dimensions are too large")]
15 DimensionsTooLarge,
16 #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
17 PixelDataLengthMismatch { expected: usize, actual: usize },
18}
19
20#[derive(Clone, Debug)]
22pub struct ImageBitmap {
23 width: u32,
24 height: u32,
25 id: u64,
26 opaque: bool,
27 pixels: Arc<[u8]>,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
32pub enum ImageSampling {
33 #[default]
35 Nearest,
36 Linear,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq)]
42pub enum ColorFilter {
43 Tint(Color),
45 Modulate(Color),
47 Matrix([f32; 20]),
52}
53
54impl ColorFilter {
55 pub fn tint(color: Color) -> Self {
57 Self::Tint(color)
58 }
59
60 pub fn modulate(color: Color) -> Self {
62 Self::Modulate(color)
63 }
64
65 pub fn matrix(matrix: [f32; 20]) -> Self {
67 Self::Matrix(matrix)
68 }
69
70 pub fn compose(self, next: ColorFilter) -> ColorFilter {
71 ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
72 }
73
74 pub fn as_matrix(self) -> [f32; 20] {
75 match self {
76 Self::Tint(tint) => [
77 0.0,
78 0.0,
79 0.0,
80 tint.r(),
81 0.0, 0.0,
83 0.0,
84 0.0,
85 tint.g(),
86 0.0, 0.0,
88 0.0,
89 0.0,
90 tint.b(),
91 0.0, 0.0,
93 0.0,
94 0.0,
95 tint.a(),
96 0.0, ],
98 Self::Modulate(modulate) => [
99 modulate.r(),
100 0.0,
101 0.0,
102 0.0,
103 0.0, 0.0,
105 modulate.g(),
106 0.0,
107 0.0,
108 0.0, 0.0,
110 0.0,
111 modulate.b(),
112 0.0,
113 0.0, 0.0,
115 0.0,
116 0.0,
117 modulate.a(),
118 0.0, ],
120 Self::Matrix(matrix) => matrix,
121 }
122 }
123
124 pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
125 apply_color_matrix(self.as_matrix(), rgba)
126 }
127
128 pub fn supports_gpu_vertex_modulation(self) -> bool {
129 matches!(self, Self::Modulate(_))
130 }
131
132 pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
133 match self {
134 Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
135 _ => None,
136 }
137 }
138
139 pub fn blend_mode(self) -> BlendMode {
140 match self {
141 Self::Tint(_) => BlendMode::SrcIn,
142 Self::Modulate(_) => BlendMode::Modulate,
143 Self::Matrix(_) => BlendMode::SrcOver,
144 }
145 }
146}
147
148fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
149 let r = rgba[0];
150 let g = rgba[1];
151 let b = rgba[2];
152 let a = rgba[3];
153 [
154 (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
155 (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
156 (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
157 .clamp(0.0, 1.0),
158 (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
159 .clamp(0.0, 1.0),
160 ]
161}
162
163fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
164 let mut composed = [0.0f32; 20];
165 for row in 0..4 {
166 let row_base = row * 5;
167 let s0 = second[row_base];
168 let s1 = second[row_base + 1];
169 let s2 = second[row_base + 2];
170 let s3 = second[row_base + 3];
171 let s4 = second[row_base + 4];
172
173 composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
174 composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
175 composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
176 composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
177 composed[row_base + 4] =
178 s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
179 }
180 composed
181}
182
183impl ImageBitmap {
184 pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
186 Self::from_rgba8_slice(width, height, &pixels)
187 }
188
189 pub fn from_rgba8_slice(
191 width: u32,
192 height: u32,
193 pixels: &[u8],
194 ) -> Result<Self, ImageBitmapError> {
195 if width == 0 || height == 0 {
196 return Err(ImageBitmapError::InvalidDimensions);
197 }
198 let expected = (width as usize)
199 .checked_mul(height as usize)
200 .and_then(|value| value.checked_mul(4))
201 .ok_or(ImageBitmapError::DimensionsTooLarge)?;
202
203 if pixels.len() != expected {
204 return Err(ImageBitmapError::PixelDataLengthMismatch {
205 expected,
206 actual: pixels.len(),
207 });
208 }
209
210 let id = bitmap_content_id(width, height, pixels);
211 let opaque = pixels.chunks_exact(4).all(|pixel| pixel[3] == u8::MAX);
212 Ok(Self {
213 width,
214 height,
215 id,
216 opaque,
217 pixels: Arc::from(pixels),
218 })
219 }
220
221 pub fn id(&self) -> u64 {
223 self.id
224 }
225
226 pub fn width(&self) -> u32 {
228 self.width
229 }
230
231 pub fn height(&self) -> u32 {
233 self.height
234 }
235
236 pub fn pixels(&self) -> &[u8] {
238 &self.pixels
239 }
240
241 pub fn is_opaque(&self) -> bool {
243 self.opaque
244 }
245
246 pub fn intrinsic_size(&self) -> Size {
248 Size {
249 width: self.width as f32,
250 height: self.height as f32,
251 }
252 }
253}
254
255impl PartialEq for ImageBitmap {
256 fn eq(&self, other: &Self) -> bool {
257 self.id() == other.id()
258 }
259}
260
261impl Eq for ImageBitmap {}
262
263impl Hash for ImageBitmap {
264 fn hash<H: Hasher>(&self, state: &mut H) {
265 self.id().hash(state);
266 }
267}
268
269fn bitmap_content_id(width: u32, height: u32, pixels: &[u8]) -> u64 {
270 let mut hasher = DefaultHasher::new();
271 width.hash(&mut hasher);
272 height.hash(&mut hasher);
273 pixels.hash(&mut hasher);
274 hasher.finish()
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn image_bitmap_ids_do_not_use_process_global_or_allocation_identity() {
283 let source = include_str!("image.rs");
284 let image_counter = ["static ", "NEXT_IMAGE_BITMAP_ID"].concat();
285 let pixel_pointer = ["Arc::", "as_ptr(&self.pixels)"].concat();
286
287 assert!(
288 !source.contains(&image_counter) && !source.contains(&pixel_pointer),
289 "image bitmap ids must be derived from bitmap content, not global counters or allocation addresses"
290 );
291 }
292
293 #[test]
294 fn from_rgba8_accepts_valid_data() {
295 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
296 .expect("valid bitmap");
297
298 assert_eq!(bitmap.width(), 2);
299 assert_eq!(bitmap.height(), 1);
300 assert_eq!(bitmap.pixels().len(), 8);
301 assert!(bitmap.is_opaque());
302 }
303
304 #[test]
305 fn from_rgba8_tracks_transparency() {
306 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 128])
307 .expect("valid bitmap");
308
309 assert!(!bitmap.is_opaque());
310 }
311
312 #[test]
313 fn from_rgba8_rejects_zero_dimensions() {
314 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
315 assert_eq!(err, ImageBitmapError::InvalidDimensions);
316 }
317
318 #[test]
319 fn from_rgba8_rejects_wrong_pixel_length() {
320 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
321 assert_eq!(
322 err,
323 ImageBitmapError::PixelDataLengthMismatch {
324 expected: 16,
325 actual: 15,
326 }
327 );
328 }
329
330 #[test]
331 fn from_rgba8_slice_accepts_valid_data() {
332 let pixels = [255u8, 0, 0, 255];
333 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
334 assert_eq!(bitmap.pixels(), &pixels);
335 }
336
337 #[test]
338 fn ids_are_content_derived() {
339 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
340 let a_clone = a.clone();
341 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
342 let c = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 1, 255]).expect("bitmap c");
343 let d = ImageBitmap::from_rgba8(2, 1, vec![0, 0, 0, 255, 0, 0, 0, 255]).expect("bitmap d");
344
345 assert_eq!(a.id(), a_clone.id());
346 assert_eq!(a.id(), b.id());
347 assert_ne!(a.id(), c.id());
348 assert_ne!(a.id(), d.id());
349 }
350
351 #[test]
352 fn intrinsic_size_matches_dimensions() {
353 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
354 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
355 }
356
357 #[test]
358 fn tint_filter_multiplies_channels() {
359 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
360 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
361 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
362 assert!((tinted[1] - 0.5).abs() < 1e-5);
363 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
364 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
365 }
366
367 #[test]
368 fn tint_constructor_matches_variant() {
369 let color = Color::from_rgba_u8(10, 20, 30, 40);
370 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
371 }
372
373 #[test]
374 fn tint_filter_uses_src_in_behavior() {
375 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
376 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
377 assert!((tinted[0] - 0.25).abs() < 1e-5);
378 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
379 assert!(tinted[2].abs() < 1e-5);
380 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
381 }
382
383 #[test]
384 fn matrix_filter_transforms_channels() {
385 let matrix = [
386 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, ];
391 let filter = ColorFilter::matrix(matrix);
392 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
393 assert!((transformed[0] - 0.3).abs() < 1e-5);
394 assert!((transformed[1] - 0.3).abs() < 1e-5);
395 assert!((transformed[2] - 0.4).abs() < 1e-5);
396 assert!((transformed[3] - 0.4).abs() < 1e-5);
397 }
398
399 #[test]
400 fn filter_compose_applies_in_order() {
401 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
402 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
403 let chained = first.compose(second);
404 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
405 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
406 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
407 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
408 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
409 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
410 }
411}