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,
86 0.0,
87 0.0,
88 0.0,
89 tint.g(),
90 0.0,
91 0.0,
92 0.0,
93 0.0,
94 tint.b(),
95 0.0,
96 0.0,
97 0.0,
98 0.0,
99 tint.a(),
100 0.0,
101 ],
102 Self::Modulate(modulate) => [
103 modulate.r(),
104 0.0,
105 0.0,
106 0.0,
107 0.0,
108 0.0,
109 modulate.g(),
110 0.0,
111 0.0,
112 0.0,
113 0.0,
114 0.0,
115 modulate.b(),
116 0.0,
117 0.0,
118 0.0,
119 0.0,
120 0.0,
121 modulate.a(),
122 0.0,
123 ],
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]
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,
293 0.0, 1.0, 0.0,
294 ]);
295 assert_eq!(
296 ColorFilter::matrix(identity.as_matrix()).as_matrix(),
297 identity.as_matrix(),
298 "a matrix filter is its own matrix"
299 );
300
301 let tint = Color(0.25, 0.5, 0.75, 1.0);
302 let matrix = ColorFilter::tint(tint).as_matrix();
303 for row in 0..4 {
304 for column in 0..3 {
305 assert_eq!(matrix[row * 5 + column], 0.0, "row {row} column {column}");
306 }
307 assert_eq!(matrix[row * 5 + 4], 0.0, "row {row} offset");
308 }
309 assert_eq!(matrix[3], tint.r());
310 assert_eq!(matrix[8], tint.g());
311 assert_eq!(matrix[13], tint.b());
312 assert_eq!(matrix[18], tint.a());
313
314 let modulate = ColorFilter::modulate(Color(0.5, 0.25, 0.125, 1.0)).as_matrix();
315 assert_eq!(modulate[0], 0.5);
316 assert_eq!(modulate[6], 0.25);
317 assert_eq!(modulate[12], 0.125);
318 assert_eq!(modulate[18], 1.0);
319 }
320
321 #[test]
322 fn image_bitmap_ids_do_not_use_process_global_or_allocation_identity() {
323 let source = include_str!("image.rs");
324 let image_counter = ["static ", "NEXT_IMAGE_BITMAP_ID"].concat();
325 let pixel_pointer = ["Arc::", "as_ptr(&self.pixels)"].concat();
326
327 assert!(
328 !source.contains(&image_counter) && !source.contains(&pixel_pointer),
329 "image bitmap ids must be derived from bitmap content, not global counters or allocation addresses"
330 );
331 }
332
333 #[test]
334 fn from_rgba8_accepts_valid_data() {
335 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
336 .expect("valid bitmap");
337
338 assert_eq!(bitmap.width(), 2);
339 assert_eq!(bitmap.height(), 1);
340 assert_eq!(bitmap.pixels().len(), 8);
341 assert!(bitmap.is_opaque());
342 }
343
344 #[test]
345 fn from_rgba8_tracks_transparency() {
346 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 128])
347 .expect("valid bitmap");
348
349 assert!(!bitmap.is_opaque());
350 }
351
352 #[test]
353 fn from_rgba8_rejects_zero_dimensions() {
354 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
355 assert_eq!(err, ImageBitmapError::InvalidDimensions);
356 }
357
358 #[test]
359 fn from_rgba8_rejects_wrong_pixel_length() {
360 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
361 assert_eq!(
362 err,
363 ImageBitmapError::PixelDataLengthMismatch {
364 expected: 16,
365 actual: 15,
366 }
367 );
368 }
369
370 #[test]
371 fn from_rgba8_slice_accepts_valid_data() {
372 let pixels = [255u8, 0, 0, 255];
373 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
374 assert_eq!(bitmap.pixels(), &pixels);
375 }
376
377 #[test]
378 fn ids_are_content_derived() {
379 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
380 let a_clone = a.clone();
381 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
382 let c = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 1, 255]).expect("bitmap c");
383 let d = ImageBitmap::from_rgba8(2, 1, vec![0, 0, 0, 255, 0, 0, 0, 255]).expect("bitmap d");
384
385 assert_eq!(a.id(), a_clone.id());
386 assert_eq!(a.id(), b.id());
387 assert_ne!(a.id(), c.id());
388 assert_ne!(a.id(), d.id());
389 }
390
391 #[test]
392 fn intrinsic_size_matches_dimensions() {
393 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
394 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
395 }
396
397 #[test]
398 fn tint_filter_multiplies_channels() {
399 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
400 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
401 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
402 assert!((tinted[1] - 0.5).abs() < 1e-5);
403 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
404 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
405 }
406
407 #[test]
408 fn tint_constructor_matches_variant() {
409 let color = Color::from_rgba_u8(10, 20, 30, 40);
410 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
411 }
412
413 #[test]
414 fn tint_filter_uses_src_in_behavior() {
415 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
416 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
417 assert!((tinted[0] - 0.25).abs() < 1e-5);
418 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
419 assert!(tinted[2].abs() < 1e-5);
420 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
421 }
422
423 #[test]
424 fn matrix_filter_transforms_channels() {
425 let matrix = [
426 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,
427 0.0, 1.0, 0.0,
428 ];
429 let filter = ColorFilter::matrix(matrix);
430 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
431 assert!((transformed[0] - 0.3).abs() < 1e-5);
432 assert!((transformed[1] - 0.3).abs() < 1e-5);
433 assert!((transformed[2] - 0.4).abs() < 1e-5);
434 assert!((transformed[3] - 0.4).abs() < 1e-5);
435 }
436
437 #[test]
438 fn filter_compose_applies_in_order() {
439 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
440 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
441 let chained = first.compose(second);
442 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
443 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
444 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
445 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
446 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
447 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
448 }
449}