Scanned PDFs can quickly balloon in size because of the high-resolution images embedded on each page, rather than the text or document structure. When a PDF comes from phone photos or scans, each page might hold a multi-megabyte JPEG, far more than needed for reading or sharing. Raster images often account for most of the size in image-based PDFs, so optimizing them is the main way to shrink files. Knowing how to spot and downsample these images efficiently can cut file sizes by over 90% while keeping the document readable.
Why Scanned PDFs Get So Large
Pages in a scanned PDF are usually full-resolution photos—sometimes 3000 pixels wide or more—placed on a standard letter-sized page. For instance, a test file used here had six pages, each with a 3024×4032 JPEG, making the PDF 30.7 MiB. The text and PDF structure made up less than 0.02% of the total size. These numbers show that shrinking the images is what really reduces the document, since embedded raster images take up nearly all the storage, not text or metadata.
Analyzing PDF Content with Python
Before making changes, it helps to check what fills up the PDF. With pymupdf, you can total the size of all embedded images and compare it to the overall file size:
import os, pymupdf
doc = pymupdf.open("scan.pdf")
img = sum(len(doc.xref_stream_raw(i[0])) for p in doc for i in p.get_images(full=True))
print(img, "of", os.path.getsize("scan.pdf"))This method gives a quick estimate of how much images contribute to the PDF's size. A full breakdown would also need to look at image masks, object streams, fonts, metadata, incremental-save sections, compression filters, and duplicated or shared XObjects. Still, this approach is useful for seeing if downsampling images will make a real difference, especially when images use up nearly all the bytes in the file.
Understanding Image Resolution in PDFs
Images in PDFs don't have a set resolution until they're drawn on a page. The effective pixels per inch (ppi) depends on how many pixels are mapped to the physical page width. For example, a 3024-pixel-wide image stretched across 8.5 inches gives about 356 ppi, calculated by dividing pixel width by page width in inches. While this math is consistent, the best ppi for readability depends on font size, contrast, compression artifacts, scan quality, color mode, and whether optical character recognition (OCR) is needed. There's no single benchmark for the minimum ppi for all scanned content, but many reading situations work with lower resolutions.
Choosing the Right Downsampling Settings
Lowering both resolution and JPEG quality can shrink file size dramatically. In the test file, reducing images from 356 ppi to 150 ppi at medium JPEG quality (quality=60) brought average image size down from over 1 MB to about 167–225 KB, with little visible loss for typical scanned text pages. Dropping quality alone at full resolution didn't save as much, and going below 150 ppi made quality drop off. Readability after downsampling depends on the original scan and the document's content, such as fine print, diagrams, or signatures.
Python Script for PDF Image Downsampling
This script uses pymupdf and Pillow to downsample images in a PDF. It targets images drawn above 225 ppi, resamples them to 150 ppi, and only replaces the image if the new version is smaller. This avoids unnecessary recompression and keeps images that are already optimized. The script goes through each page, processes unique images, and updates the PDF only when it actually reduces size.
import io, os, sys, pymupdf
from PIL import Image
def shrink(src, dst, target=150, above=225, quality=60):
doc = pymupdf.open(src)
seen = set()
for page in doc:
for info in page.get_images(full=True):
xref, w, h = info[0], info[2], info[3]
if xref in seen:
continue
seen.add(xref)
rect = page.get_image_rects(xref)[0]
ppi = w / (rect.width / 72)
if ppi <= above:
continue
raw = doc.xref_stream_raw(xref)
im = Image.open(io.BytesIO(raw)).convert("RGB")
im = im.resize((round(w * target / ppi), round(h * target / ppi)), Image.LANCZOS)
out = io.BytesIO()
im.save(out, "JPEG", quality=quality)
if out.tell() >= len(raw):
continue
doc.update_stream(xref, out.getvalue(), compress=False)
doc.xref_set_key(xref, "Width", str(im.width))
doc.xref_set_key(xref, "Height", str(im.height))
doc.xref_set_key(xref, "Filter", "/DCTDecode")
doc.xref_set_key(xref, "ColorSpace", "/DeviceRGB")
doc.save(dst, garbage=4, deflate=True)
shrink(sys.argv[1], sys.argv[2])
print(os.path.getsize(sys.argv[1]), "->", os.path.getsize(sys.argv[2]))On the test file, this script cut the PDF from 30.7 MiB to 0.92 MiB in under three seconds, replacing six images. The check out.tell() >= len(raw) makes sure only smaller images are written back, so the file doesn't grow from unnecessary recompression. The script's way of summing image sizes and updating streams works as an audit heuristic, but a full analysis would need to look at more PDF components.
Comparing with Desktop PDF Optimization Tools
The osFoundry desktop app has a similar optimization feature in its PDF Editor, letting users compress and optimize PDFs with a few clicks. In the same test, the app made a 1.0 MiB file, also cutting size by 97% and optimizing six images. The difference in output size comes from JPEG encoding details, like chroma sampling and Huffman table optimization. Both methods keep color detail and avoid unnecessary recompression. No recent software release or recognized test has set a clear benchmark for comparing PyMuPDF-based resampling with other PDF optimization tools.
Practical Considerations and Limitations
This method works best when images make up most of the PDF. For documents from word processors with few images, downsampling won't save much space. The process is lossy—original image quality drops, so always keep a backup of the original file. Both the script and the app create a new copy instead of overwriting the source.
When sending PDFs by email, keep in mind that attachments are base64-encoded, which increases the apparent size. For example, a 30.7 MiB PDF turns into 41.5 MiB when encoded, which can exceed common email limits. Optimizing images helps keep files within sharing limits. Downsampling cuts file size by reducing the number of pixels in raster images, but the result depends on the source image size, compression format, JPEG quality, page size, color depth, and how much redundancy is in the original PDF. Re-encoding a compressed JPEG can add blocking, ringing, and text-edge artifacts. Any workflow should compare sample pages at native viewing size and check that signatures, fine print, line drawings, stamps, and OCR output are still usable.
Regulatory and Standards Considerations
No government, regulatory, court, standards-body, or institutional decision currently sets a universal rule for scanned-PDF downsampling. Requirements can vary between document sharing, public-sector accessibility, legal discovery, and long-term digital preservation. The choice of resolution and compression should match the intended use and any relevant jurisdiction, standard, or records policy.
Related PDF Processing Resources
If you need to combine multiple PDFs, see this earlier breakdown for practical merging methods using Python and other tools.
Summary
Downsampling images in scanned PDFs with Python offers a practical way to cut file size for easier sharing and storage. By focusing on images drawn at high ppi and using reasonable JPEG quality settings, you can achieve major reductions while keeping the document readable. Tools like PyMuPDF and Pillow make this process accessible for developers and learners working with scanned documents. For more technical details and updates, check official developer documentation and recognized digital preservation standards.
