Skip to content

Support direction and unicode-bidi on text - #1184

Open
matthargett wants to merge 3 commits into
linebender:mainfrom
matthargett:rtl-direction
Open

matthargett wants to merge 3 commits into
linebender:mainfrom
matthargett:rtl-direction

Conversation

@matthargett

@matthargett matthargett commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #475.

direction was never read: each text chunk was laid out with a left-to-right base level, and text-anchor start/end always meant left/right. unicode-bidi was ignored too. Both were listed in docs/unsupported.md.

What it does

  • direction on the text element (or an ancestor, including the root svg) is the base level of the bidi algorithm for the chunks of that text.
  • text-anchor start and end follow the direction of the element a chunk starts in, also on a text path: right-to-left text ends at a start anchor, and at startOffset on a path. Base level and anchor come from different elements, as in the browsers (direction="ltr" on a tspan inside an rtl text flips the anchor, not the reordering).
  • unicode-bidi embed, bidi-override, isolate, isolate-override and plaintext become the Unicode formatting characters CSS defines for them. They are added only for the analysis and removed from the runs, so character indexes (dx, dy, rotate) do not move. Scopes nest like the elements; a chunk that starts inside one opens it again.
  • Vertical writing modes ignore both, as before (rtl-with-vertical-writing-mode is unchanged).
  • The text position a following y-only chunk continues from is where the previous chunk would end if it were anchored at its natural start and ran in the direction of the paragraph (as in Chromium, Firefox and WebKit), not where its own anchor and direction put it. Left-to-right chunks in left-to-right text keep the existing behaviour.
  • The writer keeps them with preserve_text, including the direction of a chunk that starts inside a unicode-bidi element of another direction. New public API: usvg::Direction, Text::direction, TextChunk::direction.
  • The bidi analysis now runs once per chunk instead of once per span font.

Evidence

Every new reference was compared with Chromium 131 and Firefox before it was made (same fonts in all engines through @font-face, ink positions within 1 px). For 15 of the 21 new reftests Chromium and Firefox both match resvg's ink exactly (1 px). The other six differ in drawing or shaping, not layout: rtl-with-filter (blur kernels), rtl-with-text-decoration (Chromium skips ink under underlines; Firefox matches), rtl-with-clip-path (Firefox draws a clipPath with text slightly differently; Chromium matches) on-svg-element (the Hebrew comes from a different font in each engine), and rtl-with-numbers and rtl-with-scientific-text, where Chromium spaces the Arabic و slightly differently (Firefox and WebKit match resvg to within 2 px). WebKit is shown where it agrees; it lays out right-to-left text on a path differently from the other two.

The example from #475 (visual order is now כתובת MAC: 00-24-AF-2A-55-FC, right to left; the browsers use a different serif for the Latin letters):

#475: resvg 0.48.1, this PR, Chromium, Firefox, WebKit

WPT text-bidi-controls-anchors-2 (direction="rtl" on a g), with one font in every engine: ink mismatch against Chromium / Firefox / WebKit went from 3,600 / 3,601 / 3,577 px to 0 / 0 / 0.

Anchors, inheritance and tspan: anchors

Decoration, word spacing, drop shadow, clip-path (on the text, and a clipPath with a text child) and mask: effects

Punctuation at the end of sentences, number and date ranges (Western and Arabic-Indic digits), nested parentheses and brackets, and scientific text (units, signs, Greek letters, p < 0.05, Na+ / Cl−): mixed scripts

Text on a path (rendered with #1166 applied; see Tests): text on a path

unicode-bidi: unicode-bidi

Tests

  • 21 new reference tests in tests/text/direction and tests/text/unicode-bidi. Existing references are untouched except the three below.
  • Three existing references had the bug in them and are regenerated: direction/rtl and color-font/mixed-text-rtl (the line started at x and ran off the right edge) and unicode-bidi/bidi-override (the override was ignored). No other test uses direction or unicode-bidi; the complete suite passes (1,778 reftests, cargo test --all --release).
  • Seven usvg tests: inheritance, anchors, the text position a following chunk continues from, anchors on a text path (rtl start equals ltr end, and so on, which does not depend on the arc-length accuracy), vertical writing mode, override order, and a to_string round trip.
  • Right-to-left text-on-path reference images are not in this PR: with main's 0.5 arc-length accuracy they would have to change again when Improve text-path positioning accuracy #1166 lands. I'll add them after it merges.
  • All new tests and the existing direction and text-anchor ones round-trip through to_string with preserve_text to identical glyph positions.
  • 6,000 random trees of nested tspans with random unicode-bidi, direction, x, Arabic and Hebrew text and text paths: no panic, no non-finite position, and the same glyphs as without the attributes (except on a path, where moved glyphs leave it).

Not changed

  • letter-spacing in an rtl paragraph still drops the spacing after the visual-last glyph (the browsers keep it after the logical last), and Chromium spaces Arabic letters where Firefox and resvg do not. No test combines them with rtl.
  • On a text path, dx is not part of the chunk extent used for middle/end/rtl start anchoring. Chromium counts it. It differs for ltr text on main too.
  • Bidi still runs per chunk, not per text element.

`direction` was never read: every chunk was laid out with a left-to-right base level, and
`text-anchor` `start`/`end` always meant left/right.

- `direction` on the `text` element is the base level of the bidi algorithm for its chunks.
- `start` and `end` anchors follow the `direction` of the element a chunk starts in, also on a
  text path, where right-to-left text ends at `startOffset` for `start`.
- The text position a following chunk continues from does not depend on the anchor or direction
  of the chunk before it, but on the direction of the paragraph.
- `unicode-bidi` `embed`, `bidi-override`, `isolate`, `isolate-override` and `plaintext` become
  the Unicode formatting characters CSS defines for them, added only for the analysis. They nest
  like the elements and are opened again in a chunk that starts inside one.
- The bidi analysis runs once for a chunk instead of once per span font.
- A vertical writing mode ignores both properties, as before.
- The writer keeps them with `preserve_text`, including the direction of a chunk that starts
  inside a `unicode-bidi` element of another direction.

Fixes linebender#475.
The references of `direction/rtl`, `unicode-bidi/bidi-override` and `color-font/mixed-text-rtl`
were rendered with the property ignored: the text ran off the right edge from `x`.

New tests cover anchors, inheritance, `direction` on a `tspan` and on the root `svg`
(the example from linebender#475), a `y`-only chunk after a `tspan` with `direction`, mixed scripts,
end-of-sentence punctuation, numbers and date ranges (Western and Arabic-Indic digits), nested
parentheses and brackets, scientific text (units, signs, Greek letters), decorations, word
spacing, a drop shadow, `clip-path`, `mask`, and `embed`, `bidi-override`, `isolate`,
`isolate-override` and `plaintext`.
Each was compared with Chromium 131 and Firefox before its reference was made.
@matthargett
matthargett force-pushed the rtl-direction branch 2 times, most recently from 0ba954c to f2aa1cd Compare October 21, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bidirectional text error

1 participant