Files
espresso_frame/server/tests/test_html_render.py
T
tfaour 5f4f8f2ea7
Build and push server image / test (push) Successful in 44s
Build and push server image / build-and-push (push) Successful in 3m33s
Build and push server image / deploy (push) Failing after 1m27s
Add a curated theme system for "modern" style widgets, inspired by Tesserae
Frame.theme (7 presets in app/theme_tokens.py) drives font family,
corner radius, drop shadow, and an accent hue for every modern-style
widget's header/accent region. Rich accent colors (not just the 6 flat
panel inks) are approximated via denser Bayer stippling confined to just
that region (html_render.ordered_dither_regions), so icon/text content
elsewhere stays exactly as crisp as it is today -- verified directly
against real Chromium renders, both in unit tests and via run-server.
"classic" is a byte-identical no-visual-change default: weather's header
keeps its original fixed blue gradient, tasks/calendar keep their flat
THEME_* ink.

Themes are purely stylistic -- battery's charge-level color, calendar/
tasks' per-owner event chips, and text's own per-widget font choice are
never touched.
2026-07-31 10:34:28 +00:00

66 lines
3.0 KiB
Python

"""app.html_render's shared ordered-dithering primitives -- ordered_dither
and ordered_dither_regions -- exercised directly against synthetic
images, no Chromium/Playwright involved (these two functions run purely
on whatever Image render_html_to_image already handed back)."""
from __future__ import annotations
from PIL import Image
from app import html_render
from app.image_pipeline import DEFAULT_PALETTE_RGB
_PALETTE = set(DEFAULT_PALETTE_RGB)
# A muddy hue nowhere near any of the 6 exact palette colors -- at the
# "modern" style's tuned default amplitude (48), this should still snap
# flatly to a single nearest ink (see theme_tokens.py's module docstring
# for why 48 was chosen for icon/text legibility); only a much higher
# amplitude (as a rich theme's accent_amplitude would use) stipples it
# into a multi-ink approximation.
_RICH_HUE = (168, 75, 42) # a terracotta-ish RGB, not one of the 6 inks
def test_ordered_dither_output_is_exact_palette_colors():
img = Image.new("RGB", (40, 30), (128, 128, 128))
dithered = html_render.ordered_dither(img, None)
assert set(dithered.getdata()) <= _PALETTE
def test_ordered_dither_regions_outside_the_region_matches_plain_dither():
"""Pixels outside every accent_regions rect must come out identical
to a plain ordered_dither call at base_amplitude -- the region-aware
variant must not perturb anything it wasn't asked to."""
img = Image.new("RGB", (100, 80), (90, 140, 200))
base_only = html_render.ordered_dither(img, None, amplitude=48.0)
regions = html_render.ordered_dither_regions(
img, None, base_amplitude=48.0, accent_regions=[((10, 10, 40, 30), 130.0)]
)
for x in range(100):
for y in range(80):
if 10 <= x < 40 and 10 <= y < 30:
continue # inside the accent region -- expected to differ
assert regions.getpixel((x, y)) == base_only.getpixel((x, y))
def test_ordered_dither_regions_stipples_a_rich_hue_the_base_amplitude_would_flatten():
"""The whole reason ordered_dither_regions exists: a rich accent hue
dithered at the base (icon/text-safe) amplitude just snaps to one
nearest ink, but the same hue in an accent region at a theme's higher
accent_amplitude resolves to a believable multi-ink stipple instead --
assert that difference directly, not just "some image came back"."""
img = Image.new("RGB", (60, 60), _RICH_HUE)
rect = (0, 0, 60, 60)
flat = html_render.ordered_dither(img, None, amplitude=48.0)
richer = html_render.ordered_dither_regions(img, None, base_amplitude=48.0, accent_regions=[(rect, 130.0)])
assert len(set(flat.getdata())) == 1
assert len(set(richer.getdata())) > 1
assert set(richer.getdata()) <= _PALETTE
def test_ordered_dither_regions_with_no_accent_regions_matches_plain_dither():
img = Image.new("RGB", (30, 30), (50, 60, 70))
assert list(html_render.ordered_dither_regions(img, None, base_amplitude=48.0).getdata()) == \
list(html_render.ordered_dither(img, None, amplitude=48.0).getdata())