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.
89 lines
2.7 KiB
Python
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
|