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
212 .as_chunks::<4>()
213 .0
214 .iter()
215 .all(|pixel| pixel[3] == u8::MAX);
216 Ok(Self {
217 width,
218 height,
219 id,
220 opaque,
221 pixels: Arc::from(pixels),
222 })
223 }
224
225 pub fn id(&self) -> u64 {
227 self.id
228 }
229
230 pub fn width(&self) -> u32 {
232 self.width
233 }
234
235 pub fn height(&self) -> u32 {
237 self.height
238 }
239
240 pub fn pixels(&self) -> &[u8] {
242 &self.pixels
243 }
244
245 pub fn is_opaque(&self) -> bool {
247 self.opaque
248 }
249
250 pub fn intrinsic_size(&self) -> Size {
252 Size {
253 width: self.width as f32,
254 height: self.height as f32,
255 }
256 }
257}
258
259impl PartialEq for ImageBitmap {
260 fn eq(&self, other: &Self) -> bool {
261 self.id() == other.id()
262 }
263}
264
265impl Eq for ImageBitmap {}
266
267impl Hash for ImageBitmap {
268 fn hash<H: Hasher>(&self, state: &mut H) {
269 self.id().hash(state);
270 }
271}
272
273fn bitmap_content_id(width: u32, height: u32, pixels: &[u8]) -> u64 {
274 let mut hasher = DefaultHasher::new();
275 width.hash(&mut hasher);
276 height.hash(&mut hasher);
277 pixels.hash(&mut hasher);
278 hasher.finish()
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
290 fn every_colour_filter_states_itself_as_a_matrix() {
291 let identity = ColorFilter::Matrix([
292 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,
296 ]);
297 assert_eq!(
298 ColorFilter::matrix(identity.as_matrix()).as_matrix(),
299 identity.as_matrix(),
300 "a matrix filter is its own matrix"
301 );
302
303 let tint = Color(0.25, 0.5, 0.75, 1.0);
306 let matrix = ColorFilter::tint(tint).as_matrix();
307 for row in 0..4 {
308 for column in 0..3 {
309 assert_eq!(matrix[row * 5 + column], 0.0, "row {row} column {column}");
310 }
311 assert_eq!(matrix[row * 5 + 4], 0.0, "row {row} offset");
312 }
313 assert_eq!(matrix[3], tint.r());
314 assert_eq!(matrix[8], tint.g());
315 assert_eq!(matrix[13], tint.b());
316 assert_eq!(matrix[18], tint.a());
317
318 let modulate = ColorFilter::modulate(Color(0.5, 0.25, 0.125, 1.0)).as_matrix();
321 assert_eq!(modulate[0], 0.5);
322 assert_eq!(modulate[6], 0.25);
323 assert_eq!(modulate[12], 0.125);
324 assert_eq!(modulate[18], 1.0);
325 }
326
327 #[test]
328 fn image_bitmap_ids_do_not_use_process_global_or_allocation_identity() {
329 let source = include_str!("image.rs");
330 let image_counter = ["static ", "NEXT_IMAGE_BITMAP_ID"].concat();
331 let pixel_pointer = ["Arc::", "as_ptr(&self.pixels)"].concat();
332
333 assert!(
334 !source.contains(&image_counter) && !source.contains(&pixel_pointer),
335 "image bitmap ids must be derived from bitmap content, not global counters or allocation addresses"
336 );
337 }
338
339 #[test]
340 fn from_rgba8_accepts_valid_data() {
341 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
342 .expect("valid bitmap");
343
344 assert_eq!(bitmap.width(), 2);
345 assert_eq!(bitmap.height(), 1);
346 assert_eq!(bitmap.pixels().len(), 8);
347 assert!(bitmap.is_opaque());
348 }
349
350 #[test]
351 fn from_rgba8_tracks_transparency() {
352 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 128])
353 .expect("valid bitmap");
354
355 assert!(!bitmap.is_opaque());
356 }
357
358 #[test]
359 fn from_rgba8_rejects_zero_dimensions() {
360 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
361 assert_eq!(err, ImageBitmapError::InvalidDimensions);
362 }
363
364 #[test]
365 fn from_rgba8_rejects_wrong_pixel_length() {
366 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
367 assert_eq!(
368 err,
369 ImageBitmapError::PixelDataLengthMismatch {
370 expected: 16,
371 actual: 15,
372 }
373 );
374 }
375
376 #[test]
377 fn from_rgba8_slice_accepts_valid_data() {
378 let pixels = [255u8, 0, 0, 255];
379 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
380 assert_eq!(bitmap.pixels(), &pixels);
381 }
382
383 #[test]
384 fn ids_are_content_derived() {
385 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
386 let a_clone = a.clone();
387 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
388 let c = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 1, 255]).expect("bitmap c");
389 let d = ImageBitmap::from_rgba8(2, 1, vec![0, 0, 0, 255, 0, 0, 0, 255]).expect("bitmap d");
390
391 assert_eq!(a.id(), a_clone.id());
392 assert_eq!(a.id(), b.id());
393 assert_ne!(a.id(), c.id());
394 assert_ne!(a.id(), d.id());
395 }
396
397 #[test]
398 fn intrinsic_size_matches_dimensions() {
399 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
400 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
401 }
402
403 #[test]
404 fn tint_filter_multiplies_channels() {
405 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
406 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
407 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
408 assert!((tinted[1] - 0.5).abs() < 1e-5);
409 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
410 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
411 }
412
413 #[test]
414 fn tint_constructor_matches_variant() {
415 let color = Color::from_rgba_u8(10, 20, 30, 40);
416 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
417 }
418
419 #[test]
420 fn tint_filter_uses_src_in_behavior() {
421 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
422 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
423 assert!((tinted[0] - 0.25).abs() < 1e-5);
424 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
425 assert!(tinted[2].abs() < 1e-5);
426 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
427 }
428
429 #[test]
430 fn matrix_filter_transforms_channels() {
431 let matrix = [
432 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, ];
437 let filter = ColorFilter::matrix(matrix);
438 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
439 assert!((transformed[0] - 0.3).abs() < 1e-5);
440 assert!((transformed[1] - 0.3).abs() < 1e-5);
441 assert!((transformed[2] - 0.4).abs() < 1e-5);
442 assert!((transformed[3] - 0.4).abs() < 1e-5);
443 }
444
445 #[test]
446 fn filter_compose_applies_in_order() {
447 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
448 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
449 let chained = first.compose(second);
450 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
451 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
452 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
453 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
454 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
455 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
456 }
457}