Summary
The bug, in plain terms. The kern writer counts subtables so it can print N subtables created. It keeps two separate tallies — one for left-to-right kerning, one for right-to-left. It increments the correct one, then prints the left-to-right one no matter which direction it just wrote.
Why that's fatal. Each tally is initialised inside its own direction's branch. A font with no left-to-right pairs never enters the left-to-right
branch, so that tally is never created, and the print statement asks for a number that doesn't exist:
AttributeError: 'run' object has no attribute 'num_subtables'
It fails before the output file is opened, so -s writes no kern.fea at all. Not a cosmetic bug.
The quieter half. In a font that has both directions, nothing crashes, but the right-to-left runs print the left-to-right number — so a font writing two right-to-left subtables cheerfully reports 0 subtables created, twice.
Why it matters. -s writes subtable breaks, one of the standard ways to keep a large kern feature inside GPOS's 16-bit subtable offset limit. With -s unusable, useExtension is the only remedy left for an RTL-only font.
Reproducer and a verified one-line fix below. Related: #26, which reports the same print for a different reason.
Full report:
kernFeatureWriter.py — _build_st_output(), defined line 1126. All line numbers below are upstream at 503b206, so they can be quoted into the issue unchanged; the vendored copy at scripts/kern/kernFeatureWriter.py is offset by its banner (+19 lines at the time of writing, and the banner is edited from time to time, so recompute rather than trust that number).
_build_st_output() keeps two counters, one per direction, and increments the right one:
if rtl:
self.num_subtables_rtl += 1
...
else:
self.num_subtables += 1
...
print(f'{self.num_subtables} subtables created') # line 1148 — LTR counter, both paths
Both counters are initialised late and separately, each inside its own direction's branch of _make_fea_data():
if any(ltr_container_dicts):
...
if self.write_subtables:
self.num_subtables = 0 # line 1221 — LTR branch only
...
if any(rtl_container_dicts):
...
if self.write_subtables:
self.num_subtables_rtl = 0 # line 1256 — RTL branch only
A font with no LTR pairs never enters the first branch, so self.num_subtables is never created. The RTL call to _build_st_output() then reaches line 1148 and raises:
AttributeError: 'run' object has no attribute 'num_subtables'
Nothing is written — the exception fires inside _make_fea_data(), before the output file is opened, so -s produces no kern.fea at all on such a font.
The same line is wrong in the non-crashing case. _build_st_output() runs four times per write — LTR glyph-to-class, LTR class-to-class, then the same two for RTL — and line 1148 prints at the end of every one of them. When a font has both directions, the two RTL calls print the LTR count, so a font whose subtables are all RTL reports 0 subtables created twice while writing two of them.
Read the counter as a running total, not a per-call count: it is reset once per direction (lines 1221 / 1256) and incremented once per non-empty subtable dict, so consecutive calls print 1, then 2, and the subtable; break is emitted only from the second one onward.
Reproduction
import defcon
f = defcon.Font()
for n in ('alef', 'beh', 'teh', 'reh'):
f.newGlyph(n).width = 500
f.groups['public.kern1.ALEF_ARA'] = ['alef'] # _ARA suffix marks the pair RTL
f.groups['public.kern2.BEH_ARA'] = ['beh', 'teh']
f.kerning[('public.kern1.ALEF_ARA', 'public.kern2.BEH_ARA')] = -50
f.kerning[('reh', 'public.kern2.BEH_ARA')] = -30
f.save('ReproRTL.ufo')
python kernFeatureWriter.py -s ReproRTL.ufo writes no kern.fea and exits 1 with:
File ".../kernFeatureWriter.py", line 1148, in _build_st_output
print(f'{self.num_subtables} subtables created')
AttributeError: 'run' object has no attribute 'num_subtables'
The LTR control needs glyphs, not just a pair. Adding f.kerning[('A', 'V')] = -40 to the fixture above changes nothing — A and V do not exist, KerningSanitizer discards the pair as invalid, ltr_container_dicts stays empty, and the run crashes exactly as before. Create the two glyphs as well:
for n in ('A', 'V'):
f.newGlyph(n).width = 500
f.kerning[('A', 'V')] = -40
Now the command succeeds, and prints 0 subtables created twice for the two RTL calls — the second half of the bug. The zero is not a sizing artifact: only glyph_group and group_group are routed through MakeMeasuredSubtables (lines 1223–1235 for LTR, 1258–1270 for RTL), while a plain glyph-to-glyph pair such as A V is written directly and never counted, so the LTR counter legitimately stays at 0 and the RTL calls print it.
Without -s the same UFO writes fine, so the fault is in the subtable path only.
Suggested fix
- print(f'{self.num_subtables} subtables created')
+ count = self.num_subtables_rtl if rtl else self.num_subtables
+ print(f'{count} subtables created')
Verified on the reproducer, 2026-08-26, against upstream 503b206. RTL-only now writes kern.fea — 22 lines, 2 pos rules, one subtable; break — and prints the running RTL total across its two calls:
1 subtables created
2 subtables created
The mixed fixture prints 0, 0, 1, 2, which is correct for it: no LTR subtable exists, and both RTL ones do. Initialising both counters in __init__ instead would stop the crash but keep the wrong count, so the direction-aware read is the better fix; doing both is better still.
Status in this project
Reached, not latent. AdobeUrdu is RTL-only — the CTX regeneration emits one RTL_kerning lookup and no LTR pairs — so kernFeatureWriterCTX.py -s AdobeUrdu-Regular.ufo crashed exactly as above. Subtable breaks are one of the documented ways to keep a large kern feature inside GPOS offset limits (KERN_FEATURE_WRITER_SPEC.md §8, criterion 4), so -s being unusable cost a remedy.
The fix is applied to the vendored scripts/kern/kernFeatureWriter.py as banner delta (2), labelled LOCAL PATCH (UNFILED) and citing this doc where a filed patch cites #N; the filing stays owed in the tracker's Filed column (row 5). Per KERN_FEATURE_WRITER_SPEC.md §6 (the local-patch process) as revised 2026-08-28, the repro doc plus its tracker row is the gate, not the issue number. Do not patch Git/ATOtools/python-modules/kernFeatureWriter.py in place — it is upstream, and the upstream line numbers quoted above describe it as it still stands.
kernFeatureWriterCTX.py -s -o /tmp/kern_s.fea AdobeUrdu-Regular.ufo now exits 0 and writes 2574 lines, 2484 pos rules, one subtable; break, printing 1 subtables created then 2 subtables created for its two RTL calls (2026-08-28). Verified with -o outside the repo so the build's committed kern.fea is not overwritten.
Summary
The bug, in plain terms. The kern writer counts subtables so it can print
N subtables created. It keeps two separate tallies — one for left-to-right kerning, one for right-to-left. It increments the correct one, then prints the left-to-right one no matter which direction it just wrote.Why that's fatal. Each tally is initialised inside its own direction's branch. A font with no left-to-right pairs never enters the left-to-right
branch, so that tally is never created, and the print statement asks for a number that doesn't exist:
AttributeError: 'run' object has no attribute 'num_subtables'It fails before the output file is opened, so
-swrites nokern.feaat all. Not a cosmetic bug.The quieter half. In a font that has both directions, nothing crashes, but the right-to-left runs print the left-to-right number — so a font writing two right-to-left subtables cheerfully reports
0 subtables created, twice.Why it matters.
-swrites subtable breaks, one of the standard ways to keep a large kern feature inside GPOS's 16-bit subtable offset limit. With-sunusable,useExtensionis the only remedy left for an RTL-only font.Reproducer and a verified one-line fix below. Related: #26, which reports the same
printfor a different reason.Full report:
kernFeatureWriter.py—_build_st_output(), defined line 1126. All line numbers below are upstream at503b206, so they can be quoted into the issue unchanged; the vendored copy atscripts/kern/kernFeatureWriter.pyis offset by its banner (+19 lines at the time of writing, and the banner is edited from time to time, so recompute rather than trust that number)._build_st_output()keeps two counters, one per direction, and increments the right one:Both counters are initialised late and separately, each inside its own direction's branch of
_make_fea_data():A font with no LTR pairs never enters the first branch, so
self.num_subtablesis never created. The RTL call to_build_st_output()then reaches line 1148 and raises:Nothing is written — the exception fires inside
_make_fea_data(), before the output file is opened, so-sproduces nokern.feaat all on such a font.The same line is wrong in the non-crashing case.
_build_st_output()runs four times per write — LTR glyph-to-class, LTR class-to-class, then the same two for RTL — and line 1148 prints at the end of every one of them. When a font has both directions, the two RTL calls print the LTR count, so a font whose subtables are all RTL reports0 subtables createdtwice while writing two of them.Read the counter as a running total, not a per-call count: it is reset once per direction (lines 1221 / 1256) and incremented once per non-empty subtable dict, so consecutive calls print 1, then 2, and the
subtable;break is emitted only from the second one onward.Reproduction
python kernFeatureWriter.py -s ReproRTL.ufowrites nokern.feaand exits 1 with:The LTR control needs glyphs, not just a pair. Adding
f.kerning[('A', 'V')] = -40to the fixture above changes nothing —AandVdo not exist,KerningSanitizerdiscards the pair as invalid,ltr_container_dictsstays empty, and the run crashes exactly as before. Create the two glyphs as well:Now the command succeeds, and prints
0 subtables createdtwice for the two RTL calls — the second half of the bug. The zero is not a sizing artifact: onlyglyph_groupandgroup_groupare routed throughMakeMeasuredSubtables(lines 1223–1235 for LTR, 1258–1270 for RTL), while a plain glyph-to-glyph pair such asA Vis written directly and never counted, so the LTR counter legitimately stays at 0 and the RTL calls print it.Without
-sthe same UFO writes fine, so the fault is in the subtable path only.Suggested fix
Verified on the reproducer, 2026-08-26, against upstream
503b206. RTL-only now writeskern.fea— 22 lines, 2posrules, onesubtable;break — and prints the running RTL total across its two calls:The mixed fixture prints
0,0,1,2, which is correct for it: no LTR subtable exists, and both RTL ones do. Initialising both counters in__init__instead would stop the crash but keep the wrong count, so the direction-aware read is the better fix; doing both is better still.Status in this project
Reached, not latent. AdobeUrdu is RTL-only — the CTX regeneration emits one
RTL_kerninglookup and no LTR pairs — sokernFeatureWriterCTX.py -s AdobeUrdu-Regular.ufocrashed exactly as above. Subtable breaks are one of the documented ways to keep a large kern feature inside GPOS offset limits (KERN_FEATURE_WRITER_SPEC.md§8, criterion 4), so-sbeing unusable cost a remedy.The fix is applied to the vendored
scripts/kern/kernFeatureWriter.pyas banner delta (2), labelledLOCAL PATCH (UNFILED)and citing this doc where a filed patch cites#N; the filing stays owed in the tracker's Filed column (row 5). PerKERN_FEATURE_WRITER_SPEC.md§6 (the local-patch process) as revised 2026-08-28, the repro doc plus its tracker row is the gate, not the issue number. Do not patchGit/ATOtools/python-modules/kernFeatureWriter.pyin place — it is upstream, and the upstream line numbers quoted above describe it as it still stands.kernFeatureWriterCTX.py -s -o /tmp/kern_s.fea AdobeUrdu-Regular.ufonow exits 0 and writes 2574 lines, 2484posrules, onesubtable;break, printing1 subtables createdthen2 subtables createdfor its two RTL calls (2026-08-28). Verified with-ooutside the repo so the build's committedkern.feais not overwritten.