What is actually making your PDF so big? Measure it in 20 lines of Python
"Compress it" is the usual answer to a heavy PDF, but it's a guess. Before you change any export settings, it's worth knowing which objects take the bytes. With PyMuPDF that takes about 20 lines. A test file th
"Compress it" is the usual answer to a heavy PDF, but it's a guess. Before you change any export settings, it's worth knowing which objects take the bytes. With PyMuPDF that takes about 20 lines.
A test file that looks like real life
We built a 3 page PDF the way people actually make them:
- page 1: a title and one 12 megapixel phone photo (4000 x 3000) placed 4 inches wide,
- page 2: 30 lines of notes in two fonts, embedded in full (Arial and Georgia),
- page 3: a letter-size page "scanned" in color at 300 DPI (2550 x 3300).
The file came out at 7.87 MB.
The script
import os
import pymupdf # pip install pymupdf
def breakdown(path):
doc = pymupdf.open(path)
total = os.path.getsize(path)
images = fonts = 0
for xref in range(1, doc.xref_length()):
subtype = doc.xref_get_key(xref, "Subtype")[1]
if subtype == "/Image":
images += len(doc.xref_stream_raw(xref))
for key in ("FontFile", "FontFile2", "FontFile3"):
kind, value = doc.xref_get_key(xref, key)
if kind == "xref":
fonts += len(doc.xref_stream_raw(int(value.split()[0])))
print(f"{path}: {total/1e6:.2f} MB")
print(f" images {images/1e6:.2f} MB ({images*100/total:.1f}%)")
print(f" fonts {fonts/1e6:.2f} MB ({fonts*100/total:.1f}%)")
breakdown("report.pdf")
It walks every object in the cross reference table, adds up the raw (still compressed) stream length of image objects, and follows each font descriptor to its embedded font file. Whatever is left is page content, structure and metadata.
For our file:
report.pdf: 7.87 MB
images 7.14 MB (90.6%)
fonts 0.70 MB (8.8%)
The text people read was about 1% of the file. Deleting a page of notes would save a few kilobytes.
Turning bytes into a diagnosis: effective DPI
Image bytes alone don't tell you whether an image is too big. What matters is pixels per inch at the size it's drawn. PyMuPDF gives you both:
doc = pymupdf.open("report.pdf")
for page in doc:
for info in page.get_image_info(xrefs=True):
w_in = (info["bbox"][2] - info["bbox"][0]) / 72 # PDF points to inches
dpi = info["width"] / w_in
print(page.number + 1, info["width"], "x", info["height"], f"{dpi:.0f} dpi")
The phone photo came out at 1,000 DPI: 4000 pixels squeezed into 4 inches. Screens and office printers can't show more than about 150 to 300, so roughly 90% of those pixels are dead weight. That single image was 74% of the file.
Fonts: full vs subset
Embedded font names tell you if a font is subset. A subset font has a six letter tag in front, like BNJWSA+ArialMT; a full one doesn't. In our file Arial alone was over half a megabyte embedded in full. After a compression pass that subsets fonts, both fonts together dropped from about 696 KB to 29 KB, and the text was still real, selectable text.
What compression did to it
We ran the same file through Ghostscript based compression at three common levels:
| Level | Result | Image resolution |
|---|---|---|
| screen | 118 KB | 72 DPI |
| ebook | 273 KB | 150 DPI |
| printer | 1.73 MB | higher |
The "ebook" level (150 DPI) is the sensible default: 96.5% smaller and still fine on an office printer.
A rule of thumb
- Images above about 70% of the file: downsample them. That's where nearly all the savings are.
- Fonts above about 20%: they're embedded in full; subsetting fixes it.
- Neither, and the file is still big: look for attachments, form fields and incremental saves (old versions of pages kept at the end of the file after each edit).
If you'd rather not run Python, we built the same breakdown into a browser tool: the PDF size analyzer splits a file into images, fonts, content, attachments and metadata, lists each image with its DPI, and reads the file locally without uploading it.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.