Files
Thomas Faour 35e80c6d1c
Build and push server image / test (push) Successful in 26s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 59s
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.
2026-07-25 08:39:09 +00:00

89 lines
2.7 KiB
Python

"""app.image_upload.decode_upload -- pure decoding logic, no HTTP, no DB.
See routers/api_widgets.py's api_widget_static_upload for the endpoint
that calls this, and tests/test_widget_config_and_queue_endpoints.py for
HTTP-level coverage of that endpoint."""
from __future__ import annotations
import io
import pytest
from fastapi import HTTPException
from PIL import Image
from app import image_upload
def _png_bytes(size=(20, 10), color=(255, 0, 0)) -> bytes:
buf = io.BytesIO()
Image.new("RGB", size, color).save(buf, format="PNG")
return buf.getvalue()
def _pdf_bytes(size=(40, 30), color=(0, 255, 0)) -> bytes:
buf = io.BytesIO()
Image.new("RGB", size, color).save(buf, format="PDF")
return buf.getvalue()
def _gif_bytes(size=(15, 15)) -> bytes:
buf = io.BytesIO()
Image.new("RGB", size, (0, 0, 255)).save(buf, format="GIF")
return buf.getvalue()
def test_decodes_a_png():
img = image_upload.decode_upload(_png_bytes(size=(20, 10)))
assert img.size == (20, 10)
assert img.mode == "RGB"
def test_decodes_a_jpeg():
buf = io.BytesIO()
Image.new("RGB", (12, 8), (1, 2, 3)).save(buf, format="JPEG")
img = image_upload.decode_upload(buf.getvalue())
assert img.size == (12, 8)
assert img.mode == "RGB"
def test_decodes_a_gif():
img = image_upload.decode_upload(_gif_bytes(size=(15, 15)))
assert img.size == (15, 15)
assert img.mode == "RGB"
def test_decodes_a_pdfs_first_page():
img = image_upload.decode_upload(_pdf_bytes(size=(40, 30)))
# Rendered at PDF_RENDER_SCALE (2.0), not the page's native point size.
assert img.size == (80, 60)
assert img.mode == "RGB"
def test_rejects_garbage_bytes():
with pytest.raises(HTTPException) as exc_info:
image_upload.decode_upload(b"this is not an image or a pdf")
assert exc_info.value.status_code == 400
def test_rejects_a_pdf_with_no_pages():
# A syntactically-valid empty PDF (no /Page objects) -- pypdfium2
# opens it fine but reports zero pages.
empty_pdf = (
b"%PDF-1.4\n"
b"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
b"2 0 obj<</Type/Pages/Kids[]/Count 0>>endobj\n"
b"xref\n0 1\n0000000000 65535 f \n"
b"trailer<</Size 3/Root 1 0 R>>\n"
b"startxref\n0\n%%EOF"
)
with pytest.raises(HTTPException) as exc_info:
image_upload.decode_upload(empty_pdf)
assert exc_info.value.status_code == 400
def test_rejects_a_file_over_the_size_limit(monkeypatch):
monkeypatch.setattr(image_upload, "MAX_UPLOAD_BYTES", 10)
with pytest.raises(HTTPException) as exc_info:
image_upload.decode_upload(_png_bytes())
assert exc_info.value.status_code == 400