Skip to content

Reverse right-to-left text in fonts without GSUB, GPOS or morx (#39) - #415

Merged
blikblum merged 2 commits into
foliojs:masterfrom
ZahraaSadeqProd:rtl-without-layout-tables
Oct 26, 2026
Merged

blikblum merged 2 commits into
foliojs:masterfrom
ZahraaSadeqProd:rtl-without-layout-tables

Conversation

@ZahraaSadeqProd

@ZahraaSadeqProd ZahraaSadeqProd commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #39

When a font has no GSUB, GPOS or morx table, LayoutEngine has no advanced layout engine, and nothing reverses right-to-left text. OTLayoutEngine.position and AATLayoutEngine.substitute both reverse the glyphs for direction: 'rtl', but without either, layout() returns the glyphs in logical order while the run says rtl. As a result:

  • Hebrew and Arabic come out backwards, as in the issue.
  • The kern table, which runs after that point, kerns logical pairs instead of the pairs that are drawn.
  • The fallback mark positioning in UnicodeLayoutEngine offsets each mark back by its base's advance, which is right for left-to-right text but not once the run is reversed and the mark is drawn before its base.

The change:

  • LayoutEngine.position reverses the glyphs and positions when there's no engine and the direction is rtl. It does that after the fallback mark positioning, which works in logical order, and before kerning, so kern pairs are visual like in HarfBuzz and in the two engines.
  • UnicodeLayoutEngine.positionGlyphs takes the direction. For rtl the marks are offset from the base's origin, as HarfBuzz's position_around_base does for backward text (no -base.xAdvance, and spacing marks in between add their advance instead of subtracting it). The OpenType path still passes 'ltr', so its output doesn't change.

With test/data/NotoSans/NotoKufiArabic-NoLayout.ttf (glyph, x offset, y offset, advance):

Text Before HarfBuzz / after
بتم ب ت م م ت ب
ال (kern lam + alef = -150) ا 592, ل 1407: not kerned ل 1257, ا 592
لا ل 1257, ا 592: kerned ا 592, ل 1407
بَ ب, fatha @-797,-234 fatha @796,-234, ب

(HarfBuzz splits the -150 as -75 on each glyph, so its advances differ per glyph, but the total is the same; fontkit puts the whole kern on the first glyph as before.)

I also checked the issue's Hebrew against HarfBuzz with Taamey Frank CLM from the test data, with its GSUB, GPOS, GDEF and kern tables removed (not committed). עברית and שלום 2024 now match HarfBuzz glyph for glyph. With nikud, every mark both engines draw has the same offset. HarfBuzz composes some pairs (shin + shin dot) into precomposed glyphs, which fontkit doesn't do without an engine. That isn't part of this change.

Not in this change: KernProcessor only kerns adjacent glyphs, so a mark between two kerned letters (اَل) still blocks the pair, as it does in left-to-right text. In the OpenType path the glyphs are already reversed when the fallback mark positioning runs, so right-to-left fonts with GSUB but no GPOS still lose their mark positions: with GPOS removed from NotoKufiArabic-Regular.ttf, the fatha of بَ stays at 0,0, where HarfBuzz puts it at 796,-234. I can send that separately.

Tests: test/issues.js "#39 - right-to-left text in fonts without GSUB, GPOS or morx". Order, marks and ltr staying in logical order use FontkitMarkFallbackTest-Italic.ttf, which is already in the test data, with direction: 'rtl'; the values match hb-shape --direction=rtl --script=hebr. Kerning in visual order uses NotoKufiArabic-NoLayout.ttf, because none of the existing test fonts without layout tables has a kern table. It is a 2 KB subset of NotoKufiArabic-Regular.ttf with its GSUB, GPOS and GDEF tables removed and a one-pair kern table added, and NotoKufiArabic-NoLayout.txt next to it notes the license and the fontTools script that made it. The reverse, mark and kern tests fail before the change.

Validation:

  • npm run format:check passes
  • npm test: 695 passing, 7 pending

@@ -0,0 +1,31 @@
NotoKufiArabic-NoLayout.ttf is derived from NotoKufiArabic-Regular.ttf in this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a existing font in the code base that can be used to test this behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly. FontkitMarkFallbackTest-Italic.ttf has no GSUB or GPOS, and the change only depends on the direction, so with direction: 'rtl' it covers the order and the marks. It matches hb-shape --direction=rtl --script=hebr:

Text fontkit / HarfBuzz
abx x, b, a
q + U+0323 + U+0307 dot above @899,-53, dot below @899,-375, q

I moved those tests to it in d3f18d9.

Kerning still needs the new font. None of the test fonts without layout tables has a kern table, so nothing else can show that the pairs are now kerned in visual order. That's all NotoKufiArabic-NoLayout.ttf is used for now. If you'd rather not add a font, I can drop that test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Keep the new font. Please rebase so i can merge

@ZahraaSadeqProd
ZahraaSadeqProd force-pushed the rtl-without-layout-tables branch from d3f18d9 to 06b11cd Compare October 26, 2026 18:29
@ZahraaSadeqProd

Copy link
Copy Markdown
Contributor Author

Rebased on master. The only conflict was the changelog, and the tests pass.

@blikblum
blikblum merged commit 2dee1a3 into foliojs:master Oct 26, 2026
3 checks passed
@ZahraaSadeqProd
ZahraaSadeqProd deleted the rtl-without-layout-tables branch October 26, 2026 18:56
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.

UnicodeLayoutEngine.positionGlyphs method doesn't position Hebrew (RTL) words

2 participants