Skip to content

harf renderer diverges from hb-shape on advance-only RTL (Hebrew) kerning #336

Description

@yannis1962

Summary

For a Hebrew (RTL) kern pair whose GPOS value record adjusts XAdvance only
(ValueFormat 4), luaotfload's Renderer=HarfBuzz (mode=harf) produces glyph
advances that differ from what standalone hb-shape produces on the same
font
. The kern adjustments are applied one glyph too early in the logical run:
the value that hb-shape places on a glyph's own advance is, in the PDF, placed
on the gap before it.

Since mode=harf wraps libharfbuzz, I'd expect its positioning to match
hb-shape on the same file. It doesn't. Is this a bug, or is advance-only RTL
kerning underspecified and placement+advance (<v 0 v 0>) required?

Versions

  • luaotfload v3.29/2024-12-03 (rev 48139bd)
  • LuaHBTeX 1.22.0, TeX Live 2025, dev id 7673
  • Lua 5.3; platform macos arm64 (Darwin 25.5.0)

Minimal reproducer

A four-glyph, IP-free test font (KernBugTest.otf): glyphs alef/shin/pe/resh as
plain rectangles, UPM 1000, neutral names. It has a single GPOS kern feature
under DFLT and hebr, one LookupType 2 PairPos, LookupFlag 0,
ValueFormat1 = 4 (XAdvance only), with two pairs:

  • pos shin pe -200;
  • pos pe resh 100;

The font is generated from scratch with fontTools (gen_kernbug.py, attached),
so every byte is inspectable and nothing is derived from any real font.

gen_kernbug.py (feature block):

languagesystem DFLT dflt;
languagesystem hebr dflt;
feature kern {
    pos shin pe -200;
    pos pe  resh 100;
} kern;

kernbug.tex:

\documentclass{article}
\usepackage{fontspec}
\usepackage[bidi=basic]{babel}
\babelprovide[main,import]{hebrew}
\babelfont{rm}[Renderer=HarfBuzz,Path=./,Extension=.otf]{KernBugTest}
\begin{document}\Huge
^^^^05d0^^^^05e9^^^^05e4^^^^05e8   % alef shin pe resh (logical order)
\end{document}

Observed vs expected

Input run, logical order: alef (U+05D0), shin (U+05E9), pe (U+05E4), resh (U+05E8).

Reference — standalone HarfBuzz (hb-shape --font-file=KernBugTest.otf --unicodes=05D0,05E9,05E4,05E8):

[resh=3+620 | pe=2+780 | shin=1+560 | alef=0+720]

i.e. kern-on advance deltas vs kern-off:

glyph advance off advance on delta meaning
shin 760 560 -200 shin->pe kern, on shin ✓
pe 680 780 +100 pe->resh kern, on pe ✓
alef 720 720 0 untouched ✓
resh 620 620 0 untouched ✓

luaotfload mode=harf (advances read from the PDF content stream): the -200
is applied to the alef->shin gap rather than to shin's advance, and each
kern lands one glyph earlier in the run than hb-shape places it. shin does not
receive the -200; the shin->pe gap is left open.

So on the same font, hb-shape puts the -200 on shin's advance while mode=harf
puts it on the preceding gap.

Note on likely cause / workaround

The pair values here are XAdvance-only (as a low-level pos A B <n> compiles
to). Building the same kerning through ufo2ft's KernFeatureWriter instead — which
emits RTL pairs as placement+advance (<v 0 v 0>) value records — renders
correctly in mode=harf (and mode=node). So the divergence may be specific to
advance-only value records on RTL runs. Whether advance-only RTL kerning is
expected to work, or placement+advance is required, is exactly the question.

Attached (in a ZIP file because github doesn't lets me upload .otf and .tex)

kernbug.zip

  • KernBugTest.otf — the reproducer font
  • gen_kernbug.py — generates it from scratch (fontTools)
  • kernbug.tex — minimal document

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions