1use std::rc::Rc;
2
3use crate::geometry::Rect;
4use crate::paint::{Color, ImageSource};
5
6const BPP: usize = 4;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct Pixels {
10 width: u32,
11 height: u32,
12 data: Vec<u8>,
13}
14
15impl Default for Pixels {
16 fn default() -> Self {
17 Self::new(0, 0)
18 }
19}
20
21impl Pixels {
22 pub fn new(width: u32, height: u32) -> Self {
23 let Some(px) = checked_pixels(width, height) else {
24 return Self {
25 width: 0,
26 height: 0,
27 data: Vec::new(),
28 };
29 };
30 Self {
31 width,
32 height,
33 data: vec![0; px * BPP],
34 }
35 }
36
37 pub fn filled(width: u32, height: u32, color: Color) -> Self {
38 let mut px = Self::new(width, height);
39 px.fill(color);
40 px
41 }
42
43 pub fn from_rgba(width: u32, height: u32, data: impl Into<Vec<u8>>) -> Option<Self> {
44 let data = data.into();
45 if width > MAX_DIM || height > MAX_DIM {
46 return None;
47 }
48 let expected = (width as usize)
49 .checked_mul(height as usize)?
50 .checked_mul(BPP)?;
51 if data.len() != expected {
52 return None;
53 }
54 Some(Self {
55 width,
56 height,
57 data,
58 })
59 }
60
61 pub fn width(&self) -> u32 {
62 self.width
63 }
64
65 pub fn height(&self) -> u32 {
66 self.height
67 }
68
69 pub fn is_empty(&self) -> bool {
70 self.width == 0 || self.height == 0
71 }
72
73 pub fn pixel_count(&self) -> usize {
74 self.data.len() / BPP
75 }
76
77 pub fn data(&self) -> &[u8] {
78 &self.data
79 }
80
81 pub fn data_mut(&mut self) -> &mut [u8] {
82 &mut self.data
83 }
84
85 pub fn into_data(self) -> Vec<u8> {
86 self.data
87 }
88
89 pub fn index(&self, x: i32, y: i32) -> Option<usize> {
90 if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
91 return None;
92 }
93 Some((y as usize * self.width as usize + x as usize) * BPP)
94 }
95
96 pub fn contains(&self, x: i32, y: i32) -> bool {
97 self.index(x, y).is_some()
98 }
99
100 pub fn get(&self, x: i32, y: i32) -> Option<Color> {
101 let i = self.index(x, y)?;
102 Some(unpack(&self.data[i..i + BPP]))
103 }
104
105 pub fn set(&mut self, x: i32, y: i32, color: Color) -> bool {
106 let Some(i) = self.index(x, y) else {
107 return false;
108 };
109 self.data[i..i + BPP].copy_from_slice(&pack(color));
110 true
111 }
112
113 pub fn blend(&mut self, x: i32, y: i32, color: Color) -> bool {
114 let Some(i) = self.index(x, y) else {
115 return false;
116 };
117 let out = blend_over(unpack(&self.data[i..i + BPP]), color);
118 self.data[i..i + BPP].copy_from_slice(&out);
119 true
120 }
121
122 pub fn fill(&mut self, color: Color) {
123 let px = pack(color);
124 for chunk in self.data.chunks_exact_mut(BPP) {
125 chunk.copy_from_slice(&px);
126 }
127 }
128
129 pub fn fill_rect(&mut self, rect: Rect, color: Color) -> Rect {
130 let Some((x0, y0, x1, y1)) = self.rect_bounds(rect) else {
131 return Rect::ZERO;
132 };
133 let px = pack(color);
134 for y in y0..y1 {
135 let row = y as usize * self.width as usize;
136 for x in x0..x1 {
137 let i = (row + x as usize) * BPP;
138 self.data[i..i + BPP].copy_from_slice(&px);
139 }
140 }
141 bounds_rect(x0, y0, x1, y1)
142 }
143
144 pub fn blend_rect(&mut self, rect: Rect, color: Color) -> Rect {
145 let Some((x0, y0, x1, y1)) = self.rect_bounds(rect) else {
146 return Rect::ZERO;
147 };
148 for y in y0..y1 {
149 let row = y as usize * self.width as usize;
150 for x in x0..x1 {
151 let i = (row + x as usize) * BPP;
152 let out = blend_over(unpack(&self.data[i..i + BPP]), color);
153 self.data[i..i + BPP].copy_from_slice(&out);
154 }
155 }
156 bounds_rect(x0, y0, x1, y1)
157 }
158
159 pub fn blit(&mut self, src: &Pixels, dx: i32, dy: i32) -> Rect {
160 self.blit_impl(src, dx, dy, false)
161 }
162
163 pub fn blend_from(&mut self, src: &Pixels, dx: i32, dy: i32) -> Rect {
164 self.blit_impl(src, dx, dy, true)
165 }
166
167 fn blit_impl(&mut self, src: &Pixels, dx: i32, dy: i32, blend: bool) -> Rect {
168 if src.is_empty() || self.is_empty() {
169 return Rect::ZERO;
170 }
171 let sx0 = (-dx).max(0);
172 let sy0 = (-dy).max(0);
173 let sx1 = (self.width as i32 - dx).min(src.width as i32);
174 let sy1 = (self.height as i32 - dy).min(src.height as i32);
175 if sx0 >= sx1 || sy0 >= sy1 {
176 return Rect::ZERO;
177 }
178 for sy in sy0..sy1 {
179 let dy_row = (dy + sy) as usize * self.width as usize;
180 let sx_row = sy as usize * src.width as usize;
181 for sx in sx0..sx1 {
182 let si = (sx_row + sx as usize) * BPP;
183 let di = (dy_row + (dx + sx) as usize) * BPP;
184 if blend {
185 let out = blend_over(
186 unpack(&self.data[di..di + BPP]),
187 unpack(&src.data[si..si + BPP]),
188 );
189 self.data[di..di + BPP].copy_from_slice(&out);
190 } else {
191 self.data[di..di + BPP].copy_from_slice(&src.data[si..si + BPP]);
192 }
193 }
194 }
195 bounds_rect(dx + sx0, dy + sy0, dx + sx1, dy + sy1)
196 }
197
198 pub fn clear(&mut self) {
199 self.data.fill(0);
200 }
201
202 pub fn crop(&self, rect: Rect) -> Pixels {
203 let Some((x0, y0, x1, y1)) = self.rect_bounds(rect) else {
204 return Pixels::new(0, 0);
205 };
206 let w = (x1 - x0) as usize;
207 let h = (y1 - y0) as usize;
208 let mut out = Vec::with_capacity(w * h * BPP);
209 for y in y0..y1 {
210 let start = (y as usize * self.width as usize + x0 as usize) * BPP;
211 out.extend_from_slice(&self.data[start..start + w * BPP]);
212 }
213 Pixels {
214 width: (x1 - x0) as u32,
215 height: (y1 - y0) as u32,
216 data: out,
217 }
218 }
219
220 pub fn flip_horizontal(&mut self) {
221 if self.is_empty() {
222 return;
223 }
224 let w = self.width as usize;
225 let row_bytes = w * BPP;
226 for y in 0..self.height as usize {
227 let row = &mut self.data[y * row_bytes..(y + 1) * row_bytes];
228 for x in 0..w / 2 {
229 let a = x * BPP;
230 let b = (w - 1 - x) * BPP;
231 for k in 0..BPP {
232 row.swap(a + k, b + k);
233 }
234 }
235 }
236 }
237
238 pub fn flip_vertical(&mut self) {
239 if self.is_empty() {
240 return;
241 }
242 let row_bytes = self.width as usize * BPP;
243 let h = self.height as usize;
244 for y in 0..h / 2 {
245 let (a, b) = (y * row_bytes, (h - 1 - y) * row_bytes);
246 for k in 0..row_bytes {
247 self.data.swap(a + k, b + k);
248 }
249 }
250 }
251
252 pub fn rotate180(&mut self) {
253 self.flip_horizontal();
254 self.flip_vertical();
255 }
256
257 pub fn resize_nearest(&self, width: u32, height: u32) -> Pixels {
258 if checked_pixels(width, height).is_none() {
259 return Pixels::new(0, 0);
260 }
261 let (w, h) = (width, height);
262 if w == 0 || h == 0 || self.is_empty() {
263 return Pixels::new(w, h);
264 }
265 let mut out = Pixels::new(w, h);
266 let xr = self.width as f32 / w as f32;
267 let yr = self.height as f32 / h as f32;
268 for y in 0..h {
269 let sy = (y as f32 * yr).floor() as i32;
270 for x in 0..w {
271 let sx = (x as f32 * xr).floor() as i32;
272 if let (Some(si), Some(di)) = (self.index(sx, sy), out.index(x as i32, y as i32)) {
273 out.data[di..di + BPP].copy_from_slice(&self.data[si..si + BPP]);
274 }
275 }
276 }
277 out
278 }
279
280 pub fn resize_bilinear(&self, width: u32, height: u32) -> Pixels {
281 if checked_pixels(width, height).is_none() {
282 return Pixels::new(0, 0);
283 }
284 let (w, h) = (width, height);
285 if w == 0 || h == 0 || self.is_empty() {
286 return Pixels::new(w, h);
287 }
288 let mut out = Pixels::new(w, h);
289 let sx = self.width as f32 / w as f32;
290 let sy = self.height as f32 / h as f32;
291 for y in 0..h {
292 let fy = (y as f32 + 0.5) * sy - 0.5;
293 for x in 0..w {
294 let fx = (x as f32 + 0.5) * sx - 0.5;
295 let c = self.sample_bilinear(fx, fy);
296 if let Some(di) = out.index(x as i32, y as i32) {
297 out.data[di..di + BPP].copy_from_slice(&pack(c));
298 }
299 }
300 }
301 out
302 }
303
304 pub fn thumbnail(&self, max_dim: u32) -> Pixels {
305 self.scaled_to_fit(max_dim).unwrap_or_else(|| self.clone())
306 }
307
308 pub fn scaled_to_fit(&self, max_dim: u32) -> Option<Pixels> {
309 if self.is_empty() || max_dim == 0 {
310 return None;
311 }
312 let longest = self.width.max(self.height);
313 if longest <= max_dim {
314 return None;
315 }
316 let scale = max_dim as f32 / longest as f32;
317 let w = ((self.width as f32 * scale).round() as u32).max(1);
318 let h = ((self.height as f32 * scale).round() as u32).max(1);
319 Some(self.resize_bilinear(w, h))
320 }
321
322 pub fn sample_nearest(&self, fx: f32, fy: f32) -> Color {
323 if self.is_empty() {
324 return Color::TRANSPARENT;
325 }
326 let x = fx.floor().clamp(0.0, self.width as f32 - 1.0) as i32;
327 let y = fy.floor().clamp(0.0, self.height as f32 - 1.0) as i32;
328 self.get(x, y).unwrap_or(Color::TRANSPARENT)
329 }
330
331 pub fn sample_bilinear(&self, fx: f32, fy: f32) -> Color {
332 if self.is_empty() {
333 return Color::TRANSPARENT;
334 }
335 if !fx.is_finite() || !fy.is_finite() {
336 return Color::TRANSPARENT;
337 }
338 let x0 = fx.floor();
339 let y0 = fy.floor();
340 let tx = (fx - x0).clamp(0.0, 1.0);
341 let ty = (fy - y0).clamp(0.0, 1.0);
342 let x0 = (x0 as i32).clamp(0, self.width as i32 - 1);
343 let y0 = (y0 as i32).clamp(0, self.height as i32 - 1);
344 let x1 = (x0 + 1).min(self.width as i32 - 1);
345 let y1 = (y0 + 1).min(self.height as i32 - 1);
346 let c00 = self.get(x0, y0).unwrap_or(Color::TRANSPARENT);
347 let c10 = self.get(x1, y0).unwrap_or(Color::TRANSPARENT);
348 let c01 = self.get(x0, y1).unwrap_or(Color::TRANSPARENT);
349 let c11 = self.get(x1, y1).unwrap_or(Color::TRANSPARENT);
350 let mix = |a: f32, b: f32, t: f32| a + (b - a) * t;
351 let lerp = |a: Color, b: Color, t: f32| Color {
352 r: mix(a.r, b.r, t),
353 g: mix(a.g, b.g, t),
354 b: mix(a.b, b.b, t),
355 a: mix(a.a, b.a, t),
356 };
357 lerp(lerp(c00, c10, tx), lerp(c01, c11, tx), ty)
358 }
359
360 pub fn for_each_pixel(&mut self, mut f: impl FnMut(i32, i32, Color) -> Color) {
361 let w = self.width as usize;
362 for y in 0..self.height as usize {
363 for x in 0..w {
364 let i = (y * w + x) * BPP;
365 let c = unpack(&self.data[i..i + BPP]);
366 self.data[i..i + BPP].copy_from_slice(&pack(f(x as i32, y as i32, c)));
367 }
368 }
369 }
370
371 pub fn map(&self, mut f: impl FnMut(i32, i32, Color) -> Color) -> Pixels {
372 let mut out = self.clone();
373 out.for_each_pixel(&mut f);
374 out
375 }
376
377 pub fn grayscale(&self) -> Pixels {
378 self.map(|_, _, c| {
379 let l = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
380 Color::rgba(l, l, l, c.a)
381 })
382 }
383
384 pub fn inverted(&self) -> Pixels {
385 self.map(|_, _, c| Color::rgba(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a))
386 }
387
388 pub fn tinted(&self, color: Color) -> Pixels {
389 self.map(|_, _, c| Color::rgba(c.r * color.r, c.g * color.g, c.b * color.b, c.a * color.a))
390 }
391
392 pub fn multiply_alpha(&self, factor: f32) -> Pixels {
393 let f = if factor.is_finite() {
394 factor.clamp(0.0, 1.0)
395 } else {
396 1.0
397 };
398 self.map(move |_, _, c| Color { a: c.a * f, ..c })
399 }
400
401 pub fn has_alpha(&self) -> bool {
402 self.data.chunks_exact(BPP).any(|p| p[3] != 255)
403 }
404
405 pub fn opaque_bounds(&self) -> Option<Rect> {
406 if self.is_empty() {
407 return None;
408 }
409 let mut min_x = self.width;
410 let mut min_y = self.height;
411 let mut max_x = 0u32;
412 let mut max_y = 0u32;
413 for y in 0..self.height {
414 for x in 0..self.width {
415 let i = ((y as usize * self.width as usize) + x as usize) * BPP;
416 if self.data[i + 3] != 0 {
417 min_x = min_x.min(x);
418 min_y = min_y.min(y);
419 max_x = max_x.max(x);
420 max_y = max_y.max(y);
421 }
422 }
423 }
424 if min_x > max_x || min_y > max_y {
425 return None;
426 }
427 Some(Rect::from_xywh(
428 min_x as f32,
429 min_y as f32,
430 (max_x - min_x + 1) as f32,
431 (max_y - min_y + 1) as f32,
432 ))
433 }
434
435 pub fn to_source(&self, id: u64, version: u64) -> ImageSource {
436 ImageSource::Dynamic {
437 id,
438 width: self.width,
439 height: self.height,
440 data: Rc::from(self.data.clone().into_boxed_slice()),
441 version,
442 }
443 }
444
445 pub fn into_source(self, id: u64, version: u64) -> ImageSource {
446 ImageSource::Dynamic {
447 id,
448 width: self.width,
449 height: self.height,
450 data: Rc::from(self.data.into_boxed_slice()),
451 version,
452 }
453 }
454
455 fn rect_bounds(&self, rect: Rect) -> Option<(i32, i32, i32, i32)> {
456 if self.is_empty() || rect.is_empty() {
457 return None;
458 }
459 let x0 = rect.left().round() as i32;
460 let y0 = rect.top().round() as i32;
461 let x1 = rect.right().round() as i32;
462 let y1 = rect.bottom().round() as i32;
463 let x0 = x0.max(0);
464 let y0 = y0.max(0);
465 let x1 = x1.min(self.width as i32);
466 let y1 = y1.min(self.height as i32);
467 if x0 >= x1 || y0 >= y1 {
468 return None;
469 }
470 Some((x0, y0, x1, y1))
471 }
472}
473
474pub const MAX_DIM: u32 = 16384;
475
476fn checked_pixels(width: u32, height: u32) -> Option<usize> {
477 if width > MAX_DIM || height > MAX_DIM {
478 return None;
479 }
480 (width as usize).checked_mul(height as usize)
481}
482
483fn bounds_rect(x0: i32, y0: i32, x1: i32, y1: i32) -> Rect {
484 if x0 >= x1 || y0 >= y1 {
485 return Rect::ZERO;
486 }
487 Rect::from_xywh(x0 as f32, y0 as f32, (x1 - x0) as f32, (y1 - y0) as f32)
488}
489
490fn pack(color: Color) -> [u8; BPP] {
491 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
492 [q(color.r), q(color.g), q(color.b), q(color.a)]
493}
494
495fn unpack(bytes: &[u8]) -> Color {
496 Color::rgba(
497 bytes[0] as f32 / 255.0,
498 bytes[1] as f32 / 255.0,
499 bytes[2] as f32 / 255.0,
500 bytes[3] as f32 / 255.0,
501 )
502}
503
504fn blend_over(dst: Color, src: Color) -> [u8; BPP] {
505 if src.a <= 0.0 {
506 return pack(dst);
507 }
508 if src.a >= 1.0 {
509 return pack(src);
510 }
511 let inv = 1.0 - src.a;
512 let out_a = src.a + dst.a * inv;
513 if out_a <= 0.0 {
514 return pack(Color::TRANSPARENT);
515 }
516 let mix = |s: f32, d: f32| (s * src.a + d * dst.a * inv) / out_a;
517 pack(Color::rgba(
518 mix(src.r, dst.r),
519 mix(src.g, dst.g),
520 mix(src.b, dst.b),
521 out_a,
522 ))
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn red() -> Color {
530 Color::rgb8(255, 0, 0)
531 }
532
533 #[test]
534 fn new_allocates_zeroed_rgba() {
535 let px = Pixels::new(3, 2);
536 assert_eq!((px.width(), px.height()), (3, 2));
537 assert_eq!(px.data().len(), 3 * 2 * 4);
538 assert!(px.data().iter().all(|&b| b == 0));
539 assert!(px.has_alpha());
540 assert_eq!(px.pixel_count(), 6);
541 }
542
543 #[test]
544 fn oversized_request_is_empty_not_panic() {
545 let px = Pixels::new(u32::MAX, 2);
546 assert!(px.is_empty());
547 assert_eq!(px.data().len(), 0);
548 assert_eq!(Pixels::new(MAX_DIM + 1, 1).width(), 0);
549 }
550
551 #[test]
552 fn from_rgba_validates_length() {
553 assert!(Pixels::from_rgba(2, 2, vec![0u8; 16]).is_some());
554 assert!(Pixels::from_rgba(2, 2, vec![0u8; 15]).is_none());
555 assert!(Pixels::from_rgba(0, 5, Vec::new()).is_some());
556 assert!(Pixels::from_rgba(MAX_DIM + 1, 1, vec![0u8; 4]).is_none());
557 }
558
559 #[test]
560 fn set_get_and_bounds() {
561 let mut px = Pixels::new(2, 2);
562 assert!(px.set(1, 1, red()));
563 assert_eq!(px.get(1, 1), Some(red()));
564 assert!(!px.set(2, 0, red()), "out of bounds");
565 assert_eq!(px.get(-1, 0), None);
566 assert!(px.contains(0, 1));
567 assert!(!px.contains(2, 0));
568 }
569
570 #[test]
571 fn blend_is_source_over() {
572 let mut px = Pixels::new(1, 1);
573 px.set(0, 0, Color::rgb8(0, 0, 255));
574 px.blend(0, 0, Color::rgba(1.0, 0.0, 0.0, 0.5));
575 let c = px.get(0, 0).unwrap();
576 assert!((c.r - 0.5).abs() < 0.02, "{c:?}");
577 assert!((c.b - 0.5).abs() < 0.02, "{c:?}");
578 assert!((c.a - 1.0).abs() < 1e-6);
579 }
580
581 #[test]
582 fn fill_and_fill_rect_clip_to_bounds() {
583 let mut px = Pixels::new(4, 4);
584 assert_eq!(
585 px.fill_rect(Rect::from_xywh(1.0, 1.0, 2.0, 2.0), red()),
586 Rect::from_xywh(1.0, 1.0, 2.0, 2.0)
587 );
588 assert_eq!(px.get(1, 1), Some(red()));
589 assert_eq!(px.get(2, 2), Some(red()));
590 assert_eq!(px.get(0, 0), Some(Color::TRANSPARENT));
591
592 let clipped = px.fill_rect(Rect::from_xywh(3.0, 3.0, 5.0, 5.0), red());
593 assert_eq!(clipped, Rect::from_xywh(3.0, 3.0, 1.0, 1.0));
594 px.fill(Color::GREEN);
595 assert_eq!(px.get(0, 0), Some(Color::GREEN));
596 }
597
598 #[test]
599 fn blit_and_blend_from_copy_region() {
600 let mut src = Pixels::filled(2, 2, red());
601 src.set(0, 0, Color::TRANSPARENT);
602 let mut dst = Pixels::filled(4, 4, Color::rgb8(0, 0, 255));
603 let r = dst.blit(&src, 1, 1);
604 assert_eq!(r, Rect::from_xywh(1.0, 1.0, 2.0, 2.0));
605 assert_eq!(dst.get(1, 1), Some(Color::TRANSPARENT), "blit overwrites");
606 assert_eq!(dst.get(2, 2), Some(red()));
607
608 let mut dst2 = Pixels::filled(4, 4, Color::rgb8(0, 0, 255));
609 dst2.blend_from(&src, 1, 1);
610 assert_eq!(
611 dst2.get(1, 1),
612 Some(Color::rgb8(0, 0, 255)),
613 "alpha 0 keeps dst"
614 );
615 }
616
617 #[test]
618 fn crop_clips_to_image() {
619 let mut px = Pixels::filled(4, 3, red());
620 px.set(0, 0, Color::GREEN);
621 let c = px.crop(Rect::from_xywh(0.0, 0.0, 2.0, 2.0));
622 assert_eq!((c.width(), c.height()), (2, 2));
623 assert_eq!(c.get(0, 0), Some(Color::GREEN));
624 assert_eq!(c.get(1, 1), Some(red()));
625 assert!(px.crop(Rect::from_xywh(10.0, 10.0, 5.0, 5.0)).is_empty());
626 }
627
628 #[test]
629 fn flips_and_rotate_match_mirror() {
630 let mut px = Pixels::new(3, 1);
631 px.set(0, 0, red());
632 px.flip_horizontal();
633 assert_eq!(px.get(2, 0), Some(red()));
634 px.flip_vertical();
635 assert_eq!(px.get(2, 0), Some(red()));
636 px.rotate180();
637 assert_eq!(px.get(0, 0), Some(red()));
638 }
639
640 #[test]
641 fn thumbnail_only_scales_down_and_keeps_aspect() {
642 let px = Pixels::filled(200, 100, red());
643 let t = px.thumbnail(50);
644 assert_eq!((t.width(), t.height()), (50, 25));
645 assert_eq!(px.thumbnail(500).width(), 200, "small image untouched");
646 assert!(px.scaled_to_fit(500).is_none());
647 let up = px.resize_nearest(400, 200);
648 assert_eq!((up.width(), up.height()), (400, 200));
649 }
650
651 #[test]
652 fn nearest_resize_preserves_hard_edges() {
653 let mut px = Pixels::new(2, 2);
654 px.set(0, 0, red());
655 let up = px.resize_nearest(4, 4);
656 assert_eq!(up.get(1, 1), Some(red()));
657 assert_eq!(up.get(0, 0), Some(red()));
658 assert_eq!(up.get(3, 3), Some(Color::TRANSPARENT));
659 }
660
661 #[test]
662 fn bilinear_sample_averages_neighbours() {
663 let mut px = Pixels::new(2, 1);
664 px.set(0, 0, Color::BLACK);
665 px.set(1, 0, Color::WHITE);
666 let mid = px.sample_bilinear(0.5, 0.0);
667 assert!((mid.r - 0.5).abs() < 1e-6, "{mid:?}");
668 assert_eq!(px.sample_nearest(0.0, 0.0), Color::BLACK);
669 }
670
671 #[test]
672 fn color_transforms_keep_alpha() {
673 let px = Pixels::filled(2, 2, Color::rgba(1.0, 0.0, 0.0, 0.5));
674 let g = px.grayscale();
675 let c = g.get(0, 0).unwrap();
676 assert!((c.r - c.g).abs() < 1e-6 && (c.g - c.b).abs() < 1e-6);
677 assert!((c.a - 0.5).abs() < 0.01);
678 let inv = px.inverted();
679 assert!((inv.get(1, 1).unwrap().r - 0.0).abs() < 1e-6);
680 let tint = Pixels::filled(1, 1, Color::WHITE).tinted(Color::rgba(1.0, 0.0, 0.0, 1.0));
681 assert_eq!(tint.get(0, 0), Some(red()));
682 let faded = Pixels::filled(1, 1, Color::WHITE).multiply_alpha(0.25);
683 assert!((faded.get(0, 0).unwrap().a - 0.25).abs() < 0.01);
684 }
685
686 #[test]
687 fn opaque_bounds_tightly_wraps_content() {
688 let mut px = Pixels::new(10, 10);
689 px.set(3, 4, red());
690 px.set(5, 6, red());
691 assert_eq!(
692 px.opaque_bounds(),
693 Some(Rect::from_xywh(3.0, 4.0, 3.0, 3.0))
694 );
695 assert_eq!(Pixels::new(2, 2).opaque_bounds(), None);
696 }
697
698 #[test]
699 fn source_conversions_carry_dimensions() {
700 let px = Pixels::filled(3, 2, red());
701 assert_eq!(px.to_source(7, 1).extent(), Some((3, 2)));
702 assert_eq!(px.clone().into_source(7, 1).dynamic_version(), Some(1));
703 assert_eq!(px.to_source(9, 1).cache_key(), 9);
704 }
705
706 #[test]
707 fn map_applies_pixel_fn() {
708 let px = Pixels::filled(2, 2, Color::rgb8(10, 20, 30));
709 let out = px.map(|_, _, c| Color::rgba(c.r, c.g, c.b, 0.5));
710 assert!((out.get(1, 1).unwrap().a - 0.5).abs() < 0.01);
711 let mut inplace = px.clone();
712 inplace.for_each_pixel(|_, _, _| Color::BLACK);
713 assert_eq!(inplace.get(0, 0), Some(Color::BLACK));
714 }
715}