mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-08 16:16:36 +00:00
fix(fonts): load 4x6 on its pixel grid, from any working directory (#565)
* fix(fonts): load 4x6 on its pixel grid, from any working directory `extra_small_font` loaded 4x6-font.ttf at 6, off the face's 7px grid. Under `draw.fontmode = "1"` the mono rasteriser thresholds each glyph at 50% coverage, so every glyph lost its fourth column and deformed: christmas-countdown rendered "UNTIL" as "VM1JL". The advance is 5px at both sizes, so snapping to 7 reflows nothing. - Sizes in DisplayManager._load_fonts go through crisp_size() instead of literals. crisp_size / FONT_PIXEL_GRID / FONT_NAME_ALIASES move to src/common/font_layout.py; sports_card re-exports them. - Mirror the fix in VisualTestDisplayManager, the harness's fork of _load_fonts. Without it every golden is blessed at the old size. - Resolve bundled font paths against the install root, not the cwd. FontManager._resolve_asset_path now delegates to font_layout.resolve_asset_path (kept by name; plugins probe for it). - The startup banner's middle rung snaps to 7; the 5 rung stays off-grid on purpose (the only size that fits a dotted quad on 64px). - loading.py reads all plugin JSON as UTF-8 (cp1252 on Windows aborted check_plugin.py on a 0x9d byte). - check_plugin.py reports in ASCII and never dies on an unencodable char. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(fonts): resolve relative asset paths from the install root, not the cwd resolve_asset_path checked os.path.exists(relative_path) unconditionally, so a relative asset path was still resolved against the process cwd first -- exactly the dependency this module exists to remove. An unrelated working directory that happens to contain assets/fonts/4x6-font.ttf (a stale checkout, a copied assets folder, another project) would shadow the real bundled font instead of the install root ever being consulted. Only an absolute path is now returned as-is; a relative path always resolves against _INSTALL_ROOT first, matching the docstring's stated contract. FontManager._resolve_asset_path delegates to this function, so it's covered by the same fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-30
@@ -22,6 +22,10 @@ from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from src.common.font_layout import ( # noqa: F401 - re-exported, see below
|
||||
FONT_NAME_ALIASES, FONT_PIXEL_GRID, crisp_size,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
@@ -52,18 +56,12 @@ FAVORITE_RESULT_COLOR_DEFAULTS: Dict[str, Tuple[int, int, int]] = {
|
||||
"tie": (255, 200, 0),
|
||||
}
|
||||
|
||||
#: Family aliases the web UI may write, mapped to the shipped filename.
|
||||
FONT_NAME_ALIASES: Dict[str, str] = {
|
||||
"press_start": "PressStart2P-Regular.ttf",
|
||||
"four_by_six": "4x6-font.ttf",
|
||||
}
|
||||
|
||||
#: Pixel grid each face renders crisply on. Off-grid sizes anti-alias, which
|
||||
#: on an LED matrix is a dim lamp rather than a soft edge.
|
||||
FONT_PIXEL_GRID: Dict[str, int] = {
|
||||
"PressStart2P-Regular.ttf": 8,
|
||||
"4x6-font.ttf": 7,
|
||||
}
|
||||
# Re-exported rather than defined: the grid tables and the snapping rule are
|
||||
# properties of the font files, which the display core needs too (it loads the
|
||||
# same two faces in DisplayManager._load_fonts). They live in
|
||||
# src/common/font_layout.py so there is one definition; they stay in this
|
||||
# module's namespace and __all__ so the eight scoreboards that delegate to
|
||||
# `sports_card.crisp_size` are untouched.
|
||||
|
||||
MONTH_ABBR = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||
@@ -357,24 +355,6 @@ def format_game_time(config: Optional[Dict[str, Any]], time_text: str) -> str:
|
||||
_SCHEMA_FONT_SIZE_CACHE: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
|
||||
def crisp_size(font_file, desired, aliases=None, grid_table=None):
|
||||
"""Snap *desired* to the nearest size *font_file* renders crisply at.
|
||||
|
||||
A face with no known grid is returned unchanged, so a user-supplied
|
||||
font is never second-guessed.
|
||||
|
||||
``aliases`` and ``grid_table`` default to the shared tables; a plugin
|
||||
that ships an extra face can pass its own without forking this.
|
||||
"""
|
||||
aliases = FONT_NAME_ALIASES if aliases is None else aliases
|
||||
grid_table = FONT_PIXEL_GRID if grid_table is None else grid_table
|
||||
font_file = aliases.get(font_file, font_file)
|
||||
grid = grid_table.get(font_file)
|
||||
if not grid or not desired or desired <= 0:
|
||||
return desired
|
||||
return max(grid, int(round(float(desired) / grid)) * grid)
|
||||
|
||||
|
||||
def schema_font_size(schema_path: str, element_key) -> Optional[int]:
|
||||
"""The font_size this plugin's config_schema.json declares, or None.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user