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
Summary
For a Hebrew (RTL) kern pair whose GPOS value record adjusts XAdvance only
(ValueFormat 4), luaotfload's
Renderer=HarfBuzz(mode=harf) produces glyphadvances that differ from what standalone
hb-shapeproduces on the samefont. The kern adjustments are applied one glyph too early in the logical run:
the value that
hb-shapeplaces on a glyph's own advance is, in the PDF, placedon the gap before it.
Since mode=harf wraps libharfbuzz, I'd expect its positioning to match
hb-shapeon the same file. It doesn't. Is this a bug, or is advance-only RTLkerning underspecified and placement+advance (
<v 0 v 0>) required?Versions
Minimal reproducer
A four-glyph, IP-free test font (
KernBugTest.otf): glyphs alef/shin/pe/resh asplain rectangles, UPM 1000, neutral names. It has a single GPOS
kernfeatureunder
DFLTandhebr, 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):kernbug.tex: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):i.e. kern-on advance deltas vs kern-off:
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-shapeplaces it. shin does notreceive the -200; the shin->pe gap is left open.
So on the same font,
hb-shapeputs the -200 on shin's advance while mode=harfputs 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>compilesto). Building the same kerning through ufo2ft's KernFeatureWriter instead — which
emits RTL pairs as placement+advance (
<v 0 v 0>) value records — renderscorrectly 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 fontgen_kernbug.py— generates it from scratch (fontTools)kernbug.tex— minimal document