d2lang/d2 · error
invalid sfnt: too short
Error message
invalid sfnt: too short
What it means
sfntTable locates a named table (here 'kern') inside an sfnt container. An sfnt header is at least 12 bytes; anything shorter cannot even hold the table directory header, so the font data is rejected outright.
Source
Thrown at lib/textmeasure/fontface.go:191
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])),
}
}
return pairs, nil
}
func sfntTable(src []byte, tag string) ([]byte, error) {
if len(src) < 12 {
return nil, fmt.Errorf("invalid sfnt: too short")
}
base := 0
if string(src[:4]) == "ttcf" {
if len(src) < 16 || binary.BigEndian.Uint32(src[8:12]) == 0 {
return nil, fmt.Errorf("invalid font collection")
}
base = int(binary.BigEndian.Uint32(src[12:16]))
if base < 0 || base+12 > len(src) {
return nil, fmt.Errorf("invalid font collection offset")
}
}
n := int(binary.BigEndian.Uint16(src[base+4 : base+6]))
records := base + 12
if n > (len(src)-records)/16 {
return nil, fmt.Errorf("invalid sfnt table directory")
}View on GitHub (pinned to 0d69dca6f5)
Solutions
- Check the font file is non-empty and starts with a valid sfnt signature (0x00010000, 'true', 'OTTO', or 'ttcf').
- Re-download or re-copy the font and verify size/checksum before embedding.
- If fonts are loaded at runtime, validate len(data) >= 12 and the magic bytes before calling the parser.
Example fix
// before
data, _ := os.ReadFile(fontPath)
face := parseFont(data)
// after
data, err := os.ReadFile(fontPath)
if err != nil || len(data) < 12 {
return fmt.Errorf("font %s missing or truncated", fontPath)
}
face := parseFont(data) Defensive patterns
Strategy: validation
Validate before calling
func isSfnt(data []byte) bool {
if len(data) < 12 { return false }
sig := string(data[:4])
return sig == "\x00\x01\x00\x00" || sig == "true" || sig == "OTTO" || sig == "ttcf"
} Type guard
func looksLikeFont(b []byte) bool { return len(b) >= 12 && isSfnt(b) } Try / catch
if err := loadAndParseFont(path); err != nil {
log.Printf("skipping font %s: %v", path, err)
return defaultFace // fallback font
} Prevention
- Check font file size and magic bytes before parsing
- Handle download failures: never save HTTP error bodies as .ttf assets
- Use go:embed with verified font files, not runtime-fetched placeholders
- Add a startup health check that parses every configured font
When it happens
Trigger: parseFont is given font bytes shorter than 12 bytes — an empty file, an HTTP error page saved as .ttf, a placeholder string, or a nil/blank embedded asset.
Common situations: embed directives picking up empty placeholder files; download failures writing 0-byte or HTML error responses into font assets; wrong env path resolving to the wrong file.
Related errors
- invalid kern table: too short
- invalid sfnt table directory
- checksum error in table: %v
- unsupported kern table version
- invalid kern table: no subtables
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/8f49fbdc65f821cb.
Report an issue: GitHub.