Files
tfaour 90a014d161
Build and push server image / test (push) Successful in 30s
Build and push server image / build-and-push (push) Successful in 2m4s
Build and push server image / deploy (push) Successful in 49s
Revert to hand-drawn weather icons, styled after EC's set but exact panel colors
The vendored EC bitmaps looked good but dither into a visible speckle
once quantized to the panel's 6-color palette (their colors are
anti-aliased/arbitrary RGB, essentially never an exact palette match).
Hand-drawn icons filled with the frame's actual ink colors quantize with
zero dithering error to diffuse -- confirmed by running both through the
real quantize pass: the bitmap version speckles, the hand-drawn one is
pixel-identical before and after.

Redrawn to look more like EC's style this time around: pointed
triangular sun rays (the earlier attempt's thin-line rays read as a
crosshair, not a sun) and dendrite snowflakes (tick marks near each tip,
not a bare asterisk), plus the same cloud/raindrop/lightning-bolt shapes
as before. Removed the vendored server/app/weather_icons/ directory
entirely -- no longer used, and removes the icon-image licensing
question along with it.
2026-07-27 17:32:51 +00:00

109 lines
5.1 KiB
Python

"""app.weather_render -- pure-logic drawing helpers, no HTTP/DB.
Mirrors calendar_render.py's own testing posture (this module has no
dedicated test file there either, since it's exercised indirectly via
test_widgets_calendar.py) but covers the non-obvious behavior worth
pinning down directly: build_multi_city's label shortening, and that
weather icons are filled with the panel's *exact* ink colors (not an
arbitrary bitmap's anti-aliased ones) -- the whole reason icons are
hand-drawn rather than a fetched/vendored image: an exact palette match
quantizes with zero dithering error to diffuse, where anything else
dithers into a visible speckle at these small on-panel sizes."""
from __future__ import annotations
from PIL import Image, ImageDraw
from app import weather_render
from app.image_pipeline import DEFAULT_PALETTE_RGB, _quantize
def test_build_multi_city_shortens_full_geocoder_labels_for_display(monkeypatch):
"""Cached entries carry the full disambiguated geocoder label (e.g.
"Seattle, Washington, United States" -- see get_or_refresh_weather_
widget_data/weather.geocode_city). Drawing the whole thing made a
single city's row overflow past the widget's own width in practice
(caught via browser verification) -- only the city name should reach
draw_weather_row, same shortening calendar_render.py's
_weather_for_day already does for its own embedded strip."""
seen_labels = []
real_draw_weather_row = weather_render.draw_weather_row
def spy(img, draw, x0, y0, max_w, entries, icon_r, font, units, show_labels=True, palette_rgb=None):
seen_labels.extend(e["label"] for e in entries)
return real_draw_weather_row(img, draw, x0, y0, max_w, entries, icon_r, font, units, show_labels,
palette_rgb)
monkeypatch.setattr(weather_render, "draw_weather_row", spy)
cities = [
{"label": "Seattle, Washington, United States", "high": 65, "low": 50, "category": "rain"},
{"label": "Portland, Oregon, United States", "high": 75, "low": 55, "category": "clear"},
]
img = weather_render.build_multi_city(cities, 400, 150)
assert img.size == (400, 150)
assert seen_labels == ["Seattle", "Portland"]
def test_build_multi_city_empty_list_returns_blank_canvas():
img = weather_render.build_multi_city([], 400, 150)
assert img.size == (400, 150)
def _colors_present(img: Image.Image) -> set[tuple[int, int, int]]:
return {c for _, c in img.getcolors(maxcolors=100_000)}
def test_draw_weather_icon_clear_uses_exact_panel_yellow():
img = Image.new("RGB", (100, 100), (255, 255, 255))
draw = ImageDraw.Draw(img)
weather_render.draw_weather_icon(draw, 50, 50, 30, "clear")
assert tuple(DEFAULT_PALETTE_RGB[2]) in _colors_present(img) # yellow sun
assert (0, 0, 0) not in _colors_present(img) # not a flat-black glyph
def test_draw_weather_icon_rain_uses_exact_panel_blue():
img = Image.new("RGB", (100, 100), (255, 255, 255))
draw = ImageDraw.Draw(img)
weather_render.draw_weather_icon(draw, 50, 50, 30, "rain")
assert tuple(DEFAULT_PALETTE_RGB[4]) in _colors_present(img) # blue raindrops
def test_draw_weather_icon_thunderstorm_uses_exact_panel_yellow():
img = Image.new("RGB", (100, 100), (255, 255, 255))
draw = ImageDraw.Draw(img)
weather_render.draw_weather_icon(draw, 50, 50, 30, "thunderstorm")
assert tuple(DEFAULT_PALETTE_RGB[2]) in _colors_present(img) # yellow bolt
def test_draw_weather_icon_respects_a_custom_frame_palette():
"""A frame with an Advanced-configuration palette override (see
Frame.palette_rgb) should still get ITS actual yellow, not the
hardcoded default -- same resolution rule as calendar_render.py's
_event_colors."""
custom_palette = [(0, 0, 0), (255, 255, 255), (10, 20, 30), (0, 0, 0), (0, 0, 0), (0, 0, 0)]
img = Image.new("RGB", (100, 100), (255, 255, 255))
draw = ImageDraw.Draw(img)
weather_render.draw_weather_icon(draw, 50, 50, 30, "clear", palette_rgb=custom_palette)
assert (10, 20, 30) in _colors_present(img)
def test_icon_fill_quantizes_uniformly_with_no_dithering_speckle():
"""The actual point of hand-drawing icons in exact panel colors: a
small crop taken from deep inside the sun disc (nowhere near its own
edge) is a single flat fill -- if that fill weren't an exact palette
match, _quantize's Floyd-Steinberg dithering would diffuse rounding
error across it, breaking the crop into a speckle of 2+ colors. An
exact match has zero error to diffuse, so the crop stays perfectly
uniform after quantizing, same as before."""
img = Image.new("RGB", (200, 200), (255, 255, 255))
draw = ImageDraw.Draw(img)
weather_render.draw_weather_icon(draw, 100, 100, 60, "clear")
quantized = _quantize(img, DEFAULT_PALETTE_RGB, dither_strength=1.0).convert("RGB")
# A 20x20 crop centered on the disc -- draw_sun's disc radius is
# 0.55*60=33px, comfortably clear of both the outer edge and the ray
# triangles' base line.
crop = quantized.crop((90, 90, 110, 110))
colors_in_crop = _colors_present(crop)
assert colors_in_crop == {tuple(DEFAULT_PALETTE_RGB[2])} # solid yellow, no speckle