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}