Bhinish Dhiman· updated 1 October 2026
What lossless PDF compression actually saves, measured on eleven files
Lossless PDF compression — repacking the file's objects without touching text or images — saved 0.8% on a Chromium-printed text document, 3.9% on a jsPDF file (85.8% once raw streams were deflated), 6.5% on a macOS Quartz file and under 0.05% on anything image-heavy, measured across eleven PDFs. The only file that grew did so by 7 bytes. Content from deleted pages survived the repack, so the tool now rebuilds such files.
Every PDF compressor offers a lossless option, and almost none of them say what it is worth. We measured ours.
Two terms, defined
Structural compression is rewriting how a PDF stores its objects — the cross-reference table, the packing of small objects into streams, the compression filter on each stream — without altering a single glyph or pixel. It is the only kind of compression that can honestly be called lossless.
An object stream is a PDF 1.5 feature that packs many small objects (page dictionaries, font descriptors, annotations) into one compressed stream instead of writing each uncompressed. It is where nearly all of a structural saving comes from, which is why the producer of the file decides the result before the compressor runs.
The measurement
We generated eleven PDFs on 1 October 2026: the same twenty pages of text printed by Chromium 153 (producer Skia/PDF m153), by macOS Quartz through cupsfilter, by jsPDF 4.2.1 and by pdf-lib with and without object streams; ten 1600-pixel photographs embedded by Chromium and by pdf-lib; five scanned-style pages, each a full-page greyscale JPEG at quality 55; a one-line, one-page document; a document that had five of its ten pages deleted; and an AES-256-encrypted file. Each went through compressStructural in src/lib/pdf/ops.ts — the exact code behind the lossless setting of our compressor — under Node 24 with @cantoo/pdf-lib 2.11.1.
| File | Producer | Before | After | Saving |
|---|---|---|---|---|
| 20 pages of text | Chromium print | 82,109 B | 81,426 B | 0.8% |
| 20 pages of text | macOS Quartz | 48,441 B | 45,278 B | 6.5% |
| 20 pages of text | jsPDF 4.2.1 | 72,814 B | 69,977 B | 3.9% |
| 20 pages of text | pdf-lib, no object streams | 34,041 B | 33,168 B | 2.6% |
| 20 pages of text | pdf-lib, object streams | 33,166 B | 33,173 B | −0.02% |
| 10 photographs | Chromium print | 1,528,699 B | 1,528,112 B | 0.04% |
| 10 photographs | pdf-lib | 1,509,614 B | 1,508,973 B | 0.04% |
| 5 scanned pages | pdf-lib | 1,816,323 B | 1,815,924 B | 0.02% |
| 1 line, 1 page | pdf-lib | 1,307 B | 1,059 B | 19.0% |
| 10 pages, 5 deleted | pdf-lib | 13,900 B | 13,441 B | 3.3% |
| Encrypted | pdf-lib | 2,940 B | refused | — |
Reading the table
None of the four producers wrote object streams, and all four wrote a classic cross-reference table. The pass does the same thing to each: it replaces the table with a cross-reference stream and packs the small objects — page dictionaries, font descriptors, resource lists — into object streams. So the saving is bounded by the bytes that sat outside streams to begin with. In the Chromium file that was 8,374 of 82,109 bytes, brought to 6,896; in the Quartz file, 8,234 of 48,441, brought to 4,224. Quartz saves the most because it wrote 71 objects for twenty pages where Chromium wrote 52, with the most verbose table of the four. The pdf-lib file that already had object streams came back 7 bytes larger — that is the whole of the “your file may get bigger” case in this sample, and it is the difference between two equally good packings of the same objects.
The jsPDF file shows the method’s limit most clearly. jsPDF 4.2.1 writes its content streams uncompressed by default: 65,255 of the file’s 72,814 bytes are raw drawing instructions, and zlib takes them to 19,053. That is a lossless 63.5% sitting in the file. The structural pass did not re-encode an existing stream, so it found 3.9% and left all 65,255 raw bytes in place. A pass that deflated raw streams would be just as lossless — Flate returns the exact bytes on decode — and on that one producer it would matter sixteen times more than repacking.
So the lossless setting now does it. Any stream written without a filter is deflated and kept only if the result is smaller; streams that already carry a filter, such as JPEG images, are left alone, and so is XMP metadata, which the specification asks to stay readable by tools that do not parse PDF. Re-measured on a comparable twenty-page jsPDF document: 132,980 bytes became 18,855, a saving of 85.8%, with every page’s text still extractable from the result.
On anything image-heavy the saving is a rounding error, because the bytes are in the image streams and a structural pass does not open them. Ten photographs at 1.5 MB gave back 587 bytes. Five scans at 1.8 MB gave back 399 bytes. The 19% on the one-line document is the same few hundred bytes of table overhead, on a file so small that overhead is most of it.
The deleted pages were still there
The edited document is the finding that changed how we describe this setting. We built ten pages, each carrying a unique marker in its text, deleted five, and saved. The saved file was 13,900 bytes and still held all ten pages’ content streams — the deleted pages are unreferenced, but pdf-lib writes every object it parsed. The structural pass repacked that file to 13,441 bytes with all ten markers still inside, and the object count went from 19 to 21.
Copying the five live pages into a fresh document instead produced 7,518 bytes — 46% smaller than the “compressed” result — with markers 0 to 4 only. Repacking preserves orphans; rebuilding drops them.
That matters for size, and it matters more for privacy: a page you deleted before sending was still extractable from a file that had been through our lossless compressor. Worse, the setting’s own description said it “drops unreferenced ones”. It did not.
What we changed
The lossless setting now copies the live pages into a fresh file whenever it is safe to, and keeps the result only if it is smaller. It is not always safe: interactive form fields and the bookmark outline belong to the document rather than to any page, so a rebuild would silently delete them. A file carrying either is only repacked, and the result says so rather than implying the orphans are gone.
Re-measured after the change on a comparable ten-page file with five pages removed: a plain repack gave 12,578 bytes with all ten markers inside; the new pass gave 9,211 bytes with markers 0 to 4 only — 27% smaller, and the deleted pages genuinely gone. A test in the repository now builds exactly this file and fails if a deleted page’s content survives the pass.
Against the destructive method
The other two settings on our tool render every page to a JPEG at 150 or 110 dpi and re-embed it. That shrinks scans by a large factor and turns a text document into pictures of text, with no selection, no search and nothing for a screen reader. The generic services get a large saving without that cost by running Ghostscript on a server to downsample the images while leaving the text layer alone; Ghostscript is AGPL-3.0, and shipping it to a browser would put this whole site under that licence.
So the honest offer is what the table shows: a structural pass that will find a few percent on a file from an older producer and nothing on a modern one, or a rasterising pass that costs the text layer. If the document has form fields or bookmarks and you want deleted content gone, the lossless pass will not do it for you, and it now says so — extract the pages you are keeping into a new file, which is what Split PDF does.