mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Merge pull request #6255 from dotCooCoo/fix/svg-outlined-text-label
SVG export: outlined type keeps its text as an accessible name
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