PDF internals research
Measuring Real PDF Compression Instead of Trusting the Label
A PDF says its content stream uses FlateDecode. Does that mean the content is actually compressed, and how would I know?
A PDF stream carries a filter name in its dictionary, and it is easy to treat that name as proof that compression happened. It is not proof. A filter name says what transformation was applied to the bytes, not how much redundancy those bytes contained. The only honest test is to inflate the stream and compare the inflated size to the compressed size. We did that on our own corpus and recorded both results.
Reviewed 2026-09-20 by BeanNest Studio
A filter name describes a transformation, not its effectiveness
FlateDecode in a stream dictionary says that Deflate was applied to the stored bytes. It does not state how much smaller those bytes became. The analyzer therefore attempts a real inflation and records compressed size, inflated size, object number, and ratio for a successful sample.
The corpus contains two measured inflation ratios
In text-40-flate.pdf, object 4 stores 250 compressed bytes that inflate to 4582 bytes, a ratio of 18.328x. In text-200-flate.pdf, the 715-byte stream inflates to 22923 bytes, a ratio of 32.06x. Those are direct measurements on the generated repetitive-text fixtures.
The raw and compressed pair gives a separate file-size result
The forty-line raw fixture is 5135 bytes and its compressed counterpart is 821 bytes, about an 84 percent reduction for that pair. That file-size comparison and the 18.328x stream inflation ratio answer related but different questions, so neither should be presented as a universal compression percentage.
Object 4 makes the inflation sample locatable
The analysis artifact identifies object 4 for the successful samples. A reader can locate that stream in the generated file, extract the stored bytes, run a Deflate decompressor, and compare the result with the recorded inflated byte count. The object number makes the measurement checkable rather than an unattributed ratio.
One sample is sufficient only for these single-stream fixtures
Each generated corpus file has one stream, so the successful sample covers the only stream in that fixture. A multi-stream document would require more samples before making a statement about all of its streams; the current analyzer and corpus do not provide that broader evidence.
Untested compression cases stay unmeasured
The corpus does not contain a near-one inflation example, an image-heavy document, encrypted content, or a deliberately corrupt Flate stream. The guide therefore does not assign measured ratios to those cases. They require separate fixtures if they become part of a future claim.
Reading the two ratios side by side
The forty-line stream inflates from 250 to 4582 bytes, 18.328 times. The two-hundred-line stream inflates from 715 to 22923 bytes, 32.06 times. The text is five times longer (22923 is 5.003 times 4582) while the compressed stream is only 2.86 times larger (715 over 250), because longer generated text repeats more. That is a property of these repetitive fixtures; real documents repeat less, so their ratios will differ.
A one-byte difference worth noticing
The raw fixture stores 4583 bytes of stream data while the compressed fixtures inflate to 4582 bytes. The analysis does not explain the one byte. It is a reminder to compare like with like, and to say exactly which bytes were counted, before concluding that compression changed nothing about the content.
How to test a FlateDecode claim on your own file
Find the stream in the file, take the exact bytes after the line break that follows the word stream and before endstream, decompress them with a zlib-compatible tool, and compare the compressed length with the inflated length. If the inflated result is not much longer, the filter name was present but the data had little to remove. This checks one stream; a document with many streams needs the check repeated.
A reproducible compression claim needs the input and method
For this corpus, the evidence names the generated file, the stream object, compressed bytes, inflated bytes, and ratio. Keeping those fields together prevents a filter label or isolated percentage from being repeated without the fixture that produced it.
Scope of this guide
This research measures whether a declared compression filter corresponds to real reducible data. It does not evaluate the quality of any compressor and does not claim a compression guarantee.
Method and evidence
We generated four single-page PDFs with BeanNest's own writer, then parsed each file's raw byte structure with BeanNest's own analyzer. Both tools are dependency-free and deterministic, so the same commands reproduce the same numbers on any machine. For this guide we inflated a real FlateDecode stream from each compressed corpus file and compared the compressed size to the inflated size, so the compression claim rests on a decompressed result rather than on a filter name.
Environment: Node.js on a desktop workstation. No PDF library, no network access, and no third-party file was used. The corpus is produced from text BeanNest owns.
Captured: 2026-09-20. Re-review trigger: Re-review when the analyzer's inflation logic or the corpus content changes, because both ratios are derived from them.
Evidence files: kept in BeanNest's source repository, not published on this site.
docs/research-evidence/pdf/analysis-2026-09-20.jsondocs/research-evidence/pdf/README.md
What this guide does not claim
- claims that a declared filter guarantees a size reduction
- compression percentages for files outside our regenerable corpus
- quality comparisons of compression methods
- any speed or throughput statement
