render: paint backgrounds to a tiny-skia pixmap
Adds a paint feature to obscura-render (tiny-skia: pure Rust, CPU, deterministic, no system deps). paint_dom lays out the DOM and fills each element's border box with its parsed background-color over a white page, in tree order so descendants paint over ancestors. Style parsing now also extracts background-color (hex #rgb/#rgba/ later enhancements.
This commit is contained in:
1 parent
399181b975
commit
4ca85e87d8
6 files changed
+228
No files matched your search
@@ -19,6 +19,11 @@ pub use style::compute_style;
|
||||
mod dom;
|
||||
pub use dom::{layout_dom, DomLayout};
|
||||
|
||||
#[cfg(feature = "paint")]
|
||||
mod paint;
|
||||
#[cfg(feature = "paint")]
|
||||
pub use paint::paint_dom;
|
||||
|
||||
/// An axis-aligned rectangle in CSS pixels, relative to the containing block.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct Rect {
|
||||
@@ -58,6 +63,8 @@ pub struct LayoutStyle {
|
||||
pub margin: Edges,
|
||||
pub padding: Edges,
|
||||
pub border: Edges,
|
||||
/// RGBA for the paint step. Parsed always (cheap), used only with `paint`.
|
||||
pub background_color: Option<[u8; 4]>,
|
||||
}
|
||||
|
||||
/// A node in the input layout tree. `text` is carried for the paint phase; it
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Paint: rasterize the laid-out DOM into a [`tiny_skia::Pixmap`].
|
||||
//!
|
||||
//! Phase 5a. Fills each element's border box with its background color over a
|
||||
//! white page. Text rendering arrives with the text step; borders and images
|
||||
//! are later enhancements. Pure Rust (tiny-skia, CPU), deterministic, no system
|
||||
//! dependencies, so a screenshot is reproducible across hosts.
|
||||
|
||||
use obscura_dom::tree::DomTree;
|
||||
use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Rect, Transform};
|
||||
|
||||
use crate::{compute_style, layout_dom};
|
||||
|
||||
/// Render `tree` at `viewport` (width, height) in CSS pixels to a Pixmap, or
|
||||
/// None if the viewport is zero-sized.
|
||||
pub fn paint_dom(tree: &DomTree, viewport: (f32, f32)) -> Option<Pixmap> {
|
||||
let (w, h) = (viewport.0 as u32, viewport.1 as u32);
|
||||
let mut pixmap = Pixmap::new(w, h)?;
|
||||
pixmap.fill(Color::WHITE);
|
||||
|
||||
let laid = layout_dom(tree, viewport);
|
||||
// Tree order so later elements paint over earlier ones (normal flow).
|
||||
for nid in tree.descendants(tree.document()) {
|
||||
let bg = match bg_color_for(tree, nid) {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
let rect = match laid.rects.get(&nid) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let box_rect = match Rect::from_xywh(rect.x, rect.y, rect.width, rect.height) {
|
||||
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);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use obscura_dom::tree_sink::parse_html;
|
||||
|
||||
#[test]
|
||||
fn paints_background_color() {
|
||||
let tree = parse_html(
|
||||
"<html><body><div style=\"background-color: #ff0000; width: 100px; height: 80px\"></div></body></html>",
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (200.0, 200.0)).expect("pixmap");
|
||||
assert_eq!(pixmap.width(), 200);
|
||||
// The red div is laid out at the origin; sample inside it.
|
||||
let inside = pixmap.pixel(10, 10).expect("pixel");
|
||||
assert!(inside.red() > 200, "expected red bg, got {:?}", inside);
|
||||
assert!(inside.green() < 60);
|
||||
assert!(inside.blue() < 60);
|
||||
// Outside the 100x80 div the page background is white.
|
||||
let outside = pixmap.pixel(150, 150).expect("pixel");
|
||||
assert_eq!(outside.red(), 255);
|
||||
assert_eq!(outside.green(), 255);
|
||||
assert_eq!(outside.blue(), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_element_paints_over_earlier() {
|
||||
// A blue div nested inside a red one: both cover the origin, and blue
|
||||
// (a descendant, later in tree order) paints over red.
|
||||
let tree = parse_html(
|
||||
"<html><body>\
|
||||
<div style=\"background-color:red; width:100px; height:100px\">\
|
||||
<div style=\"background-color:blue; width:50px; height:50px\"></div>\
|
||||
</div>\
|
||||
</body></html>",
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (200.0, 200.0)).expect("pixmap");
|
||||
let p = pixmap.pixel(5, 5).expect("pixel");
|
||||
assert!(p.blue() > 200, "expected blue to paint over red, got {:?}", p);
|
||||
}
|
||||
}
|
||||
@@ -71,10 +71,60 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
"border-right-width" | "border-right" => set_edge(&mut style.border, Side::Right, px(value)),
|
||||
"border-bottom-width" | "border-bottom" => set_edge(&mut style.border, Side::Bottom, px(value)),
|
||||
"border-left-width" | "border-left" => set_edge(&mut style.border, Side::Left, px(value)),
|
||||
"background-color" | "background" => style.background_color = parse_color(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a CSS color to RGBA. Handles #rgb, #rgba, #rrggbb, #rrggbbaa hex and a
|
||||
/// small set of named colors. Returns None for anything else (transparent).
|
||||
fn parse_color(value: &str) -> Option<[u8; 4]> {
|
||||
let v = value.split_whitespace().next()?.to_ascii_lowercase();
|
||||
if let Some(h) = v.strip_prefix('#') {
|
||||
let (r, g, b, a) = match h.len() {
|
||||
3 => (
|
||||
u8::from_str_radix(&h[0..1].repeat(2), 16).ok()?,
|
||||
u8::from_str_radix(&h[1..2].repeat(2), 16).ok()?,
|
||||
u8::from_str_radix(&h[2..3].repeat(2), 16).ok()?,
|
||||
255u8,
|
||||
),
|
||||
4 => (
|
||||
u8::from_str_radix(&h[0..1].repeat(2), 16).ok()?,
|
||||
u8::from_str_radix(&h[1..2].repeat(2), 16).ok()?,
|
||||
u8::from_str_radix(&h[2..3].repeat(2), 16).ok()?,
|
||||
u8::from_str_radix(&h[3..4].repeat(2), 16).ok()?,
|
||||
),
|
||||
6 => (
|
||||
u8::from_str_radix(&h[0..2], 16).ok()?,
|
||||
u8::from_str_radix(&h[2..4], 16).ok()?,
|
||||
u8::from_str_radix(&h[4..6], 16).ok()?,
|
||||
255u8,
|
||||
),
|
||||
8 => (
|
||||
u8::from_str_radix(&h[0..2], 16).ok()?,
|
||||
u8::from_str_radix(&h[2..4], 16).ok()?,
|
||||
u8::from_str_radix(&h[4..6], 16).ok()?,
|
||||
u8::from_str_radix(&h[6..8], 16).ok()?,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
return Some([r, g, b, a]);
|
||||
}
|
||||
// Named colors (the common few).
|
||||
let named = match v.as_str() {
|
||||
"transparent" => [0, 0, 0, 0],
|
||||
"black" => [0, 0, 0, 255],
|
||||
"white" => [255, 255, 255, 255],
|
||||
"red" => [255, 0, 0, 255],
|
||||
"green" => [0, 128, 0, 255],
|
||||
"blue" => [0, 0, 255, 255],
|
||||
"yellow" => [255, 255, 0, 255],
|
||||
"gray" | "grey" => [128, 128, 128, 255],
|
||||
_ => return None,
|
||||
};
|
||||
Some(named)
|
||||
}
|
||||
|
||||
enum Side { Top, Right, Bottom, Left }
|
||||
|
||||
fn set_edge(edges: &mut Edges, side: Side, v: Option<f32>) {
|
||||
|
||||
Reference in new issue
Block a user