Toolmingo
Guides6 min read

How to Compress a PDF (Reduce File Size Without Losing Quality)

Learn how to reduce PDF file size by compressing images, removing metadata, and downsampling fonts — with code examples in JavaScript, Python, Go, and PHP.

A 50 MB PDF that was 2 MB before someone hit "Export as PDF" is a familiar problem. PDF files balloon in size because of embedded high-resolution images, unsubsetted fonts, and metadata most readers never see. Compression brings them back down — often by 60–90% — without any visible quality loss.

What makes a PDF large?

Before compressing, understand what takes up space:

Component Typical share Compressible?
Embedded images 60–95% Yes — JPEG re-encode or downscale
Embedded fonts 2–20% Yes — subset to used glyphs only
Document metadata < 1% Yes — strip XMP, thumbnails
Page content streams 1–10% Yes — re-compress with deflate
Embedded files / attachments variable Remove if unneeded

Images are almost always the culprit. A scanned document at 300 DPI embedded as a lossless PNG can be compressed by 80% just by re-encoding as JPEG at quality 75 — the human eye cannot tell the difference at screen resolution.

Compression strategies

1. Image downsampling and re-encoding

Most PDFs destined for screen viewing don't need images above 150 DPI. Print-quality PDFs need 300 DPI; screen viewing is fine at 72–150 DPI.

Three sub-strategies:

  • Subsample: reduce resolution before re-encoding (300 DPI → 96 DPI)
  • Re-encode: keep resolution, switch from PNG/TIFF to JPEG at ~75 quality
  • Both: downsample and re-encode (maximum reduction)

2. Font subsetting

An embedded font file contains all glyphs — usually thousands. A document that uses only 40 characters of a font is still carrying the full 200 KB font file. Subsetting strips unused glyphs, reducing font overhead by 80–95%.

3. Removing invisible data

PDFs accumulate metadata from editors: XMP packets, document info dictionaries, embedded thumbnails, JavaScript, form fields, and named destinations. Stripping these is lossless from a visual standpoint.

4. Re-compressing content streams

PDF page descriptions (text positions, vector paths) are stored as byte streams compressed with deflate. If a PDF was exported by a bad tool, these streams may be uncompressed. Re-writing them with maximum deflate level is lossless and free size reduction.

Compression levels at a glance

Level DPI target JPEG quality Metadata Typical reduction
Screen 72 50 Strip 80–95%
eBook 150 75 Strip 60–85%
Printer 300 90 Keep 20–50%
Prepress 300 100 Keep 5–15%

JavaScript (Node.js)

The pdf-lib library can re-write PDFs but doesn't re-encode embedded JPEGs. For full image compression, use ghostscript via a child process — it is the industry-standard PDF transformer.

import { execFile } from "node:child_process";
import { promisify } from "node:util";

const exec = promisify(execFile);

/**
 * Compress a PDF using Ghostscript.
 * @param {string} input  - path to source PDF
 * @param {string} output - path for compressed PDF
 * @param {"screen"|"ebook"|"printer"|"prepress"} level
 */
async function compressPdf(input, output, level = "ebook") {
  const args = [
    "-sDEVICE=pdfwrite",
    "-dCompatibilityLevel=1.4",
    `-dPDFSETTINGS=/${level}`,
    "-dNOPAUSE",
    "-dQUIET",
    "-dBATCH",
    `-sOutputFile=${output}`,
    input,
  ];

  await exec("gs", args);
}

// Usage
await compressPdf("input.pdf", "output.pdf", "ebook");

// Check reduction
import { statSync } from "node:fs";
const before = statSync("input.pdf").size;
const after  = statSync("output.pdf").size;
console.log(`${((1 - after / before) * 100).toFixed(1)}% reduction`);

If Ghostscript is not available, sharp can re-encode individual images you extract from the PDF, though a full pipeline is more involved.

Python

The pypdf library handles metadata removal and stream compression natively. For image re-encoding, use pikepdf — it gives direct access to embedded image streams.

import pikepdf
from PIL import Image
import io

def compress_pdf(input_path: str, output_path: str, jpeg_quality: int = 75) -> None:
    with pikepdf.open(input_path) as pdf:
        for page in pdf.pages:
            for name, image in page.images.items():
                raw = image.read_raw_bytes()
                img = Image.open(io.BytesIO(raw)).convert("RGB")

                # Re-encode as JPEG at target quality
                buf = io.BytesIO()
                img.save(buf, format="JPEG", quality=jpeg_quality, optimize=True)
                buf.seek(0)

                image.write(buf.read(), filter=pikepdf.Name("/DCTDecode"))

        pdf.save(
            output_path,
            compress_streams=True,     # deflate content streams
            object_stream_mode=pikepdf.ObjectStreamMode.generate,
            linearize=True,            # fast web view (byte-serving)
        )

compress_pdf("input.pdf", "output.pdf", jpeg_quality=75)

For the Ghostscript approach in Python:

import subprocess

def compress_pdf_gs(input_path: str, output_path: str, level: str = "ebook") -> None:
    subprocess.run([
        "gs",
        "-sDEVICE=pdfwrite",
        "-dCompatibilityLevel=1.4",
        f"-dPDFSETTINGS=/{level}",
        "-dNOPAUSE", "-dQUIET", "-dBATCH",
        f"-sOutputFile={output_path}",
        input_path,
    ], check=True)

Go

The Go ecosystem has pdfcpu — a pure-Go PDF processor that handles optimization, metadata removal, and stream compression without external dependencies.

package main

import (
    "fmt"
    "os"

    "github.com/pdfcpu/pdfcpu/pkg/api"
    "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)

func compressPDF(input, output string) error {
    conf := model.NewDefaultConfiguration()
    conf.Optimize = true   // remove duplicates, compress streams
    conf.WriteObjectStream = true

    if err := api.OptimizeFile(input, output, conf); err != nil {
        return fmt.Errorf("optimize: %w", err)
    }
    return nil
}

func main() {
    before, _ := os.Stat("input.pdf")
    if err := compressPDF("input.pdf", "output.pdf"); err != nil {
        panic(err)
    }
    after, _ := os.Stat("output.pdf")
    pct := (1 - float64(after.Size())/float64(before.Size())) * 100
    fmt.Printf("%.1f%% reduction (%d → %d bytes)\n",
        pct, before.Size(), after.Size())
}

pdfcpu optimizes object streams and removes redundant resources. For image re-encoding, Ghostscript via os/exec remains the most reliable cross-platform option.

PHP

<?php

/**
 * Compress a PDF using Ghostscript (must be installed on the server).
 */
function compressPdf(
    string $input,
    string $output,
    string $level = 'ebook'   // screen | ebook | printer | prepress
): bool {
    $cmd = sprintf(
        'gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/%s ' .
        '-dNOPAUSE -dQUIET -dBATCH -sOutputFile=%s %s 2>&1',
        escapeshellarg($level),
        escapeshellarg($output),
        escapeshellarg($input)
    );

    exec($cmd, $out, $code);
    return $code === 0;
}

// Usage
if (compressPdf('input.pdf', 'output.pdf', 'ebook')) {
    $before = filesize('input.pdf');
    $after  = filesize('output.pdf');
    $pct    = round((1 - $after / $before) * 100, 1);
    echo "Reduced by {$pct}% ({$before} → {$after} bytes)\n";
}

Security note: never pass user-supplied file names directly to exec. Always store uploads with a server-generated name and resolve the absolute path yourself before passing to shell commands.

6 common pitfalls

  1. Compressing an already-compressed PDF: Re-encoding JPEGs already at JPEG quality 75 through another JPEG round-trip degrades quality without meaningful size reduction. Check the existing image encoding before deciding whether to re-encode.

  2. Choosing "screen" for print: /screen caps images at 72 DPI and quality 50 — suitable for email previews, unusable for anything printed.

  3. Stripping required metadata: Legal, archival, and accessibility PDFs (PDF/A, PDF/UA) carry metadata that must be preserved. Strip only what you know is optional.

  4. Ignoring linearization: A linearized (fast web view) PDF starts serving the first page before the whole file is downloaded — critical for large PDFs on the web. pikepdf's linearize=True and Ghostscript's -dFastWebView=true handle this.

  5. Output smaller than expected for text-only PDFs: Text with embedded fonts is already very compact. If a PDF contains only text, compression savings will be small (5–20%). The gains are almost entirely in image-heavy documents.

  6. Measuring before comparing versions: Always compare input.pdf to output.pdf file sizes numerically — don't trust the filename or operating system icon.

Frequently asked questions

Can I compress a PDF without losing any quality?
Yes — stream re-compression, metadata removal, and font subsetting are all lossless. Image re-encoding (JPEG at quality 90+) is visually lossless for most documents. True lossless compression saves 10–30%; lossy image compression saves 60–90%.

What DPI do I need for printing?
Commercial printing requires 300 DPI. A laser printer at home is fine at 150–200 DPI. Screen display needs only 72–96 DPI.

Does Ghostscript work on shared hosting?
Not always — shared hosts often restrict exec(). Check with your host or use a dedicated library like pikepdf (Python) or pdfcpu (Go) that don't need a system process.

Why did my compressed PDF get larger?
This happens when the input is already well-optimized, or when a low-quality Ghostscript version re-wraps content without meaningful compression. Try a different -dPDFSETTINGS level, or skip the re-encoding pass.

Can I compress encrypted PDFs?
Only after decryption. You must have the owner password to modify an encrypted PDF. pikepdf.open("file.pdf", password="owner") decrypts on open; pdfcpu also supports password-protected files.

What is the maximum compression ratio I can expect?
A 50 MB scanned document with uncompressed TIFF images can often reach 2–3 MB with /screen settings — a 95% reduction. A 100 KB text-only PDF may only compress to 85 KB.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools