d2lang/d2 · error

invalid kern table: no subtables

Error message

invalid kern table: no subtables

What it means

After the kern table header, nSubtables (bytes 2–4) must be non-zero; a version-0 kern table with zero subtables contains no kerning data at all, which the parser treats as malformed rather than empty.

Solutions

  1. Remove the empty kern table entirely with fonttools so the parser skips it gracefully.
  2. Regenerate/re-download the font from the original source.
  3. Verify with fonttools (f['kern'].kernTables) that the table actually contains subtables; if kerning lives in GPOS, accept reduced kerning fidelity or pre-convert the font.

Example fix

# python fonttools: drop the empty kern table
from fontTools.ttLib import TTFont
f = TTFont('broken.ttf')
if 'kern' in f and not f['kern'].kernTables:
    del f['kern']
f.save('fixed.ttf')
Defensive patterns

Strategy: fallback

Validate before calling

// nSubtables := binary.BigEndian.Uint16(kern[2:4]); usable := nSubtables != 0

Try / catch

pairs, err := parseLegacyKern(fontData)
if err != nil {
    log.Printf("kerning unavailable (%v), continuing without", err)
    pairs = nil
}

Prevention

When it happens

Trigger: parseFont loads a font whose kern table declares version 0 but nSubtables == 0, typically from a broken subsetting or editing operation.

Common situations: Fonts processed by buggy subsetting tools; fonts where GPOS kerning was added and the legacy kern table emptied but not removed.

Related errors


AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31). Data as JSON: /api/errors/1d633d533a44a44e. Report an issue: GitHub.

Appendix: source

Thrown at lib/textmeasure/fontface.go:165

	} else {
		x -= int64(unitsPerEm) / 2
	}
	return fixed.Int26_6(x / int64(unitsPerEm))
}

func parseLegacyKern(src []byte) ([]kernPair, error) {
	table, err := sfntTable(src, "kern")
	if err != nil || table == nil {
		return nil, err
	}
	if len(table) < 18 {
		return nil, fmt.Errorf("invalid kern table: too short")
	}
	if binary.BigEndian.Uint16(table[0:2]) != 0 {
		return nil, fmt.Errorf("unsupported kern table version")
	}
	if binary.BigEndian.Uint16(table[2:4]) == 0 {
		return nil, fmt.Errorf("invalid kern table: no subtables")
	}

	length := int(binary.BigEndian.Uint16(table[6:8]))
	coverage := binary.BigEndian.Uint16(table[8:10])
	if coverage != 0x0001 {
		return nil, fmt.Errorf("unsupported kern table coverage 0x%04x", coverage)
	}
	n := int(binary.BigEndian.Uint16(table[10:12]))
	if length < 14 || 6*n != length-14 || 4+length > len(table) {
		return nil, fmt.Errorf("invalid kern table length")
	}

	pairs := make([]kernPair, n)
	for i := range pairs {
		offset := 18 + 6*i
		pairs[i] = kernPair{
			key:   binary.BigEndian.Uint32(table[offset : offset+4]),
			value: int16(binary.BigEndian.Uint16(table[offset+4 : offset+6])),

View on GitHub (pinned to 0d69dca6f5)