2026-10-05 22:44:41 +08:00
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
|
|
from pigway_frame_flow.models import DisplayGeometry
|
|
|
|
|
from pigway_frame_flow.providers.photo_names import folder_caption
|
|
|
|
|
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
|
|
|
|
|
from pigway_frame_flow.renderers.frame_geometry import frame_canvas
|
|
|
|
|
from pigway_frame_flow.renderers.photo_caption import render_captioned_photo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("rotation", [0, 90, 180, 270])
|
|
|
|
|
@pytest.mark.parametrize("mode", [None, "auto", "portrait", "landscape"])
|
|
|
|
|
def test_canvas_respects_rotation_and_explicit_direction(rotation, mode):
|
|
|
|
|
display = DisplayGeometry(
|
|
|
|
|
width=1200, height=1600, rotation=rotation, color_mode="color"
|
|
|
|
|
)
|
|
|
|
|
expected = (
|
|
|
|
|
(1600, 1200)
|
|
|
|
|
if mode == "landscape" or (mode in (None, "auto") and rotation in (90, 270))
|
|
|
|
|
else (1200, 1600)
|
|
|
|
|
)
|
|
|
|
|
assert frame_canvas(display, mode) == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("profile", ["color", "grayscale"])
|
2026-10-06 10:03:59 +08:00
|
|
|
def test_landscape_caption_uses_compact_watermark(service, profile):
|
2026-10-05 22:44:41 +08:00
|
|
|
out = BytesIO()
|
|
|
|
|
Image.new("RGB", (1600, 900), "blue").save(out, "JPEG")
|
|
|
|
|
data = render_captioned_photo(
|
|
|
|
|
out.getvalue(),
|
|
|
|
|
folder_caption("20261005 家庭出游@示例地点"),
|
|
|
|
|
service.renderer.font,
|
|
|
|
|
profile=profile,
|
|
|
|
|
size=(1600, 1200),
|
|
|
|
|
)
|
|
|
|
|
with Image.open(BytesIO(data)) as image:
|
|
|
|
|
assert image.size == (1600, 1200)
|
2026-10-06 10:03:59 +08:00
|
|
|
assert image.crop((30, 1128, 1570, 1200)).convert("L").getextrema()[0] < 50
|
|
|
|
|
assert image.getpixel((20, 1100))[0] < 230
|
|
|
|
|
assert image.getpixel((20, 1190))[0] > 240
|
2026-10-05 22:44:41 +08:00
|
|
|
pixel = image.getpixel((800, 480))
|
|
|
|
|
assert (
|
|
|
|
|
len(set(pixel)) <= 2
|
|
|
|
|
if profile == "grayscale"
|
|
|
|
|
else pixel[2] > pixel[0] + 100
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"mode,expected",
|
|
|
|
|
[("auto", (1600, 1200)), ("landscape", (1600, 1200)), ("portrait", (1200, 1600))],
|
|
|
|
|
)
|
|
|
|
|
def test_select_and_render_use_same_direction(
|
|
|
|
|
client, service, settings, mode, expected
|
|
|
|
|
):
|
|
|
|
|
root = settings.data_dir.parent / "photos"
|
|
|
|
|
settings.photo_root = root
|
|
|
|
|
folder = root / "20261005 出游@金山"
|
|
|
|
|
folder.mkdir(parents=True)
|
|
|
|
|
for name, size in [("wide.jpg", (300, 200)), ("tall.jpg", (200, 300))]:
|
|
|
|
|
Image.new("RGB", size, "blue").save(folder / name)
|
|
|
|
|
service.photo_sources.save(
|
|
|
|
|
"family",
|
|
|
|
|
PhotoSourceInput(
|
|
|
|
|
name="Family",
|
|
|
|
|
kind="local",
|
|
|
|
|
location=".",
|
|
|
|
|
folder_caption=True,
|
|
|
|
|
orientation=mode,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
rows = client.get("/api/v1/photo-sources/family/images").json()["items"]
|
|
|
|
|
assert len(rows) == 1 and rows[0]["name"] == (
|
|
|
|
|
"wide.jpg" if expected[0] > expected[1] else "tall.jpg"
|
|
|
|
|
)
|
|
|
|
|
before = service.registry.get("living-room").get_current_image()
|
|
|
|
|
accepted = client.post(
|
|
|
|
|
"/api/v1/devices/living-room/upload",
|
|
|
|
|
json={"type": "photo_source", "source_id": "family"},
|
|
|
|
|
)
|
|
|
|
|
assert accepted.status_code == 202 and service.process_one()
|
|
|
|
|
op = client.get("/api/v1/operations/" + accepted.json()["operation_id"]).json()
|
|
|
|
|
assert op["status"] == "COMPLETED"
|
|
|
|
|
with Image.open(BytesIO(client.get(op["result"]["url"]).content)) as image:
|
|
|
|
|
assert image.size == expected
|
|
|
|
|
assert service.registry.get("living-room").get_current_image() == before
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_auto_catalog_cache_is_not_shared_between_opposite_canvases(service, settings):
|
|
|
|
|
root = settings.data_dir.parent / "photos"
|
|
|
|
|
settings.photo_root = root
|
|
|
|
|
root.mkdir()
|
|
|
|
|
for name, size in [("wide.jpg", (300, 200)), ("tall.jpg", (200, 300))]:
|
|
|
|
|
Image.new("RGB", size).save(root / name)
|
|
|
|
|
p = service.photo_sources
|
|
|
|
|
p.save(
|
|
|
|
|
"family",
|
|
|
|
|
PhotoSourceInput(name="Family", kind="local", location=".", orientation="auto"),
|
|
|
|
|
)
|
|
|
|
|
assert p.catalog("family", target_size=(1600, 1200))[0]["name"] == "wide.jpg"
|
|
|
|
|
assert p.catalog("family", target_size=(1200, 1600))[0]["name"] == "tall.jpg"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_auto_preview_uses_requested_device_not_default(
|
|
|
|
|
client, service, settings, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
import copy
|
|
|
|
|
|
|
|
|
|
driver = service.registry.get("living-room")
|
|
|
|
|
other = copy.copy(driver)
|
|
|
|
|
model = driver.device().model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"id": "bedroom",
|
|
|
|
|
"display": driver.device().display.model_copy(update={"rotation": 0}),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(other, "device", lambda: model)
|
|
|
|
|
monkeypatch.setitem(service.registry.drivers, "bedroom", other)
|
|
|
|
|
root = settings.data_dir.parent / "photos"
|
|
|
|
|
root.mkdir()
|
|
|
|
|
settings.photo_root = root
|
|
|
|
|
for name, size in [("wide.jpg", (300, 200)), ("tall.jpg", (200, 300))]:
|
|
|
|
|
Image.new("RGB", size).save(root / name)
|
|
|
|
|
service.photo_sources.save(
|
|
|
|
|
"family",
|
|
|
|
|
PhotoSourceInput(name="Family", kind="local", location=".", orientation="auto"),
|
|
|
|
|
)
|
|
|
|
|
r = client.get("/api/v1/photo-sources/family/images?device_id=bedroom")
|
|
|
|
|
assert r.status_code == 200 and r.json()["items"][0]["name"] == "tall.jpg"
|
|
|
|
|
accepted = client.post(
|
|
|
|
|
"/api/v1/devices/bedroom/upload",
|
|
|
|
|
json={"type": "photo_source", "source_id": "family"},
|
|
|
|
|
)
|
|
|
|
|
assert accepted.status_code == 202 and service.process_one()
|
|
|
|
|
op = client.get("/api/v1/operations/" + accepted.json()["operation_id"]).json()
|
|
|
|
|
assert op["status"] == "COMPLETED"
|
|
|
|
|
with Image.open(BytesIO(client.get(op["result"]["url"]).content)) as image:
|
|
|
|
|
assert image.size == (1200, 1600)
|