Files
epub2pdf/converters/image_converter.py
T

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)