feat(render): implement text rendering using ab_glyph
This commit is contained in:
1 parent
48e3fac55a
commit
dec56fa42f
2 files changed
+119
No files matched your search
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
use obscura_dom::tree::DomTree;
|
use obscura_dom::tree::DomTree;
|
||||||
use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Rect, Transform};
|
use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Rect, Transform};
|
||||||
|
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::{compute_style, layout_dom};
|
||||||
|
|
||||||
@@ -20,6 +23,16 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32)) -> Option<Pixmap> {
|
|||||||
let laid = layout_dom(tree, viewport);
|
let laid = layout_dom(tree, viewport);
|
||||||
// Tree order so later elements paint over earlier ones (normal flow).
|
// Tree order so later elements paint over earlier ones (normal flow).
|
||||||
for nid in tree.descendants(tree.document()) {
|
for nid in tree.descendants(tree.document()) {
|
||||||
|
let node = match tree.get_node(nid) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if node.is_text() {
|
||||||
|
paint_text_node(tree, nid, &laid, &mut pixmap);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let bg = match bg_color_for(tree, nid) {
|
let bg = match bg_color_for(tree, nid) {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -59,6 +72,91 @@ pub fn screenshot_png(tree: &DomTree, viewport: (f32, f32)) -> Option<Vec<u8>> {
|
|||||||
paint_dom(tree, viewport)?.encode_png().ok()
|
paint_dom(tree, viewport)?.encode_png().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_text_node(
|
||||||
|
tree: &DomTree,
|
||||||
|
nid: obscura_dom::tree::NodeId,
|
||||||
|
laid: &crate::DomLayout,
|
||||||
|
pixmap: &mut Pixmap,
|
||||||
|
) -> Option<()> {
|
||||||
|
let node = tree.get_node(nid)?;
|
||||||
|
let text = match &node.data {
|
||||||
|
obscura_dom::tree::NodeData::Text { contents } => contents,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find parent element to get geometry and color
|
||||||
|
let parent_id = node.parent?;
|
||||||
|
let parent = tree.get_node(parent_id)?;
|
||||||
|
let parent_name = parent.as_element()?;
|
||||||
|
|
||||||
|
let rect = laid.rects.get(&parent_id)?;
|
||||||
|
let style = crate::compute_style(parent_name.local.as_ref(), parent.get_attribute("style"));
|
||||||
|
// Default color to black if not specified.
|
||||||
|
let color = style.color.unwrap_or([0, 0, 0, 255]);
|
||||||
|
|
||||||
|
// Paint text at rect.x, rect.y
|
||||||
|
draw_text(pixmap, text, rect.x, rect.y, color);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4]) {
|
||||||
|
let font = FontRef::try_from_slice(FONT_BYTES).unwrap();
|
||||||
|
let scale = PxScale::from(16.0); // Default font size
|
||||||
|
let scaled_font = font.as_scaled(scale);
|
||||||
|
let mut caret = ab_glyph::point(x, y + scaled_font.ascent());
|
||||||
|
|
||||||
|
for c in text.chars() {
|
||||||
|
if c.is_control() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let glyph_id = font.glyph_id(c);
|
||||||
|
let id = glyph_id;
|
||||||
|
let glyph = glyph_id.with_scale_and_position(scale, caret);
|
||||||
|
if let Some(outlined) = font.outline_glyph(glyph) {
|
||||||
|
let bounds = outlined.px_bounds();
|
||||||
|
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 alpha > 0 {
|
||||||
|
blend_pixel(pixmap, px as u32, py as u32, color[0], color[1], color[2], alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
caret.x += scaled_font.h_advance(id);
|
||||||
|
} else {
|
||||||
|
caret.x += scaled_font.h_advance(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -98,4 +196,24 @@ mod tests {
|
|||||||
let p = pixmap.pixel(5, 5).expect("pixel");
|
let p = pixmap.pixel(5, 5).expect("pixel");
|
||||||
assert!(p.blue() > 200, "expected blue to paint over red, got {:?}", p);
|
assert!(p.blue() > 200, "expected blue to paint over red, got {:?}", p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paints_text_color() {
|
||||||
|
let tree = parse_html(
|
||||||
|
"<html><body><div style=\"color: #00ff00; width: 100px; height: 100px\">Hello</div></body></html>",
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom(&tree, (200.0, 200.0)).expect("pixmap");
|
||||||
|
let mut found_green = false;
|
||||||
|
for y in 0..200 {
|
||||||
|
for x in 0..200 {
|
||||||
|
let p = pixmap.pixel(x, y).expect("pixel");
|
||||||
|
if p.green() > 200 && p.red() < 50 && p.blue() < 50 {
|
||||||
|
found_green = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found_green { break; }
|
||||||
|
}
|
||||||
|
assert!(found_green, "expected green text to be painted");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
"border-bottom-width" | "border-bottom" => set_edge(&mut style.border, Side::Bottom, 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)),
|
"border-left-width" | "border-left" => set_edge(&mut style.border, Side::Left, px(value)),
|
||||||
"background-color" | "background" => style.background_color = parse_color(value),
|
"background-color" | "background" => style.background_color = parse_color(value),
|
||||||
|
"color" => style.color = parse_color(value),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user