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 fpdf2 or reportlab with 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.