Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for stylesheet injection #785

Merged
merged 16 commits into from
Sep 21, 2024
1 change: 1 addition & 0 deletions crates/resvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ fn parse_args() -> Result<Args, String> {
image_href_resolver: usvg::ImageHrefResolver::default(),
font_resolver: usvg::FontResolver::default(),
fontdb: Arc::new(fontdb::Database::new()),
injected_stylesheet: None,
};

Ok(Args {
Expand Down
1 change: 1 addition & 0 deletions crates/usvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ fn process(args: Args) -> Result<(), String> {
image_href_resolver: usvg::ImageHrefResolver::default(),
font_resolver: usvg::FontResolver::default(),
fontdb: Arc::new(fontdb),
injected_stylesheet: None,
};

let input_svg = match in_svg {
Expand Down
11 changes: 10 additions & 1 deletion crates/usvg/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ impl crate::Tree {

/// Parses `Tree` from `roxmltree::Document`.
pub fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result<Self, Error> {
let doc = svgtree::Document::parse_tree(doc)?;
let doc = svgtree::Document::parse_tree(doc, opt.injected_stylesheet.as_ref())?;
self::converter::convert_doc(&doc, opt)
}
}
Expand Down Expand Up @@ -152,3 +152,12 @@ pub(crate) fn f32_bound(min: f32, val: f32, max: f32) -> f32 {
val
}
}

/// An injected stylesheet.
#[derive(Clone, Debug)]
pub struct InjectedStylesheet<'a> {
/// Whether the injected stylesheet should take priority over existing declarations in the SVG.
pub has_priority: bool,
/// The stylesheet that should be injected.
pub style_sheet: &'a str,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An owned string here is fine.

8 changes: 7 additions & 1 deletion crates/usvg/src/parser/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::sync::Arc;

#[cfg(feature = "text")]
use crate::FontResolver;
use crate::{ImageHrefResolver, ImageRendering, ShapeRendering, Size, TextRendering};
use crate::{
ImageHrefResolver, ImageRendering, InjectedStylesheet, ShapeRendering, Size, TextRendering,
};

/// Processing options.
#[derive(Debug)]
Expand Down Expand Up @@ -96,6 +98,9 @@ pub struct Options<'a> {
/// be the same as this one.
#[cfg(feature = "text")]
pub fontdb: Arc<fontdb::Database>,
/// A CSS stylesheet that should be injected into the SVG. Can be used to overwrite
/// certain attributes.
pub injected_stylesheet: Option<InjectedStylesheet<'a>>,
}

impl Default for Options<'_> {
Expand All @@ -116,6 +121,7 @@ impl Default for Options<'_> {
font_resolver: FontResolver::default(),
#[cfg(feature = "text")]
fontdb: Arc::new(fontdb::Database::new()),
injected_stylesheet: None,
}
}
}
Expand Down
93 changes: 76 additions & 17 deletions crates/usvg/src/parser/svgtree/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@

use std::collections::HashMap;

use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange};
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved
use crate::InjectedStylesheet;
use roxmltree::Error;
use simplecss::Declaration;
use svgtypes::FontShorthand;

use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange};

const SVG_NS: &str = "http://www.w3.org/2000/svg";
const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace";

impl<'input> Document<'input> {
/// Parses a [`Document`] from a [`roxmltree::Document`].
pub fn parse_tree(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
parse(xml)
pub fn parse_tree(
xml: &roxmltree::Document<'input>,
injected_stylesheet: Option<&InjectedStylesheet<'input>>,
) -> Result<Document<'input>, Error> {
parse(xml, injected_stylesheet)
}

pub(crate) fn append(&mut self, parent_id: NodeId, kind: NodeKind) -> NodeId {
Expand Down Expand Up @@ -51,7 +54,10 @@ impl<'input> Document<'input> {
}
}

fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
fn parse<'input>(
xml: &roxmltree::Document<'input>,
injected_stylesheet: Option<&InjectedStylesheet<'input>>,
) -> Result<Document<'input>, Error> {
let mut doc = Document {
nodes: Vec::new(),
attrs: Vec::new(),
Expand Down Expand Up @@ -83,6 +89,7 @@ fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>,
xml.root(),
doc.root().id,
&style_sheet,
injected_stylesheet,
false,
0,
&mut doc,
Expand Down Expand Up @@ -134,6 +141,7 @@ fn parse_xml_node_children<'input>(
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
ignore_ids: bool,
depth: u32,
doc: &mut Document<'input>,
Expand All @@ -145,6 +153,7 @@ fn parse_xml_node_children<'input>(
origin,
parent_id,
style_sheet,
injected_style_sheet,
ignore_ids,
depth,
doc,
Expand All @@ -160,6 +169,7 @@ fn parse_xml_node<'input>(
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
ignore_ids: bool,
depth: u32,
doc: &mut Document<'input>,
Expand All @@ -184,17 +194,35 @@ fn parse_xml_node<'input>(
tag_name = EId::G;
}

let node_id = parse_svg_element(node, parent_id, tag_name, style_sheet, ignore_ids, doc)?;
let node_id = parse_svg_element(
node,
parent_id,
tag_name,
style_sheet,
injected_style_sheet,
ignore_ids,
doc,
)?;
if tag_name == EId::Text {
super::text::parse_svg_text_element(node, node_id, style_sheet, doc)?;
super::text::parse_svg_text_element(node, node_id, style_sheet, injected_style_sheet, doc)?;
} else if tag_name == EId::Use {
parse_svg_use_element(node, origin, node_id, style_sheet, depth + 1, doc, id_map)?;
parse_svg_use_element(
node,
origin,
node_id,
style_sheet,
injected_style_sheet,
depth + 1,
doc,
id_map,
)?;
} else {
parse_xml_node_children(
node,
origin,
node_id,
style_sheet,
injected_style_sheet,
ignore_ids,
depth + 1,
doc,
Expand All @@ -210,6 +238,7 @@ pub(crate) fn parse_svg_element<'input>(
parent_id: NodeId,
tag_name: EId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
ignore_ids: bool,
doc: &mut Document<'input>,
) -> Result<NodeId, Error> {
Expand Down Expand Up @@ -321,19 +350,47 @@ pub(crate) fn parse_svg_element<'input>(
};

// Apply CSS.
for rule in &style_sheet.rules {
if rule.selector.matches(&XmlNode(xml_node)) {
for declaration in &rule.declarations {
write_declaration(declaration);
let mut declarations = style_sheet
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved
.rules
.iter()
.filter(|r| r.selector.matches(&XmlNode(xml_node)))
.flat_map(|r| r.declarations.clone())
.collect::<Vec<Declaration>>();
// Extend by inline declarations in `style`. They take precedence over declarations from style
// sheets.
declarations.extend(
xml_node
.attribute("style")
.map(|v| simplecss::DeclarationTokenizer::from(v).collect::<Vec<_>>())
.unwrap_or_default(),
);

match injected_style_sheet {
Some(iss) => {
let sheet = simplecss::StyleSheet::parse(iss.style_sheet);
let mut injected_declarations = sheet
.rules
.iter()
.filter(|r| r.selector.matches(&XmlNode(xml_node)))
.flat_map(|r| r.declarations.clone())
.collect::<Vec<Declaration>>();

if iss.has_priority {
// Injected stylesheet should have priority, meaning we just add the declarations
// to the end of the vector. Later-appearing declarations will override previous ones.
declarations.extend(injected_declarations)
} else {
// In this case, we prepend the injected declarations so that they will be overwritten
// by the ones defined in the SVG.
injected_declarations.extend(declarations);
declarations = injected_declarations;
}
}
None => {}
}

// Split a `style` attribute.
if let Some(value) = xml_node.attribute("style") {
for declaration in simplecss::DeclarationTokenizer::from(value) {
write_declaration(&declaration);
}
for declaration in declarations {
write_declaration(&declaration);
}

if doc.nodes.len() > 1_000_000 {
Expand Down Expand Up @@ -492,6 +549,7 @@ fn parse_svg_use_element<'input>(
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
depth: u32,
doc: &mut Document<'input>,
id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
Expand Down Expand Up @@ -558,6 +616,7 @@ fn parse_svg_use_element<'input>(
node,
parent_id,
style_sheet,
injected_style_sheet,
true,
depth + 1,
doc,
Expand Down
35 changes: 29 additions & 6 deletions crates/usvg/src/parser/svgtree/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

#![allow(clippy::comparison_chain)]

use roxmltree::Error;

use super::{AId, Document, EId, NodeId, NodeKind, SvgNode};
use crate::InjectedStylesheet;
use roxmltree::Error;
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved

const XLINK_NS: &str = "http://www.w3.org/1999/xlink";

pub(crate) fn parse_svg_text_element<'input>(
parent: roxmltree::Node<'_, 'input>,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
doc: &mut Document<'input>,
) -> Result<(), Error> {
debug_assert_eq!(parent.tag_name().name(), "text");
Expand All @@ -32,7 +33,14 @@ pub(crate) fn parse_svg_text_element<'input>(
}
};

parse_svg_text_element_impl(parent, parent_id, style_sheet, space, doc)?;
parse_svg_text_element_impl(
parent,
parent_id,
style_sheet,
injected_style_sheet,
space,
doc,
)?;

trim_text_nodes(parent_id, space, doc);
Ok(())
Expand All @@ -42,6 +50,7 @@ fn parse_svg_text_element_impl<'input>(
parent: roxmltree::Node<'_, 'input>,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
injected_style_sheet: Option<&InjectedStylesheet>,
space: XmlSpace,
doc: &mut Document<'input>,
) -> Result<(), Error> {
Expand Down Expand Up @@ -78,8 +87,15 @@ fn parse_svg_text_element_impl<'input>(
is_tref = true;
}

let node_id =
super::parse::parse_svg_element(node, parent_id, tag_name, style_sheet, false, doc)?;
let node_id = super::parse::parse_svg_element(
node,
parent_id,
tag_name,
style_sheet,
injected_style_sheet,
false,
doc,
)?;
let space = get_xmlspace(doc, node_id, space);

if is_tref {
Expand All @@ -94,7 +110,14 @@ fn parse_svg_text_element_impl<'input>(
}
}
} else {
parse_svg_text_element_impl(node, node_id, style_sheet, space, doc)?;
parse_svg_text_element_impl(
node,
node_id,
style_sheet,
injected_style_sheet,
space,
doc,
)?;
}
}

Expand Down
Loading
Loading