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