Skip to main content

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}