mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
SVG export: outlined type keeps its text as an accessible name
Type that SVG text can't draw is written as outlines: vertical type, ruby, right-to-left text, type on a path with an effect other than Rainbow, an end bracket, Align to Path, Spacing, hanging punctuation, bullets or inline graphics. Only glyphs bent along a path gave the outlined group a `role="img"` and `aria-label`; every other case left screen readers and search with no text. Each of them now names the group with its text. Convert to Outlines still writes the paths alone, as before. Inline graphics' placeholder characters stay out of the name, and empty type gets none. An object whose id is its name also gets `data-name`, so opening the file again keeps the name instead of taking the label.
This commit is contained in:
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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("<text"), "{name}: {svg}");
|
||||
assert!(svg.contains("role=\"img\" aria-label=\"On a curve\""), "{name}: {svg}");
|
||||
assert!(!svg_of(t, true).contains("aria-label"), "{name} with Convert to Outlines");
|
||||
}
|
||||
let mut empty = TextObject::point(Point::new(50.0, 20.0), "", style());
|
||||
empty.vertical = true;
|
||||
assert!(!svg_of(empty, false).contains("aria-label"));
|
||||
// An inline graphic's placeholder character stays out of the name; type on a path holding
|
||||
// only inline graphics gets none.
|
||||
let inline = || vectorcraft_doc::TextRun::inline(vectorcraft_doc::InlineArt::new("Mark"), style());
|
||||
let mut mixed = TextObject::point(Point::ZERO, "On a curve", style());
|
||||
mixed.kind = TextKind::OnPath { path: PathData::from_bezpath(&curve), start: 0.1, end: None };
|
||||
mixed.runs.insert(0, inline());
|
||||
let mut only = mixed.clone();
|
||||
only.runs = vec![inline()];
|
||||
let svg = svg_of(mixed, false);
|
||||
assert!(svg.contains("aria-label=\"On a curve\""), "{svg}");
|
||||
assert!(!svg.contains('\u{FFFC}'), "{svg}");
|
||||
let svg = svg_of(only, false);
|
||||
assert!(!svg.contains("aria-label"), "{svg}");
|
||||
// A named object keeps its name when the file is opened again, whether the name is its id or not.
|
||||
for name in ["Heading", "Main heading"] {
|
||||
let mut t = TextObject::point(Point::new(50.0, 20.0), "Hello", style());
|
||||
t.vertical = true;
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let mut node = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
node.name = Some(name.to_string());
|
||||
let svg = export(&doc_with(vec![node]), &ExportOptions::default());
|
||||
assert!(svg.contains("aria-label=\"Hello\""), "{svg}");
|
||||
let r = import(&svg).unwrap();
|
||||
assert!(art(&r).iter().any(|n| n.name.as_deref() == Some(name)), "{name}: {svg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type on a path whose punctuation hangs outside its brackets (Optical Margin Alignment or Roman
|
||||
/// Hanging Punctuation) keeps its look as outlines; with the option on and nothing at its ends to
|
||||
/// hang, it stays a `<textPath>`.
|
||||
|
||||
Reference in New Issue
Block a user