Two latent issues found in a self-review pass:
- LayoutContext._fit_cache was an unbounded dict (the image cache got an
LRU cap, the text-fit cache didn't). Cache keys embed the fitted TEXT,
so a plugin fitting changing strings — a live game clock, a ticker —
on a 24/7 service grows it forever. Now LRU-bounded at 512 entries via
the same pattern as the image cache.
- fit_image returned the caller's ORIGINAL image object when the source
was already RGBA at target size (contain/fill_height, no ink crop).
ImageFitResult is documented as an independent copy, and LayoutContext
caches results — an aliased image lets later mutations of the source
corrupt cached fits (or vice versa). Copy in that branch.
Both covered by new regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Add src/adaptive_images.py — the image counterpart to fit_text:
- fit_image(img, box, mode=contain|cover|fill_height|stretch,
crop_to_ink, anchor, resample, upscale) promoting the proven plugin
patterns (football's crop-to-ink fill-height logos, masters' cover
crop + NEAREST flags, static-image's letterbox). Upscales by default —
thumbnail()'s downscale-only behavior is why imagery stays tiny on
big panels.
- draw_fitted_image() pastes aligned within a Region with alpha mask.
- One central Pillow>=9.1 RESAMPLE shim replacing ~15 plugin copies.
LayoutContext.fit_image() caches results per (identity, box size,
options) with a 64-entry LRU; id()-keyed entries pin the source image.
BasePlugin.draw_image() is the one-liner adoption path beside draw_fit.
Composites in adaptive_layout.py: Region.offset() (user x/y-offset
passthrough), scoreboard_regions() (the two-logos-plus-score card math
duplicated across six sports plugins, logo_slot = min(H, W//2)), and
media_row() (art-left/text-right).
Fix LogoHelper's size-blind cache key (stale sizes on panel change);
deprecation note on dead image_utils.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>