diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index 39204efd..473a820e 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -21,6 +21,7 @@ use text::TextSlots; pub(crate) use fx::BLEND; mod css; +mod css_transform; mod files; mod fx; mod spread; @@ -468,7 +469,10 @@ impl Edits { fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) { let mut found = Found::default(); // (`:` for prefixed elements such as ``.) - if !["(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) { } } found.files = files::Files::read(svg, &xml, opts, &mut edits, &mut found.warnings); + if css_transform { + edits.list.extend(css_transform::rewrites(svg, &xml)); + } (edits.apply(svg), found) } +/// Is there a `transform` CSS declaration in `svg` (`transform` followed by `:`)? +fn has_css_transform(svg: &str) -> bool { + svg.match_indices("transform").any(|(i, t)| svg.get(i + t.len()..).is_some_and(|s| s.trim_start().starts_with(':'))) +} + /// The elements that are objects (what [`hidden_objects`] shows). const OBJECTS: &[&str] = &["g", "a", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "image", "text", "use"]; diff --git a/crates/svg/src/import/css.rs b/crates/svg/src/import/css.rs index dd2e1c7e..7ad6c7b2 100644 --- a/crates/svg/src/import/css.rs +++ b/crates/svg/src/import/css.rs @@ -15,7 +15,8 @@ use usvg::roxmltree; pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>; -/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. +/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. The +/// `font` shorthand becomes its longhands, as usvg reads it for the rest of the drawing (#1557). fn parse_decls(s: &str) -> Vec<(String, String, bool)> { s.split(';') .filter_map(|d| { @@ -27,9 +28,28 @@ fn parse_decls(s: &str) -> Vec<(String, String, bool)> { }; Some((k.trim().to_ascii_lowercase(), v.to_string(), important)) }) + .flat_map(|(k, v, important)| match k.as_str() { + "font" => font_longhands(&v).into_iter().map(|(k, v)| (k.to_string(), v, important)).collect(), + _ => vec![(k, v, important)], + }) .collect() } +/// The longhands of a `font` shorthand: those it leaves out back to their initial values. None +/// when it doesn't parse (it is then ignored, as in CSS). +fn font_longhands(v: &str) -> Vec<(&'static str, String)> { + let Ok(f) = svgtypes::FontShorthand::from_str(v) else { return Vec::new() }; + let or = |o: Option<&str>| o.unwrap_or("normal").to_string(); + vec![ + ("font-style", or(f.font_style)), + ("font-variant", or(f.font_variant)), + ("font-weight", or(f.font_weight)), + ("font-stretch", or(f.font_stretch)), + ("font-size", f.font_size.to_string()), + ("font-family", f.font_family.to_string()), + ] +} + /// One compound selector (`text.a.b#c[x=y]`). #[derive(Debug, Default)] struct Compound { @@ -343,6 +363,19 @@ mod tests { ); } + #[test] + fn the_font_shorthand_sets_its_longhands() { + let d = parse_decls("fill: red; font: italic bold 30px 'DejaVu Sans' ; font-weight: 300 !important; font: nonsense"); + let get = |k: &str| d.iter().filter(|(n, ..)| n == k).map(|(_, v, i)| (v.as_str(), *i)).collect::>(); + assert_eq!(get("font-style"), [("italic", false)]); + assert_eq!(get("font-weight"), [("bold", false), ("300", true)]); + assert_eq!(get("font-size"), [("30px", false)]); + assert_eq!(get("font-family"), [("'DejaVu Sans'", false)]); + assert_eq!(get("font-variant"), [("normal", false)]); + assert_eq!(get("fill"), [("red", false)]); + assert!(get("font").is_empty(), "the shorthand itself is not kept"); + } + #[test] fn unsupported_selectors_are_dropped() { for s in ["a + b", "a ~ b", "a:hover", "svg|text", "> a", "a >", ""] { diff --git a/crates/svg/src/import/css_transform.rs b/crates/svg/src/import/css_transform.rs new file mode 100644 index 00000000..235d2374 --- /dev/null +++ b/crates/svg/src/import/css_transform.rs @@ -0,0 +1,151 @@ +//! CSS `transform` declarations in SVG syntax, for usvg. +//! +//! usvg takes the `transform` property (from a `style` attribute or a `transform: rotate(9deg)"#; + let xml = roxmltree::Document::parse(svg).unwrap(); + let found = rewrites(svg, &xml); + let r: Vec<(&str, &str)> = found.iter().map(|(r, v)| (&svg[r.clone()], v.as_str())).collect(); + assert_eq!(r, vec![(" rotate(45deg) ", " rotate(45)"), ("translate(3px,4px)", " translate(3 4)")]); + } +} diff --git a/crates/svg/tests/import_details.rs b/crates/svg/tests/import_details.rs index b6cbf72c..3890aa59 100644 --- a/crates/svg/tests/import_details.rs +++ b/crates/svg/tests/import_details.rs @@ -181,3 +181,24 @@ fn reflect_and_repeat_match_resvg() { assert_similar(&render_artboard(&d), &resvg_render(&svg(&body), 200, 120), 24.0, 0.01); } } + +/// #1557: `transform` as a CSS property, in CSS syntax, moves the object (and wins over the +/// attribute, as in CSS), from a `style` attribute or a ` + + + "#), + &ImportOptions::default(), + ) + .unwrap() + .0; + let b: Vec = art(&d).iter().map(|n| n.geometric_bounds().unwrap()).collect(); + let near = + |r: Rect, x0: f64, y0: f64, x1: f64, y1: f64| [(r.x0, x0), (r.y0, y0), (r.x1, x1), (r.y1, y1)].iter().all(|(a, b)| (a - b).abs() < 1e-6); + assert!(near(b[0], 100.0, 100.0, 150.0, 150.0), "{:?}", b[0]); + assert!(near(b[1], 100.0, 0.0, 150.0, 50.0), "{:?}", b[1]); + assert!(near(b[2], -20.0, 0.0, 0.0, 50.0), "{:?}", b[2]); +} diff --git a/crates/svg/tests/import_text.rs b/crates/svg/tests/import_text.rs index ce68fd65..56ec8636 100644 --- a/crates/svg/tests/import_text.rs +++ b/crates/svg/tests/import_text.rs @@ -583,3 +583,14 @@ fn text_path_to_an_empty_shape_still_falls_back_to_point_type() { assert!(matches!(only_text(&d).kind, TextKind::Point)); assert!(w.iter().any(|w| w.contains("textPath")), "{w:?}"); } + +/// #1557: the `font` shorthand sets the type's size, weight and style. +#[test] +fn the_font_shorthand_styles_type() { + let d = open(r#"Deco"#); + let t = only_text(&d); + let st = &t.runs[0].style; + assert!(close(st.size * t.xf.determinant().abs().sqrt(), 30.0, 1e-6), "{}", st.size); + let style = st.font_style.to_ascii_lowercase(); + assert!(style.contains("bold") && style.contains("italic"), "{style}"); +}