Ghostscript PDF Compression: Presets, Commands, and Why the Output Is Sometimes Bigger
The situation
You have a server, a folder of PDFs, and no interest in clicking through a web form 200 times. Ghostscript has been the standard command-line PDF rewriter for decades, it is free software, and it is what many "online compressor" sites run behind the upload button.
It is also easy to misuse. The presets are coarse, some flags are counter-intuitive, and under certain conditions the "compressed" file is larger than the original.
Install
- Debian / Ubuntu:
sudo apt install ghostscript - macOS (Homebrew):
brew install ghostscript - Windows: download the installer from ghostscript.com; the console executable is
gswin64c.exe.
Ghostscript is licensed under the AGPL, with a separate commercial license from Artifex for products that cannot comply with the AGPL. If you are embedding it in something you sell, read the license first.
The basic command
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 -dPDFSETTINGS=/ebook \
-dNOPAUSE -dQUIET -dBATCH -sOutputFile=out.pdf in.pdf
-sDEVICE=pdfwrite selects the PDF output device. -dPDFSETTINGS picks a preset. -dNOPAUSE -dBATCH -dQUIET make it run non-interactively. -dCompatibilityLevel=1.5 or higher enables object streams, which shave a few percent off text-heavy files; 1.4 is the common default in old tutorials and leaves that saving on the table.
What the presets do
| Preset | Color/gray image resolution | Intended use |
|---|---|---|
/screen |
72 dpi | Screen viewing only; small print suffers |
/ebook |
150 dpi | Reading on screen, general use — the usual starting point |
/printer |
300 dpi | Office printing |
/prepress |
300 dpi, color preserved | Print production; large output |
/default |
Roughly /screen behavior with fewer assumptions |
Rarely what you want |
Every preset downsamples images above its resolution, re-encodes color and gray images as JPEG (DCT), and subsets embedded fonts. Presets do not touch vector content or text, which is why a text-only PDF comes out about the same size under every preset.
Taking control of individual settings
Presets are bundles of distiller parameters; you can set the parameters yourself.
Downsampling resolution and method:
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 \
-dDownsampleColorImages=true -dColorImageResolution=120 -dColorImageDownsampleType=/Bicubic \
-dDownsampleGrayImages=true -dGrayImageResolution=120 -dGrayImageDownsampleType=/Bicubic \
-dDownsampleMonoImages=true -dMonoImageResolution=300 \
-dColorImageDownsampleThreshold=1.0 -dGrayImageDownsampleThreshold=1.0 \
-dNOPAUSE -dQUIET -dBATCH -sOutputFile=out.pdf in.pdf
The threshold matters: by default (1.5) an image is only downsampled if it is at least 1.5× the target resolution. Setting it to 1.0 downsamples everything above the target.
Convert to grayscale:
-sColorConversionStrategy=Gray -dProcessColorModel=/DeviceGray
JPEG quality (lower QFactor = higher quality; 0.4 is good, 0.76 is the /screen level, above 1.0 shows artifacts):
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 -dNOPAUSE -dQUIET -dBATCH \
-dAutoFilterColorImages=false -dColorImageFilter=/DCTEncode \
-sOutputFile=out.pdf \
-c "<< /ColorImageDict << /QFactor 0.6 /Blend 1 /HSamples [1 1 1 1] /VSamples [1 1 1 1] >> >> setdistillerparams" \
-f in.pdf
(The -c … -f form passes PostScript before the input file; it is the documented way to set the JPEG dictionary. Keep all -d/-s switches before -c.)
Hitting a target size
Ghostscript has no "make it 9.5 MB" option. You loop:
#!/usr/bin/env bash
# usage: ./fit.sh in.pdf out.pdf 9500000 (target in bytes)
in=$1; out=$2; target=$3
for res in 200 150 120 100 72; do
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 -dNOPAUSE -dQUIET -dBATCH \
-dDownsampleColorImages=true -dColorImageResolution=$res -dColorImageDownsampleThreshold=1.0 \
-dDownsampleGrayImages=true -dGrayImageResolution=$res -dGrayImageDownsampleThreshold=1.0 \
-sOutputFile="$out" "$in"
size=$(stat -c%s "$out" 2>/dev/null || stat -f%z "$out")
echo "res=$res size=$size"
[ "$size" -le "$target" ] && exit 0
done
echo "target not reached at 72 dpi; split or convert to grayscale" >&2; exit 1
This is exactly the loop a target-size tool automates — with the difference that PDF300 estimates the result from sample pages first instead of re-running the whole file at every step, which matters on a 500 MB input.
Why the output is sometimes bigger
- Already-compressed images get re-encoded. A low-quality JPEG decoded and re-encoded at a higher QFactor can grow, and loses quality on the way.
- Line art becomes JPEG. Lossless (Flate) screenshots and diagrams converted to DCT get larger and get halos. Set
-dAutoFilterColorImages=falseand choose/FlateEncodefor those files. - Fonts are re-embedded. If the original had a partial or oddly packed font, the rewritten subset can be larger. With CJK fonts this is noticeable.
- Compatibility level 1.4 disables object streams, undoing the small structural savings a modern exporter had already made.
- Vector-heavy pages (CAD, maps) do not shrink. Nothing to downsample.
What pdfwrite changes besides size
pdfwrite is not an editor; it interprets the document and writes a new one. Expect:
- certificate-based digital signatures to be invalidated;
- form fields, JavaScript and embedded files to be dropped or flattened in many cases;
- bookmarks, links and annotations to usually survive, but verify;
- PDF/A conformance to need re-validation.
Check the output before you replace the original. Rewrite on a copy.
FAQ
Is /screen always the smallest?
For image-heavy files, yes. For text-only files the presets produce nearly identical sizes, because text is not downsampled. If /screen did not help, the weight is in fonts, attachments or vector content.
Can Ghostscript hit an exact file size?
No. It applies the settings you give it and the size is whatever results. To hit a limit, loop downward as above or use a tool built around a target.
Does it keep the OCR text layer of a scanned PDF?
The invisible text layer is ordinary text objects with an invisible render mode, and pdfwrite preserves text. Confirm with a search (Ctrl+F) in the output, because some upstream OCR tools produce layers that do not round-trip cleanly.