BDF fonts are fixed-size bitmap strikes: FreeType accepts only the pixel
size baked into the file and raises for anything else. 32 of the 35
shipped fonts are BDF, so a size picked in the web UI usually is not a
valid strike -- and load_font caught that failure with its generic
"unloadable font" handler, which substitutes PressStart2P. Asking for
5x7.bdf at size 10 therefore rendered a completely different typeface,
silently.
It now falls back to the file's own native size instead, which is what
SportsCore._load_custom_font_from_element_config has always done. The
native size is read via FontManager._read_bdf_native_size rather than a
fourth copy of that parser, matching how core.py already delegates.
Also here, because they are the same code path:
- native_bdf_size() is exposed for the web UI, which needs to know when a
size field can take effect at all. None means "free choice".
- ElementStyle.font_size now reports the size actually realised rather
than the one requested. Callers lay out from it, and reserving space
for a size nothing was drawn at is how this surfaces.
- The module font cache is a bounded LRU (256) instead of an unbounded
dict. The display process runs for weeks and every config save can add
a (font, size) pair; every other hot cache in the codebase is bounded
this way.
Untouched configs are unaffected: the shipped classic fonts are the three
TTFs, so nothing was hitting the substitution path by default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>