The trap with PNG-to-PDF: most libraries decode the PNG, then re-encode it as part of the PDF, which loses quality and inflates file size. img2pdf is the right answer because it embeds the PNG bytes verbatim — true lossless conversion. Use Pillow only when you need to apply transformations (resize, rotate) along the way.
Method 1: img2pdf (lossless, the right default)
img2pdf embeds the original PNG bytes directly into the PDF — no re-encoding, no quality loss, smallest output file.
pip install img2pdf
import img2pdf
from pathlib import Path
def png_to_pdf(in_path: str, out_path: str) -> None:
with open(out_path, "wb") as f:
f.write(img2pdf.convert(in_path))
png_to_pdf("diagram.png", "diagram.pdf")
# Multi-page from many PNGs (sorted):
def pngs_to_pdf(in_dir: str, out_path: str) -> None:
pngs = sorted(Path(in_dir).glob("*.png"))
with open(out_path, "wb") as f:
f.write(img2pdf.convert([str(p) for p in pngs]))
pngs_to_pdf("./pages", "document.pdf")
Three things to know:
- Lossless — PNG bytes embedded as-is. Output PDF size ≈ sum of PNG sizes + ~5% overhead.
- Page size auto-derived from image dimensions. A 1920x1080 PNG becomes a PDF page of those exact pixel dimensions (mapped to points).
- For specific page sizes, use
img2pdf.convert(..., layout_fun=img2pdf.get_layout_fun(img2pdf.get_paper_square_layout('A4'))).
Method 2: Pillow (when you need to transform)
If you need to resize, rotate, or composite before saving, Pillow is the natural choice. Tradeoff: it re-encodes the PNG as a JPG inside the PDF (lossy).
pip install Pillow
from PIL import Image
from pathlib import Path
def png_to_pdf(in_path: str, out_path: str, target_dpi: int = 150) -> None:
img = Image.open(in_path)
if img.mode == "RGBA":
# PDF doesn't support transparency well — composite onto white
bg = Image.new("RGB", img.size, "white")
bg.paste(img, mask=img.split()[3])
img = bg
elif img.mode != "RGB":
img = img.convert("RGB")
img.save(out_path, "PDF", resolution=target_dpi)
png_to_pdf("diagram.png", "diagram.pdf")
# Multi-page:
def pngs_to_pdf(in_dir: str, out_path: str, dpi: int = 150) -> None:
pngs = sorted(Path(in_dir).glob("*.png"))
images = []
for p in pngs:
img = Image.open(p)
if img.mode != "RGB":
img = img.convert("RGB")
images.append(img)
images[0].save(out_path, "PDF", resolution=dpi, save_all=True, append_images=images[1:])
pngs_to_pdf("./pages", "document.pdf")
Pillow's PDF output is convenient but lossy. For pixel-perfect output (diagrams, screenshots, charts), use img2pdf.
Method 3: ChangeThisFile API (no library)
If you don't want either dep, the API uses img2pdf-equivalent lossless embedding server-side. Free tier covers 1,000 conversions/month.
import requests
API_KEY = "ctf_sk_your_key_here"
def png_to_pdf(in_path: str, out_path: str) -> None:
with open(in_path, "rb") as f:
response = requests.post(
"https://changethisfile.com/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
data={"source": "png", "target": "pdf"},
timeout=60,
)
response.raise_for_status()
with open(out_path, "wb") as out:
out.write(response.content)
png_to_pdf("diagram.png", "diagram.pdf")
The API is single-PNG by default. For multi-PNG-to-one-PDF, use the /v1/jobs endpoint with multiple file fields, or upload a zip of PNGs with target=pdf and pass combine=true in form data.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| img2pdf | Lossless conversion, smallest output, the right default | No image transformations |
| Pillow | When you need resize/rotate/composite first | Lossy re-encoding, larger files |
| ChangeThisFile API | No library install, edge runtimes | Network call, file size limit (25MB free) |
Production tips
- Always prefer img2pdf for lossless. Pillow's PDF mode is convenient but quietly recompresses. For diagrams, screenshots, or any sharp-edge content, img2pdf is the right answer.
- Decide on page size explicitly. img2pdf defaults to image-pixel-as-page-point (a 1920x1080 PNG becomes a 1920x1080-pt page — much larger than A4). For US Letter or A4 output, use img2pdf.get_layout_fun.
- For PNG with transparency, decide handling. PDF supports transparency but many viewers render it inconsistently. Composite onto white background before save if your audience is using older PDF readers.
- Multi-page from many PNGs is one call. Don't loop over single-page PDFs and merge — pass the list to img2pdf.convert() in one call.
- For very large batches, stream to disk. img2pdf.convert with a list of paths processes lazily; memory stays bounded.
For nearly every PNG-to-PDF case in Python, img2pdf is the right answer. Pillow only when you need transformations. The API for environments where you can't add either dep. Free tier covers 1,000 conversions/month.