Skip to content

Commit

Permalink
Inline SVG support
Browse files Browse the repository at this point in the history
This support is currently "hacked" by serializing the
DOM to string before parsing with usvg. In future we
should implement SVG rendering directly from the DOM.
  • Loading branch information
nicoburns committed Nov 10, 2024
1 parent 76c91c2 commit cbe8640
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 25 deletions.
2 changes: 1 addition & 1 deletion examples/assets/google.html
Original file line number Diff line number Diff line change
Expand Up @@ -3090,7 +3090,7 @@
alt=""
height="24"
width="24"
style="border:none;display:none \9"
style="border:none;display:none"
></image>
</svg>
</a>
Expand Down
18 changes: 18 additions & 0 deletions packages/blitz-dom/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::events::{EventData, HitResult, RendererEvent};
use crate::node::{ImageData, NodeSpecificData, TextBrush};
use crate::util::parse_svg;
use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport};
use app_units::Au;
use html5ever::local_name;
Expand Down Expand Up @@ -675,6 +676,23 @@ impl Document {
self.add_stylesheet_for_node(sheet, target_id);
}

pub fn process_svg_element(&mut self, target_id: usize) {
let outer_html = self.nodes[target_id].outer_html();
println!("{}", outer_html);
match parse_svg(outer_html.as_bytes()) {
Ok(svg) => {
println!("SVG parsed successfully");
self.nodes[target_id]
.element_data_mut()
.unwrap()
.node_specific_data = NodeSpecificData::Svg(svg);
}
Err(err) => {
dbg!(err);
}
};
}

pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
if let Some(sheet) = self.ua_stylesheets.remove(contents) {
self.stylist.remove_stylesheet(sheet, &self.guard.read());
Expand Down
9 changes: 8 additions & 1 deletion packages/blitz-dom/src/htmlsink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute {

pub struct DocumentHtmlParser<'a> {
doc: RefCell<&'a mut Document>,

style_nodes: RefCell<Vec<usize>>,
svg_nodes: RefCell<Vec<usize>>,

/// Errors that occurred during parsing.
pub errors: RefCell<Vec<Cow<'static, str>>>,
Expand All @@ -41,6 +41,7 @@ impl DocumentHtmlParser<'_> {
DocumentHtmlParser {
doc: RefCell::new(doc),
style_nodes: RefCell::new(Vec::new()),
svg_nodes: RefCell::new(Vec::new()),
errors: RefCell::new(Vec::new()),
quirks_mode: Cell::new(QuirksMode::NoQuirks),
net_provider,
Expand Down Expand Up @@ -180,6 +181,11 @@ impl<'b> TreeSink for DocumentHtmlParser<'b> {
doc.process_style_element(*id);
}

// Parse inline SVGs (<svg> elements)
for id in self.svg_nodes.borrow().iter() {
doc.process_svg_element(*id);
}

for error in self.errors.borrow().iter() {
println!("ERROR: {}", error);
}
Expand Down Expand Up @@ -234,6 +240,7 @@ impl<'b> TreeSink for DocumentHtmlParser<'b> {
"img" => self.load_image(id),
"input" => self.process_button_input(id),
"style" => self.style_nodes.borrow_mut().push(id),
"svg" => self.svg_nodes.borrow_mut().push(id),
_ => {}
}

Expand Down
27 changes: 4 additions & 23 deletions packages/blitz-dom/src/net.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
use image::DynamicImage;
use selectors::context::QuirksMode;
use std::{
io::Cursor,
str::FromStr,
sync::atomic::AtomicBool,
sync::{Arc, OnceLock},
};
use std::{io::Cursor, str::FromStr, sync::atomic::AtomicBool, sync::Arc};
use style::{
font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, Source},
media_queries::MediaList,
Expand All @@ -27,7 +22,7 @@ use blitz_traits::net::{Bytes, RequestHandler, SharedCallback, SharedProvider};
use url::Url;
use usvg::Tree;

static FONT_DB: OnceLock<Arc<usvg::fontdb::Database>> = OnceLock::new();
use crate::util::parse_svg;

#[derive(Clone, Debug)]
pub enum Resource {
Expand Down Expand Up @@ -256,24 +251,10 @@ impl RequestHandler for ImageHandler {
callback.call(Resource::Image(self.0, Arc::new(image)));
return;
};
// Try parse SVG

// TODO: Use fontique
let fontdb = FONT_DB.get_or_init(|| {
let mut fontdb = usvg::fontdb::Database::new();
fontdb.load_system_fonts();
Arc::new(fontdb)
});

let options = usvg::Options {
fontdb: fontdb.clone(),
..Default::default()
};

// Try parse SVG
const DUMMY_SVG : &[u8] = r#"<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>"#.as_bytes();

let tree = Tree::from_data(&bytes, &options)
.unwrap_or_else(|_| Tree::from_data(DUMMY_SVG, &options).unwrap());
let tree = parse_svg(&bytes).unwrap_or(parse_svg(DUMMY_SVG).unwrap());
callback.call(Resource::Svg(self.0, Box::new(tree)));
}
}
60 changes: 60 additions & 0 deletions packages/blitz-dom/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use style::{
stylesheets::CssRuleType,
};
use style_dom::ElementState;
use style_traits::values::ToCss;
use taffy::{
prelude::{Layout, Style},
Cache,
Expand Down Expand Up @@ -793,6 +794,65 @@ impl Node {
s
}

pub fn outer_html(&self) -> String {
let mut output = String::new();
self.write_outer_html(&mut output);
output
}

pub fn write_outer_html(&self, writer: &mut String) {
let has_children = !self.children.is_empty();
let current_color = self
.primary_styles()
.map(|style| style.clone_color())
.map(|color| color.to_css_string());

match &self.raw_dom_data {
NodeData::Document => {}
NodeData::Comment => {}
NodeData::AnonymousBlock(_) => {}
// NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
NodeData::Text(data) => {
writer.push_str(data.content.as_str());
}
NodeData::Element(data) => {
writer.push('<');
writer.push_str(&data.name.local);

for attr in data.attrs() {
writer.push(' ');
writer.push_str(&attr.name.local);
writer.push_str("=\"");
#[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
if current_color.is_some() && attr.value.contains("currentColor") {
writer.push_str(
&attr
.value
.replace("currentColor", current_color.as_ref().unwrap()),
);
} else {
writer.push_str(&attr.value);
}
writer.push('"');
}
if !has_children {
writer.push_str(" /");
}
writer.push('>');

if has_children {
for &child_id in &self.children {
self.tree()[child_id].write_outer_html(writer);
}

writer.push_str("</");
writer.push_str(&data.name.local);
writer.push('>');
}
}
}
}

pub fn attrs(&self) -> Option<&[Attribute]> {
Some(&self.element_data()?.attrs)
}
Expand Down
19 changes: 19 additions & 0 deletions packages/blitz-dom/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
use std::sync::{Arc, LazyLock};

use crate::node::{Node, NodeData};
use peniko::Color as PenikoColor;
use style::color::AbsoluteColor;
use usvg::fontdb;

pub(crate) static FONT_DB: LazyLock<Arc<fontdb::Database>> = LazyLock::new(|| {
let mut db = fontdb::Database::new();
db.load_system_fonts();
Arc::new(db)
});

// Debug print an RcDom
pub fn walk_tree(indent: usize, node: &Node) {
Expand Down Expand Up @@ -68,6 +77,16 @@ pub fn walk_tree(indent: usize, node: &Node) {
}
}

pub(crate) fn parse_svg(source: &[u8]) -> Result<usvg::Tree, usvg::Error> {
let options = usvg::Options {
fontdb: Arc::clone(&*FONT_DB),
..Default::default()
};

let tree = usvg::Tree::from_data(source, &options)?;
Ok(tree)
}

pub trait ToPenikoColor {
fn as_peniko(&self) -> PenikoColor;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/blitz-renderer-vello/src/renderer/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,12 @@ impl VelloSceneGenerator<'_> {
cx.stroke_border(scene);
cx.stroke_devtools(scene);

// Render inline SVG elements
if element.local_name() == "svg" {
cx.draw_svg(scene);
return;
}

// Now that background has been drawn, offset pos and cx in order to draw our contents scrolled
let pos = Point {
x: pos.x - element.scroll_offset.x,
Expand Down

0 comments on commit cbe8640

Please sign in to comment.