Skip to main content

agg_gui/
framebuffer.rs

1//! RGBA framebuffer — the rendering target for the entire widget tree.
2//!
3//! # Memory layout
4//!
5//! Pixels are stored in **bottom-up row order** (Y-up): row 0 is at the start
6//! of the `Vec<u8>` and corresponds to the bottom edge of the image (Y = 0).
7//! Row `height - 1` is at the end and corresponds to the top edge.
8//!
9//! This matches OpenGL's texture layout: `glTexImage2D` treats the first byte
10//! as the bottom-left pixel, so the buffer can be uploaded directly without
11//! any Y-flip at the GL boundary.
12//!
13//! For display targets that use a top-down layout (e.g. HTML Canvas
14//! `putImageData`), use [`Framebuffer::pixels_flipped`] to obtain a copy with
15//! rows reversed.
16
17/// RGBA framebuffer with bottom-up (Y-up) row ordering.
18pub struct Framebuffer {
19    pixels: Vec<u8>, // RGBA8, row 0 = bottom (Y = 0)
20    width: u32,
21    height: u32,
22}
23
24impl Framebuffer {
25    /// Create a new zeroed framebuffer.
26    pub fn new(width: u32, height: u32) -> Self {
27        Self {
28            pixels: vec![0u8; (width * height * 4) as usize],
29            width,
30            height,
31        }
32    }
33
34    pub fn width(&self) -> u32 {
35        self.width
36    }
37
38    pub fn height(&self) -> u32 {
39        self.height
40    }
41
42    /// Raw RGBA8 pixels in bottom-up row order. Row 0 = Y=0 = bottom of image.
43    /// Upload directly to OpenGL without modification.
44    pub fn pixels(&self) -> &[u8] {
45        &self.pixels
46    }
47
48    /// Mutable access to the raw pixel data.
49    pub fn pixels_mut(&mut self) -> &mut [u8] {
50        &mut self.pixels
51    }
52
53    /// Consume the framebuffer and return ownership of the underlying
54    /// `Vec<u8>`.  Used by widget backbuffer caching to hand the bitmap
55    /// off to an `Arc` without copying.
56    pub fn into_pixels(self) -> Vec<u8> {
57        self.pixels
58    }
59
60    /// Resize the framebuffer (pixels are zeroed on resize).
61    pub fn resize(&mut self, width: u32, height: u32) {
62        if self.width != width || self.height != height {
63            self.width = width;
64            self.height = height;
65            self.pixels = vec![0u8; (width * height * 4) as usize];
66        }
67    }
68
69    /// Return a copy of the pixels with rows reversed (top-down / Y-down order).
70    ///
71    /// Use this for HTML Canvas `putImageData`, which expects the first row to
72    /// be the top of the image.
73    pub fn pixels_flipped(&self) -> Vec<u8> {
74        let row_bytes = (self.width * 4) as usize;
75        let mut flipped = vec![0u8; self.pixels.len()];
76        for y in 0..self.height as usize {
77            let src = (self.height as usize - 1 - y) * row_bytes;
78            let dst = y * row_bytes;
79            flipped[dst..dst + row_bytes].copy_from_slice(&self.pixels[src..src + row_bytes]);
80        }
81        flipped
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Alpha conversion helpers
87// ---------------------------------------------------------------------------
88//
89// The AGG rasterizer writes **premultiplied** RGBA into its framebuffer.  The
90// `DrawCtx::draw_image_rgba` API, in contrast, takes **straight-alpha** input
91// (PNG/markdown images, screenshots, etc.).  These helpers convert between
92// the two representations at the boundary — Label un-premultiplies its AGG
93// backbuffer before handing it to `draw_image_rgba`, and the software
94// compositor premultiplies straight input before writing it back into an AGG
95// framebuffer for blending.
96
97/// Convert an RGBA8 buffer from **premultiplied** to **straight** alpha
98/// in place.  For each pixel, divides the colour channels by the alpha.
99/// Zero-alpha pixels are zeroed out (there is no colour information to recover).
100pub fn unpremultiply_rgba_inplace(data: &mut [u8]) {
101    for px in data.chunks_exact_mut(4) {
102        let a = px[3];
103        if a == 0 {
104            px[0] = 0;
105            px[1] = 0;
106            px[2] = 0;
107        } else if a < 255 {
108            let af = a as u32;
109            // Round-half-up division: (c * 255 + a/2) / a.
110            px[0] = (((px[0] as u32) * 255 + af / 2) / af).min(255) as u8;
111            px[1] = (((px[1] as u32) * 255 + af / 2) / af).min(255) as u8;
112            px[2] = (((px[2] as u32) * 255 + af / 2) / af).min(255) as u8;
113        }
114    }
115}
116
117/// Convert an RGBA8 buffer from **straight** to **premultiplied** alpha
118/// in place.  Each colour channel is multiplied by the alpha.
119pub fn premultiply_rgba_inplace(data: &mut [u8]) {
120    for px in data.chunks_exact_mut(4) {
121        let a = px[3] as u32;
122        if a < 255 {
123            // Round-half-up: (c * a + 127) / 255.
124            px[0] = (((px[0] as u32) * a + 127) / 255) as u8;
125            px[1] = (((px[1] as u32) * a + 127) / 255) as u8;
126            px[2] = (((px[2] as u32) * a + 127) / 255) as u8;
127        }
128    }
129}
130
131#[cfg(test)]
132mod alpha_tests {
133    use super::*;
134
135    /// Round-trip premul → unpremul on a half-alpha white pixel must recover
136    /// the original colour channels (within 1/255 rounding error).
137    #[test]
138    fn test_premul_roundtrip_half_alpha_white() {
139        // Straight white at 50 % opacity: (255, 255, 255, 128).
140        let mut px = [255, 255, 255, 128];
141        premultiply_rgba_inplace(&mut px);
142        // Premultiplied: (128, 128, 128, 128).
143        assert_eq!(px, [128, 128, 128, 128]);
144        unpremultiply_rgba_inplace(&mut px);
145        // Round-tripped: should recover (255, 255, 255, 128) exactly.
146        assert_eq!(px, [255, 255, 255, 128]);
147    }
148
149    /// Fully-opaque pixels are unchanged in both directions.
150    #[test]
151    fn test_premul_opaque_unchanged() {
152        let mut px = [60, 120, 240, 255];
153        premultiply_rgba_inplace(&mut px);
154        assert_eq!(px, [60, 120, 240, 255]);
155        unpremultiply_rgba_inplace(&mut px);
156        assert_eq!(px, [60, 120, 240, 255]);
157    }
158
159    /// Fully-transparent pixels zero out their colour when un-premultiplied.
160    #[test]
161    fn test_unpremul_zero_alpha_zeros_colour() {
162        let mut px = [10, 20, 30, 0];
163        unpremultiply_rgba_inplace(&mut px);
164        assert_eq!(px, [0, 0, 0, 0]);
165    }
166}