MRC PDF compression — a self-contained CLI that shrinks scanned PDFs while keeping text sharp at full resolution, and fully functional. Output preserves the original text (as a searchable invisible layer), bookmarks, comments, links, and embedded attachments.
Input: Any PDF
Output: MRC-compressed PDF (Mixed Raster Content (MRC) compression) — typically 3–10× smaller on scanned documents, with already-digital (vector) pages passed through unchanged. Measure your own files with ./scripts/benchmark_mrc.sh.
git clone https://github.com/msmarkgu/mrcpdf.git
cd mrcpdf
./bootstrap.sh # installs JDK 21, Gradle, jbig2enc, CJK font (~120 MB)
./run.sh input.pdf -o output.pdfA pure-MRC PDF compressor split out of the TrulyFreeOCR project (OCR removed — no Tesseract, no PaddleOCR, no GPU, no cloud):
- Text stays sharp — MRC splits each page into a downsampled + smoothed background JPEG and a full-resolution JBIG2/CCITT foreground mask, so text keeps its sharp, full-resolution edges (the background is deliberately softened) while the page compresses heavily.
- Per-page judgement — a trained gate scores each page's image features before any compression. Pages MRC would degrade keep a full-resolution fidelity path instead of a lossy rebuild, and pages with no page-filling raster are digital-native and pass through completely unchanged — original fonts, layout, and vector content intact.
- No OCR, no recognition errors — searchable text is re-used from the source PDF and re-embedded, unlike OCR-based MRC pipelines (e.g. Internet Archive's
archive-pdf-tools) that run Tesseract and layer in an OCR'd text layer. - Preserves content — existing text is re-embedded as an invisible searchable layer; bookmarks, annotations, interactive form fields, and embedded attachments are deep-copied into the output.
- Business-friendly license — Apache 2.0, no disclosure obligations. All runtime dependencies are permissively licensed.
- Self-contained — single fat JAR +
bootstrap.sh/bootstrap.bat. JDK, Gradle, and the JBIG2 native binary are all project-local. No sudo, no Python, no system deps. Copy the folder and it runs anywhere. - No cloud / no GPU — CPU-only, fully offline, zero data leaves the machine.
Prerequisites: git, curl, tar, unzip, and bash (or a terminal emulator on Windows).
No admin rights needed — every dependency is downloaded into project subdirectories and stays there. The bootstrap script works on Linux, macOS, and Windows.
-
Clone the repository:
git clone https://github.com/msmarkgu/mrcpdf.git cd mrcpdf -
Run the bootstrap script for your platform:
# Linux / macOS ./bootstrap.sh # Windows (PowerShell or Command Prompt) bootstrap.bat
What gets installed (4 components, all under deps/)
- OpenJDK 21 LTS →
deps/jdk/(Adoptium Temurin primary; falls back tojdk.java.net/archiveif Adoptium is unreachable) - Gradle 8.0.1 →
deps/gradle/ - jbig2enc for JBIG2 foreground compression →
deps/jbig2enc/$OS/(Linux: prebuilt Debian 11 binary using host leptonica; Windows: static MSVC v0.32) - Noto Sans SC CJK font (SIL OFL 1.1) →
deps/fonts/
./run.sh input.pdf -o output.pdfBuild the fat JAR manually (not needed if you use run.sh):
./gradlew build # produces build/mrcpdf.jar (skips tests for speed)
./deps/jdk/bin/java -jar build/mrcpdf.jar input.pdf -o output.pdf| Option | Description |
|---|---|
input.pdf |
Input PDF file (positional). |
-o, --output <file> |
Output PDF path (default output.pdf). |
--dpi <n> |
Rendering DPI for page images (default 300). |
--bg-scale <0.1-1.0> |
Background downscale factor (default 0.33). |
--jpeg-quality <0.1-1.0> |
Background JPEG quality (default 0.50 with MRC on, 0.85 with MRC off). |
--pdfa |
Stamp PDF/A-2b-shaped output: pdfaid XMP metadata and an sRGB OutputIntent. This is not verified conformance — see the limitations below. |
--fg-color |
Re-render the foreground in the source's true text colors via a soft-mask color layer. Off by default. Soft masks are permitted by PDF/A-2b, so this does not conflict with --pdfa — though see the PDF/A caveat below before relying on either. |
--mrc / --no-mrc |
Enable/disable MRC compression (default on). With MRC off, rebuilt pages are a single high-quality JPEG + invisible text layer (no mask) — faster, for downstream pipelines that don't need MRC layers. |
--pass-through / --no-pass-through |
Pass digital-native pages (no page-filling raster) through unchanged (default on). Applies with MRC on or off, and under --pdfa. Disable to force every page through render/re-encode. |
--password <pw> |
Password for an encrypted input PDF (prompted if given without a value). |
--ignore-encryption |
Proceed on a permission-restricted (owner-password) PDF. mrcpdf output is never re-encrypted, so those restrictions cannot be carried over — without this flag such a file is refused. |
--max-pages <n> |
Refuse documents longer than n pages (0 = no limit, the default). Memory use grows with the document. |
--max-dpi <n> |
Refuse a render DPI above n (0 = no limit, the default). Raster area grows with the square of DPI, so a high value is the usual cause of an out-of-memory failure. |
--keep-smaller-only |
If MRC would make the output larger than the input, keep the original bytes instead. MRC adds a background layer per page, so an already-tight bilevel scan can come out bigger; without this flag the run warns and keeps the (larger) MRC result. |
--threads <n> |
Worker threads for page prep (default: available processors). |
--settings <file> |
Path to settings.jsonc (default ./settings.jsonc). |
| Code | Meaning |
|---|---|
0 |
Completed. Warnings may still have been printed — the output is valid. |
1 |
The run failed: unreadable or corrupt input, wrong password, missing file, or an internal error. |
2 |
Refused before processing: the input was permission-restricted, or it exceeded --max-pages / --max-dpi. Nothing was rendered and no output file was written. |
The jbig2enc binary and CJK font bundled in deps/ are used automatically; all defaults are configurable via settings.jsonc, and CLI flags take precedence.
Rendering memory scales with DPI²: going from 300 to 600 DPI quadruples the pixel count, so each page needs ~4× the memory. At 600 DPI a US Letter page is ≈ 5100×6600 px and needs roughly 1 GB per page in flight (the pipeline keeps about --threads + 2 pages in memory at once).
Recommendations for high DPI:
| Setting | Linux / macOS | Windows |
|---|---|---|
| 300 DPI (default) | ./run.sh input.pdf -o out.pdf |
run.bat input.pdf -o out.pdf (2 GB default) |
| 600 DPI | MRCPDF_HEAP=8g ./run.sh --dpi 600 --threads 2 input.pdf -o out.pdf |
set MRCPDF_HEAP=8g then run.bat --dpi 600 --threads 2 input.pdf -o out.pdf |
Rule of thumb: allow ~1–1.5 GB per page in flight and cap --threads (or pipeline.maxThreads in settings.jsonc) so (threads + 2) × per-page fits your RAM. The default heap is 2 GB (MRCPDF_HEAP) on Linux/mac, and 2 GB on Windows after the run.bat change.
| Element | Status |
|---|---|
| Document info (title, author, subject, keywords, creator, producer) | ✅ Preserved |
| Bookmarks / outlines | ✅ Preserved (deep copy) |
| Annotations (comments, underlines, …) | ✅ Preserved (deep copy); geometry and link destinations re-mapped to the output pages |
| Embedded attachments | ✅ Preserved (deep copy) |
| XMP metadata | ✅ Preserved |
| Page size / orientation | ✅ Preserved (per crop box) |
| Digital-native pages (no page-filling raster) | ✅ Passed through unchanged — original fonts, layout, and vector content intact |
--pdfadoes not produce a conformant PDF/A-2b file. It stamps the shape —pdfaid:part 2 / conformance BXMP and an sRGB OutputIntent — and that is all. Two known hard failures against the specification: the output header and catalog/Versionare 1.6, where PDF/A-2 requires 1.7, and the invisible text layer uses a non-embedded Standard 14 font (setpdf.fontPathto embed one). Conformance is never validated, here or in CI. If PDF/A conformance matters, run the output through veraPDF and do not rely on this flag.- Encrypted input loses its encryption. mrcpdf cannot re-encrypt, so an encrypted source produces an unencrypted output that carries none of the original permissions. A permission-restricted (owner-password) file is therefore refused by default;
--ignore-encryptionproceeds and still warns. A user-password file is readable with--password. There is no output encryption option. - Rebuilt pages re-render the invisible text layer with a single font (the Standard 14 font, or a TTF/OTF configured via
pdf.fontPathin settings.jsonc). Original font family and size styling are not reproduced in the hidden layer — the visual layer (background + mask) is unaffected, and digital-native pages passed through unchanged keep their source fonts entirely. Text color is preserved in the visual layer when--fg-coloris enabled. - Passed-through pages under
--pdfaare carried over as they are — their embedded fonts, and any other source-side PDF/A issues, are never inspected. - Solid oversized elements — a component wider or taller than
segmenter.maxComponentDim(default 10% of the page's long side) or larger thansegmenter.maxComponentArea(default 0.5% of page area) is dropped from the foreground mask and softened with the background. That is deliberate for photos, logos and halftones, but text merged with such an element — large display type, a dark banner behind a heading — is therefore rebuilt from the downsampled background, not the full-resolution mask. Thin line-work is exempt: rules, underlines, column dividers and hollow table borders stay in the sharp mask (seeImageSegmenter). - Named destinations (the
/Names/Deststree) are not preserved. The tree is dropped rather than copied, because its page references stop resolving once pages are rebuilt, and a broken destination reference fails silently at click time. The run reports how many were dropped. Bookmarks and annotation links that address a page directly are re-mapped to the rebuilt pages, and their geometry is transformed through the page's rotation and crop-box offset.
./gradlew build # compile + build build/mrcpdf.jar (skips tests)
./gradlew test # run tests only
./gradlew generateTestPdfs # regenerate the tests/*.pdf fixtures- Source:
src/main/java/com/mrcpdf/ - Tests:
src/test/java/com/mrcpdf/(JUnit 5; requires Java 21) - How MRC compression works, and when not to use it:
docs/mrc-compression-explained.md,docs/when-not-to-mrc.md - Quality-gate training / synthetic-corpus labeling:
docs/synthetic-labels.md; dev-only corpus tooling lives insrc/main/java/com/mrcpdf/tools/ - Retraining the quality gate on your own scans:
docs/quality-gate-training.md - Test runs (sample CLI output, compression results):
docs/test-runs.md
MRCPDF_BENCH_DIR=/path/to/pdfs ./scripts/benchmark_mrc.sh # all PDFs in a directory
./scripts/benchmark_mrc.sh /path/to/one.pdf # a single fileThe input corpus is not part of the repository — point MRCPDF_BENCH_DIR at
your own PDFs, or pass a path as the argument. The script records input size,
output size and the ratio measured against the input file, so its numbers are
comparable with the headline claim above. It rebuilds the JAR first if any source
is newer, so it cannot silently benchmark a stale build.
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.md for third-party attribution.
Runtime dependencies: Apache PDFBox (Apache 2.0), picocli (Apache 2.0), jbig2enc (Apache 2.0), jai-imageio-jpeg2000, OpenJDK 21 (GPLv2 + Classpath Exception), Noto Sans SC font (SIL OFL 1.1).