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
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.
Choosing "screen" for print:
/screencaps images at 72 DPI and quality 50 — suitable for email previews, unusable for anything printed.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.
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'slinearize=Trueand Ghostscript's-dFastWebView=truehandle this.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.
Measuring before comparing versions: Always compare
input.pdftooutput.pdffile 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.