Skip to content

fix(scoreboards): announce the last-ditch font fallback - #412

Merged
ChuckBuilds merged 2 commits into
mainfrom
fix/announce-font-fallback
Sep 4, 2026
Merged

fix(scoreboards): announce the last-ditch font fallback#412
ChuckBuilds merged 2 commits into
mainfrom
fix/announce-font-fallback

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

_load_custom_font's final fallback caught every exception, passed, and returned PIL's built-in face.

That face doesn't match the panel's pixel grid, so a missing or unreadable font file rendered as fuzzy text with nothing in the log to explain it — it reads as a rendering bug rather than a missing file.

Both paths now warn: the one where the fallback font itself fails to load (naming the path and the exception), and the one that reaches PIL's built-in face regardless.

Nothing renders differently. This only makes an existing failure visible.

Scope

An audit of the font and odds paths across all eight scoreboards:

broad handlers (except Exception / bare) 77
already logging 74
broad and silent 5 → 1

football/game_renderer.py keeps its silent BDF handler deliberately — its own comment states the warning is deferred to the downstream fallback, and one is emitted there. Changing it would add noise for an expected condition.

Companion to core ChuckBuilds/LEDMatrix#520, which does the same for the schema lookup — the failure that shipped a 3px-wide glyph and was found by a user counting pixels rather than by any log line.

Fleet: 246 passed, 0 failed.

_load_custom_font's final fallback caught every exception, passed, and returned
PIL's built-in face. That face does not match the panel's pixel grid, so a
missing or unreadable font file rendered as fuzzy text with nothing in the log
to explain it -- it reads as a rendering bug rather than a missing file.

Both paths now warn: the one where the fallback font itself fails to load
(naming the path and the exception), and the one that reaches PIL's built-in
face regardless.

Nothing renders differently. This only makes an existing failure visible.

Scope: an audit of the font and odds paths across all eight scoreboards found
77 broad handlers, of which 74 already log. These two were the only ones that
were both broad and silent. football/game_renderer.py keeps its silent BDF
handler deliberately -- its comment states the warning is deferred to the
downstream fallback, and one is emitted there.
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: a7740097-107d-473d-8001-81775d2ad07c


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

ChuckBuilds pushed a commit that referenced this pull request Sep 4, 2026
Addresses three CodeRabbit findings on #417, all of them real.

The first is the one that mattered: ledmatrix-flights had six anti-aliased
draws this PR had already claimed to fix. None of its ten Draw sites calls
.text() itself -- every renderer hands the Draw to _draw_centered()/_draw(),
which do. The first version of this gate matched `<var>.text(` file-wide and
caught them; tightening it to same-scope AST matching to cut false positives
threw the real findings away and reported the file clean. The runtime probe
missed them too, because the harness never renders those flight paths.

So the gate now resolves, to a fixpoint, which functions draw text on a
parameter, and treats a Draw handed to one of those as text-rendering. That
sits between the file-wide regex (130 findings, mostly noise) and same-scope
matching (missed real ones): an overlay Draw passed to a compositing helper is
still ignored, while _draw_centered(draw, ...) counts.

Two smaller gate defects, also reported and also real:

  * any `.fontmode` assignment satisfied the check, so `fontmode = "L"` -- the
    anti-aliasing default -- would have passed. Now only the constant "1".
  * ast.walk() descended into nested scopes and ignored statement order, so a
    fontmode set *before* its Draw() counted. Now scoped and ordered.

Each is mutation-tested: removing a hand-off fontmode, setting it to "L", or
moving it above its Draw() each make the gate fail.

That found 40 further sites in 13 plugins, including overlay and celebration
paths in all eight scoreboards that the harness never renders.

Versions are picked above every number claimed by #409 and #412; football takes
3.4.3 so #424 keeps 3.5.0. Merge order: #409, #412, this, then #424.

Verified: 246 passed / 2 skipped / 0 failed, 72-card scroll guard passes, all
repo gates pass, and the runtime probe still reports 0 anti-aliased text draws
across 31 plugins at every panel size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
Both sides had bumped basketball to 1.29.2 and soccer to 2.24.2 -- main
from the docs PRs that landed since, this branch from the font-fallback
warning. Kept main's entries exactly as merged, including their changes
arrays, and re-filed this branch's change as 1.29.3 and 2.24.3 above
them. No released entry is rewritten and the version gate still sees a
bump.

plugins.json regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 4, 2026
Seventeen manifests conflicted. The docs pass has been landing on main
one plugin at a time, and this branch bumped the same manifests for the
1-bit text fix, so most collided on the version number.

Resolved from main's history in every case, with this branch's entry
added above it. Matching by version number alone would have been wrong:
on seven plugins -- baseball, elections, leaderboard, masters, mqtt, ufc
and youtube-stats -- both sides had numbered a DIFFERENT release the
same, so main's docs entry and this branch's text fix shared a number.
Dropping either would have lost a release note for a change that
shipped, so both are kept and ours takes the next free number.

Four numbers were skipped because other open PRs already claim them:
basketball 1.29.3 and soccer 2.24.3 (#412), football 3.5.0 (#424) and
lacrosse 1.24.3 (#428). Those four can now merge in any order relative
to this one instead of colliding again on whichever lands second.

Every manifest re-checked: valid JSON, version matches versions[0], no
duplicates, descending order. plugins.json regenerated and in sync with
all 43 manifests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ChuckBuilds
ChuckBuilds merged commit 4cd13d6 into main Sep 4, 2026
3 of 4 checks passed
ChuckBuilds added a commit that referenced this pull request Sep 4, 2026
Second pass: #412, #424 and #428 merged since the last one, so the same
manifests collided again.

Four manifests resolved from main's history with this branch's entry
above it. Football needed care -- main reached 3.5.0 from #424 while
this branch sat at 3.4.3, so taking the next free number would have
sorted our entry into the middle of the list rather than the top. Ours
now takes the next number above whatever main has reached, which is
3.5.1.

The module-collisions workflow conflicted because both sides added a
guard: this branch's 1-bit text check and main's scroll-reachability
check. They are independent, so both the path filters and both steps are
kept.

All 43 manifests re-validated -- valid JSON, version matches
versions[0], no duplicates, descending order -- and plugins.json is in
sync with every one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 5, 2026
…417)

* fix(plugins): draw text 1-bit, so glyphs stay crisp on the LED grid

An LED panel has no partial brightness. PIL defaults ImageDraw's fontmode to
"L", which anti-aliases TrueType glyphs into a grey fringe the panel can only
round off -- a 4px glyph arrives smeared into 3px.

Measured at draw time rather than grepped, because the source-level signal
misleads in both directions. A missing fontmode is often harmless: birdnet-go
and youtube-stats draw PressStart2P at its native 8px, which emits no partial
pixels whatever the mode. A present one is not coverage: ledmatrix-flights set
it at 3 of its 12 Draw() sites, and the one that actually blurred was not among
them. pomodoro-timer looked like the worst offender and was already correct --
it masks, thresholds at >=128 and paints flat, so only its intermediate mask
was ever anti-aliased.

Hooking ImageDraw.text across 31 plugins x 8 panel sizes found 18 genuinely
anti-aliased sites in 9 plugins, all of them 4x6-font.ttf at 6 (74% of lit
pixels at partial coverage) or PressStart2P off its 8px grid.

The other 52 sites here were clean only by luck: their faces happen to sit on
the pixel grid at the size currently configured. Font sizes are user settings,
so that is not a property to rely on -- typing 10 into the web UI reintroduces
the blur. The setting belongs on every draw that renders text.

Excluded: the offline asset generators (download_assets.py, logo_downloader.py
and friends). They bake placeholder logos on a developer machine, not text on
the panel, and logo resampling is a separately-decided question.

Not render-verified: mqtt-notifications, nfl-draft, on-air and static-image
render only with live data the harness has none of. They are fixed statically,
on the same reasoning, but no render proves it.

scripts/test_pixel_perfect_text.py keeps it from coming back, and is wired into
the Plugin Structure workflow. It is AST-scoped, not a file-wide regex: a
Draw() is reported only where that binding is used for .text() in the same
function, which is the difference between 43 real findings and 130 mostly
spurious ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* fix(plugins): catch draws handed to a text-drawing helper

Addresses three CodeRabbit findings on #417, all of them real.

The first is the one that mattered: ledmatrix-flights had six anti-aliased
draws this PR had already claimed to fix. None of its ten Draw sites calls
.text() itself -- every renderer hands the Draw to _draw_centered()/_draw(),
which do. The first version of this gate matched `<var>.text(` file-wide and
caught them; tightening it to same-scope AST matching to cut false positives
threw the real findings away and reported the file clean. The runtime probe
missed them too, because the harness never renders those flight paths.

So the gate now resolves, to a fixpoint, which functions draw text on a
parameter, and treats a Draw handed to one of those as text-rendering. That
sits between the file-wide regex (130 findings, mostly noise) and same-scope
matching (missed real ones): an overlay Draw passed to a compositing helper is
still ignored, while _draw_centered(draw, ...) counts.

Two smaller gate defects, also reported and also real:

  * any `.fontmode` assignment satisfied the check, so `fontmode = "L"` -- the
    anti-aliasing default -- would have passed. Now only the constant "1".
  * ast.walk() descended into nested scopes and ignored statement order, so a
    fontmode set *before* its Draw() counted. Now scoped and ordered.

Each is mutation-tested: removing a hand-off fontmode, setting it to "L", or
moving it above its Draw() each make the gate fail.

That found 40 further sites in 13 plugins, including overlay and celebration
paths in all eight scoreboards that the harness never renders.

Versions are picked above every number claimed by #409 and #412; football takes
3.4.3 so #424 keeps 3.5.0. Merge order: #409, #412, this, then #424.

Verified: 246 passed / 2 skipped / 0 failed, 72-card scroll guard passes, all
repo gates pass, and the runtime probe still reports 0 anti-aliased text draws
across 31 plugins at every panel size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants