mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Ran a doc-vs-filesystem crosscheck: extracted every backtick-quoted path with a file extension or known directory prefix from docs/*.md and verified each exists. After filtering false positives (placeholder paths, config keys mistaken for paths, paths inside docs that already have historical-status banners), found 4 real broken references — 3 fixed in docs, 1 fixed by creating the missing file: docs/HOW_TO_RUN_TESTS.md:339 - Claimed ".github/workflows/tests.yml" exists and runs pytest on multiple Python versions in CI. There is no such workflow. The only GitHub Actions file is security-audit.yml (bandit + semgrep). - Pytest runs locally but is NOT gated on PRs. - Replaced the fictional CI section with the actual state and a note explaining how someone could contribute a real test workflow. docs/MIGRATION_GUIDE.md:92 - Referenced scripts/fix_perms/README.md "(if exists)" — the hedge betrays that the writer wasn't sure. The README didn't exist. The 6 scripts in scripts/fix_perms/ were never documented. - Created the missing scripts/fix_perms/README.md from scratch with one-line descriptions of all 6 scripts (fix_assets, fix_cache, fix_plugin, fix_web, fix_nhl_cache, safe_plugin_rm) + when-to-use-each guidance + usage examples. - Updated MIGRATION_GUIDE link to drop the "(if exists)" hedge since the file now exists. docs/FONT_MANAGER.md:376 - "See test/font_manager_example.py for a complete working example" — that file does not exist. Verified by listing test/ directory. - Replaced with a pointer to src/font_manager.py itself and the existing scoreboard base classes in src/base_classes/ that actually use the font manager API in production. Path-existence check methodology: - Walked docs/ recursively, regex-extracted backtick-quoted paths matching either /\.(py|sh|json|yml|yaml|md|txt|service|html|js|css|ttf|bdf|png)/ or paths starting with known directory prefixes (scripts/, src/, config/, web_interface/, systemd/, assets/, docs/, test/, etc.) - Filtered out URLs, absolute paths (placeholders), and paths without slashes (likely not relative refs). - Checked existence relative to project root. - Out of 80 unique relative paths in docs/, 32 didn't exist on disk. Most were false positives (configkeys mistaken for paths, example placeholders like 'assets/myfont.ttf', historical references inside docs that already have status banners). The 4 above were genuine broken refs. This pattern is reusable for future iterations and worth wiring into CI (link checker like lychee, scoped to fenced code paths rather than just markdown links, would catch the same class). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
381 lines
12 KiB
Markdown
381 lines
12 KiB
Markdown
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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 should ship custom fonts as
|
|
> regular files inside the plugin directory (e.g., `assets/myfont.ttf`)
|
|
> and reference them by relative path from the plugin's `manager.py`
|
|
> via `display_manager.font_manager.resolve_font(...)` or by loading
|
|
> with PIL directly. The user-facing font override system in the
|
|
> **Fonts** tab still works for any element that's been registered via
|
|
> `register_manager_font()`.
|
|
|
|
### Plugin Font Registration (planned)
|
|
|
|
In your plugin's `manifest.json`:
|
|
|
|
```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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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)
|
|
```python
|
|
self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
|
```
|
|
|
|
### New Way (FontManager)
|
|
```python
|
|
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.
|
|
|