diff --git a/crates/svg/src/export/output.rs b/crates/svg/src/export/output.rs index bcbfe4715..fe7a51401 100644 --- a/crates/svg/src/export/output.rs +++ b/crates/svg/src/export/output.rs @@ -292,14 +292,18 @@ impl Writer<'_> { format!("matrix({} {} {} {} {} {})", self.num(c[0]), self.num(c[1]), self.num(c[2]), self.num(c[3]), self.num(c[4]), self.num(c[5])) } + /// How style properties are written: the styling option, but SVG Tiny has presentation + /// attributes alone. + pub(super) fn styling(&self) -> Styling { + if self.tiny() { Styling::PresentationAttributes } else { self.opts.styling } + } + /// Style properties as attributes, an inline style, or a CSS class, per the styling option. pub(super) fn attrs(&mut self, props: &Props) -> String { if props.is_empty() { return String::new(); } - // SVG Tiny has presentation attributes alone. - let styling = if self.tiny() { Styling::PresentationAttributes } else { self.opts.styling }; - match styling { + match self.styling() { Styling::PresentationAttributes => { // CSS-only properties go in a style attribute (SVG Tiny has none: left out). let (style, attrs): (Vec<_>, Vec<_>) = props.iter().partition(|(k, _)| { diff --git a/crates/svg/src/export/text.rs b/crates/svg/src/export/text.rs index 20d87e2d1..a051fcfae 100644 --- a/crates/svg/src/export/text.rs +++ b/crates/svg/src/export/text.rs @@ -1,8 +1,8 @@ use super::output::xml_text; use super::{ Affine, AppearanceItem, Arc, BTreeSet, BezPath, BlendMode, CharStyle, FillRule, FontDb, FontFace, GradientKind, HashMap, Justify, Node, NodeId, - NodeKind, Paint, PathData, Props, Rect, StrokeAlign, TextKind, TextObject, Writer, base64_encode, css, css_string, fmt_num, font_descriptor, - xml_escape, + NodeKind, Paint, PathData, Props, Rect, StrokeAlign, Styling, TextKind, TextObject, Writer, base64_encode, css, css_string, fmt_num, + font_descriptor, xml_escape, }; /// `chars` (sorted) as a CSS `unicode-range` (`U+41-5A,U+61`). @@ -36,7 +36,10 @@ impl Writer<'_> { /// (as on the canvas). fn char_props(&mut self, st: &CharStyle, space: (Rect, Affine)) -> Props { let decimals = self.opts.decimals; - let len = |v: f64| fmt_num(v, decimals); + // Only presentation attributes take a bare number (in user units). Written as CSS, a + // length needs its unit, a user unit being a `px`: a browser drops `font-size:8.6` (#3209). + let unit = if self.styling() == Styling::PresentationAttributes { "" } else { "px" }; + let len = |v: f64| format!("{}{unit}", fmt_num(v, decimals)); let mut p = css::font_props(st, &len); // Character paints resolve in the text's space (`to_user` maps it into user space). let (bounds, to_user) = space; diff --git a/crates/svg/tests/it/css_rules.rs b/crates/svg/tests/it/css_rules.rs index 0a7633db4..1c385a095 100644 --- a/crates/svg/tests/it/css_rules.rs +++ b/crates/svg/tests/it/css_rules.rs @@ -228,8 +228,16 @@ fn shared_properties_read_as_svg_export_writes_them() { let decl = format!("{k}:{v}").replace('"', """).replace('\'', "'"); assert!(style.contains(&decl), "{decl} as in SVG export: {style}"); } - // Lengths are the same numbers, with units on the web. - assert!(style.contains("font-size:18") && prop(&r, "font-size") == Some("18px"), "{style}"); + // Lengths are the same, with units: CSS takes a bare number only for 0, and a browser drops + // `font-size:18`, leaving the type at its default size (#3209). Presentation attributes take + // bare numbers (`text_layout`). + let spacing = prop(&r, "letter-spacing").unwrap_or_default(); + assert!(prop(&r, "font-size") == Some("18px") && spacing.ends_with("px"), "{r:?}"); + for decl in ["font-size:18px".to_string(), format!("letter-spacing:{spacing}")] { + assert!(style.contains(&decl), "{decl} as in SVG export: {style}"); + } + let inline = export(&d, &ExportOptions { styling: Styling::InlineStyle, ..ExportOptions::default() }); + assert!(inline.contains("font-size:18px") && inline.contains(&format!("letter-spacing:{spacing}")), "{inline}"); } #[test]