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.
109 lines
5.1 KiB
Python
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
|