1use crate::{BlendMode, Color, Size};
4use std::hash::{Hash, Hasher};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7use thiserror::Error;
8
9static NEXT_IMAGE_BITMAP_ID: AtomicU64 = AtomicU64::new(1);
10
11#[derive(Debug, Clone, PartialEq, Eq, Error)]
13pub enum ImageBitmapError {
14 #[error("image dimensions must be greater than zero")]
15 InvalidDimensions,
16 #[error("image dimensions are too large")]
17 DimensionsTooLarge,
18 #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
19 PixelDataLengthMismatch { expected: usize, actual: usize },
20}
21
22#[derive(Clone, Debug)]
24pub struct ImageBitmap {
25 id: u64,
26 width: u32,
27 height: u32,
28 pixels: Arc<[u8]>,
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub enum ImageSampling {
34 #[default]
36 Nearest,
37 Linear,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
43pub enum ColorFilter {
44 Tint(Color),
46 Modulate(Color),
48 Matrix([f32; 20]),
53}
54
55impl ColorFilter {
56 pub fn tint(color: Color) -> Self {
58 Self::Tint(color)
59 }
60
61 pub fn modulate(color: Color) -> Self {
63 Self::Modulate(color)
64 }
65
66 pub fn matrix(matrix: [f32; 20]) -> Self {
68 Self::Matrix(matrix)
69 }
70
71 pub fn compose(self, next: ColorFilter) -> ColorFilter {
72 ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
73 }
74
75 pub fn as_matrix(self) -> [f32; 20] {
76 match self {
77 Self::Tint(tint) => [
78 0.0,
79 0.0,
80 0.0,
81 tint.r(),
82 0.0, 0.0,
84 0.0,
85 0.0,
86 tint.g(),
87 0.0, 0.0,
89 0.0,
90 0.0,
91 tint.b(),
92 0.0, 0.0,
94 0.0,
95 0.0,
96 tint.a(),
97 0.0, ],
99 Self::Modulate(modulate) => [
100 modulate.r(),
101 0.0,
102 0.0,
103 0.0,
104 0.0, 0.0,
106 modulate.g(),
107 0.0,
108 0.0,
109 0.0, 0.0,
111 0.0,
112 modulate.b(),
113 0.0,
114 0.0, 0.0,
116 0.0,
117 0.0,
118 modulate.a(),
119 0.0, ],
121 Self::Matrix(matrix) => matrix,
122 }
123 }
124
125 pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
126 apply_color_matrix(self.as_matrix(), rgba)
127 }
128
129 pub fn supports_gpu_vertex_modulation(self) -> bool {
130 matches!(self, Self::Modulate(_))
131 }
132
133 pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
134 match self {
135 Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
136 _ => None,
137 }
138 }
139
140 pub fn blend_mode(self) -> BlendMode {
141 match self {
142 Self::Tint(_) => BlendMode::SrcIn,
143 Self::Modulate(_) => BlendMode::Modulate,
144 Self::Matrix(_) => BlendMode::SrcOver,
145 }
146 }
147}
148
149fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
150 let r = rgba[0];
151 let g = rgba[1];
152 let b = rgba[2];
153 let a = rgba[3];
154 [
155 (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
156 (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
157 (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
158 .clamp(0.0, 1.0),
159 (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
160 .clamp(0.0, 1.0),
161 ]
162}
163
164fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
165 let mut composed = [0.0f32; 20];
166 for row in 0..4 {
167 let row_base = row * 5;
168 let s0 = second[row_base];
169 let s1 = second[row_base + 1];
170 let s2 = second[row_base + 2];
171 let s3 = second[row_base + 3];
172 let s4 = second[row_base + 4];
173
174 composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
175 composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
176 composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
177 composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
178 composed[row_base + 4] =
179 s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
180 }
181 composed
182}
183
184impl ImageBitmap {
185 pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
187 Self::from_rgba8_slice(width, height, &pixels)
188 }
189
190 pub fn from_rgba8_slice(
192 width: u32,
193 height: u32,
194 pixels: &[u8],
195 ) -> Result<Self, ImageBitmapError> {
196 if width == 0 || height == 0 {
197 return Err(ImageBitmapError::InvalidDimensions);
198 }
199 let expected = (width as usize)
200 .checked_mul(height as usize)
201 .and_then(|value| value.checked_mul(4))
202 .ok_or(ImageBitmapError::DimensionsTooLarge)?;
203
204 if pixels.len() != expected {
205 return Err(ImageBitmapError::PixelDataLengthMismatch {
206 expected,
207 actual: pixels.len(),
208 });
209 }
210
211 Ok(Self {
212 id: NEXT_IMAGE_BITMAP_ID.fetch_add(1, Ordering::Relaxed),
213 width,
214 height,
215 pixels: Arc::from(pixels),
216 })
217 }
218
219 pub fn id(&self) -> u64 {
221 self.id
222 }
223
224 pub fn width(&self) -> u32 {
226 self.width
227 }
228
229 pub fn height(&self) -> u32 {
231 self.height
232 }
233
234 pub fn pixels(&self) -> &[u8] {
236 &self.pixels
237 }
238
239 pub fn intrinsic_size(&self) -> Size {
241 Size {
242 width: self.width as f32,
243 height: self.height as f32,
244 }
245 }
246}
247
248impl PartialEq for ImageBitmap {
249 fn eq(&self, other: &Self) -> bool {
250 self.id == other.id
251 }
252}
253
254impl Eq for ImageBitmap {}
255
256impl Hash for ImageBitmap {
257 fn hash<H: Hasher>(&self, state: &mut H) {
258 self.id.hash(state);
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn from_rgba8_accepts_valid_data() {
268 let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
269 .expect("valid bitmap");
270
271 assert_eq!(bitmap.width(), 2);
272 assert_eq!(bitmap.height(), 1);
273 assert_eq!(bitmap.pixels().len(), 8);
274 }
275
276 #[test]
277 fn from_rgba8_rejects_zero_dimensions() {
278 let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
279 assert_eq!(err, ImageBitmapError::InvalidDimensions);
280 }
281
282 #[test]
283 fn from_rgba8_rejects_wrong_pixel_length() {
284 let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
285 assert_eq!(
286 err,
287 ImageBitmapError::PixelDataLengthMismatch {
288 expected: 16,
289 actual: 15,
290 }
291 );
292 }
293
294 #[test]
295 fn from_rgba8_slice_accepts_valid_data() {
296 let pixels = [255u8, 0, 0, 255];
297 let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
298 assert_eq!(bitmap.pixels(), &pixels);
299 }
300
301 #[test]
302 fn ids_are_unique() {
303 let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
304 let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
305 assert_ne!(a.id(), b.id());
306 }
307
308 #[test]
309 fn intrinsic_size_matches_dimensions() {
310 let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
311 assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
312 }
313
314 #[test]
315 fn tint_filter_multiplies_channels() {
316 let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
317 let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
318 assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
319 assert!((tinted[1] - 0.5).abs() < 1e-5);
320 assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
321 assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
322 }
323
324 #[test]
325 fn tint_constructor_matches_variant() {
326 let color = Color::from_rgba_u8(10, 20, 30, 40);
327 assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
328 }
329
330 #[test]
331 fn tint_filter_uses_src_in_behavior() {
332 let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
333 let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
334 assert!((tinted[0] - 0.25).abs() < 1e-5);
335 assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
336 assert!(tinted[2].abs() < 1e-5);
337 assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
338 }
339
340 #[test]
341 fn matrix_filter_transforms_channels() {
342 let matrix = [
343 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, ];
348 let filter = ColorFilter::matrix(matrix);
349 let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
350 assert!((transformed[0] - 0.3).abs() < 1e-5);
351 assert!((transformed[1] - 0.3).abs() < 1e-5);
352 assert!((transformed[2] - 0.4).abs() < 1e-5);
353 assert!((transformed[3] - 0.4).abs() < 1e-5);
354 }
355
356 #[test]
357 fn filter_compose_applies_in_order() {
358 let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
359 let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
360 let chained = first.compose(second);
361 let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
362 let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
363 assert!((direct_second[0] - composed[0]).abs() < 1e-5);
364 assert!((direct_second[1] - composed[1]).abs() < 1e-5);
365 assert!((direct_second[2] - composed[2]).abs() < 1e-5);
366 assert!((direct_second[3] - composed[3]).abs() < 1e-5);
367 }
368}