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