Skip to content

Commit

Permalink
Add support for stylesheet injection (#785)
Browse files Browse the repository at this point in the history
Closes  #277
  • Loading branch information
LaurenzV authored Sep 21, 2024
1 parent 2e93560 commit 5141a83
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 6 deletions.
15 changes: 15 additions & 0 deletions crates/resvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ OPTIONS:
[default: 96] [possible values: 10..4000 (inclusive)]
--background COLOR Sets the background color
Examples: red, #fff, #fff000
--stylesheet PATH Inject a stylesheet that should be used when resolving
CSS attributes.
--languages LANG Sets a comma-separated list of languages that
will be used during the 'systemLanguage'
Expand Down Expand Up @@ -238,6 +240,7 @@ struct CliArgs {
font_dirs: Vec<path::PathBuf>,
skip_system_fonts: bool,
list_fonts: bool,
style_sheet: Option<path::PathBuf>,

query_all: bool,
export_id: Option<String>,
Expand Down Expand Up @@ -307,6 +310,7 @@ fn collect_args() -> Result<CliArgs, pico_args::Error> {
export_area_page: input.contains("--export-area-page"),

export_area_drawing: input.contains("--export-area-drawing"),
style_sheet: input.opt_value_from_str("--stylesheet").unwrap_or_default(),

perf: input.contains("--perf"),
quiet: input.contains("--quiet"),
Expand Down Expand Up @@ -548,6 +552,16 @@ fn parse_args() -> Result<Args, String> {
}
};

let style_sheet = match args.style_sheet.as_ref() {
Some(p) => Some(
std::fs::read(&p)
.ok()
.and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string()))
.ok_or("failed to read stylesheet".to_string())?,
),
None => None,
};

let usvg = usvg::Options {
resources_dir,
dpi: args.dpi as f32,
Expand All @@ -564,6 +578,7 @@ fn parse_args() -> Result<Args, String> {
image_href_resolver: usvg::ImageHrefResolver::default(),
font_resolver: usvg::FontResolver::default(),
fontdb: Arc::new(fontdb::Database::new()),
style_sheet,
};

Ok(Args {
Expand Down
15 changes: 15 additions & 0 deletions crates/usvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ OPTIONS:
--dpi DPI Sets the resolution
[default: 96] [possible values: 10..4000 (inclusive)]
--stylesheet PATH Inject a stylesheet that should be used when resolving
CSS attributes.
--languages LANG Sets a comma-separated list of languages that
will be used during the 'systemLanguage'
attribute resolving
Expand Down Expand Up @@ -137,6 +139,7 @@ struct Args {
attrs_indent: xmlwriter::Indent,
coordinates_precision: Option<u8>,
transforms_precision: Option<u8>,
style_sheet: Option<PathBuf>,

quiet: bool,

Expand Down Expand Up @@ -206,6 +209,7 @@ fn collect_args() -> Result<Args, pico_args::Error> {
coordinates_precision: input
.opt_value_from_fn("--coordinates-precision", parse_precision)?,
transforms_precision: input.opt_value_from_fn("--transforms-precision", parse_precision)?,
style_sheet: input.opt_value_from_str("--stylesheet").unwrap_or_default(),

quiet: input.contains("--quiet"),

Expand Down Expand Up @@ -400,6 +404,16 @@ fn process(args: Args) -> Result<(), String> {
}
};

let style_sheet = match args.style_sheet.as_ref() {
Some(p) => Some(
std::fs::read(&p)
.ok()
.and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string()))
.ok_or("failed to read stylesheet".to_string())?,
),
None => None,
};

let re_opt = usvg::Options {
resources_dir,
dpi: args.dpi as f32,
Expand All @@ -418,6 +432,7 @@ fn process(args: Args) -> Result<(), String> {
image_href_resolver: usvg::ImageHrefResolver::default(),
font_resolver: usvg::FontResolver::default(),
fontdb: Arc::new(fontdb),
style_sheet,
};

let input_svg = match in_svg {
Expand Down
2 changes: 1 addition & 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.style_sheet.as_deref())?;
self::converter::convert_doc(&doc, opt)
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/usvg/src/parser/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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 style_sheet: Option<String>,
}

impl Default for Options<'_> {
Expand All @@ -116,6 +119,7 @@ impl Default for Options<'_> {
font_resolver: FontResolver::default(),
#[cfg(feature = "text")]
fontdb: Arc::new(fontdb::Database::new()),
style_sheet: None,
}
}
}
Expand Down
25 changes: 20 additions & 5 deletions crates/usvg/src/parser/svgtree/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ 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<&'input str>,
) -> 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<&'input str>,
) -> Result<Document<'input>, Error> {
let mut doc = Document {
nodes: Vec::new(),
attrs: Vec::new(),
Expand All @@ -76,7 +82,7 @@ fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>,
kind: NodeKind::Root,
});

let style_sheet = resolve_css(xml);
let style_sheet = resolve_css(xml, injected_stylesheet);

parse_xml_node_children(
xml.root(),
Expand Down Expand Up @@ -565,9 +571,18 @@ fn parse_svg_use_element<'input>(
)
}

fn resolve_css<'a>(xml: &'a roxmltree::Document<'a>) -> simplecss::StyleSheet<'a> {
fn resolve_css<'a>(
xml: &'a roxmltree::Document<'a>,
style_sheet: Option<&'a str>,
) -> simplecss::StyleSheet<'a> {
let mut sheet = simplecss::StyleSheet::new();

// Injected style sheets do not override internal ones (we mimic the logic of rsvg-convert),
// so we need to parse it first.
if let Some(style_sheet) = style_sheet {
sheet.parse_more(style_sheet);
}

for node in xml.descendants().filter(|n| n.has_tag_name("style")) {
match node.attribute("type") {
Some("text/css") => {}
Expand Down
61 changes: 61 additions & 0 deletions crates/usvg/tests/parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use usvg::Color;

#[test]
fn clippath_with_invalid_child() {
let svg = "
Expand All @@ -14,6 +16,65 @@ fn clippath_with_invalid_child() {
assert_eq!(tree.root().has_children(), false);
}

#[test]
fn stylesheet_injection() {
let svg = "<svg id='svg1' viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'>
<style>
#rect4 {
fill: green
}
</style>
<rect id='rect1' x='20' y='20' width='60' height='60'/>
<rect id='rect2' x='120' y='20' width='60' height='60' fill='green'/>
<rect id='rect3' x='20' y='120' width='60' height='60' style='fill: green'/>
<rect id='rect4' x='120' y='120' width='60' height='60'/>
</svg>
";

let stylesheet = "rect { fill: red }".to_string();

let options = usvg::Options {
style_sheet: Some(stylesheet),
..usvg::Options::default()
};

let tree = usvg::Tree::from_str(&svg, &options).unwrap();

let usvg::Node::Path(ref first) = &tree.root().children()[0] else {
unreachable!()
};

// Only the rects with no CSS attributes should be overridden.
assert_eq!(
first.fill().unwrap().paint(),
&usvg::Paint::Color(Color::new_rgb(255, 0, 0))
);

let usvg::Node::Path(ref second) = &tree.root().children()[1] else {
unreachable!()
};
assert_eq!(
second.fill().unwrap().paint(),
&usvg::Paint::Color(Color::new_rgb(255, 0, 0))
);

let usvg::Node::Path(ref third) = &tree.root().children()[2] else {
unreachable!()
};
assert_eq!(
third.fill().unwrap().paint(),
&usvg::Paint::Color(Color::new_rgb(0, 128, 0))
);

let usvg::Node::Path(ref third) = &tree.root().children()[3] else {
unreachable!()
};
assert_eq!(
third.fill().unwrap().paint(),
&usvg::Paint::Color(Color::new_rgb(0, 128, 0))
);
}

#[test]
fn simplify_paths() {
let svg = "
Expand Down

0 comments on commit 5141a83

Please sign in to comment.