drmkit_capture/image.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! An in-memory image, and the blend that builds one.
5
6/// A premultiplied ARGB8888 image, one `u32` per pixel, rows contiguous.
7///
8/// Premultiplied because that is what KMS scans out: a plane's `ARGB8888`
9/// pixels already have their color scaled by their coverage, and compositing
10/// them any other way would double-apply it. The layout is `0xAARRGGBB` in a
11/// native-endian `u32`, which is what a little-endian host sees in a
12/// `DRM_FORMAT_ARGB8888` buffer.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Image {
15 width: u32,
16 height: u32,
17 pixels: Vec<u32>,
18}
19
20impl Image {
21 /// A transparent image of the given size.
22 ///
23 /// A zero in either dimension gives the same empty image as
24 /// [`Image::default`] -- there is no such thing as a 0x100 picture, and
25 /// carrying one would mean every consumer checking both dimensions.
26 #[must_use]
27 pub fn new(width: u32, height: u32) -> Self {
28 if width == 0 || height == 0 {
29 return Self::default();
30 }
31 Self {
32 width,
33 height,
34 pixels: vec![0; width as usize * height as usize],
35 }
36 }
37
38 /// Width in pixels.
39 #[must_use]
40 pub const fn width(&self) -> u32 {
41 self.width
42 }
43
44 /// Height in pixels.
45 #[must_use]
46 pub const fn height(&self) -> u32 {
47 self.height
48 }
49
50 /// Bytes per row. Always `width * 4`; the storage is tightly packed.
51 #[must_use]
52 pub const fn stride_bytes(&self) -> u32 {
53 self.width * 4
54 }
55
56 /// Whether either dimension is zero.
57 #[must_use]
58 pub const fn is_empty(&self) -> bool {
59 self.width == 0 || self.height == 0
60 }
61
62 /// The pixels, row-major.
63 #[must_use]
64 pub fn pixels(&self) -> &[u32] {
65 &self.pixels
66 }
67
68 /// The pixels, row-major, mutable.
69 pub fn pixels_mut(&mut self) -> &mut [u32] {
70 &mut self.pixels
71 }
72}
73
74/// Split a premultiplied `0xAARRGGBB` pixel into its channels.
75const fn channels(px: u32) -> (u32, u32, u32, u32) {
76 (px >> 24, (px >> 16) & 0xff, (px >> 8) & 0xff, px & 0xff)
77}
78
79/// Round `x / 255` to nearest, without dividing.
80///
81/// The `+ 128` then fold-and-shift is the standard trick: exact for every
82/// product of two bytes, which is the only input it ever gets. Truncating with
83/// a plain `>> 8` instead loses a little coverage on every composite, which
84/// shows up as a seam where two translucent layers meet.
85const fn div255(x: u32) -> u32 {
86 let t = x + 128;
87 (t + (t >> 8)) >> 8
88}
89
90/// Premultiplied `SRC_OVER`: `dst = src + dst * (1 - src_alpha)`.
91#[must_use]
92pub const fn src_over(dst: u32, src: u32) -> u32 {
93 let (sa, sr, sg, sb) = channels(src);
94 if sa == 255 {
95 return src;
96 }
97 if sa == 0 {
98 return dst;
99 }
100 let (da, dr, dg, db) = channels(dst);
101 let inv = 255 - sa;
102 ((sa + div255(da * inv)) << 24)
103 | ((sr + div255(dr * inv)) << 16)
104 | ((sg + div255(dg * inv)) << 8)
105 | (sb + div255(db * inv))
106}
107
108/// Recover straight-alpha channels from a premultiplied pixel.
109///
110/// PNG stores straight alpha, so this runs on the way out. Fully opaque and
111/// fully transparent are exact; everything between divides by the alpha it was
112/// multiplied by, rounding to nearest, and cannot recover precision that the
113/// premultiply threw away. A pixel at alpha 1 has one bit of color left,
114/// and no amount of care here puts them back.
115///
116/// Returns `[r, g, b, a]`, the order PNG wants.
117#[must_use]
118pub fn unpremultiply(px: u32) -> [u8; 4] {
119 let (a, r, g, b) = channels(px);
120 if a == 0 {
121 // Nothing was multiplied in, so there is nothing to recover, and the
122 // color channels of a fully transparent premultiplied pixel are zero
123 // by construction anyway.
124 return [0, 0, 0, 0];
125 }
126 // Clamped because a buffer can hold a pixel whose color exceeds its
127 // alpha -- nothing stops a producer writing one, and the division would
128 // then land above 255.
129 let straight = |c: u32| u8::try_from((((c * 255) + (a / 2)) / a).min(255)).unwrap_or(u8::MAX);
130 let alpha = u8::try_from(a).unwrap_or(u8::MAX);
131 [straight(r), straight(g), straight(b), alpha]
132}