Skip to content

Commit 5141a83

Browse files
authored
Add support for stylesheet injection (#785)
Closes #277
1 parent 2e93560 commit 5141a83

File tree

6 files changed

+116
-6
lines changed

6 files changed

+116
-6
lines changed

crates/resvg/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ OPTIONS:
144144
[default: 96] [possible values: 10..4000 (inclusive)]
145145
--background COLOR Sets the background color
146146
Examples: red, #fff, #fff000
147+
--stylesheet PATH Inject a stylesheet that should be used when resolving
148+
CSS attributes.
147149
148150
--languages LANG Sets a comma-separated list of languages that
149151
will be used during the 'systemLanguage'
@@ -238,6 +240,7 @@ struct CliArgs {
238240
font_dirs: Vec<path::PathBuf>,
239241
skip_system_fonts: bool,
240242
list_fonts: bool,
243+
style_sheet: Option<path::PathBuf>,
241244

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

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

311315
perf: input.contains("--perf"),
312316
quiet: input.contains("--quiet"),
@@ -548,6 +552,16 @@ fn parse_args() -> Result<Args, String> {
548552
}
549553
};
550554

555+
let style_sheet = match args.style_sheet.as_ref() {
556+
Some(p) => Some(
557+
std::fs::read(&p)
558+
.ok()
559+
.and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string()))
560+
.ok_or("failed to read stylesheet".to_string())?,
561+
),
562+
None => None,
563+
};
564+
551565
let usvg = usvg::Options {
552566
resources_dir,
553567
dpi: args.dpi as f32,
@@ -564,6 +578,7 @@ fn parse_args() -> Result<Args, String> {
564578
image_href_resolver: usvg::ImageHrefResolver::default(),
565579
font_resolver: usvg::FontResolver::default(),
566580
fontdb: Arc::new(fontdb::Database::new()),
581+
style_sheet,
567582
};
568583

569584
Ok(Args {

crates/usvg/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ OPTIONS:
2626
2727
--dpi DPI Sets the resolution
2828
[default: 96] [possible values: 10..4000 (inclusive)]
29+
--stylesheet PATH Inject a stylesheet that should be used when resolving
30+
CSS attributes.
2931
--languages LANG Sets a comma-separated list of languages that
3032
will be used during the 'systemLanguage'
3133
attribute resolving
@@ -137,6 +139,7 @@ struct Args {
137139
attrs_indent: xmlwriter::Indent,
138140
coordinates_precision: Option<u8>,
139141
transforms_precision: Option<u8>,
142+
style_sheet: Option<PathBuf>,
140143

141144
quiet: bool,
142145

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

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

@@ -400,6 +404,16 @@ fn process(args: Args) -> Result<(), String> {
400404
}
401405
};
402406

407+
let style_sheet = match args.style_sheet.as_ref() {
408+
Some(p) => Some(
409+
std::fs::read(&p)
410+
.ok()
411+
.and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string()))
412+
.ok_or("failed to read stylesheet".to_string())?,
413+
),
414+
None => None,
415+
};
416+
403417
let re_opt = usvg::Options {
404418
resources_dir,
405419
dpi: args.dpi as f32,
@@ -418,6 +432,7 @@ fn process(args: Args) -> Result<(), String> {
418432
image_href_resolver: usvg::ImageHrefResolver::default(),
419433
font_resolver: usvg::FontResolver::default(),
420434
fontdb: Arc::new(fontdb),
435+
style_sheet,
421436
};
422437

423438
let input_svg = match in_svg {

crates/usvg/src/parser/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl crate::Tree {
121121

122122
/// Parses `Tree` from `roxmltree::Document`.
123123
pub fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result<Self, Error> {
124-
let doc = svgtree::Document::parse_tree(doc)?;
124+
let doc = svgtree::Document::parse_tree(doc, opt.style_sheet.as_deref())?;
125125
self::converter::convert_doc(&doc, opt)
126126
}
127127
}

crates/usvg/src/parser/options.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ pub struct Options<'a> {
9696
/// be the same as this one.
9797
#[cfg(feature = "text")]
9898
pub fontdb: Arc<fontdb::Database>,
99+
/// A CSS stylesheet that should be injected into the SVG. Can be used to overwrite
100+
/// certain attributes.
101+
pub style_sheet: Option<String>,
99102
}
100103

101104
impl Default for Options<'_> {
@@ -116,6 +119,7 @@ impl Default for Options<'_> {
116119
font_resolver: FontResolver::default(),
117120
#[cfg(feature = "text")]
118121
fontdb: Arc::new(fontdb::Database::new()),
122+
style_sheet: None,
119123
}
120124
}
121125
}

crates/usvg/src/parser/svgtree/parse.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace";
1616

1717
impl<'input> Document<'input> {
1818
/// Parses a [`Document`] from a [`roxmltree::Document`].
19-
pub fn parse_tree(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
20-
parse(xml)
19+
pub fn parse_tree(
20+
xml: &roxmltree::Document<'input>,
21+
injected_stylesheet: Option<&'input str>,
22+
) -> Result<Document<'input>, Error> {
23+
parse(xml, injected_stylesheet)
2124
}
2225

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

54-
fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
57+
fn parse<'input>(
58+
xml: &roxmltree::Document<'input>,
59+
injected_stylesheet: Option<&'input str>,
60+
) -> Result<Document<'input>, Error> {
5561
let mut doc = Document {
5662
nodes: Vec::new(),
5763
attrs: Vec::new(),
@@ -76,7 +82,7 @@ fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>,
7682
kind: NodeKind::Root,
7783
});
7884

79-
let style_sheet = resolve_css(xml);
85+
let style_sheet = resolve_css(xml, injected_stylesheet);
8086

8187
parse_xml_node_children(
8288
xml.root(),
@@ -565,9 +571,18 @@ fn parse_svg_use_element<'input>(
565571
)
566572
}
567573

568-
fn resolve_css<'a>(xml: &'a roxmltree::Document<'a>) -> simplecss::StyleSheet<'a> {
574+
fn resolve_css<'a>(
575+
xml: &'a roxmltree::Document<'a>,
576+
style_sheet: Option<&'a str>,
577+
) -> simplecss::StyleSheet<'a> {
569578
let mut sheet = simplecss::StyleSheet::new();
570579

580+
// Injected style sheets do not override internal ones (we mimic the logic of rsvg-convert),
581+
// so we need to parse it first.
582+
if let Some(style_sheet) = style_sheet {
583+
sheet.parse_more(style_sheet);
584+
}
585+
571586
for node in xml.descendants().filter(|n| n.has_tag_name("style")) {
572587
match node.attribute("type") {
573588
Some("text/css") => {}

crates/usvg/tests/parser.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use usvg::Color;
2+
13
#[test]
24
fn clippath_with_invalid_child() {
35
let svg = "
@@ -14,6 +16,65 @@ fn clippath_with_invalid_child() {
1416
assert_eq!(tree.root().has_children(), false);
1517
}
1618

19+
#[test]
20+
fn stylesheet_injection() {
21+
let svg = "<svg id='svg1' viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'>
22+
<style>
23+
#rect4 {
24+
fill: green
25+
}
26+
</style>
27+
<rect id='rect1' x='20' y='20' width='60' height='60'/>
28+
<rect id='rect2' x='120' y='20' width='60' height='60' fill='green'/>
29+
<rect id='rect3' x='20' y='120' width='60' height='60' style='fill: green'/>
30+
<rect id='rect4' x='120' y='120' width='60' height='60'/>
31+
</svg>
32+
";
33+
34+
let stylesheet = "rect { fill: red }".to_string();
35+
36+
let options = usvg::Options {
37+
style_sheet: Some(stylesheet),
38+
..usvg::Options::default()
39+
};
40+
41+
let tree = usvg::Tree::from_str(&svg, &options).unwrap();
42+
43+
let usvg::Node::Path(ref first) = &tree.root().children()[0] else {
44+
unreachable!()
45+
};
46+
47+
// Only the rects with no CSS attributes should be overridden.
48+
assert_eq!(
49+
first.fill().unwrap().paint(),
50+
&usvg::Paint::Color(Color::new_rgb(255, 0, 0))
51+
);
52+
53+
let usvg::Node::Path(ref second) = &tree.root().children()[1] else {
54+
unreachable!()
55+
};
56+
assert_eq!(
57+
second.fill().unwrap().paint(),
58+
&usvg::Paint::Color(Color::new_rgb(255, 0, 0))
59+
);
60+
61+
let usvg::Node::Path(ref third) = &tree.root().children()[2] else {
62+
unreachable!()
63+
};
64+
assert_eq!(
65+
third.fill().unwrap().paint(),
66+
&usvg::Paint::Color(Color::new_rgb(0, 128, 0))
67+
);
68+
69+
let usvg::Node::Path(ref third) = &tree.root().children()[3] else {
70+
unreachable!()
71+
};
72+
assert_eq!(
73+
third.fill().unwrap().paint(),
74+
&usvg::Paint::Color(Color::new_rgb(0, 128, 0))
75+
);
76+
}
77+
1778
#[test]
1879
fn simplify_paths() {
1980
let svg = "

0 commit comments

Comments
 (0)