Skip to main content

denise_render/
blit.rs

1//! Drawing somebody else's pixels: the positioned, blended blit.
2//!
3//! [`Canvas::copy_from`] is the damage-publish blit — same coordinates both
4//! sides, no blending, not for pictures. These are the picture operations: a
5//! borrowed block of pixels drawn at a position, composited per pixel, clipped
6//! like everything else.
7//!
8//! # The source is premultiplied
9//!
10//! Source pixels are `0xAARRGGBB` with the colour channels already multiplied
11//! by alpha — the same convention [`Paint`](crate::Paint) uses internally, and
12//! for the same reason: the multiply happens once when the image is prepared,
13//! not once per pixel per frame. Decoders produce straight alpha;
14//! [`blend::premultiply`](crate::blend::premultiply) converts a buffer in
15//! place, once. Fully opaque pixels are identical in both conventions, so an
16//! image with no transparency needs no conversion at all.
17//!
18//! # Scaling is nearest-neighbour
19//!
20//! [`Canvas::blit_scaled`] samples at pixel centres, integer arithmetic only —
21//! exact for icons, QR codes and pixel art, and for integer upscales each
22//! source pixel becomes a crisp block. Bilinear is what a photo wants and is
23//! deliberately absent until it has been benched on the Pi-class targets;
24//! pre-sizing assets is the embedded answer in the meantime.
25
26use denise::{Point, Rect, Size};
27
28use crate::blend::source_over;
29use crate::canvas::{Canvas, PixelView};
30
31/// Composites one premultiplied source word over a destination pixel, with the
32/// two cheap exits a picture is mostly made of.
33#[inline(always)]
34fn blend_word(dst: &mut u32, src: u32) {
35    match src >> 24 {
36        0 => {}
37        255 => *dst = src,
38        a => *dst = source_over(*dst, src, a),
39    }
40}
41
42/// Maps a destination index to a source index by pixel centre: the nearest
43/// source pixel to `(d + 0.5) * src_len / dst_len`, in integers.
44#[inline(always)]
45fn nearest(d: i64, src_len: i64, dst_len: i64) -> i64 {
46    (2 * d + 1) * src_len / (2 * dst_len)
47}
48
49impl Canvas<'_> {
50    /// Draws `src` with its top-left corner at `at`, one source pixel per
51    /// destination pixel, composited with source-over alpha.
52    ///
53    /// Source pixels are premultiplied `0xAARRGGBB` — see the
54    /// [module docs](self). Clipped to the canvas clip like every other
55    /// operation; any part of the image outside it is simply not drawn.
56    pub fn blit(&mut self, src: &PixelView<'_>, at: Point) {
57        let Size { width, height } = src.size();
58        let dest = Rect::new(at.x, at.y, width as i32, height as i32);
59        let Some(visible) = self.visible(dest) else {
60            return;
61        };
62        for y in visible.y..visible.bottom() {
63            let Some(srow) = src.row(y - at.y, visible.x - at.x, visible.right() - at.x) else {
64                continue;
65            };
66            let Some(drow) = self.row_span(y, visible.x, visible.right()) else {
67                continue;
68            };
69            for (d, &s) in drow.iter_mut().zip(srow) {
70                blend_word(d, s);
71            }
72        }
73    }
74
75    /// Draws all of `src` into `dest`, nearest-neighbour resampled, composited
76    /// with source-over alpha.
77    ///
78    /// Source pixels are premultiplied `0xAARRGGBB` — see the
79    /// [module docs](self). When `dest` is exactly the source size this is
80    /// [`Canvas::blit`]. Sampling is at pixel centres, so an integer upscale
81    /// turns each source pixel into an even block, and a downscale picks
82    /// representative pixels rather than always the top-left ones.
83    pub fn blit_scaled(&mut self, src: &PixelView<'_>, dest: Rect) {
84        // A `PixelView` is never empty by construction, and an empty or
85        // negative `dest` never survives `visible`, so the divisions in
86        // `nearest` cannot see a zero.
87        let Size { width, height } = src.size();
88        let (sw, sh) = (width as i64, height as i64);
89        if dest.width == width as i32 && dest.height == height as i32 {
90            return self.blit(src, Point::new(dest.x, dest.y));
91        }
92        let Some(visible) = self.visible(dest) else {
93            return;
94        };
95        for y in visible.y..visible.bottom() {
96            let sy = nearest((y - dest.y) as i64, sh, dest.height as i64);
97            let Some(srow) = src.row(sy as i32, 0, width as i32) else {
98                continue;
99            };
100            let Some(drow) = self.row_span(y, visible.x, visible.right()) else {
101                continue;
102            };
103            for (i, d) in drow.iter_mut().enumerate() {
104                let dx = (visible.x - dest.x) as i64 + i as i64;
105                let sx = nearest(dx, sw, dest.width as i64);
106                blend_word(d, srow[sx as usize]);
107            }
108        }
109    }
110
111    /// Draws all of `src` into `dest`, masked to `shape` with rounded corners
112    /// of `radius`, anti-aliased.
113    ///
114    /// `shape` is the rectangle whose corners are rounded and outside which
115    /// nothing is drawn; sampling is still mapped from the whole of `dest`.
116    /// They are separate arguments because they genuinely differ in the *Cover*
117    /// case — an image scaled past its box so the box is filled edge to edge —
118    /// where `dest` overflows and `shape` is the box. When the picture and the
119    /// mask are the same rectangle, pass it twice. `radius` is clamped to half
120    /// of `shape`'s shorter side, so a full radius on a square shape is a
121    /// circle — the avatar crop. Zero draws exactly [`Canvas::blit_scaled`]
122    /// restricted to `shape`.
123    ///
124    /// The mask must not come from the clip: the clip is damage, and a
125    /// damage-restricted repaint of half an image has to round the image's
126    /// corners, never the damage rectangle's.
127    pub fn blit_rounded(&mut self, src: &PixelView<'_>, dest: Rect, shape: Rect, radius: i32) {
128        use crate::blend::scale_premul;
129        use crate::rounded::{Scan, ceil_px, floor_px};
130
131        let Size { width, height } = src.size();
132        let (sw, sh) = (width as i64, height as i64);
133        let radius = radius.clamp(0, shape.width.min(shape.height) / 2);
134        let Some(painted) = dest.intersect(&shape) else {
135            return;
136        };
137        let Some(visible) = self.visible(painted) else {
138            return;
139        };
140        for y in visible.y..visible.bottom() {
141            let sy = nearest((y - dest.y) as i64, sh, dest.height as i64);
142            let Some(srow) = src.row(sy as i32, 0, width as i32) else {
143                continue;
144            };
145            // Everything between the deepest left inset and the shallowest
146            // right one is fully covered, so only the fringes pay for coverage.
147            let scan = (radius > 0).then(|| Scan::new(shape, radius, y));
148            let (solid0, solid1) = match &scan {
149                None => (visible.x, visible.right()),
150                Some(scan) => (ceil_px(scan.max_left()), floor_px(scan.min_right())),
151            };
152            let Some(drow) = self.row_span(y, visible.x, visible.right()) else {
153                continue;
154            };
155            for (i, d) in drow.iter_mut().enumerate() {
156                let x = visible.x + i as i32;
157                let coverage = if (solid0..solid1).contains(&x) {
158                    255
159                } else if let Some(scan) = &scan {
160                    scan.coverage(x)
161                } else {
162                    255
163                };
164                if coverage == 0 {
165                    continue;
166                }
167                let sx = nearest((x - dest.x) as i64, sw, dest.width as i64);
168                let s = srow[sx as usize];
169                blend_word(
170                    d,
171                    if coverage == 255 {
172                        s
173                    } else {
174                        scale_premul(s, coverage)
175                    },
176                );
177            }
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::blend::{Paint, premultiply};
186    use crate::testing::TestCanvas;
187    use denise::Color;
188
189    /// A source whose every pixel encodes its own coordinates, so any
190    /// misplacement is visible in the value itself.
191    fn coordinate_source(width: u32, height: u32) -> Vec<u32> {
192        (0..height)
193            .flat_map(|y| (0..width).map(move |x| 0xFF00_0000 | (x << 8) | y))
194            .collect()
195    }
196
197    #[test]
198    fn a_blit_lands_exactly_where_it_is_put() {
199        let pixels = coordinate_source(3, 2);
200        let src = PixelView::new(&pixels, Size::new(3, 2), 3).unwrap();
201        let mut t = TestCanvas::new(8, 8);
202        t.canvas().blit(&src, Point::new(2, 3));
203        for y in 0..8 {
204            for x in 0..8 {
205                let inside = (2..5).contains(&x) && (3..5).contains(&y);
206                let expected = if inside {
207                    0xFF00_0000 | (((x - 2) as u32) << 8) | (y - 3) as u32
208                } else {
209                    0
210                };
211                assert_eq!(t.at(x, y), expected, "at {x},{y}");
212            }
213        }
214    }
215
216    #[test]
217    fn a_blit_is_clipped_at_every_edge() {
218        // Hang the image off each corner in turn; only the overlap may change.
219        let pixels = coordinate_source(4, 4);
220        let src = PixelView::new(&pixels, Size::new(4, 4), 4).unwrap();
221        for at in [
222            Point::new(-2, -2),
223            Point::new(6, -2),
224            Point::new(-2, 6),
225            Point::new(6, 6),
226        ] {
227            let mut t = TestCanvas::new(8, 8);
228            t.canvas().blit(&src, at);
229            for y in 0..8i32 {
230                for x in 0..8i32 {
231                    let (sx, sy) = (x - at.x, y - at.y);
232                    let inside = (0..4).contains(&sx) && (0..4).contains(&sy);
233                    let expected = if inside {
234                        0xFF00_0000 | ((sx as u32) << 8) | sy as u32
235                    } else {
236                        0
237                    };
238                    assert_eq!(t.at(x, y), expected, "at {x},{y} for corner {at:?}");
239                }
240            }
241        }
242    }
243
244    #[test]
245    fn a_blit_respects_the_canvas_clip() {
246        let pixels = vec![0xFFFF_FFFF; 64];
247        let src = PixelView::new(&pixels, Size::new(8, 8), 8).unwrap();
248        let mut t = TestCanvas::new(8, 8);
249        {
250            let mut c = t.canvas();
251            c.clip_to(Rect::new(2, 2, 4, 4));
252            c.blit(&src, Point::new(0, 0));
253        }
254        for y in 0..8 {
255            for x in 0..8 {
256                let inside = (2..6).contains(&x) && (2..6).contains(&y);
257                assert_eq!(t.at(x, y) != 0, inside, "at {x},{y}");
258            }
259        }
260    }
261
262    #[test]
263    fn a_blit_respects_the_stride_padding() {
264        let pixels = vec![0xFFFF_FFFF; 4];
265        let src = PixelView::new(&pixels, Size::new(2, 2), 2).unwrap();
266        let mut t = TestCanvas::with_stride(4, 4, 9);
267        t.canvas().blit(&src, Point::new(2, 0));
268        // The two padding words after the first drawn row stay untouched.
269        assert_eq!(t.at(3, 0), 0xFFFF_FFFF);
270        assert_eq!(t.pixels()[4], 0, "padding written");
271        assert_eq!(t.pixels()[8], 0, "padding written");
272        assert_eq!(t.at(3, 1), 0xFFFF_FFFF);
273    }
274
275    #[test]
276    fn alpha_pixels_composite_by_the_blend_rules() {
277        // A premultiplied half-alpha source pixel must land exactly where the
278        // rasteriser's own arithmetic puts it — one rule, not two.
279        let color = Color::rgba(200, 100, 50, 128);
280        let paint = Paint::new(color);
281        let mut pixels = vec![u32::from_be_bytes([color.a, color.r, color.g, color.b])];
282        premultiply(&mut pixels);
283        assert_eq!(pixels[0], paint.premultiplied());
284
285        let src = PixelView::new(&pixels, Size::new(1, 1), 1).unwrap();
286        let mut t = TestCanvas::new(1, 1);
287        t.canvas().clear(Color::rgb(0, 0, 64));
288        let background = t.at(0, 0);
289        t.canvas().blit(&src, Point::new(0, 0));
290        assert_eq!(
291            t.at(0, 0),
292            source_over(background, paint.premultiplied(), paint.alpha())
293        );
294    }
295
296    #[test]
297    fn transparent_pixels_leave_the_destination_alone() {
298        let pixels = vec![0u32; 4];
299        let src = PixelView::new(&pixels, Size::new(2, 2), 2).unwrap();
300        let mut t = TestCanvas::new(2, 2);
301        t.canvas().clear(Color::rgb(10, 20, 30));
302        let before = t.pixels().to_vec();
303        t.canvas().blit(&src, Point::new(0, 0));
304        assert_eq!(t.pixels(), &before[..]);
305    }
306
307    #[test]
308    fn scaling_to_the_source_size_is_a_plain_blit() {
309        let pixels = coordinate_source(3, 3);
310        let src = PixelView::new(&pixels, Size::new(3, 3), 3).unwrap();
311        let mut plain = TestCanvas::new(8, 8);
312        plain.canvas().blit(&src, Point::new(2, 2));
313        let mut scaled = TestCanvas::new(8, 8);
314        scaled.canvas().blit_scaled(&src, Rect::new(2, 2, 3, 3));
315        assert_eq!(plain.pixels(), scaled.pixels());
316    }
317
318    #[test]
319    fn an_integer_upscale_makes_even_blocks() {
320        let pixels = coordinate_source(2, 2);
321        let src = PixelView::new(&pixels, Size::new(2, 2), 2).unwrap();
322        let mut t = TestCanvas::new(4, 4);
323        t.canvas().blit_scaled(&src, Rect::new(0, 0, 4, 4));
324        for y in 0..4 {
325            for x in 0..4 {
326                let expected = 0xFF00_0000 | (((x / 2) as u32) << 8) | (y / 2) as u32;
327                assert_eq!(t.at(x, y), expected, "at {x},{y}");
328            }
329        }
330    }
331
332    #[test]
333    fn a_downscale_samples_pixel_centres_not_corners() {
334        // Halving 8 wide to 4 must pick columns 1, 3, 5, 7 — the centres —
335        // not 0, 2, 4, 6, which a floor-of-left-edge mapping would give.
336        let pixels = coordinate_source(8, 1);
337        let src = PixelView::new(&pixels, Size::new(8, 1), 8).unwrap();
338        let mut t = TestCanvas::new(4, 1);
339        t.canvas().blit_scaled(&src, Rect::new(0, 0, 4, 1));
340        for x in 0..4 {
341            let expected = 0xFF00_0000 | (((2 * x + 1) as u32) << 8);
342            assert_eq!(t.at(x, 0), expected, "at {x}");
343        }
344    }
345
346    #[test]
347    fn a_clipped_scaled_blit_samples_as_if_unclipped() {
348        // The part of a scaled image that survives clipping must be the same
349        // pixels it would have been without the clip.
350        let pixels = coordinate_source(5, 5);
351        let src = PixelView::new(&pixels, Size::new(5, 5), 5).unwrap();
352        let dest = Rect::new(-3, -3, 13, 13);
353
354        let mut whole = TestCanvas::new(16, 16);
355        whole.canvas().blit_scaled(&src, Rect::new(5, 5, 13, 13));
356        let mut clipped = TestCanvas::new(8, 8);
357        clipped.canvas().blit_scaled(&src, dest);
358
359        for y in 0..8 {
360            for x in 0..8 {
361                assert_eq!(clipped.at(x, y), whole.at(x + 8, y + 8), "at {x},{y}");
362            }
363        }
364    }
365
366    #[test]
367    fn absurd_rectangles_neither_read_nor_write_out_of_bounds() {
368        let pixels = coordinate_source(4, 4);
369        let src = PixelView::new(&pixels, Size::new(4, 4), 4).unwrap();
370        let mut t = TestCanvas::new(8, 8);
371        for dest in [
372            Rect::new(-1_000_000, -1_000_000, 3_000_000, 3_000_000),
373            Rect::new(i32::MIN / 2, i32::MIN / 2, i32::MAX, i32::MAX),
374            Rect::new(0, 0, i32::MAX, 1),
375            Rect::new(4, 4, 0, 5),
376            Rect::new(4, 4, 5, 0),
377            Rect::new(100, 100, 4, 4),
378        ] {
379            t.canvas().blit_scaled(&src, dest);
380        }
381        let empty = PixelView::new(&pixels, Size::new(4, 4), 4).unwrap();
382        t.canvas()
383            .blit(&empty, Point::new(i32::MAX - 1, i32::MAX - 1));
384        t.canvas().blit(&empty, Point::new(i32::MIN, i32::MIN));
385    }
386
387    #[test]
388    fn a_rounded_blit_of_solid_white_is_a_rounded_fill() {
389        // The mask arithmetic must be the same arithmetic the rounded fill
390        // uses, not a lookalike: a solid white image drawn through the mask
391        // has to produce fill_rounded_rect's pixels exactly, fringes included.
392        let pixels = vec![0xFFFF_FFFFu32; 32 * 32];
393        let src = PixelView::new(&pixels, Size::new(32, 32), 32).unwrap();
394        let shape = Rect::new(2, 2, 28, 28);
395
396        let mut blitted = TestCanvas::new(32, 32);
397        blitted.canvas().blit_rounded(&src, shape, shape, 8);
398        let mut filled = TestCanvas::new(32, 32);
399        filled.canvas().fill_rounded_rect(shape, 8, Color::WHITE);
400
401        assert_eq!(blitted.pixels(), filled.pixels());
402    }
403
404    #[test]
405    fn a_zero_radius_rounded_blit_is_a_scaled_blit() {
406        let pixels = coordinate_source(5, 5);
407        let src = PixelView::new(&pixels, Size::new(5, 5), 5).unwrap();
408        let dest = Rect::new(1, 1, 10, 10);
409
410        let mut rounded = TestCanvas::new(12, 12);
411        rounded.canvas().blit_rounded(&src, dest, dest, 0);
412        let mut scaled = TestCanvas::new(12, 12);
413        scaled.canvas().blit_scaled(&src, dest);
414
415        assert_eq!(rounded.pixels(), scaled.pixels());
416    }
417
418    #[test]
419    fn a_full_radius_on_a_square_is_the_avatar_circle() {
420        let pixels = vec![0xFFFF_FFFFu32; 16 * 16];
421        let src = PixelView::new(&pixels, Size::new(16, 16), 16).unwrap();
422        let shape = Rect::new(0, 0, 16, 16);
423        let mut t = TestCanvas::new(16, 16);
424        t.canvas().blit_rounded(&src, shape, shape, 999);
425        assert_eq!(t.at(0, 0), 0, "corner outside the circle");
426        assert_eq!(t.at(15, 15), 0, "corner outside the circle");
427        assert_eq!(t.at(8, 8), 0xFFFF_FFFF, "centre inside the circle");
428        assert_eq!(t.at(8, 0) >> 24, 255, "top of the circle touches the edge");
429    }
430
431    #[test]
432    fn the_shape_crops_an_overflowing_dest_the_cover_case() {
433        // A 2x-scaled image mapped past its box: pixels must stop at the
434        // shape, and the ones inside must be the same pixels the unmasked
435        // mapping would have put there.
436        let pixels = coordinate_source(8, 8);
437        let src = PixelView::new(&pixels, Size::new(8, 8), 8).unwrap();
438        let dest = Rect::new(-4, -4, 16, 16);
439        let shape = Rect::new(2, 2, 4, 4);
440
441        let mut masked = TestCanvas::new(8, 8);
442        masked.canvas().blit_rounded(&src, dest, shape, 0);
443        let mut unmasked = TestCanvas::new(8, 8);
444        unmasked.canvas().blit_scaled(&src, dest);
445
446        for y in 0..8 {
447            for x in 0..8 {
448                let inside = (2..6).contains(&x) && (2..6).contains(&y);
449                let expected = if inside { unmasked.at(x, y) } else { 0 };
450                assert_eq!(masked.at(x, y), expected, "at {x},{y}");
451            }
452        }
453    }
454
455    #[test]
456    fn damage_clipping_rounds_the_image_corners_not_the_damage_rect() {
457        // Repainting half the image through a clip must reproduce exactly the
458        // pixels a full repaint puts there — the mask follows the shape, and
459        // the clip must not create its own corners.
460        let pixels = vec![0xFFFF_FFFFu32; 24 * 24];
461        let src = PixelView::new(&pixels, Size::new(24, 24), 24).unwrap();
462        let shape = Rect::new(0, 0, 24, 24);
463
464        let mut whole = TestCanvas::new(24, 24);
465        whole.canvas().blit_rounded(&src, shape, shape, 8);
466
467        let mut damaged = TestCanvas::new(24, 24);
468        {
469            let mut c = damaged.canvas();
470            c.clip_to(Rect::new(0, 0, 12, 24));
471            c.blit_rounded(&src, shape, shape, 8);
472        }
473        for y in 0..24 {
474            for x in 0..12 {
475                assert_eq!(damaged.at(x, y), whole.at(x, y), "at {x},{y}");
476            }
477            for x in 12..24 {
478                assert_eq!(damaged.at(x, y), 0, "leaked past the clip at {x},{y}");
479            }
480        }
481    }
482
483    #[test]
484    fn rounded_blits_survive_absurd_rectangles() {
485        let pixels = coordinate_source(4, 4);
486        let src = PixelView::new(&pixels, Size::new(4, 4), 4).unwrap();
487        let mut t = TestCanvas::new(8, 8);
488        for (dest, shape) in [
489            (
490                Rect::new(-1_000_000, -1_000_000, 3_000_000, 3_000_000),
491                Rect::new(0, 0, 8, 8),
492            ),
493            (Rect::new(0, 0, 8, 8), Rect::new(100, 100, 4, 4)),
494            (Rect::new(0, 0, 0, 0), Rect::new(0, 0, 8, 8)),
495            (
496                Rect::new(i32::MIN / 2, i32::MIN / 2, i32::MAX, i32::MAX),
497                Rect::new(2, 2, 4, 4),
498            ),
499        ] {
500            t.canvas().blit_rounded(&src, dest, shape, 3);
501        }
502    }
503
504    #[test]
505    fn premultiply_is_exact_and_paints_agree() {
506        for (r, g, b, a) in [
507            (255, 255, 255, 255),
508            (0xAB, 0xCD, 0xEF, 0),
509            (200, 100, 50, 128),
510        ] {
511            let mut px = [u32::from_be_bytes([a, r, g, b])];
512            premultiply(&mut px);
513            assert_eq!(
514                px[0],
515                Paint::new(Color::rgba(r, g, b, a)).premultiplied(),
516                "rgba({r},{g},{b},{a})"
517            );
518        }
519    }
520}