perf(common): rasterize outlined text once instead of nine times (#718)

New draw_text_outlined(draw, xy, text, font, fill, outline_color=(0, 0, 0), offsets=OUTLINE_SQUARE) in src/common/text_helper.py, with OUTLINE_SQUARE and OUTLINE_CROSS. It rasterizes the string once and stamps the mask at each outline offset instead of one draw.text per offset: the same pixels as the nine-draw loop (an equivalence sweep across the bundled fonts, image and font modes, colours and positions pins it, on Windows and Linux), about 8x faster per outlined string. Fractional coordinates, multiline text, fonts other than a plain FreeTypeFont, other image modes and a replaced draw.text take the old loop. SportsCoreSharedMixin._draw_text_with_outline and TextHelper.draw_text_with_outline draw through it; the scoreboards' own game_renderer loops adopt it in a ledmatrix-plugins change after a core release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-01 20:19:30 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 34be83d595
commit f5793e2134
5 changed files with 801 additions and 25 deletions
+24
View File
@@ -19,6 +19,30 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
### Outlined text: one rasterization
- New `draw_text_outlined(draw, xy, text, font, fill, outline_color=(0, 0,
0), offsets=OUTLINE_SQUARE)` in `src/common/text_helper.py`, with
`OUTLINE_SQUARE` (the eight-sided outline the scoreboards draw) and
`OUTLINE_CROSS` (four sides). Outlined text was one `draw.text` per
outline offset plus one for the text, so FreeType rasterized the same
string nine times. This rasterizes it once and stamps the mask at each
offset: the same pixels, about 8x faster per outlined string (Pillow 12.3,
desktop). `test/test_text_helper.py` compares it with the nine-draw loop
across the bundled fonts, image and font modes, colours and positions,
and fails if it stops rasterizing once. Fractional coordinates, multiline
text, fonts other than a plain `FreeTypeFont`, image modes other than
RGB, RGBA and L, and a subclassed or replaced `draw.text` take the old
loop unchanged. A whole-pixel float such as `52.0`, which the scorebugs'
centring passes, is not fractional.
- `SportsCoreSharedMixin._draw_text_with_outline`, which eight of the nine
scoreboards inherit for their switch-mode scorebug (ufc has its own), and
`TextHelper.draw_text_with_outline` now draw through it. Scroll and Vegas
cards still use each plugin's own `game_renderer.py` loop, so building a
scroll strip costs the same until the plugins adopt `draw_text_outlined`,
importing it with an `ImportError` fallback to their own loop (a separate
ledmatrix-plugins change after a core release ships it).
### Shared fetch service (stage 1)
Core's own HTTP fetch paths now go through one service, so the plugins that