diff --git a/crates/svg/src/export/text.rs b/crates/svg/src/export/text.rs index c9d9027b8..19bbbd5be 100644 --- a/crates/svg/src/export/text.rs +++ b/crates/svg/src/export/text.rs @@ -1,4 +1,4 @@ -use super::output::{xml_attr_valid, xml_text}; +use super::output::{xml_attr_escape, xml_attr_valid, 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, Styling, TextKind, TextObject, Writer, base64_encode, css, css_string, fmt_num, @@ -166,10 +166,19 @@ impl Writer<'_> { } let id = self.id_attr(n); let a = self.attrs(&css::transparency(n)); - // Glyphs bent along a path (joined scripts on a curve) can only be outlines: the group - // names its text for screen readers and search. - let label = if lay.glyphs.iter().any(|g| g.bend.is_some()) { - format!(" role=\"img\" aria-label=\"{}\"", xml_attr_valid(&t.plain_text())) + // Text outlined because SVG text can't draw it (bent glyphs, vertical type, ruby, effects on + // a path and the like) names its text for screen readers and search; Convert to Outlines + // leaves only the paths, except for bent glyphs. Inline graphics are written apart from the + // glyphs, so their placeholder characters stay out of the name; empty text has none. + let text: String = t.plain_text().chars().filter(|&c| c != vectorcraft_doc::text::INLINE_CHAR).collect(); + let label = if !text.trim().is_empty() && (!self.opts.outline_text || lay.glyphs.iter().any(|g| g.bend.is_some())) { + // SVG import names an object by its `aria-label` before its id, so an id that stands for + // the name is written as `data-name` too, and the object keeps its name. + let name = match self.names.get(&n.id) { + Some(named) if !id.is_empty() && !id.contains(" data-name=\"") => format!(" data-name=\"{}\"", xml_attr_escape(named)), + _ => String::new(), + }; + format!("{name} role=\"img\" aria-label=\"{}\"", xml_attr_valid(&text)) } else { String::new() }; diff --git a/crates/svg/tests/it/roundtrip.rs b/crates/svg/tests/it/roundtrip.rs index 8f479f348..b13ecf160 100644 --- a/crates/svg/tests/it/roundtrip.rs +++ b/crates/svg/tests/it/roundtrip.rs @@ -527,6 +527,66 @@ fn text_on_path_effects_svg_cannot_express_are_outlined() { } } +/// Type that SVG text can't draw is outlined with its text as the group's accessible name: type on +/// a path with an effect other than Rainbow, vertical type, right-to-left text, and type on a path +/// with inline graphics. With Convert to Outlines the paths carry no name, and empty type gets none +/// either. +#[test] +fn text_outlined_for_svg_keeps_an_accessible_name() { + use vectorcraft_doc::PathEffect; + let mut curve = vectorcraft_geom::BezPath::new(); + curve.move_to((20.0, 150.0)); + curve.curve_to((60.0, 40.0), (140.0, 40.0), (180.0, 150.0)); + let style = || CharStyle { size: 14.0, ..CharStyle::default() }; + let svg_of = |t: TextObject, outline_text: bool| { + let mut d = Document::new(200.0, 200.0); + let id = d.alloc_id(); + let opts = ExportOptions { outline_text, ..ExportOptions::default() }; + export(&doc_with(vec![Node::new(id, NodeKind::Text(Box::new(t)))]), &opts) + }; + let mut skewed = TextObject::point(Point::ZERO, "On a curve", style()); + skewed.kind = TextKind::OnPath { path: PathData::from_bezpath(&curve), start: 0.1, end: None }; + skewed.path_effect = PathEffect::Skew; + let mut vertical = TextObject::point(Point::new(50.0, 20.0), "On a curve", style()); + vertical.vertical = true; + let mut rtl = TextObject::point(Point::new(20.0, 50.0), "On a curve", style()); + rtl.runs[0].style.char_direction = Some(vectorcraft_doc::ParaDirection::RightToLeft); + for (name, t) in [("skew", skewed), ("vertical", vertical), ("rtl", rtl)] { + let svg = svg_of(t.clone(), false); + assert!(!svg.contains("`.