Files
LEDMatrix/docs/FONT_MANAGER.md
Chuck eba2d4a711 docs: address CodeRabbit review comments on #306
Reviewed all 12 CodeRabbit comments on PR #306, verified each against
the current code, and fixed the 11 valid ones. The 12th finding is a
real code bug (cache_manager.delete() calls in api_helper.py and
resource_monitor.py) that's already in the planned follow-up code-fix
PR, so it stays out of this docs PR.

Fixed:

.cursor/plugins_guide.md, .cursor/README.md, .cursorrules
- I claimed "there is no --emulator flag" in 3 places. Verified in
  run.py:19-20 that the -e/--emulator flag is defined and functional
  (it sets os.environ["EMULATOR"]="true" before the display imports).
  Other docs I didn't touch (.cursor/plugin_templates/QUICK_START.md,
  docs/PLUGIN_DEVELOPMENT_GUIDE.md) already use the flag correctly.
  Replaced all 3 wrong statements with accurate guidance that
  both forms work and explains the CLI flag's relationship to the
  env var.

.cursorrules, docs/GETTING_STARTED.md, docs/WEB_INTERFACE_GUIDE.md,
docs/PLUGIN_DEVELOPMENT_GUIDE.md
- Four places claimed "the plugin loader also falls back to plugins/".
  Verified that PluginManager.discover_plugins()
  (src/plugin_system/plugin_manager.py:154) only scans the
  configured directory — no fallback. The fallback to plugins/
  exists only in two narrower places: store_manager.py:1700-1718
  (store install/update/uninstall operations) and
  schema_manager.py:70-80 (schema lookup for the web UI form
  generator). Rewrote all four mentions with the precise scope.
  Added a recommendation to set plugin_system.plugins_directory
  to "plugins" for the smoothest dev workflow with
  dev_plugin_setup.sh symlinks.

docs/FONT_MANAGER.md
- The "Status" warning told plugin authors to use
  display_manager.font_manager.resolve_font(...) as a workaround for
  loading plugin fonts. Verified in src/font_manager.py that
  resolve_font() takes a family name, not a file path — so the
  workaround as written doesn't actually work. Rewrote to tell
  authors to load the font directly with PIL or freetype-py in their
  plugin.
- The same section said "the user-facing font override system in the
  Fonts tab still works for any element that's been registered via
  register_manager_font()". Verified in
  web_interface/blueprints/api_v3.py:5404-5428 that
  /api/v3/fonts/overrides is a placeholder implementation that
  returns empty arrays and contains "would integrate with the actual
  font system" comments — the Fonts tab does not have functional
  integration with register_manager_font() or the override system.
  Removed the false claim and added an explicit note that the tab
  is a placeholder.

docs/ADVANCED_FEATURES.md:523
- The on-demand section said REST/UI calls write a request "into the
  cache manager (display_on_demand_config key)". Wrong — verified
  via grep that api_v3.py:1622 and :1687 write to
  display_on_demand_request, and display_on_demand_config is only
  written by the controller during activation
  (display_controller.py:1195, cleared at :1221). Corrected the key
  name and added controller file:line references so future readers
  can verify.

docs/ADVANCED_FEATURES.md:803
- "Plugins using the background service" paragraph listed all
  scoreboard plugins but an orphaned " MLB (baseball)" bullet
  remained below from the old version of the section. Removed the
  orphan and added "baseball/MLB" to the inline list for clarity.

web_interface/README.md
- The POST /api/v3/system/action action list was incomplete. Verified
  in web_interface/app.py:1383,1386 that enable_autostart and
  disable_autostart are valid actions. Added both.
- The Plugin Store section was missing
  GET /api/v3/plugins/store/github-status (verified at
  api_v3.py:3296). Added it.
- The SSE line-range reference was app.py:607-615 but line 619
  contains the "Exempt SSE streams from CSRF and add rate limiting"
  block that's semantically part of the same feature. Extended the
  range to 607-619.

docs/GETTING_STARTED.md
- Rows/Columns step said "Columns: 64 or 96 (match your hardware)".
  The web UI's validation accepts any integer in 16-128. Clarified
  that 64 and 96 are the common bundled-hardware values but the
  valid range is wider.

Not addressed (out of scope for docs PR):

- .cursorrules:184 CodeRabbit comment flagged the non-existent
  cache_manager.delete() calls in src/common/api_helper.py:287 and
  src/plugin_system/resource_monitor.py:343. These are real CODE
  bugs, not doc bugs, and they're the first item in the planned
  post-docs-refresh code-cleanup PR (see
  /home/chuck/.claude/plans/warm-imagining-river.md). The docs in
  this PR correctly state that delete() doesn't exist on
  CacheManager — the fix belongs in the follow-up code PR that
  either adds a delete() shim or updates the two callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:11:41 -04:00

12 KiB

FontManager Usage Guide

Overview

The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:

  • Manager font registration and detection
  • Plugin font management
  • Manual font overrides via web interface
  • Performance monitoring and caching
  • Dynamic font discovery

Architecture

Manager-Centric Design

Managers define their own fonts, but the FontManager:

  1. Loads and caches fonts for performance
  2. Detects font usage for visibility
  3. Allows manual overrides when needed
  4. Supports plugin fonts with namespacing

Font Resolution Flow

Manager requests font → Check manual overrides → Apply manager choice → Cache & return

For Manager Developers

Basic Font Usage

from src.font_manager import FontManager

class MyManager:
    def __init__(self, config, display_manager, cache_manager):
        self.font_manager = display_manager.font_manager  # Access shared FontManager
        self.manager_id = "my_manager"
        
    def display(self):
        # Define your font choices
        element_key = "my_manager.title"
        font_family = "press_start"
        font_size_px = 10
        color = (255, 255, 255)  # RGB white
        
        # Register your font choice (for detection and future overrides)
        self.font_manager.register_manager_font(
            manager_id=self.manager_id,
            element_key=element_key,
            family=font_family,
            size_px=font_size_px,
            color=color
        )
        
        # Get the font (checks for manual overrides automatically)
        font = self.font_manager.resolve_font(
            element_key=element_key,
            family=font_family,
            size_px=font_size_px
        )
        
        # Use the font for rendering
        self.display_manager.draw_text(
            "Hello World",
            x=10, y=10,
            color=color,
            font=font
        )

Advanced Font Usage

class AdvancedManager:
    def __init__(self, config, display_manager, cache_manager):
        self.font_manager = display_manager.font_manager
        self.manager_id = "advanced_manager"
        
        # Define your font specifications
        self.font_specs = {
            "title": {"family": "press_start", "size_px": 12, "color": (255, 255, 0)},
            "body": {"family": "four_by_six", "size_px": 8, "color": (255, 255, 255)},
            "footer": {"family": "five_by_seven", "size_px": 7, "color": (128, 128, 128)}
        }
        
        # Register all font specs
        for element_type, spec in self.font_specs.items():
            element_key = f"{self.manager_id}.{element_type}"
            self.font_manager.register_manager_font(
                manager_id=self.manager_id,
                element_key=element_key,
                family=spec["family"],
                size_px=spec["size_px"],
                color=spec["color"]
            )
    
    def get_font(self, element_type: str):
        """Helper method to get fonts with override support."""
        spec = self.font_specs[element_type]
        element_key = f"{self.manager_id}.{element_type}"
        
        return self.font_manager.resolve_font(
            element_key=element_key,
            family=spec["family"],
            size_px=spec["size_px"]
        )
    
    def display(self):
        # Get fonts (automatically checks for overrides)
        title_font = self.get_font("title")
        body_font = self.get_font("body")
        footer_font = self.get_font("footer")
        
        # Render with fonts
        self.display_manager.draw_text("Title", font=title_font, color=self.font_specs["title"]["color"])
        self.display_manager.draw_text("Body Text", font=body_font, color=self.font_specs["body"]["color"])
        self.display_manager.draw_text("Footer", font=footer_font, color=self.font_specs["footer"]["color"])

Using Size Tokens

# Get available size tokens
tokens = self.font_manager.get_size_tokens()
# Returns: {'xs': 6, 'sm': 8, 'md': 10, 'lg': 12, 'xl': 14, 'xxl': 16}

# Use token to get size
size_px = tokens.get('md', 10)  # 10px

# Then use in font resolution
font = self.font_manager.resolve_font(
    element_key="my_manager.text",
    family="press_start",
    size_px=size_px
)

For Plugin Developers

⚠️ Status: the plugin-font registration described below is implemented in src/font_manager.py:150 (register_plugin_fonts()) but is not currently wired into the plugin loader. Adding a "fonts" block to your plugin's manifest.json will silently have no effect — the FontManager method exists but nothing calls it.

Until that's connected, plugin authors who need a custom font should load it directly with PIL (or freetype-py for BDF) in their plugin's manager.pyFontManager.resolve_font(family=…, size_px=…) takes a family name, not a file path, so it can't be used to pull a font from your plugin directory. The plugin://… source URIs described below are only honored by register_plugin_fonts() itself, which isn't wired up.

The /api/v3/fonts/overrides endpoints and the Fonts tab in the web UI are currently placeholder implementations — they return empty arrays and contain "would integrate with the actual font system" comments. Manually registered manager fonts do not yet flow into that tab. If you need an override today, load the font directly in your plugin and skip the override system.

Plugin Font Registration (planned)

In your plugin's manifest.json:

{
  "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": {
          "description": "Downloaded font",
          "checksum": "sha256:abc123..."
        }
      }
    ]
  }
}

Using Plugin Fonts

class PluginManager:
    def __init__(self, config, display_manager, cache_manager, plugin_id):
        self.font_manager = display_manager.font_manager
        self.plugin_id = plugin_id
        
    def display(self):
        # Use plugin font (automatically namespaced)
        font = self.font_manager.resolve_font(
            element_key=f"{self.plugin_id}.text",
            family="custom_font",  # Will be resolved as "my-plugin::custom_font"
            size_px=10,
            plugin_id=self.plugin_id
        )
        
        self.display_manager.draw_text("Plugin Text", font=font)

Manual Font Overrides

Users can override any font through the web interface:

  1. Navigate to Fonts tab
  2. View Detected Manager Fonts to see what's currently in use
  3. In Element Overrides section:
    • Select the element (e.g., "nfl.live.score")
    • Choose a different font family
    • Choose a different size
    • Click Add Override

Overrides are stored in config/font_overrides.json and persist across restarts.

Programmatic Overrides

# Set override
font_manager.set_override(
    element_key="nfl.live.score",
    family="four_by_six",
    size_px=8
)

# Remove override
font_manager.remove_override("nfl.live.score")

# Get all overrides
overrides = font_manager.get_overrides()

Font Discovery

Available Fonts

The FontManager automatically scans assets/fonts/ for TTF and BDF fonts:

# Get all available fonts
fonts = font_manager.get_available_fonts()
# Returns: {'press_start': 'assets/fonts/PressStart2P-Regular.ttf', ...}

# Check if font exists
if "my_font" in fonts:
    font = font_manager.get_font("my_font", 10)

Adding Custom Fonts

Place font files in assets/fonts/ directory:

  • Supported formats: .ttf, .bdf
  • Font family name is derived from filename (without extension)
  • Will be automatically discovered on next initialization

Performance Monitoring

# Get performance stats
stats = font_manager.get_performance_stats()

print(f"Cache hit rate: {stats['cache_hit_rate']*100:.1f}%")
print(f"Total fonts cached: {stats['total_fonts_cached']}")
print(f"Failed loads: {stats['failed_loads']}")
print(f"Manager fonts: {stats['manager_fonts']}")
print(f"Plugin fonts: {stats['plugin_fonts']}")

Text Measurement

# Measure text dimensions
width, height, baseline = font_manager.measure_text("Hello", font)

# Get font height
font_height = font_manager.get_font_height(font)

Best Practices

For Managers

  1. Register all fonts you use for visibility
  2. Use consistent element keys (e.g., {manager_id}.{element_type})
  3. Cache font references if using same font multiple times
  4. Use resolve_font() not get_font() directly to support overrides
  5. Define sensible defaults that work well on LED matrix

For Plugins

  1. Use plugin-relative paths (plugin://fonts/...)
  2. Include font metadata (license, description)
  3. Provide fallback fonts if custom fonts fail to load
  4. Test with different display sizes

General

  1. BDF fonts are often better for small sizes on LED matrices
  2. TTF fonts work well for larger sizes
  3. Monospace fonts are easier to align
  4. Test on actual hardware - what looks good on screen may not work on LED matrix

Migration from Old System

Old Way (Direct Font Loading)

self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)

New Way (FontManager)

element_key = f"{self.manager_id}.text"
self.font_manager.register_manager_font(
    manager_id=self.manager_id,
    element_key=element_key,
    family="pressstart2p-regular",
    size_px=8
)
self.font = self.font_manager.resolve_font(
    element_key=element_key,
    family="pressstart2p-regular",
    size_px=8
)

Troubleshooting

Font Not Found

  • Check font file exists in assets/fonts/
  • Verify font family name matches filename (without extension, lowercase)
  • Check logs for font discovery errors

Override Not Working

  • Verify element key matches exactly what manager registered
  • Check config/font_overrides.json for correct syntax
  • Restart application to ensure overrides are loaded

Performance Issues

  • Check cache hit rate in performance stats
  • Reduce number of unique font/size combinations
  • Clear cache if it grows too large: font_manager.clear_cache()

Plugin Fonts Not Loading

  • Verify plugin manifest syntax
  • Check plugin directory structure
  • Review logs for download/registration errors
  • Ensure font URLs are accessible

API Reference

FontManager Methods

  • register_manager_font(manager_id, element_key, family, size_px, color=None) - Register font usage
  • resolve_font(element_key, family, size_px, plugin_id=None) - Get font with override support
  • get_font(family, size_px) - Get font directly (bypasses overrides)
  • measure_text(text, font) - Measure text dimensions
  • get_font_height(font) - Get font height
  • set_override(element_key, family=None, size_px=None) - Set manual override
  • remove_override(element_key) - Remove override
  • get_overrides() - Get all overrides
  • get_detected_fonts() - Get all detected font usage
  • get_manager_fonts(manager_id=None) - Get fonts by manager
  • get_available_fonts() - Get font catalog
  • get_size_tokens() - Get size token definitions
  • get_performance_stats() - Get performance metrics
  • clear_cache() - Clear font cache
  • register_plugin_fonts(plugin_id, font_manifest) - Register plugin fonts
  • unregister_plugin_fonts(plugin_id) - Unregister plugin fonts

Example: Complete Manager Implementation

For a working example of the font manager API in use, see src/font_manager.py itself and the bundled scoreboard base classes in src/base_classes/ (e.g., hockey.py, football.py) which register and resolve fonts via the patterns documented above.