Merge pull request #7536 from dotCooCoo/fix/svg-rtl-absolute-chunks

SVG import: absolutely placed characters in right-to-left text land on their x
This commit is contained in:
BFlat
2026-11-15 00:38:32 +07:00
committed by GitHub
3 changed files with 241 additions and 36 deletions
+131 -35
View File
@@ -40,7 +40,7 @@ use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{CharStyle, Dash, Justify, LineCap, LineJoin, ParaDirection, ParaStyle, StrokeLayer, TextKind, TextObject, TextRun};
use vectorcraft_geom::kurbo::ParamCurveArclen;
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Rect};
use vectorcraft_text::{FontDb, TextLayout};
use vectorcraft_text::{FontDb, PositionedGlyph, TextLayout, spacing_scale};
use super::css::{Styles, XNode};
use super::{DEFAULT_FONT_SIZE, PT_PER_IN, href, vector_effect};
@@ -1466,7 +1466,12 @@ impl Ctx {
obj.xf = Affine::IDENTITY;
} else {
let origin = Point::new(x[0].unwrap_or(0.0) + dx[0], y[0].unwrap_or(0.0) + dy[0]);
let (justify, left, starts) = lines(&mut cells, &x, &y, &dx, &dy, &anchor_of, &pin, origin.x, &mut breaks);
let (justify, left, starts, unplaced) = lines(&mut cells, &x, &y, &dx, &dy, &anchor_of, &pin, origin.x, &mut breaks);
if unplaced > 0 {
warnings.push(format!(
"{label}: {unplaced} absolutely placed character(s) between characters of the other direction keep their place in the line"
));
}
obj.para.justify = justify;
obj.xf = Affine::translate((finite(left), finite(origin.y)));
// Lines anchored differently or at different x (paragraphs aligned or indented
@@ -1599,7 +1604,8 @@ fn leading_tolerance(spacing: f64) -> f64 {
/// character, alignment (as for [`justify_of`]) and anchor x. `anchor_of` gives where the chunk
/// starting at a character sits on its anchor, and `pin` sets the paragraph directions of the
/// text ([`pin_direction`]). When characters are placed on their own, every line is left-aligned
/// and its anchor x is its left edge.
/// and its anchor x is its left edge. Also returns how many absolutely placed characters kerning
/// cannot put on their x.
#[allow(clippy::too_many_arguments)]
fn lines(
cells: &mut [Cell],
@@ -1611,7 +1617,7 @@ fn lines(
pin: &dyn Fn(&mut TextObject),
x0: f64,
breaks: &mut Vec<Break>,
) -> (Justify, f64, Vec<(usize, f64, f64)>) {
) -> (Justify, f64, Vec<(usize, f64, f64)>, usize) {
let n = cells.len();
let y0 = y[0].unwrap_or(0.0) + dy[0];
let (mut base, mut cur) = (y0, y0);
@@ -1654,7 +1660,7 @@ fn lines(
}
if targets.is_empty() {
let starts = lines.iter().map(|&(s, lx)| (s, anchor_of(s), lx)).collect();
return (justify_of(anchor_of(0)), x0, starts);
return (justify_of(anchor_of(0)), x0, starts, 0);
}
// Absolutely placed characters: lay the text out left-aligned and kern each one onto its x.
// SVG anchors every absolutely placed chunk on its own, so a chunk not anchored at its left
@@ -1664,59 +1670,122 @@ fn lines(
// Chunk starts in order: line starts and absolutely placed characters.
let mut starts: Vec<usize> = lines.iter().map(|l| l.0).chain(targets.iter().map(|t| t.0)).collect();
starts.sort_unstable();
// The left and right edges of the chunk starting at character `s`, from each character's pen and end.
let edges = |s: usize, pens: &[Option<f64>], ends: &[Option<f64>]| {
// The left and right edges of the chunk starting at character `s`, and the character drawn at its left.
let edges = |s: usize, pens: &[Option<Pen>]| {
let e = starts.get(starts.partition_point(|&c| c <= s)).copied().unwrap_or(n);
let left = (s..e).filter_map(|j| pens[j]).fold(f64::INFINITY, f64::min);
let right = (s..e).filter_map(|j| ends[j]).fold(f64::NEG_INFINITY, f64::max);
(left.is_finite() && right.is_finite()).then_some((left, right))
let (mut left, mut right, mut at) = (f64::INFINITY, f64::NEG_INFINITY, s);
for (j, p) in pens.iter().enumerate().take(e).skip(s) {
let Some(p) = p else { continue };
if p.start < left {
(left, at) = (p.start, j);
}
right = right.max(p.end);
}
(left.is_finite() && right.is_finite()).then_some((left, right, at))
};
let mut line_left: Vec<f64> = lines.iter().map(|l| l.1).collect();
// Where each line's first chunk's left edge goes.
let mut first_left: Vec<f64> = vec![];
// Where each absolutely placed character's pen goes, from the left edge of its line's first chunk.
let mut wants: Vec<(usize, f64)> = vec![];
// Absolutely placed characters drawn right next to a placed neighbor with no kerning between
// them (a left-to-right character between right-to-left ones): they stay where the layout puts them.
let mut unplaced = 0;
// Kerning changes shaping (pair kerning is off for manually kerned characters): repeat until
// every character sits on its x.
for round in 0..4 {
let (runs, bytes) = assemble(cells, breaks);
let mut text = with_runs(runs.0);
pin(&mut text);
let (pens, ends) = pens(&measure(&text), &bytes);
let pens = pens(&measure(&text), &bytes, cells);
// The edges of each line's first chunk, found once a round.
let first: Vec<Option<(f64, f64)>> = lines.iter().map(|&(s, _)| edges(s, &pens, &ends)).collect();
let first: Vec<Option<(f64, f64, usize)>> = lines.iter().map(|&(s, _)| edges(s, &pens)).collect();
if round == 0 {
// The chunks as they lie before any is moved: each one's left edge on its anchor.
let left_at = |s: usize, x: f64, chunk: Option<(f64, f64)>| chunk.map_or(x, |(left, right)| x - anchor_of(s) * (right - left));
let first_left: Vec<f64> = lines.iter().zip(&first).map(|(&(s, lx), &chunk)| left_at(s, lx, chunk)).collect();
let left_at = |s: usize, x: f64, chunk: Option<(f64, f64, usize)>| chunk.map_or(x, |(left, right, _)| x - anchor_of(s) * (right - left));
first_left = lines.iter().zip(&first).map(|(&(s, lx), &chunk)| left_at(s, lx, chunk)).collect();
wants = targets
.iter()
.map(|&(i, x)| {
let chunk = edges(i, &pens, &ends);
let chunk = edges(i, &pens);
// The pen of a right-to-left chunk's first character is right of the chunk's left edge.
let pen = pens[i].zip(chunk).map_or(0.0, |(p, (left, _))| p - left);
let pen = pens[i].zip(chunk).map_or(0.0, |(p, (left, ..))| p.start - left);
(i, left_at(i, x, chunk) + pen - first_left[line_of(i)])
})
.collect();
// A line's layout starts at the line's left edge, and its first chunk's left edge is `left` from there.
line_left = first_left.iter().zip(&first).map(|(&x, chunk)| x - chunk.map_or(0.0, |(left, _)| left)).collect();
}
let (mut line, mut moved, mut worst) = (usize::MAX, 0.0, 0.0f64);
for &(i, want) in &wants {
let l = line_of(i);
if l != line {
(line, moved) = (l, 0.0);
// A line's layout starts at the line's left edge, and its first chunk's left edge is `left`
// from there (right-to-left kerning can move that chunk).
line_left = first_left.iter().zip(&first).map(|(&x, chunk)| x - chunk.map_or(0.0, |(left, ..)| left)).collect();
// Kerning a character widens its advance to the right: the glyphs drawn right of it move
// right, and so does a right-to-left glyph's own left edge (its kerning is at its left). In
// drawing order the kerning before an absolutely placed character is a cut after the
// character before it (before that character when it runs right to left), and what is drawn
// after the cut moves. Places in drawing order are doubled (from 2) to put cuts between them.
// Each gap between two placed characters drawn one after the other takes the kerning of one
// cut in it, so placing a character moves none placed before it. Kerning is drawn at the
// character's horizontal scale, so each correction is divided by it.
let mut worst = 0.0f64;
let mut from = 0;
unplaced = 0;
while from < wants.len() {
let l = line_of(wants[from].0);
let to = from + wants[from..].partition_point(|&(i, _)| line_of(i) == l);
let on_line = &wants[from..to];
from = to;
let Some((p0, _, at)) = first[l] else { continue };
let Some(home) = pens[at].map(|p| 2 * p.rank + 2) else { continue };
// A character drawn as part of the glyph before it (a mark) is kerned where that glyph is.
// The kerning stays on the character just before the placed one: a mark whose kerning
// differs from its base's is drawn as a glyph of its own, and kerning the base would put
// the space between the base and its mark.
// Each search stops at the nearest earlier character with a glyph, at the latest the
// placed character before it, so the searches of a line read each character once.
let mut cuts: Vec<(usize, usize)> = on_line
.iter()
.filter(|&&(i, _)| pens[i].is_some())
.filter_map(|&(i, _)| (lines[l].0..i).rev().find_map(|j| pens[j]).map(|k| (2 * k.rank + if k.rtl { 1 } else { 3 }, i - 1)))
.collect();
cuts.sort_unstable();
let mut points: Vec<(usize, f64, f64)> =
on_line.iter().filter_map(|&(i, want)| pens[i].map(|p| (2 * p.rank + 2, p.start, want))).collect();
points.sort_by_key(|p| p.0);
// The character kerned at the first cut between places `a` and `b`.
let cut = |a: usize, b: usize| cuts.get(cuts.partition_point(|c| c.0 <= a)).filter(|c| c.0 < b).map(|c| c.1);
let split = points.partition_point(|p| p.0 < home);
// Drawn right of the line's first chunk: kerning moves the character away from it.
let (mut prev, mut moved) = (home, 0.0);
for &(place, start, want) in &points[split..] {
if let Some(k) = cut(prev, place) {
let d = want - (start - p0 + moved);
cells[k].kern += d / spacing_scale(&cells[k].style, false);
moved += d;
worst = worst.max(d.abs());
} else {
unplaced += 1;
}
prev = place;
}
// Drawn left of it: kerning moves the chunk (and what lies between) away from the character.
(prev, moved) = (home, 0.0);
for &(place, start, want) in points[..split].iter().rev() {
if let Some(k) = cut(place, prev) {
let d = start - p0 - moved - want;
cells[k].kern += d / spacing_scale(&cells[k].style, false);
moved += d;
worst = worst.max(d.abs());
} else {
unplaced += 1;
}
prev = place;
}
let (Some(p), Some((p0, _))) = (pens[i], first[l]) else { continue };
let d = want - (p - p0 + moved);
cells[i - 1].kern += d;
moved += d;
worst = worst.max(d.abs());
}
if worst < 0.01 {
break;
}
}
let starts = lines.iter().zip(&line_left).map(|(&(s, _), &lx)| (s, 0.0, lx)).collect();
(Justify::Left, line_left[0], starts)
(Justify::Left, line_left[0], starts, unplaced)
}
/// Centred and right-aligned lines of point type `obj` where a reader draws them. `text-anchor`
@@ -1875,13 +1944,40 @@ fn advance(t: &TextObject) -> f64 {
measure(t).glyphs.iter().map(|g| g.advance).sum()
}
/// Pen x (start) and end x of the glyph that starts at each character's byte offset.
fn pens(lay: &TextLayout, bytes: &[usize]) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
let mut at: HashMap<usize, (f64, f64)> = HashMap::with_capacity(lay.glyphs.len());
for g in &lay.glyphs {
at.entry(g.byte).or_insert((g.origin.x, g.origin.x + g.advance));
/// Where the glyph that starts a character is drawn on a line of point type.
#[derive(Clone, Copy)]
struct Pen {
/// Its left edge. Manual kerning on a right-to-left glyph is at its left, and the glyph is
/// drawn right of it: its left edge is past that kerning.
start: f64,
/// The right edge of its advance.
end: f64,
/// Its place in the order its line is drawn in, from the left.
rank: usize,
rtl: bool,
}
/// The [`Pen`] of the glyph that starts at each character's byte offset (`bytes`, parallel to `cells`).
fn pens(lay: &TextLayout, bytes: &[usize], cells: &[Cell]) -> Vec<Option<Pen>> {
// A line's glyphs are laid out in the order they are drawn.
let mut at: HashMap<usize, (usize, &PositionedGlyph)> = HashMap::with_capacity(lay.glyphs.len());
for (rank, g) in lay.glyphs.iter().enumerate() {
at.entry(g.byte).or_insert((rank, g));
}
bytes.iter().map(|b| at.get(b).map_or((None, None), |&(s, e)| (Some(s), Some(e)))).unzip()
bytes
.iter()
.zip(cells)
.map(|(b, c)| {
let &(rank, g) = at.get(b)?;
let mut start = g.origin.x;
if g.rtl {
let st = c.final_style();
let kern = st.kerning.unwrap_or(0.0) / 1000.0 * st.size * spacing_scale(&st, false);
start += finite(kern);
}
Some(Pen { start, end: g.origin.x + g.advance, rank, rtl: g.rtl })
})
.collect()
}
type Key = (Option<usize>, Option<usize>);
+109 -1
View File
@@ -5,7 +5,7 @@ use vectorcraft_color::{GradientKind, Paint};
use vectorcraft_doc::{CharStyle, Document, Justify, Node, NodeKind, TextKind, TextObject};
use vectorcraft_geom::{Point, Rect};
use vectorcraft_svg::{import, import_with_report};
use vectorcraft_text::{FontDb, layout};
use vectorcraft_text::{FontDb, layout, spacing_scale};
fn svg(body: &str) -> String {
format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300">{body}</svg>"#)
@@ -808,6 +808,114 @@ fn absolutely_placed_rtl_chunks_of_several_characters_anchor_by_their_visual_edg
}
}
/// Document-space left and right edges of the natural advance of the characters in `bytes` (the
/// manual kerning that places the next chunk left out: it is at a glyph's right when the glyph runs
/// left to right and at its left when it runs right to left).
fn chunk_edges(t: &TextObject, bytes: std::ops::Range<usize>) -> (f64, f64) {
let lay = layout(FontDb::global(), t);
let mut starts = vec![];
let mut at = 0;
for r in &t.runs {
starts.push((at, &r.style));
at += r.text.len();
}
let style = |byte: usize| starts.iter().rev().find(|(s, _)| *s <= byte).map(|(_, st)| *st).unwrap();
let (mut left, mut right) = (f64::INFINITY, f64::NEG_INFINITY);
for g in lay.glyphs.iter().filter(|g| bytes.contains(&g.byte)) {
let st = style(g.byte);
let kern = st.kerning.unwrap_or(0.0) / 1000.0 * st.size * spacing_scale(st, false);
let x = (t.xf * g.origin).x;
let (l, r) = if g.rtl { (x + kern, x + g.advance) } else { (x, x + g.advance - kern) };
(left, right) = (left.min(l), right.max(r));
}
assert!(left.is_finite(), "no glyph in {bytes:?}");
(left, right)
}
/// SVG 1.1 §10.4 and SVG 2 §11.10.1.1 (https://www.w3.org/TR/SVG2/text.html#TextAnchoringProperties):
/// every absolutely placed character starts a chunk anchored on its own x, also in right-to-left
/// text whose characters run right to left, on any line.
#[test]
fn absolutely_placed_right_to_left_characters_sit_on_their_own_x() {
// (markup, share of each chunk's advance left of its x, each chunk's bytes and x)
let two_lines = |anchor: &str| {
format!(
r#"<text direction="rtl" text-anchor="{anchor}" font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100 160" y="80 80">אב</tspan></text>"#
)
};
type Chunks = Vec<(std::ops::Range<usize>, f64)>;
let cases: Vec<(String, f64, Chunks)> = vec![
(two_lines("start"), 1.0, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]),
(two_lines("middle"), 0.5, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]),
(two_lines("end"), 0.0, vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)]),
(r#"<text x="100 160" y="80" direction="rtl" font-size="20">אב</text>"#.into(), 1.0, vec![(0..2, 100.0), (2..4, 160.0)]),
(r#"<text x="100 160" y="80" direction="rtl" text-anchor="end" font-size="20">אב</text>"#.into(), 0.0, vec![(0..2, 100.0), (2..4, 160.0)]),
// Three lines, the characters of the last out of order and overlapping.
(
r#"<text direction="rtl" font-size="20"><tspan x="300" y="40">אב</tspan><tspan x="60 120" y="80">גד</tspan><tspan x="200 90 205" y="120">הוז</tspan></text>"#.into(),
1.0,
vec![(0..4, 300.0), (5..7, 60.0), (7..9, 120.0), (10..12, 200.0), (12..14, 90.0), (14..16, 205.0)],
),
// Left-to-right characters among right-to-left ones on the same line.
(r#"<text x="100 160 220" y="80" direction="rtl" font-size="20">אbc</text>"#.into(), 1.0, vec![(0..2, 100.0), (2..3, 160.0), (3..4, 220.0)]),
// Chunks of several characters.
(
r#"<text direction="rtl" font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100" y="80">אב</tspan><tspan x="250">גד</tspan></text>"#.into(),
1.0,
vec![(0..2, 20.0), (3..7, 100.0), (7..11, 250.0)],
),
(
r#"<text direction="rtl" text-anchor="middle" font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100" y="80">אב</tspan><tspan x="250">גד</tspan></text>"#.into(),
0.5,
vec![(0..2, 20.0), (3..7, 100.0), (7..11, 250.0)],
),
// A placed character after a letter with a combining mark.
(
r#"<text direction="rtl" font-size="20"><tspan x="100" y="80">&#x5D0;&#x5B8;</tspan><tspan x="160">&#x5D1;</tspan></text>"#.into(),
1.0,
vec![(4..6, 160.0)],
),
(r#"<text font-size="20"><tspan x="100" y="80">e&#x301;</tspan><tspan x="160">b</tspan></text>"#.into(), 0.0, vec![(3..4, 160.0)]),
// Characters stretched by `lengthAdjust`, whose kerning is drawn at their scale.
(
r#"<text direction="rtl" font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100 160" y="80 80" textLength="12" lengthAdjust="spacingAndGlyphs">אב</tspan></text>"#.into(),
1.0,
vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)],
),
// Left to right, the right-to-left characters still each start at their own x.
(
r#"<text direction="ltr" font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100 160" y="80 80">אב</tspan></text>"#.into(),
0.0,
vec![(0..2, 20.0), (3..5, 100.0), (5..7, 160.0)],
),
(
r#"<text font-size="20"><tspan x="20" y="40">ab</tspan><tspan x="100 160" y="80 80">cd</tspan></text>"#.into(),
0.0,
vec![(0..2, 20.0), (3..4, 100.0), (4..5, 160.0)],
),
];
let unplaced = |w: &[String]| w.iter().any(|w| w.contains("absolutely placed"));
for (body, share, chunks) in cases {
let (d, w) = import_with_report(&svg(&body)).unwrap();
assert!(!unplaced(&w), "{body}: {w:?}");
let t = only_text(&d);
if body.contains("lengthAdjust") {
assert!(t.runs.iter().any(|r| r.style.h_scale < 99.0), "{body}: {:?}", t.runs);
}
for (bytes, x) in chunks {
let (left, right) = chunk_edges(t, bytes.clone());
let want = x - share * (right - left);
assert!(close(left, want, 0.05), "{body}: the chunk {bytes:?} spans {left}..{right}, so it does not start at {want}");
}
}
// A left-to-right character between right-to-left ones is drawn right next to the one after
// it, with no kerning between them: that one keeps its place in the line, and a warning says so.
let (d, w) = import_with_report(&svg(r#"<text x="100 160 220" y="80" direction="rtl" font-size="20">אbג</text>"#)).unwrap();
assert!(unplaced(&w), "{w:?}");
let (left, right) = chunk_edges(only_text(&d), 2..3);
assert!(close(right, 160.0, 0.05), "b spans {left}..{right}");
}
#[test]
fn a_small_line_gap_in_tiny_type_keeps_its_leading() {
// 0.0085 against an auto leading of 0.006 differs by less than 0.01 but is not a whole multiple of it.
+1
View File
@@ -1692,6 +1692,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
A gap between glyphs of two sizes is measured against the smaller one, so a word set beside a much larger one with no space glyph between them (a browser's margin between two sizes, as in `≤ 4` and a small `hours`) keeps its word space (#3623).
SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `<style>` rules for its id, its classes and its element name, and the rules whose selector ends in none of these (`*`, `[x]`).
SVG import places right-to-left point type and type on a path as [SVG 2's `text-anchor`](https://www.w3.org/TR/SVG2/text.html#TextAnchoringProperties) says: with `direction: rtl`, `start` puts the right edge of the text at its position (on a path, the text ends at `startOffset`) and `end` puts the left edge there. Each line and each absolutely placed chunk is anchored by the `text-anchor` and `direction` of the element holding its first character, a `<tspan>`'s own included, and each line starts at its own `x`. An absolutely placed chunk is anchored by the left and right edges of its glyphs as they are drawn, so a chunk of several right-to-left characters, whose first character is drawn at its right, sits on its anchor too. A letter-spaced right-to-left line keeps the tracking of all its letters inside the advance it anchors (`rtl_point_text_start_and_end_use_directional_edges`, `rtl_type_on_a_path_starts_at_its_directional_edge`, `rtl_letter_spaced_lines_stay_on_their_anchor`, `rtl_lines_use_each_chunk_first_character_direction_and_anchor`, `each_line_starts_at_its_own_x`, `absolutely_placed_chunks_use_their_own_rtl_anchor` and `absolutely_placed_rtl_chunks_of_several_characters_anchor_by_their_visual_edges` in `crates/svg/tests/it/import_text.rs`).
SVG import puts each absolutely placed character of right-to-left text on its own `x`, as [SVG 1.1's text chunks](https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction) require, also when the characters run right to left on a later line: in `<text direction="rtl" font-size="20">`, `<tspan x="100 160" y="80 80">אב</tspan>` after a first line puts the right edges of `א` and `ב` at 100 and 160, where import put the right edge of `א` at 393.48 before. Chunks that overlap keep their own anchors, and characters stretched by `lengthAdjust="spacingAndGlyphs"` land on their `x` too. A left-to-right character drawn between two absolutely placed right-to-left characters leaves the one after it where the line puts it, and the import warns about it (`absolutely_placed_right_to_left_characters_sit_on_their_own_x` in `crates/svg/tests/it/import_text.rs`).
SVG import places type on a path of one closed subpath as [SVG 2's `startOffset` attribute](https://www.w3.org/TR/SVG2/text.html#TextPathElementStartOffsetAttribute) describes: text anchored to start before the path's start point, or offset past its end, wraps around the path, so text with `text-anchor="end"` at `startOffset` 0 ends at the start point, and a negative `startOffset` counts back from the end. Text longer than the loop draws its first characters whatever its anchor. A path of several subpaths, and an open path, still start the text at the path's start or end (`text_on_a_closed_path_wraps_past_the_path_start` in `crates/svg/tests/it/import_text.rs`).
SVG import reads the bidi embedding of a `<tspan>`, `<textPath>` or `<a>` inside a `<text>` as [SVG 1.1's bidi section](https://www.w3.org/TR/SVG11/text.html#RelationshipWithBi-directionality) describes: with `unicode-bidi: embed` its characters take its `direction` as their Character Direction, an element with `normal` stays in its parent's embedding, and the `<text>` keeps its own paragraph direction, so a run that SVG export writes with `direction` and `unicode-bidi="embed"` opens with its Character Direction again. Character Direction holds one level of embedding, so `bidi-override`, `isolate` and `isolate-override` are imported as embeddings in their `direction`, `plaintext` as an embedding in the direction of its first strong character, and an embedding inside another as an embedding of its own, each with an import warning. With `unicode-bidi: plaintext` on the `<text>`, each of its paragraphs runs, and each chunk is anchored, in the direction of its first strong character (`a_tspan_bidi_embedding_becomes_its_character_direction`, `unicode_bidi_is_not_inherited_and_reads_from_the_cascade` and `unicode_bidi_values_the_text_model_cannot_hold_are_reported` in `crates/svg/tests/it/import_text.rs`; `character_direction_reads_back` in `crates/svg/tests/it/text_layout.rs`).
A `.svgz` file copied in a file manager pastes as its art, as a copied `.svg` file does.