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]
286 fn image_bitmap_ids_do_not_use_process_global_or_allocation_identity() {
287 let source = include_str!("image.rs");
288 let image_counter = ["static ", "NEXT_IMAGE_BITMAP_ID"].concat();
289 let pixel_pointer = ["Arc::", "as_ptr(&self.pixels)"].concat();
290
291 assert!(
292 !source.contains(&image_counter) && !source.contains(&pixel_pointer),
293 "image bitmap ids must be derived from bitmap content, not global counters or allocation addresses"
294 );
295 }
296
297 #[test]
298 fn from_rgba8_accepts_valid_data() {
299 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
300 .expect("valid bitmap");
301
302 assert_eq!(bitmap.width(), 2);
303 assert_eq!(bitmap.height(), 1);
304 assert_eq!(bitmap.pixels().len(), 8);
305 assert!(bitmap.is_opaque());
306 }
307
308 #[test]
309 fn from_rgba8_tracks_transparency() {
310 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 128])
311 .expect("valid bitmap");
312
313 assert!(!bitmap.is_opaque());
314 }
315
316 #[test]
317 fn from_rgba8_rejects_zero_dimensions() {
318 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
319 assert_eq!(err, ImageBitmapError::InvalidDimensions);
320 }
321
322 #[test]
323 fn from_rgba8_rejects_wrong_pixel_length() {
324 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
325 assert_eq!(
326 err,
327 ImageBitmapError::PixelDataLengthMismatch {
328 expected: 16,
329 actual: 15,
330 }
331 );
332 }
333
334 #[test]
335 fn from_rgba8_slice_accepts_valid_data() {
336 let pixels = [255u8, 0, 0, 255];
337 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
338 assert_eq!(bitmap.pixels(), &pixels);
339 }
340
341 #[test]
342 fn ids_are_content_derived() {
343 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
344 let a_clone = a.clone();
345 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
346 let c = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 1, 255]).expect("bitmap c");
347 let d = ImageBitmap::from_rgba8(2, 1, vec![0, 0, 0, 255, 0, 0, 0, 255]).expect("bitmap d");
348
349 assert_eq!(a.id(), a_clone.id());
350 assert_eq!(a.id(), b.id());
351 assert_ne!(a.id(), c.id());
352 assert_ne!(a.id(), d.id());
353 }
354
355 #[test]
356 fn intrinsic_size_matches_dimensions() {
357 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
358 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
359 }
360
361 #[test]
362 fn tint_filter_multiplies_channels() {
363 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
364 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
365 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
366 assert!((tinted[1] - 0.5).abs() < 1e-5);
367 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
368 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
369 }
370
371 #[test]
372 fn tint_constructor_matches_variant() {
373 let color = Color::from_rgba_u8(10, 20, 30, 40);
374 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
375 }
376
377 #[test]
378 fn tint_filter_uses_src_in_behavior() {
379 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
380 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
381 assert!((tinted[0] - 0.25).abs() < 1e-5);
382 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
383 assert!(tinted[2].abs() < 1e-5);
384 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
385 }
386
387 #[test]
388 fn matrix_filter_transforms_channels() {
389 let matrix = [
390 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, ];
395 let filter = ColorFilter::matrix(matrix);
396 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
397 assert!((transformed[0] - 0.3).abs() < 1e-5);
398 assert!((transformed[1] - 0.3).abs() < 1e-5);
399 assert!((transformed[2] - 0.4).abs() < 1e-5);
400 assert!((transformed[3] - 0.4).abs() < 1e-5);
401 }
402
403 #[test]
404 fn filter_compose_applies_in_order() {
405 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
406 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
407 let chained = first.compose(second);
408 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
409 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
410 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
411 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
412 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
413 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
414 }
415}