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
15 changes: 15 additions & 0 deletions crates/resvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ OPTIONS:

--export-area-drawing Use drawing's tight bounding box instead of image size.
Used during normal rendering and not during --export-id
--stylesheet Inject a stylesheet that should be used when resolving
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved
CSS attributes.

--perf Prints performance stats
--quiet Disables warnings
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 @@ -102,6 +102,8 @@ OPTIONS:
Smaller precision can lead to a malformed output in some cases
[values: 2..8 (inclusive)] [default: 8]
--quiet Disables warnings
--stylesheet Inject a stylesheet that should be used when resolving
CSS attributes.

ARGS:
<in-svg> Input file
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_ref().map(|s| s.as_str()))?;
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved
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
28 changes: 21 additions & 7 deletions crates/usvg/src/parser/svgtree/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@

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 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<&'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 +53,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 +81,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 +570,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
3 changes: 1 addition & 2 deletions crates/usvg/src/parser/svgtree/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

#![allow(clippy::comparison_chain)]

use roxmltree::Error;

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

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

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
Loading