Skip to content

-s (subtable breaks) crashes on any RTL-only font, because the RTL path prints the LTR subtable counter #38

Description

@pauldhunt

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions