From 013a2663e52bc6e40f769486ac928eaff5bed6ce Mon Sep 17 00:00:00 2001 From: Chuck Date: Fri, 10 Jul 2026 11:58:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(layout):=20add=20measure=5Ffont=5Fcrispnes?= =?UTF-8?q?s=20=E2=80=94=20verify=20a=20ladder=20rung=20isn't=20blurry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIL antialiases TTF outlines by default; a 'pixel-style' font only rasterizes without antialiasing at specific sizes (for PressStart2P: exact multiples of its 8px design grid). A ladder rung at an unverified size silently renders blurry on an LED panel — this exact bug shipped in both text-display's and football-scoreboard's custom TTF ladders (non-8-multiple PressStart2P sizes, and '5by7.regular'/'4x6-font' at sizes that were never actually crisp). measure_font_crispness(font, sample_text) renders the sample and reports the fraction of ink-bbox pixels that are neither pure black nor pure white. BDF fonts (real bitmaps) always score 0.0; TTF ladders should be verified against this before shipping — see the new TestFontFitting::test_ladder_arcade_is_crisp pattern. Co-Authored-By: Claude Fable 5 --- src/adaptive_layout.py | 49 ++++++++++++++++++++++++++++++------ test/test_adaptive_layout.py | 22 ++++++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/adaptive_layout.py b/src/adaptive_layout.py index a496e52c..7eccaffe 100644 --- a/src/adaptive_layout.py +++ b/src/adaptive_layout.py @@ -273,6 +273,38 @@ def font_line_height(font: Any) -> int: return ascent + descent +def measure_font_crispness(font: Any, sample_text: str = "Ay0", + canvas_size: Tuple[int, int] = (250, 60)) -> float: + """Fraction of the rendered sample's ink-bbox pixels that are neither + pure black nor pure white — i.e. antialiased. + + BDF (freetype.Face) glyphs are true bitmaps and always render at 0.0. + "Pixel-style" TTFs (PressStart2P, and similar fonts bundled for + plugins that draw through ImageDraw.text() and so can't take a BDF + face) are NOT automatically crisp at arbitrary sizes — PIL antialiases + TTF outlines by default, and a pixel-grid font only lands on whole + pixels at specific sizes (for PressStart2P: exact multiples of 8). + Requesting an unverified size silently produces soft/blurry glyphs on + an LED panel, which reads as fuzzy compared to a true BDF rung. + + Use this to vet any custom FontLadder rung that mixes TTF fonts before + shipping it — see test_adaptive_layout.py::test_ladder_is_crisp for the + pattern. A rung should score 0.0 (or very close, to allow for the odd + diagonal stroke) before it belongs in a "crisp" ladder. + """ + if isinstance(font, freetype.Face): + return 0.0 + from PIL import Image, ImageDraw + img = Image.new("L", canvas_size, 0) + ImageDraw.Draw(img).text((2, 2), sample_text, font=font, fill=255) + bbox = img.getbbox() + if bbox is None: + return 0.0 + pixels = img.crop(bbox).tobytes() + pure = sum(1 for p in pixels if p == 0 or p == 255) + return (len(pixels) - pure) / len(pixels) + + class LayoutContext: """Per-render-size layout facts and fit-text queries for one panel size. @@ -542,10 +574,13 @@ def scoreboard_regions(bounds: Region, *, ctx: Optional["LayoutContext"] = None, Encodes the invariant duplicated across the sports plugins: ``logo_slot = min(height, width // 2)`` (capped at half the card so the home slot never collapses), away logo centered in the left slot, home in - the right, score/status/detail stacked in the middle column. Band - heights default to the classic 128x32 values, scaled by the context's - geometry factor when one is provided. Works on a full panel or on a - scroll-mode card Region. + the right. The text bands (status/score/detail) span the FULL card width + and overlay the logo slots — exactly like the classic layouts, where + outlined text is drawn over the logos; on square-ish panels the column + between the slots can be zero-wide, so full-width bands are the only + correct home for text. Band heights default to the classic 128x32 + values, scaled by the context's geometry factor when one is provided. + Works on a full panel or on a scroll-mode card Region. """ if status_h is None: status_h = ctx.px(9, minimum=7) if ctx else 9 @@ -557,9 +592,9 @@ def scoreboard_regions(bounds: Region, *, ctx: Optional["LayoutContext"] = None, home_slot = bounds.right_col(logo_slot) center_col = Region(bounds.x + logo_slot, bounds.y, bounds.w - 2 * logo_slot, bounds.h) - status_band = center_col.top_band(status_h) - detail_band = center_col.bottom_band(detail_h) - score_area = center_col.middle(status_band.h, detail_band.h) + status_band = bounds.top_band(status_h) + detail_band = bounds.bottom_band(detail_h) + score_area = bounds.middle(status_band.h, detail_band.h) bottom = bounds.bottom_band(detail_h) return ScoreboardRegions( bounds=bounds, logo_slot=logo_slot, diff --git a/test/test_adaptive_layout.py b/test/test_adaptive_layout.py index 13688bed..08435f31 100644 --- a/test/test_adaptive_layout.py +++ b/test/test_adaptive_layout.py @@ -9,6 +9,7 @@ from src.adaptive_layout import ( LayoutContext, Region, draw_fitted_text, + measure_font_crispness, measure_ink, media_row, scoreboard_regions, @@ -182,6 +183,27 @@ class TestFontFitting: assert heights == sorted(heights, reverse=True), ( f"ladder not monotonically shrinking: {heights}") + def test_ladder_grid_is_crisp(self, font_manager): + """LADDER_GRID's BDF fonts are real bitmaps — always 0% antialiased.""" + for step in LADDER_GRID: + font = font_manager.get_font(step.family, step.size_px) + assert measure_font_crispness(font, "Ay0") == 0.0 + + def test_ladder_arcade_is_crisp(self, font_manager): + """PressStart2P only rasterizes without antialiasing at exact + multiples of its 8px design grid — every LADDER_ARCADE rung must + land on one.""" + for step in LADDER_ARCADE: + assert step.size_px % 8 == 0, f"{step} is not a multiple of 8" + font = font_manager.get_font(step.family, step.size_px) + assert measure_font_crispness(font, "17-21") == 0.0 + + def test_crispness_catches_a_bad_size(self, font_manager): + """Sanity check the measurement itself: a known-bad size for a + pixel-grid font must NOT read as crisp.""" + font = font_manager.get_font("press_start", 10) # not a multiple of 8 + assert measure_font_crispness(font, "17-21") > 0.1 + def test_fit_text_grows_on_taller_panel(self, font_manager): small = LayoutContext(64, 32, font_manager) large = LayoutContext(128, 64, font_manager)