The most common use case: you've taken 10 screenshots of a bug, a tutorial, or a UI flow, and you need to send them as a single attachment. Attaching 10 PNGs to an email is unwieldy. A single PDF is professional and opens on every device. This guide covers how to do it in Python, with curl, and via the API — plus how to control page size and image quality.
TL;DR
- Single screenshot → PDF:
Image.open('shot.png').convert('RGB').save('doc.pdf') - Multiple → single PDF: Pillow
save_all=True, append_images=[...] - Control page size: set a target width and scale all images to the same width before merging
- API: POST each image, then merge — or upload a zip of images
Single screenshot to PDF (Pillow)
pip install Pillow
from PIL import Image
# Single PNG to PDF
Image.open("screenshot.png").convert("RGB").save("document.pdf")
# Single JPG to PDF
Image.open("screenshot.jpg").save("document.pdf")
Why .convert("RGB")? PNG can have an alpha channel (RGBA). PDF doesn't support transparency in image streams — saving an RGBA image directly to PDF raises an error in older Pillow versions. Converting to RGB composites the alpha onto white.
Multiple screenshots to one PDF
from PIL import Image
from pathlib import Path
def screenshots_to_pdf(input_dir: str, out_pdf: str, pattern: str = "*.png") -> int:
"""
Combine all matching images in input_dir into a single PDF.
Images are sorted alphabetically — rename to 001_shot.png, 002_shot.png etc. for ordering.
"""
src_dir = Path(input_dir)
image_paths = sorted(src_dir.glob(pattern))
if not image_paths:
raise ValueError(f"No images found matching {pattern} in {input_dir}")
images = []
for p in image_paths:
img = Image.open(p).convert("RGB")
images.append(img)
# First image is the base, rest are appended
images[0].save(
out_pdf,
save_all=True,
append_images=images[1:],
)
return len(images)
count = screenshots_to_pdf("./screenshots", "bug-report.pdf")
print(f"Combined {count} screenshots into bug-report.pdf")
File size example: 10 × 1920×1080 PNG screenshots (avg 400KB each = 4MB total) → one PDF ≈ 3.8MB (Pillow applies light JPEG compression to image streams inside the PDF by default).
Standardize page width (professional docs)
If your screenshots are different sizes (different monitor resolutions, mobile vs desktop), the PDF pages will have inconsistent widths. Fix this by scaling all images to the same width first:
from PIL import Image
from pathlib import Path
def screenshots_to_pdf_uniform(input_dir: str, out_pdf: str, page_width_px: int = 1920):
image_paths = sorted(Path(input_dir).glob("*.png")) + sorted(Path(input_dir).glob("*.jpg"))
images = []
for p in image_paths:
img = Image.open(p).convert("RGB")
# Scale to uniform width, maintain aspect ratio
ratio = page_width_px / img.width
new_h = int(img.height * ratio)
img = img.resize((page_width_px, new_h), Image.LANCZOS)
images.append(img)
images[0].save(out_pdf, save_all=True, append_images=images[1:])
print(f"Wrote {len(images)}-page PDF at {page_width_px}px width")
screenshots_to_pdf_uniform("./ui-flow", "flow-documentation.pdf")
ChangeThisFile API
Convert individual screenshots via API (each POST returns a single-page PDF):
# PNG to PDF
curl -X POST https://changethisfile.com/v1/convert \
-H "Authorization: Bearer ctf_sk_your_key_here" \
-F "file=@screenshot.png" \
-F "target=pdf" \
--output page1.pdf
import requests
from pathlib import Path
API_KEY = "ctf_sk_your_key_here"
def png_to_pdf(png_path: str, pdf_path: str) -> None:
with open(png_path, "rb") as f:
resp = requests.post(
"https://changethisfile.com/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
data={"target": "pdf"},
timeout=30,
)
resp.raise_for_status()
Path(pdf_path).write_bytes(resp.content)
# Convert each screenshot then merge with PyPDF2 or pypdf
from pypdf import PdfWriter
def screenshots_to_pdf_via_api(png_paths: list, out_pdf: str) -> None:
writer = PdfWriter()
for p in png_paths:
tmp = f"/tmp/{Path(p).stem}.pdf"
png_to_pdf(p, tmp)
writer.append(tmp)
with open(out_pdf, "wb") as f:
writer.write(f)
Edge cases and gotchas
- Screenshot ordering. Pillow sorts files alphabetically. Name screenshots with zero-padded numbers:
001_login.png,002_dashboard.png. Don't rely on file modification time for sort order — it's not preserved when copying. - Retina/HiDPI screenshots. macOS Retina screenshots are 2× resolution (a 1920px monitor → 3840px screenshot). These will make very large PDFs. Scale down to 1920px width before combining: the resulting PDF looks identical at any normal viewing size.
- PDF/A for archiving. Standard Pillow PDFs are not PDF/A compliant. For long-term document archiving (legal, compliance), use
fpdf2orreportlabwith explicit PDF/A output settings. - JPEG screenshots. Mix of JPG and PNG in the same folder? Use
sorted(Path(d).glob('*.[jJpP][pnPN][gGgG]'))or separate globs for each extension.
Watch-folder automation for recurring docs
# One-liner: all PNGs in current directory to a dated PDF
python3 -c "
from PIL import Image
from pathlib import Path
from datetime import date
imgs = sorted(Path('.').glob('*.png'))
if not imgs: exit('No PNGs found')
pages = [Image.open(p).convert('RGB') for p in imgs]
pdf_name = f'screenshots-{date.today()}.pdf'
pages[0].save(pdf_name, save_all=True, append_images=pages[1:])
print(f'Wrote {len(pages)} pages to {pdf_name}')
"
Pillow handles 90% of screenshot-to-PDF needs in 5 lines. For mixed-size screenshots, add the uniform-width scaling step. API free tier works well for occasional single-page conversions.