PDFOven is live on DevHunt this week — check it out →
← All guides

How to compress a PDF programmatically (2026)

Compressing a PDF isn't one operation; it's several, each aimed at a different reason the file is big. Before you pick a tool, find out where the bytes are, because the right fix for a 40 MB scanned contract is different from the right fix for a 4 MB text report. This guide explains what makes PDFs large, shows the two standard open-source tools, Ghostscript and qpdf, and then the PDFOven API.

Why PDFs get big

Images. By far the most common cause. A phone photo pasted into a document is often 4,000 pixels wide, stored at full resolution even if it prints two inches across. Scanners produce one large image per page. Downscaling to the resolution you actually need and re-encoding as JPEG regularly cuts these files by 70 to 90 percent.

Uncompressed streams. Page content and other data can be stored raw instead of Flate-compressed. Some generators and some editing tools do this. Compressing them is lossless and often worth 50 percent or more on text-heavy files.

Incremental-save leftovers. PDF editors often append changes to the end of the file instead of rewriting it. Deleted pages, old versions of edited images and replaced objects stay in the file, unreferenced. A full rewrite drops them.

Embedded fonts. A full CJK font can be 10 MB or more. Most generators embed only the glyphs they use (a subset), but some embed whole fonts, and merged documents can carry many copies of the same font.

Uncompressed object structure. Older PDFs store every object, including thousands of small dictionaries, as plain text. PDF 1.5 object streams pack these together and compress them.

To see which applies to you, pdfimages -list file.pdf (from Poppler) lists every image with its pixel size and compressed size, and pdffonts file.pdf shows which fonts are embedded and whether they're subset.

Option 1: Ghostscript

Ghostscript re-interprets the whole PDF and writes a new one, which lets it downsample images, subset fonts and rebuild everything in one pass.

gs -sDEVICE=pdfwrite \
   -dCompatibilityLevel=1.5 \
   -dPDFSETTINGS=/ebook \
   -dNOPAUSE -dBATCH -dQUIET \
   -sOutputFile=smaller.pdf \
   input.pdf

-dPDFSETTINGS picks a preset. /screen downsamples color and grayscale images to 72 dpi, /ebook to 150 dpi, and /printer and /prepress to 300 dpi. /ebook is a common choice for documents read on screen.

Things to know:

  • Because Ghostscript rebuilds the document rather than editing it, some things may not survive unchanged: interactive form fields, some annotations, document structure for accessibility, and occasionally the exact look of transparency. Test with your real documents.
  • The output is sometimes larger than the input, especially for files that were already well optimized. Compare sizes and keep the original if it wins.
  • Licensing. Ghostscript is licensed under the AGPL, with commercial licenses sold by Artifex. Running it internally is generally fine, but if you distribute it with your product or offer it to users over a network as part of a service, the AGPL's obligations may apply. Check with whoever handles licensing before you build it into a SaaS.

Option 2: qpdf for lossless compression

qpdf (Apache 2.0 licensed) restructures PDFs without touching images or rendering anything. It can't shrink photos, but it's safe, fast, and preserves everything else.

qpdf --object-streams=generate \
     --compress-streams=y \
     --recompress-flate \
     --compression-level=9 \
     input.pdf smaller.pdf

That packs objects into compressed object streams, compresses any uncompressed streams, and recompresses existing Flate streams at maximum effort. Because qpdf writes a fresh file containing only the objects the document still references, incremental-save leftovers disappear too.

On a text-heavy PDF with uncompressed streams this can make a big difference. On a scanned PDF it does almost nothing, because the size is in images that are already compressed.

A practical pipeline is to try qpdf first, and only reach for lossy image compression when images are the problem.

The PDFOven way

POST /v1/pdf/compress does the image and structural work in one request:

  • recompresses and downscales large photos,
  • compresses streams that are stored uncompressed,
  • drops unused objects and writes object streams.

Text and vector graphics are left alone and stay sharp. level is low (closest to the original), medium (the default) or high (smallest). remove_metadata also strips the title, author and XMP metadata.

curl https://pdfoven.com/v1/pdf/compress \
  -H "Authorization: Bearer $PDFOVEN_KEY" \
  -F file=@scan.pdf \
  -F level=high \
  --dump-header headers.txt \
  --output smaller.pdf

The response headers tell you what happened:

Header Meaning
X-Original-Bytes Size of the PDF you sent
X-Compressed-Bytes Size of the PDF returned
X-Saved-Percent The reduction, e.g. 68.4
X-Already-Optimized true if it couldn't be made smaller

If PDFOven can't make the file smaller, you get your original back unchanged, with X-Already-Optimized: true. You never receive a bigger file.

From Python

import os
import requests

with open("scan.pdf", "rb") as f:
    resp = requests.post(
        "https://pdfoven.com/v1/pdf/compress",
        headers={"Authorization": f"Bearer {os.environ['PDFOVEN_KEY']}"},
        files={"file": ("scan.pdf", f, "application/pdf")},
        data={"level": "medium", "remove_metadata": "true"},
        timeout=120,
    )
resp.raise_for_status()

if resp.headers.get("X-Already-Optimized") == "true":
    print("Already as small as it gets; keeping the original.")
else:
    print(f"Saved {resp.headers['X-Saved-Percent']}%")
    with open("scan-small.pdf", "wb") as out:
        out.write(resp.content)

From Node.js, by URL

const res = await fetch("https://pdfoven.com/v1/pdf/compress", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PDFOVEN_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ file: { url: "https://example.com/brochure.pdf" }, level: "medium" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
console.log(`${res.headers.get("X-Original-Bytes")} -> ${res.headers.get("X-Compressed-Bytes")} bytes`);

What it won't do

To be clear about the limits:

  • Fonts are not subset or removed. If your file is big because it embeds a full font, PDFOven won't fix that; Ghostscript will.
  • Vector content is not re-encoded or simplified. A huge CAD drawing or a chart with millions of points stays the size it is.
  • Results depend on the file. Scanned documents and photo-heavy PDFs shrink the most. A text-only PDF exported from a modern tool is often already compact and may come back marked as already optimized.

Compression counts as one render. Files can be up to 50 MB, and password-protected inputs take password; the output is never encrypted. AI agents can use the compress_pdf MCP tool, and there is a free compress PDF tool for single files.

When to use which

  • qpdf first, for anything: it's lossless, permissively licensed and quick.
  • Ghostscript when fonts are the problem or you want aggressive downsampling, and the AGPL works for how you use it.
  • PDFOven when you want image compression without running or licensing Ghostscript, and want the size results reported back. 100 renders a month are free; paid plans are $9, $39 and $99 for 2,000, 15,000 and 60,000.

Skip the browser plumbing

PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.

Get a free API key