"""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")