跪拜 Guibai
← All articles
Frontend · Backend · GitHub

Pure-Go PDF Compression Cuts Out Ghostscript and Re-Encodes Images One by One

By 沙蒿同学 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Desktop tools can now ship PDF compression as a self-contained, zero-CGO binary without bundling Ghostscript or taking on AGPL obligations; the cost is a conservative subset that only touches images it is sure about, leaving unsupported encodings byte-for-byte.

Summary

Image Workshop 0.2.0 ships PDF compression built entirely in Go. pdfcpu handles parsing and structure optimization, while a custom image layer re-encodes embedded JPEGs and downsamples scanned pages. Ghostscript's /screen, /ebook, and /printer presets solve the same problem but come as an external AGPL process that must be packaged and maintained on macOS, Windows, and Kylin; the new path keeps everything inside the installer and never uploads documents.

The recompression step only touches images that match a narrow predicate: a single DCTDecode filter, DeviceRGB or DeviceGray, 8 bits per component, no Decode array. Each one is decoded, re-encoded at JPEG quality 75, and written back only if the output is smaller. Full-page scans are detected from structural signals rather than content-stream parsing, and anything above 150 DPI is downsampled to 150 DPI; text and vector content remain byte-identical, while encrypted and signed files are rejected. A spike on a 2400x2400 DCT image cut the whole file by 42.9% with a rendered pixel difference of 0.41/765. The implementation is in the open-source file-processor repository, 363 lines including comments, with installers for macOS, Windows, and Kylin amd64/arm64.

Takeaways
— Image Workshop 0.2.0 compresses PDFs in pure Go: pdfcpu parses and writes the file, and custom Go code re-encodes images.
— Only single-filter DCTDecode images with DeviceRGB/DeviceGray, 8-bit depth, and no Decode array are recompressed; everything else keeps its original bytes and is counted as unsupported.
— JPEG re-encoding is fixed at quality 75 and replaces the original only when the encoded result is smaller, so a second pass on an already-compressed PDF changes nothing.
— Full-page scans are downsampled to 150 DPI only when computed DPI exceeds 150; detection uses page geometry and aspect ratio, not content-stream parsing.
— A 2400x2400 DCT image re-encoded at quality 75 reduced the whole file by 42.9%, passed pdfcpu validate, and showed an average pixel difference of 0.41/765.
— Encrypted and signed PDFs are rejected; text and vector bytes stay untouched; the caps are 512 MB per file and 100 MP per image.
— Output reports five counts: total, recompressed, downsampled, unsupported, and already-efficient.
Conclusions

Ghostscript's external-process cost is bigger than its command-line simplicity suggests; for a distributed desktop tool, AGPL assessment and per-platform packaging can outweigh the effort of reimplementing image compression.

Preset-based compression treats every image the same, which is why online tools often make files worse; comparing each re-encoded image against the original turns compression into a monotonic operation.

Avoiding the content stream is a deliberate trade: structural signals identify scanned pages safely, but the 15% aspect-ratio rule means some compressible pages are left alone, accepting less gain for zero layout risk.

Fixing JPEG quality at 75 and DPI at 150 is a product decision as much as a technical one: exposing more knobs would offload engineering judgment onto users.

Concepts & terms
Ghostscript
A PostScript and PDF interpreter often used to recompress PDFs; its /screen, /ebook, and /printer presets downsample images to fixed DPI levels with matching JPEG quality. It is AGPL-licensed and normally runs as an external process.
pdfcpu
A pure-Go PDF library used here for parsing, xref traversal, and lossless structural optimization. It has no image recompression, so the tool implements that layer itself.
xref table
The PDF cross-reference table lists every indirect object and its location in the file; compressing it into an xref stream is one of the lossless optimizations applied after image re-encoding.
DCTDecode
The PDF filter name for JPEG image streams. A sole DCTDecode filter means the stream can be decoded with Go's standard JPEG library and re-encoded at a lower quality.
Image XObject
A stream object in PDF that carries image pixel data and is referenced from page content; the tool finds these objects by traversing the xref table instead of parsing content streams.
From the discussion
Featured comments
AI向善

I ran it through Ghostscript following the article's approach; QPS went from 1200 to 1900, at the cost of an extra 300 MB of memory. How would you weigh that trade-off?

沙蒿同学

The reasons the article gives for excluding Ghostscript, such as AGPL, an external process, packaging a separate build for each of the three platforms, and a self-contained installer, none of them are touched by a QPS number. In a single-user scenario, the fair comparison axis is per-file latency and peak RSS, not server QPS. On that axis, an in-process pure Go implementation naturally avoids process startup and a resident interpreter, which is a structural advantage. QPS is a server-side benchmark, and using it to evaluate a desktop tool is grabbing the wrong yardstick.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗