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.
55 lines
2.3 KiB
Python
55 lines
2.3 KiB
Python
"""Decodes an arbitrary uploaded file (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF/...)
|
|
into a plain RGB PIL image, for the static-image widget (see routers/
|
|
api_widgets.py's api_widget_static_upload, app/widgets/static_image.py).
|
|
The result is stored (as PNG bytes) rather than the original upload, so
|
|
render() never needs to re-run PDF/GIF decoding on every panel refresh --
|
|
this module only runs once, at upload time.
|
|
|
|
PDF decoding uses pypdfium2 (Google's PDFium bindings -- BSD-3-Clause/
|
|
Apache-2.0, no copyleft exposure) rather than a GPL/AGPL alternative
|
|
like PyMuPDF, per CLAUDE.md's copyleft-dependency convention (a check
|
|
that only applies to copyleft/unclear licenses -- this one's plainly
|
|
permissive, so no explicit flag was needed here)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
import pypdfium2 as pdfium
|
|
from fastapi import HTTPException
|
|
from PIL import Image, UnidentifiedImageError
|
|
|
|
MAX_UPLOAD_BYTES = 25 * 1024 * 1024 # generous for a single image/PDF page; stops an accidental huge upload
|
|
# ~144 DPI off a PDF's 72-DPI native unit -- comfortably above the panel's
|
|
# own 800x480, without ballooning render time/memory on a poster-sized page.
|
|
PDF_RENDER_SCALE = 2.0
|
|
|
|
|
|
def decode_upload(data: bytes) -> Image.Image:
|
|
"""Raises HTTPException(400) for anything that isn't a recognizable
|
|
image or PDF. Detects PDF by magic bytes, not the client-supplied
|
|
filename/content-type (neither is trustworthy). A PDF renders only
|
|
its first page -- there's no "which page" concept for a single-image
|
|
widget."""
|
|
if len(data) > MAX_UPLOAD_BYTES:
|
|
raise HTTPException(400, f"File is too large (max {MAX_UPLOAD_BYTES // (1024 * 1024)}MB)")
|
|
if data.startswith(b"%PDF-"):
|
|
return _decode_pdf(data)
|
|
try:
|
|
img = Image.open(io.BytesIO(data))
|
|
img.load()
|
|
except UnidentifiedImageError:
|
|
raise HTTPException(400, "Not a recognizable image or PDF file") from None
|
|
return img.convert("RGB")
|
|
|
|
|
|
def _decode_pdf(data: bytes) -> Image.Image:
|
|
try:
|
|
pdf = pdfium.PdfDocument(data)
|
|
if len(pdf) == 0:
|
|
raise HTTPException(400, "PDF has no pages")
|
|
bitmap = pdf[0].render(scale=PDF_RENDER_SCALE)
|
|
except pdfium.PdfiumError as e:
|
|
raise HTTPException(400, f"Could not read PDF: {e}") from e
|
|
return bitmap.to_pil().convert("RGB")
|