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