"""Raster images (.png/.jpg/.jpeg/.webp/.tiff/.bmp) -> PDF, one page per image. Rendered directly by Pillow (img.save(format='PDF')) — no WeasyPrint in this path, so image fidelity is exactly what the encoder produces. Transparency (RGBA/P) is flattened to RGB over white. Images wider or taller than MAX_DIM are downscaled proportionally to keep the PDF sane for e.g. 12 MP camera photos. An unopenable/corrupt file is a job error: "Invalid image: ..." (the contract asserts 'image' in error.lower()). """ from __future__ import annotations import io from pathlib import Path from PIL import Image from . import common MAX_DIM = 2000 def convert(path: Path, job_id: str, progress_cb=None, output_dir=None, source_name=None) -> str: """Render a raster image to a one-page PDF.""" try: img = Image.open(path) img.load() # force full decode: a truncated file fails here, not later except Exception as exc: # noqa: BLE001 - PIL raises many types raise ValueError(f"Invalid image: {path.name} could not be opened ({exc})") from exc if img.mode in ("RGBA", "P"): bg = Image.new("RGB", img.size, (255, 255, 255)) if img.mode == "P": img = img.convert("RGBA") bg.paste(img, mask=img.getchannel("A")) img = bg elif img.mode != "RGB": img = img.convert("RGB") w, h = img.size scale = min(1.0, MAX_DIM / max(w, h)) if scale < 1.0: img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS) common.report(progress_cb, 30.0, f"Rendering {path.name} (1/1)") buf = io.BytesIO() img.save(buf, format="PDF") out_dir = common.default_output_dir(output_dir) filename = common.derive_export_name(source_name or path.name, out_dir) return common.finalize_pdf(buf.getvalue(), out_dir, filename, job_id, progress_cb=progress_cb)