Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
PDFs
Categories

Why is my generated PDF so large, and how do I shrink it?

The usual cause

An image embedded at source resolution keeps that resolution no matter how small you draw it. A 3000 by 3000 pixel logo placed in a 120 point box still stores 3000 by 3000 pixels, because scaling on the page is a drawing instruction and not a change to the stored image. Fonts are the second suspect: each embedded face carries its glyph data, and a full CJK family is heavy on its own.

Find the heavy object before you change anything

import pikepdf

with pikepdf.open("big.pdf") as pdf:
    for page_no, page in enumerate(pdf.pages, start=1):
        for name, image in page.get_images().items():
            print(page_no, name, int(image.Width), int(image.Height),
                  len(image.read_raw_bytes()), image.Filter)

That prints the pixel dimensions, the stored byte count and the compression filter for every image on every page. get_images recurses into form XObjects by default, which matters because a page's visible content is often drawn entirely through them, so a naive scan of the page's own resources finds nothing. If one row accounts for most of your file you have your answer and can stop guessing about fonts. See pikepdf's images documentation for the wider image API.

Downsample, do not just recompress

Resaving with tighter stream settings barely moves a file whose bulk is an already-compressed image, because those bytes have been compressed once already. Resizing the pixels is what actually shrinks it.

import io
import pikepdf
from PIL import Image

MAX_PX = 500

with pikepdf.open("big.pdf") as pdf:
    for page in pdf.pages:
        for name, image in page.get_images().items():
            pil = pikepdf.PdfImage(image).as_pil_image()
            if max(pil.size) <= MAX_PX:
                continue
            pil.thumbnail((MAX_PX, MAX_PX))
            out = io.BytesIO()
            pil.convert("RGB").save(out, format="JPEG", quality=80, optimize=True)
            image.write(out.getvalue(), filter=pikepdf.Name("/DCTDecode"))
            image.Width, image.Height = pil.size
            image.ColorSpace = pikepdf.Name("/DeviceRGB")
            image.BitsPerComponent = 8
    pdf.save("small.pdf")

Two caveats

Pick MAX_PX from the space the image occupies on the page, not from the page size. An image drawn into a 120 point box is 120 divided by 72 of an inch wide, so 300 DPI output needs about 500 pixels across. Anything past that is invisible in print and costs you bytes.

Do this while the document is still yours to change. The loop above replaces image streams, so the file it writes is not the file you started with. Put the compression step in your generation pipeline, before the document goes out for signature or into an archive, and treat what comes back from signing as final.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms