Skip to main content

sequoia_core/
pixel.rs

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}