perf(render): optimize text rendering & feat(render): add border support

This commit is contained in:
SGavrl committed 2026-07-02 19:38:00 +00:00
1 parent dec56fa42f
commit 6d33783ddf
6 files changed
+118 -52

No files matched your search

+75 -49
View File
@@ -11,7 +11,7 @@ use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
static FONT_BYTES: &[u8] = include_bytes!("../assets/dejavu-sans.ttf");
use crate::{compute_style, layout_dom};
use crate::layout_dom;
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
/// None if the viewport is zero-sized.
@@ -33,8 +33,8 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32)) -> Option<Pixmap> {
continue;
}
let bg = match bg_color_for(tree, nid) {
Some(c) => c,
let name = match node.as_element() {
Some(name) => name,
None => continue,
};
let rect = match laid.rects.get(&nid) {
@@ -45,27 +45,55 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32)) -> Option<Pixmap> {
Some(r) => r,
None => continue,
};
let mut path = PathBuilder::new();
path.push_rect(box_rect);
let path = match path.finish() {
Some(p) => p,
None => continue,
};
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
paint.anti_alias = false;
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
let style = crate::compute_style(name.local.as_ref(), node.get_attribute("style"));
if let Some(bg) = style.background_color {
let mut path = PathBuilder::new();
path.push_rect(box_rect);
if let Some(path) = path.finish() {
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
paint.anti_alias = false;
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
if style.border.top > 0.0 || style.border.right > 0.0 || style.border.bottom > 0.0 || style.border.left > 0.0 {
let bc = style.border_color.or(style.color).unwrap_or([0, 0, 0, 255]);
let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(bc[0], bc[1], bc[2], bc[3]));
paint.anti_alias = false;
let mut path = PathBuilder::new();
if style.border.top > 0.0 {
if let Some(r) = Rect::from_xywh(rect.x, rect.y, rect.width, style.border.top) {
path.push_rect(r);
}
}
if style.border.right > 0.0 {
if let Some(r) = Rect::from_xywh(rect.x + rect.width - style.border.right, rect.y, style.border.right, rect.height) {
path.push_rect(r);
}
}
if style.border.bottom > 0.0 {
if let Some(r) = Rect::from_xywh(rect.x, rect.y + rect.height - style.border.bottom, rect.width, style.border.bottom) {
path.push_rect(r);
}
}
if style.border.left > 0.0 {
if let Some(r) = Rect::from_xywh(rect.x, rect.y, style.border.left, rect.height) {
path.push_rect(r);
}
}
if let Some(path) = path.finish() {
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
}
Some(pixmap)
}
fn bg_color_for(tree: &DomTree, nid: obscura_dom::tree::NodeId) -> Option<[u8; 4]> {
let node = tree.get_node(nid)?;
let name = node.as_element()?;
let style = compute_style(name.local.as_ref(), node.get_attribute("style"));
style.background_color
}
/// Render `tree` at `viewport` to PNG bytes (RGBA 8-bit). Returns None if the
/// viewport is zero-sized. Convenience over `paint_dom` + `encode_png`.
pub fn screenshot_png(tree: &DomTree, viewport: (f32, f32)) -> Option<Vec<u8>> {
@@ -105,6 +133,11 @@ fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4]) {
let scaled_font = font.as_scaled(scale);
let mut caret = ab_glyph::point(x, y + scaled_font.ascent());
let width = pixmap.width() as i32;
let height = pixmap.height() as i32;
let pixels = pixmap.pixels_mut();
let (r, g, b, a_full) = (color[0], color[1], color[2], color[3]);
for c in text.chars() {
if c.is_control() {
continue;
@@ -117,10 +150,29 @@ fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4]) {
outlined.draw(|gx, gy, c| {
let px = (bounds.min.x + gx as f32) as i32;
let py = (bounds.min.y + gy as f32) as i32;
if px >= 0 && px < pixmap.width() as i32 && py >= 0 && py < pixmap.height() as i32 {
let alpha = (color[3] as f32 * c) as u8;
if px >= 0 && px < width && py >= 0 && py < height {
let alpha = (a_full as f32 * c) as u8;
if alpha > 0 {
blend_pixel(pixmap, px as u32, py as u32, color[0], color[1], color[2], alpha);
let idx = (py * width + px) as usize;
let dst = pixels[idx];
let src_a = alpha as u32;
let src_r = (r as u32 * src_a) / 255;
let src_g = (g as u32 * src_a) / 255;
let src_b = (b as u32 * src_a) / 255;
let dst_a = dst.alpha() as u32;
let out_a = src_a + (dst_a * (255 - src_a) / 255);
if out_a > 0 {
let out_r = src_r + (dst.red() as u32 * (255 - src_a) / 255);
let out_g = src_g + (dst.green() as u32 * (255 - src_a) / 255);
let out_b = src_b + (dst.blue() as u32 * (255 - src_a) / 255);
pixels[idx] = tiny_skia::PremultipliedColorU8::from_rgba(
out_r as u8, out_g as u8, out_b as u8, out_a as u8
).unwrap_or_else(|| tiny_skia::PremultipliedColorU8::from_rgba(0,0,0,0).unwrap());
}
}
}
});
@@ -131,32 +183,6 @@ fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4]) {
}
}
fn blend_pixel(pixmap: &mut Pixmap, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
let width = pixmap.width();
let pixels = pixmap.pixels_mut();
let idx = (y * width + x) as usize;
let dst = pixels[idx];
// source pre-multiplied
let src_a = a as u32;
let src_r = (r as u32 * src_a) / 255;
let src_g = (g as u32 * src_a) / 255;
let src_b = (b as u32 * src_a) / 255;
let dst_a = dst.alpha() as u32;
let out_a = src_a + (dst_a * (255 - src_a) / 255);
if out_a > 0 {
let out_r = src_r + (dst.red() as u32 * (255 - src_a) / 255);
let out_g = src_g + (dst.green() as u32 * (255 - src_a) / 255);
let out_b = src_b + (dst.blue() as u32 * (255 - src_a) / 255);
pixels[idx] = tiny_skia::PremultipliedColorU8::from_rgba(
out_r as u8, out_g as u8, out_b as u8, out_a as u8
).unwrap_or_else(|| tiny_skia::PremultipliedColorU8::from_rgba(0,0,0,0).unwrap());
}
}
#[cfg(test)]
mod tests {
use super::*;