54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
"""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)
|