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.
66 lines
3.0 KiB
Python
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())
|