use crate::geometry::LogicalRect;
use crate::style::Rgba;
#[derive(Clone)]
pub struct Framebuffer {
width: u32,
height: u32,
data: Vec<u8>,
}
impl std::fmt::Debug for Framebuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Framebuffer")
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
impl Framebuffer {
pub fn new(width: u32, height: u32) -> Self {
let (w, h) = (width.max(1), height.max(1));
Framebuffer {
width: w,
height: h,
data: vec![0u8; (w as usize) * (h as usize) * 4],
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn pixels(&self) -> &[u8] {
&self.data
}
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.data
}
pub fn fill(&mut self, color: Rgba) {
let a = color.a as u32;
let (r, g, b) = (
(color.r as u32 * a / 255) as u8,
(color.g as u32 * a / 255) as u8,
(color.b as u32 * a / 255) as u8,
);
for px in self.data.chunks_exact_mut(4) {
px[0] = r;
px[1] = g;
px[2] = b;
px[3] = color.a;
}
}
pub fn draw(&mut self, src: &Framebuffer, x: i32, y: i32) {
let (dw, dh) = (self.width as i32, self.height as i32);
let (sw, sh) = (src.width as i32, src.height as i32);
for row in 0..sh {
let dy = y + row;
if dy < 0 || dy >= dh {
continue;
}
for col in 0..sw {
let dx = x + col;
if dx < 0 || dx >= dw {
continue;
}
let s = ((row * sw + col) * 4) as usize;
let sa = src.data[s + 3];
if sa == 0 {
continue;
}
let d = ((dy * dw + dx) * 4) as usize;
let inv = 255 - sa as u32;
for c in 0..4 {
let out = src.data[s + c] as u32 + self.data[d + c] as u32 * inv / 255;
self.data[d + c] = out.min(255) as u8;
}
}
}
}
pub fn to_straight_rgba(&self) -> Vec<u8> {
let mut out = vec![0u8; self.data.len()];
for (dst, src) in out.chunks_exact_mut(4).zip(self.data.chunks_exact(4)) {
let a = src[3];
if a == 0 {
continue;
}
let a32 = a as u32;
dst[0] = (src[0] as u32 * 255 / a32).min(255) as u8;
dst[1] = (src[1] as u32 * 255 / a32).min(255) as u8;
dst[2] = (src[2] as u32 * 255 / a32).min(255) as u8;
dst[3] = a;
}
out
}
pub fn encode_png(&self) -> Vec<u8> {
let straight = self.to_straight_rgba();
let mut out = Vec::new();
{
let mut encoder = png::Encoder::new(&mut out, self.width, self.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().expect("png header");
writer.write_image_data(&straight).expect("png data");
}
out
}
}
pub(crate) fn encode_rgba_png(rgba: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
if width == 0 || height == 0 {
return None;
}
let expected = (width as usize)
.checked_mul(height as usize)?
.checked_mul(4)?;
if rgba.len() != expected {
return None;
}
let mut out = Vec::new();
{
let mut encoder = png::Encoder::new(&mut out, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().ok()?;
writer.write_image_data(rgba).ok()?;
}
Some(out)
}
#[inline]
pub(crate) fn blend_pixel(dst: &mut [u8], off: usize, src: Rgba, a: u8) {
let sa = a as u32;
let inv = 255 - sa;
let sr = src.r as u32 * sa / 255;
let sg = src.g as u32 * sa / 255;
let sb = src.b as u32 * sa / 255;
dst[off] = (sr + dst[off] as u32 * inv / 255).min(255) as u8;
dst[off + 1] = (sg + dst[off + 1] as u32 * inv / 255).min(255) as u8;
dst[off + 2] = (sb + dst[off + 2] as u32 * inv / 255).min(255) as u8;
dst[off + 3] = (sa + dst[off + 3] as u32 * inv / 255).min(255) as u8;
}
#[inline]
fn round_rect_sdf(px: f32, py: f32, x: f32, y: f32, w: f32, h: f32, r: f32) -> f32 {
let cx = x + w / 2.0;
let cy = y + h / 2.0;
let qx = (px - cx).abs() - (w / 2.0 - r);
let qy = (py - cy).abs() - (h / 2.0 - r);
let ox = qx.max(0.0);
let oy = qy.max(0.0);
let outside = (ox * ox + oy * oy).sqrt();
let inside = qx.max(qy).min(0.0);
outside + inside - r
}
pub(crate) fn fill_round_rect(
fb: &mut Framebuffer,
x: f32,
y: f32,
w: f32,
h: f32,
r: f32,
color: Rgba,
) {
if w <= 0.0 || h <= 0.0 || color.a == 0 {
return;
}
let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
let (fw, fh) = (fb.width as i32, fb.height as i32);
let x0 = ((x.floor() as i32) - 1).clamp(0, fw);
let y0 = ((y.floor() as i32) - 1).clamp(0, fh);
let x1 = (((x + w).ceil() as i32) + 1).clamp(0, fw);
let y1 = (((y + h).ceil() as i32) + 1).clamp(0, fh);
let ca = color.a as f32;
let pixels = fb.pixels_mut();
for py in y0..y1 {
for px in x0..x1 {
let d = round_rect_sdf(px as f32 + 0.5, py as f32 + 0.5, x, y, w, h, r);
let cov = (0.5 - d).clamp(0.0, 1.0);
if cov <= 0.0 {
continue;
}
let a = (ca * cov).round() as u8;
if a == 0 {
continue;
}
blend_pixel(pixels, ((py * fw + px) * 4) as usize, color, a);
}
}
}
pub(crate) fn fill_rect(fb: &mut Framebuffer, x: f32, y: f32, w: f32, h: f32, color: Rgba) {
if color.a == 0 {
return;
}
let (fw, fh) = (fb.width as i32, fb.height as i32);
let x0 = (x.round() as i32).clamp(0, fw);
let y0 = (y.round() as i32).clamp(0, fh);
let x1 = ((x + w).round() as i32).clamp(0, fw);
let y1 = ((y + h).round() as i32).clamp(0, fh);
let pixels = fb.pixels_mut();
for py in y0..y1 {
for px in x0..x1 {
blend_pixel(pixels, ((py * fw + px) * 4) as usize, color, color.a);
}
}
}
const MAX_ICON_DIM: u32 = 4096;
const MAX_ICON_PIXELS: u64 = 2048 * 2048;
pub fn decode_png(bytes: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
let mut decoder = png::Decoder::new(bytes);
decoder.set_transformations(png::Transformations::normalize_to_color8());
let mut reader = decoder.read_info().ok()?;
let hdr = reader.info();
let (hw, hh) = (hdr.width, hdr.height);
if hw == 0 || hh == 0 || hw > MAX_ICON_DIM || hh > MAX_ICON_DIM {
return None;
}
if (hw as u64) * (hh as u64) > MAX_ICON_PIXELS {
return None;
}
let mut buf = vec![0u8; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).ok()?;
let (w, h) = (info.width, info.height);
let count = (w as usize).checked_mul(h as usize)?;
let mut out = vec![0u8; count * 4];
match info.color_type {
png::ColorType::Rgba => {
out.copy_from_slice(&buf[..count * 4]);
}
png::ColorType::Rgb => {
for (dst, src) in out.chunks_exact_mut(4).zip(buf.chunks_exact(3)) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = 255;
}
}
png::ColorType::GrayscaleAlpha => {
for (dst, src) in out.chunks_exact_mut(4).zip(buf.chunks_exact(2)) {
dst[0] = src[0];
dst[1] = src[0];
dst[2] = src[0];
dst[3] = src[1];
}
}
png::ColorType::Grayscale => {
for (dst, &g) in out.chunks_exact_mut(4).zip(buf.iter()) {
dst[0] = g;
dst[1] = g;
dst[2] = g;
dst[3] = 255;
}
}
png::ColorType::Indexed => return None,
}
Some((out, w, h))
}
#[inline]
pub(crate) fn scaled(rect: LogicalRect, scale: f32) -> (f32, f32, f32, f32) {
(
rect.origin.x * scale,
rect.origin.y * scale,
rect.size.width * scale,
rect.size.height * scale,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn png_crc32(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &b in bytes {
crc ^= b as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
fn png_header_with_dims(w: u32, h: u32) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1A, b'\n']);
let mut chunk = Vec::new();
chunk.extend_from_slice(b"IHDR");
chunk.extend_from_slice(&w.to_be_bytes());
chunk.extend_from_slice(&h.to_be_bytes());
chunk.push(8); chunk.push(6); chunk.push(0); chunk.push(0); chunk.push(0); out.extend_from_slice(&13u32.to_be_bytes()); out.extend_from_slice(&chunk);
out.extend_from_slice(&png_crc32(&chunk).to_be_bytes());
out
}
#[test]
fn decode_png_rejects_oversize_dimensions_before_allocating() {
let bytes = png_header_with_dims(60_000, 60_000);
assert!(decode_png(&bytes).is_none());
}
#[test]
fn decode_png_rejects_a_square_image_over_the_pixel_cap_but_under_the_dim_cap() {
let bytes = png_header_with_dims(3000, 3000);
const { assert!((3000u64 * 3000) <= MAX_ICON_DIM as u64 * MAX_ICON_DIM as u64) };
const { assert!((3000u64 * 3000) > MAX_ICON_PIXELS) };
assert!(decode_png(&bytes).is_none());
}
#[test]
fn decode_png_rejects_malformed_bytes() {
assert!(decode_png(&[]).is_none());
assert!(decode_png(b"not a png at all").is_none());
assert!(decode_png(&[0x89, b'P', b'N', b'G']).is_none()); }
#[test]
fn decode_png_roundtrips_a_small_real_image() {
let mut fb = Framebuffer::new(2, 2);
fb.fill(Rgba::opaque(255, 0, 0));
let bytes = fb.encode_png();
let (rgba, w, h) = decode_png(&bytes).expect("a real, small PNG decodes");
assert_eq!((w, h), (2, 2));
assert_eq!(rgba.len(), 2 * 2 * 4);
assert_eq!(&rgba[0..4], &[255, 0, 0, 255]);
}
#[test]
fn encode_rgba_png_roundtrips_through_decode() {
let rgba = vec![
255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 255, 9, 8, 7, 0, ];
let png = encode_rgba_png(&rgba, 2, 2).expect("valid RGBA encodes");
let (decoded, w, h) = decode_png(&png).expect("the encoded PNG decodes");
assert_eq!((w, h), (2, 2));
assert_eq!(&decoded[0..8], &rgba[0..8]);
assert_eq!(&decoded[8..12], &rgba[8..12]);
assert_eq!(decoded[15], 0, "the transparent pixel stays transparent");
}
#[test]
fn encode_rgba_png_rejects_bad_dimensions_and_lengths() {
assert!(encode_rgba_png(&[0, 0, 0, 0], 0, 1).is_none());
assert!(encode_rgba_png(&[0, 0, 0, 0], 1, 0).is_none());
assert!(encode_rgba_png(&[0, 0, 0], 1, 1).is_none());
assert!(encode_rgba_png(&[0, 0, 0, 0], 2, 2).is_none());
}
#[test]
fn fill_round_rect_corners_are_more_transparent_than_center() {
let mut fb = Framebuffer::new(20, 20);
fill_round_rect(&mut fb, 2.0, 2.0, 16.0, 16.0, 6.0, Rgba::opaque(10, 20, 30));
let alpha_at = |fb: &Framebuffer, x: u32, y: u32| -> u8 {
fb.pixels()[((y * fb.width() + x) * 4 + 3) as usize]
};
let center = alpha_at(&fb, 10, 10);
let corner = alpha_at(&fb, 2, 2); assert_eq!(center, 255, "well inside the rect should be fully opaque");
assert!(
corner < center,
"a rounded corner should be more transparent than the center (corner={corner}, center={center})"
);
}
#[test]
fn fill_round_rect_clamps_radius_to_half_the_smaller_dimension() {
let mut fb = Framebuffer::new(10, 10);
fill_round_rect(&mut fb, 0.0, 0.0, 8.0, 4.0, 1000.0, Rgba::opaque(1, 2, 3));
let idx = ((2 * fb.width() + 4) * 4 + 3) as usize;
assert_eq!(
fb.pixels()[idx],
255,
"center of the clamped fill is opaque"
);
}
}