PDF internals research
Measuring Real PDF Compression Instead of Trusting the Label
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.
What this guide helps you decide
A PDF says its content stream uses FlateDecode. Does that mean the content is actually compressed, and how would I know?
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.
Try it on the anchor product: BeanNest Tools /pdf/.
Filter name versus compression result
When a PDF stream declares FlateDecode, the writer deflated the data before storing it. That statement is about the transformation, not the outcome. Our analyzer therefore does more than read the dictionary: it locates the stream bytes, runs the matching decompressor, and records compressed size against inflated size.
The two measured inflation ratios
For the 40-line document, the content is stored as a 250-byte Deflate stream that inflates to 4582 bytes, a ratio of 18.328x. For the 200-line document, a 715-byte stream inflates to 22923 bytes, a ratio of 32.06x. Both are high because the content is highly repetitive fixed-width prose, which is exactly the case Deflate is strong at.
What a low inflation ratio would mean
If a stream inflated by a factor near one, the writer applied Flate and achieved almost nothing, because the source data had no redundancy left. That happens when the stream is already-compressed image data wrapped in an unnecessary Deflate layer, or when the content is high-entropy binary. In that case the filter name is present and the size benefit is absent.
Why this matters for size expectations
The 84 percent reduction from 5135 to 821 bytes is not a general property of PDF compression. It is the result of a specific 18.328x inflation on a corpus chosen to be repetitive. Reading a filter name and predicting a large reduction from it would be a mistake for any file whose content is not text-like, and our corpus does not claim otherwise.
The reproducible procedure
The analyzer reports the declared filter list, the observed stream length, and one verified inflation sample with its object number. That last field is the difference between a report and a measurement: it names the exact object it inflated and gives both sizes, so you can reopen the file and check that object yourself.
Reading a compression claim critically
A credible compression claim names the input set, states the measured before and after sizes, and identifies the method used to obtain them. A claim that only says a filter was applied, or that reports a percentage without an input set, cannot be checked and should not be repeated. This is the standard we hold our own guides to.
Why a filter list is not a compression measurement
A PDF stream dictionary can declare a chain of filters, and a reader is entitled to apply them in order to recover the original bytes. What the dictionary does not state is how much redundancy those bytes contained before the filter was applied. A file could declare FlateDecode on content that was already incompressible, in which case the stored bytes are marginally smaller than the source and the filter accomplished almost nothing. The only way to distinguish an effective compression from a decorative one is to decompress and compare, which is exactly what the analyzer does when it reports an inflation sample.
The inflation sample names its object
The analyzer does not report an average ratio. It reports one verified sample and identifies the object number it inflated, which in the forty-line file is object 4. That specificity matters, because it makes the claim falsifiable: a reader can locate object 4 in the file, extract the stream, inflate it independently, and check that 250 bytes becomes 4582. An averaged or unattributed ratio cannot be checked this way, so it is weaker evidence even when the number happens to be correct.
Why we stop after one successful inflation
Inflating one stream proves that the file contains genuinely compressible content and that the declared filter is real. Inflating all of them would add cost without adding much evidence, because the corpus files are single content streams by construction. The limitation is worth stating: on a multi-stream document, one sample would not establish that every stream behaves the same way, and this analyzer would need to be extended before it could support that broader claim.
What a failed inflation would mean
If a stream declared FlateDecode but refused to inflate, that would be a different and more serious finding than a poor ratio. It could indicate a truncated stream, an unsupported compression variant, or a stream whose declared length does not match its actual bytes. The analyzer is written to skip such a stream and continue rather than crash, which means a future corpus could include that case deliberately. The guides do not claim to have measured it, because the current corpus does not contain it.
Reading the inflated size as a content fingerprint
The forty-line stream inflates to 4582 bytes and the two-hundred-line stream to 22923 bytes. Five times the lines produce roughly five times the inflated bytes, 4582 times 5 equals 22910 against a measured 22923, a difference of thirteen bytes from the varying line numbers. That near-exact proportionality is a useful check: if an inflated size did not scale with the stated content, it would suggest the stream contained something the description did not mention, such as an embedded resource.
Why compressed size grows more slowly than content
The compressed streams measure 250 and 715 bytes for a five-fold content increase, a factor of only 2.86 rather than 5. Deflate builds a dictionary of repeated substrings as it works, so later occurrences of the same phrase become short references. More repetitive prose therefore compresses more efficiently per byte than less prose does. This is why the two-hundred-line file shows a higher ratio than the forty-line file, and it is a property that would reverse if the content were varied rather than repetitive.
The dictionary-window caveat
Deflate works within a bounded window, so a repeated phrase that appears after a long gap may not be referenced. The corpus lines repeat with a short period, well inside any window, which is why the ratios are high. Real prose repeats less predictably and at longer distances, so a genuine document would likely show a lower ratio than this corpus. The measured numbers describe the corpus, and the guides state that plainly rather than presenting them as typical of documents.
Verifying the sample yourself
To check object 4 in the forty-line file independently, locate the object header, read the stream between the stream and endstream markers, and run a Deflate decompressor over it. The inflated result should be 4582 bytes of ordinary text beginning with the first line of the corpus. The analyzer does exactly this and reports both sizes and the object number; nothing about the procedure depends on the analyzer being correct, which is what makes the claim checkable rather than persuasive.
Why a declared filter can be honest and useless
A writer that applies Deflate to a stream is describing its own behaviour accurately when it declares FlateDecode. The declaration is true, and it still tells a reader nothing about whether the operation helped. This is the distinction the guide rests on: a filter name is a statement about a transformation, while an inflation ratio is a measurement of an outcome. Only the second supports a size expectation, and only the second is reported here.
How to interpret a ratio near one
A stream that inflates by a factor close to one contained almost no redundancy. That happens when the source data is already compressed, when it is high-entropy binary, or when the content is too short for the compressor to build a useful dictionary. In all three cases the filter is present and the benefit is absent, and a size expectation built on the filter name alone would be wrong. Our corpus does not contain such a stream, and the guides say so rather than illustrating a case they have not measured.
Why the object number is part of the evidence
Reporting object four alongside the two sizes makes the claim locatable. A reader can open the file, find that object, extract the stream between the stream and endstream markers, run a decompressor, and check that the result is 4582 bytes. Without the object number the ratio would be an assertion about the file as a whole; with it the ratio is an assertion about a specific, checkable location. That is the difference the evidence contract in this repository calls the boundary between a claim and its proof.
How the measurement would change on a real document
A real document's content stream contains more varied text, font selection operators, and positioning commands. Those are less repetitive than the corpus lines, so a lower inflation ratio is the honest expectation. The guides therefore present the measured ratios as properties of a deliberately repetitive corpus and describe what would move them, rather than presenting them as typical of documents. That separation is what keeps the claim correct.
Why one verified sample is enough here
The corpus files are single content streams by construction, so inflating one stream verifies the only stream that carries text. On a file with several streams the same approach would need to sample more than one, and the analyzer would need to report each sample rather than stopping at the first success. The limitation is stated plainly so that an extension to multi-stream documents would begin from an accurate description of what the current tool does.
First-party evidence and provenance
How we checked this
We generated four single-page PDFs with this repository's own writer, then parsed each file's raw byte structure with this repository'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.
- Method
- We generated four single-page PDFs with this repository's own writer, then parsed each file's raw byte structure with this repository's own analyzer. Both tools are dependency-free and deterministic, so the same commands reproduce the same numbers on any machine.
- 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 this repository owns.
- Captured
- Reviewed by
- BeanNest Studio
Repository evidence artifacts:
tools/research-evidence/pdf/analysis-2026-09-20.jsontools/research-evidence/pdf/README.md