Files
LEDMatrix/docs/FONT_MANAGER.md
T
ChuckandClaude Opus 5.5 2601cb4cbb chore(deprecation): remove the 35 APIs deprecated for 3.8.0 (#708)
* chore(deprecation): remove the 35 APIs deprecated for 3.8.0

The usage scan (docs/DEPRECATIONS_3.8.md, regenerated 2026-10-01 and
committed here) finds no call or override of any of them in the 46
monorepo plugins or the 8 third-party plugins plugins.json lists; the
only core callers were other deprecated methods removed alongside.

- CacheManager: 13 methods, plus the private helpers only
  has_data_changed used (_has_*_changed, _is_market_open).
- DisplayManager: 7 methods, plus WEATHER_COLORS and the private
  _draw_sun/_cloud/_rain/_snow/_storm helpers only the icon methods used.
- FontManager: 14 methods, plus size_tokens, _save_overrides and
  _clear_plugin_font_cache. font_overrides and _load_overrides stay:
  resolve_font() still applies config/font_overrides.json.
  performance_stats stays: get_font() keeps it and tests read it.
- PluginManager.get_enabled_plugins.

test_deprecation.py pins only the two 3.9.0 markers now; the scanner
tests run against a stand-in core instead of the real markers. The
memory-tier tests read stats through log_memory_cache_stats() and the
component, and the test of the removed _clear_plugin_font_cache goes.
Docs drop the removed methods' reference entries; the Deprecated APIs
table becomes "Removed in 3.8.0". CHANGELOG gains a Removed section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deprecation): drop the test harness's copies of the removed icon methods

VisualTestDisplayManager still drew weather icons that DisplayManager no
longer has, so a plugin's visual tests could pass on calls that raise
AttributeError on the real display. Its draw_sun/draw_cloud/draw_rain/
draw_snow/draw_weather_icon/draw_text_with_icons, WEATHER_COLORS and the
private helpers go, with the tests that exercised them. The CHANGELOG's
Deprecations entries no longer say nothing is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:45:28 -04:00

8.3 KiB

FontManager Usage Guide

Picking a size automatically: if you want the largest font that fits a given area rather than a fixed size, use the adaptive layout system's font ladders, which resolve through this FontManager. BasePlugin subclasses get this as self.layout.fit_text(...); other code can build a LayoutContext(width, height, font_manager) directly — see ADAPTIVE_LAYOUT.md.

Overview

src/font_manager.py loads and caches the TTF and BDF fonts in assets/fonts/, registers fonts that plugins ship, and records which plugin uses which font so the web UI can show it.

Several methods were removed in LEDMatrix 3.8.0 after a release of deprecation warnings; Removed methods below lists them with what to use instead.

Getting the FontManager

There is one shared FontManager per display process. The display controller creates it and hands it to the PluginManager, so a plugin reaches it through its plugin_manager:

class MyPlugin(BasePlugin):
    def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
        super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
        self.font_manager = self._get_font_manager()

BasePlugin._get_font_manager() returns plugin_manager.font_manager, or a standalone FontManager when none is available (test harnesses, mocks). DisplayManager has no font_manager attribute — display_manager.font_manager raises AttributeError.

Resolving a font

element_key = f"{self.plugin_id}.title"

# Register the choice so the web UI's Fonts tab can list it.
self.font_manager.register_manager_font(
    manager_id=self.plugin_id,
    element_key=element_key,
    family="press_start",
    size_px=10,
    color=(255, 255, 255),
)

font = self.font_manager.resolve_font(
    element_key=element_key,
    family="press_start",
    size_px=10,
)
self.display_manager.draw_text("Hello", x=10, y=10, font=font)

resolve_font() applies any entry for element_key in config/font_overrides.json, maps a plugin-local family to its namespaced name when plugin_id is passed, and then calls get_font(family, size_px). On error it returns a fallback font rather than raising.

get_font(family, size_px) looks the family up in font_catalog and loads it (cached per family and size).

Font families

At start-up the FontManager scans assets/fonts/ for .ttf and .bdf files. Each becomes a family named after the file, lower-cased and without the extension (PressStart2P-Regular.ttf → pressstart2p-regular). Four aliases are added on top:

Alias File
press_start assets/fonts/PressStart2P-Regular.ttf
four_by_six assets/fonts/4x6-font.ttf
five_by_seven assets/fonts/5x7.bdf
tom_thumb assets/fonts/tom-thumb.bdf

Read the catalog directly: font_manager.font_catalog is a dict of family name to file path. Files added later are picked up on the next start of the display service.

Plugin fonts

Plugins that ship their own fonts declare them in a "fonts" block in manifest.json. The plugin manager calls FontManager.register_plugin_fonts() during plugin load. plugin://… sources are resolved relative to the plugin's install directory.

{
  "id": "my-plugin",
  "name": "My Plugin",
  "fonts": {
    "fonts": [
      {
        "family": "custom_font",
        "source": "plugin://fonts/custom.ttf",
        "metadata": {"description": "Custom plugin font", "license": "MIT"}
      },
      {
        "family": "web_font",
        "source": "https://example.com/fonts/font.ttf",
        "metadata": {"checksum": "sha256:abc123..."}
      }
    ]
  }
}

Registered families are namespaced as <plugin_id>::<family>. Pass plugin_id to resolve_font() to use the short name:

font = self.font_manager.resolve_font(
    element_key=f"{self.plugin_id}.text",
    family="custom_font",          # resolved as "my-plugin::custom_font"
    size_px=10,
    plugin_id=self.plugin_id,
)

Overrides

resolve_font() still honours config/font_overrides.json (a map of element key to family and/or size_px), which is read once at start-up. The methods that edited it — set_override(), remove_override(), get_overrides() — were removed in 3.8.0, and there is no web UI or REST endpoint for overrides (the override editor and /api/v3/fonts/overrides were removed). To let users choose a font, add a field to your plugin's config schema.

Font usage in the web UI

The web UI's Fonts tab lists, uploads, previews and deletes the font files in assets/fonts/. The web interface runs in its own process and has no FontManager, so the display service publishes which plugin uses which font (src/font_usage.py), and the tab's Used by column reads it:

  • Source: register_manager_font() registrations of the loaded plugins. get_font() and resolve_font() do not know the calling plugin and are not counted, and neither is a plugin that opens a font file directly with PIL — register the fonts your plugin draws with if you want them listed.
  • Names: a family, alias or path is resolved through font_catalog to the file it loads and reported under that file's name without extension (PressStart2P-Regular, 4x6-font, 5x7, tom-thumb), which is how the Fonts tab keys its rows. Fonts outside assets/fonts/ (a plugin's own plugin_id::family fonts) and families that resolve to nothing are left out.
  • When: a daemon thread started once plugins have loaded checks every 10 seconds and writes the font_usage_snapshot cache key only when the usage changed (and once a day, so the cache's cleanup never expires it). Unloading a plugin drops its registrations (forget_manager_fonts).
  • Unknown: until the display service has published, the column reads "unknown" and GET /api/v3/fonts/catalog returns used_by: null.
  • The tab warns before deleting a font that a loaded plugin registered.

Text measurement

width, height, baseline = font_manager.measure_text("Hello", font)
font_height = font_manager.get_font_height(font)

Tips

  • BDF fonts usually look better than TTF at small sizes on LED panels.
  • Use {plugin_id}.{element} element keys.
  • Register the fonts you draw with, so the Fonts tab can warn before one is deleted.
  • Replace direct ImageFont.truetype("assets/fonts/...", 8) calls with resolve_font(): it caches, resolves paths against the install directory, and handles BDF files.

Troubleshooting

Font not found

  • Check the file exists in assets/fonts/.
  • The family name is the filename without extension, lower-cased.
  • Check the display service log for font discovery errors.

Plugin fonts not loading

  • Check the manifest's "fonts" block.
  • Check the log for download or registration errors, and that font URLs are reachable.

API reference

Current methods:

Method Purpose
register_manager_font(manager_id, element_key, family, size_px, color=None) Record a font choice (feeds the Fonts tab)
forget_manager_fonts(manager_id) Drop a manager's registrations (core calls it when a plugin unloads)
resolve_font(element_key, family, size_px, plugin_id=None) Get a font, applying overrides and plugin namespacing
get_font(family, size_px) Get a font directly
get_native_bdf_size(family) Native pixel size of a BDF family, or None
measure_text(text, font) (width, height, baseline)
get_font_height(font) Line height
register_plugin_fonts(plugin_id, font_manifest) Register a plugin's fonts (core calls it at load)
clear_cache() Drop cached fonts and metrics
font_catalog (attribute) Family name → file path

Removed methods

Removed in 3.8.0, after logging a deprecation warning on first call since 3.5.0.

Method Use instead
get_available_fonts(), get_font_catalog() read font_catalog
get_size_tokens() pass a pixel size
get_performance_stats() —
set_override(), remove_override(), get_overrides() a font field in your plugin's config schema
get_manager_fonts(), get_detected_fonts() —
get_plugin_fonts(), unregister_plugin_fonts() —
add_font(), remove_font(), validate_font() the web UI's Fonts tab