Add a static image widget (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF upload)
A new widget type showing a single user-uploaded image with no live upstream to poll -- decoded once at upload time (PDF's first page via pypdfium2, BSD-3-Clause/Apache-2.0, no copyleft exposure) into plain RGB PNG bytes, then composed per a crop/stretch/letterbox display mode like the photos widget.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""app.widgets.static_image -- unit-level, no HTTP: constructs Widget/
|
||||
StaticWidgetConfig rows directly. The decode-at-upload-time path (see
|
||||
app/image_upload.py) is covered separately in test_image_upload.py and
|
||||
the HTTP-level upload endpoint in
|
||||
test_widget_config_and_queue_endpoints.py -- these tests only exercise
|
||||
render(), which always starts from already-decoded PNG bytes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from app import widgets
|
||||
from app.models import Frame, StaticWidgetConfig, Widget
|
||||
|
||||
|
||||
def _png_bytes(size=(40, 30), color=(200, 50, 50)) -> bytes:
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", size, color).save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _make_widget(db_session, **cfg_kwargs) -> tuple[Frame, Widget]:
|
||||
frame = db_session.get(Frame, 1)
|
||||
widget = Widget(frame_id=frame.id, widget_type="static", x=0, y=0, w=2, h=2,
|
||||
sort_order=0, created_at=time.time())
|
||||
db_session.add(widget)
|
||||
db_session.flush()
|
||||
db_session.add(StaticWidgetConfig(widget_id=widget.id, **cfg_kwargs))
|
||||
db_session.commit()
|
||||
return frame, widget
|
||||
|
||||
|
||||
def test_render_shows_a_placeholder_when_no_image_uploaded(db_session):
|
||||
frame, widget = _make_widget(db_session)
|
||||
img = widgets.static_image.render(db_session, frame, widget, 300, 200)
|
||||
assert img.size == (300, 200)
|
||||
assert img.mode == "RGB"
|
||||
|
||||
|
||||
def test_render_composes_the_uploaded_image(db_session):
|
||||
frame, widget = _make_widget(db_session, image=_png_bytes())
|
||||
img = widgets.static_image.render(db_session, frame, widget, 300, 200)
|
||||
assert img.size == (300, 200)
|
||||
assert img.mode == "RGB"
|
||||
|
||||
|
||||
def test_render_respects_display_mode_stretch_fill(db_session):
|
||||
frame, widget = _make_widget(db_session, image=_png_bytes(), display_mode="stretch_fill")
|
||||
img = widgets.static_image.render(db_session, frame, widget, 300, 200)
|
||||
assert img.size == (300, 200)
|
||||
|
||||
|
||||
def test_render_respects_display_mode_letterbox(db_session):
|
||||
frame, widget = _make_widget(db_session, image=_png_bytes(size=(100, 10)), display_mode="letterbox")
|
||||
img = widgets.static_image.render(db_session, frame, widget, 300, 200)
|
||||
assert img.size == (300, 200)
|
||||
|
||||
|
||||
def test_render_at_minimum_grid_footprint(db_session):
|
||||
"""grid.MIN_FOOTPRINT["static"] is (1, 1) cells -- on an 8x5 grid
|
||||
against a full 800x480 panel that's a 100x96 box, the smallest a
|
||||
static widget can actually be placed at."""
|
||||
frame, widget = _make_widget(db_session, image=_png_bytes())
|
||||
img = widgets.static_image.render(db_session, frame, widget, 100, 96)
|
||||
assert img.size == (100, 96)
|
||||
|
||||
|
||||
def test_no_button_actions():
|
||||
"""A fixed uploaded image -- nothing to advance/back/check."""
|
||||
assert widgets.static_image.ACTIONS == {}
|
||||
assert widgets.static_image.ACTION_LABELS == {}
|
||||
Reference in New Issue
Block a user