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