Shrink Scanned PDFs by Downsampling Images with Python

Shrink Scanned PDFs by Downsampling Images with Python EduTechBuddy.com © edutechbuddy.com
Shrink Scanned PDFs by Downsampling Images with Python © edutechbuddy.com
Learn how to make scanned PDFs dramatically smaller by resampling embedded images with Python. This guide explains the process, provides a working script, and compares results with a desktop PDF optimization app.

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.

Camila Ferreira Editor EduTechBuddy.com
Co-Founder

Camila Ferreira

Camila Ferreira is the Co-Founder of EduTechBuddy, where her editorial focus is programming education and practical software development skills. Her primary subjects include Python programming, Git version control, and the fundamental concepts beginners need to understand when working with code.