mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +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:
@@ -25,11 +25,20 @@ def find_plugin_dir(plugin_id: str, search_dirs: Sequence[Union[str, Path]]) ->
|
||||
|
||||
|
||||
def load_manifest(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
||||
"""Load and return manifest.json from a plugin directory."""
|
||||
"""Load and return manifest.json from a plugin directory.
|
||||
|
||||
Read as UTF-8 explicitly, not in the platform default encoding: JSON is
|
||||
UTF-8 by RFC 8259, but `open()` honours the locale, which is cp1252 on
|
||||
Windows. A manifest carrying any non-ASCII byte (an em dash in a
|
||||
description, a degree sign in a mode name) therefore raised
|
||||
UnicodeDecodeError and aborted the whole `check_plugin.py --all` run on the
|
||||
byte rather than failing just that plugin. The three sibling loaders below
|
||||
read JSON from the same plugin trees and had the same bug.
|
||||
"""
|
||||
manifest_path = Path(plugin_dir) / 'manifest.json'
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"No manifest.json in {plugin_dir}")
|
||||
with open(manifest_path, 'r') as f:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@@ -77,7 +86,7 @@ def load_config_defaults(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
||||
schema_path = Path(plugin_dir) / 'config_schema.json'
|
||||
if not schema_path.exists():
|
||||
return {}
|
||||
with open(schema_path, 'r') as f:
|
||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||
schema = json.load(f)
|
||||
return _defaults_from_properties(schema.get('properties', {}))
|
||||
|
||||
@@ -106,7 +115,7 @@ def load_harness_spec(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
||||
spec_path = Path(plugin_dir) / 'test' / 'harness.json'
|
||||
if not spec_path.exists():
|
||||
return {}
|
||||
with open(spec_path, 'r') as f:
|
||||
with open(spec_path, 'r', encoding='utf-8') as f:
|
||||
spec = json.load(f)
|
||||
|
||||
# Resolve mock_data path and inline its contents for convenience.
|
||||
@@ -120,7 +129,7 @@ def load_harness_spec(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
||||
f"harness.json references mock_data '{mock_rel}' but "
|
||||
f"{mock_path} does not exist"
|
||||
)
|
||||
with open(mock_path, 'r') as mf:
|
||||
with open(mock_path, 'r', encoding='utf-8') as mf:
|
||||
spec['mock_data_contents'] = json.load(mf)
|
||||
return spec
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from src.common.font_layout import load_truetype
|
||||
from src.common.font_layout import crisp_size, load_truetype
|
||||
|
||||
from src.logging_config import get_logger
|
||||
|
||||
@@ -141,9 +141,10 @@ class VisualTestDisplayManager:
|
||||
fonts_dir = project_root / 'assets' / 'fonts'
|
||||
|
||||
# Press Start 2P — regular and small (both 8px)
|
||||
ttf_path = str(fonts_dir / 'PressStart2P-Regular.ttf')
|
||||
self.regular_font = load_truetype(ttf_path, 8)
|
||||
self.small_font = load_truetype(ttf_path, 8)
|
||||
press_start = 'PressStart2P-Regular.ttf'
|
||||
ttf_path = str(fonts_dir / press_start)
|
||||
self.regular_font = load_truetype(ttf_path, crisp_size(press_start, 8))
|
||||
self.small_font = load_truetype(ttf_path, crisp_size(press_start, 8))
|
||||
self.font = self.regular_font # alias used by some code paths
|
||||
|
||||
# 5x7 BDF font via freetype
|
||||
@@ -160,10 +161,15 @@ class VisualTestDisplayManager:
|
||||
self.calendar_font = self.small_font
|
||||
self.bdf_5x7_font = self.small_font
|
||||
|
||||
# 4x6 extra small TTF
|
||||
# 4x6 extra small TTF, snapped to the face's 7px grid exactly as
|
||||
# DisplayManager._load_fonts does. Sizing this independently is how
|
||||
# the harness would render -- and bless goldens -- in a face the
|
||||
# panel never uses: at the off-grid 6 this asked for, every glyph
|
||||
# loses its fourth column under `draw.fontmode = "1"`.
|
||||
try:
|
||||
xs_path = str(fonts_dir / '4x6-font.ttf')
|
||||
self.extra_small_font = load_truetype(xs_path, 6)
|
||||
four_by_six = '4x6-font.ttf'
|
||||
xs_path = str(fonts_dir / four_by_six)
|
||||
self.extra_small_font = load_truetype(xs_path, crisp_size(four_by_six, 6))
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
logger.debug("Extra small font not available, using fallback: %s", e)
|
||||
self.extra_small_font = self.small_font
|
||||
|
||||
Reference in New Issue
Block a user