Skip to content

Compress on export: offer the setting that actually shrinks a scan - #159

Merged
JamesmarkeyUK merged 1 commit into
mainfrom
claude/pdf-compression-discrepancy-cunpvc
Aug 14, 2026
Merged

Compress on export: offer the setting that actually shrinks a scan#159
JamesmarkeyUK merged 1 commit into
mainfrom
claude/pdf-compression-discrepancy-cunpvc

Conversation

@JamesmarkeyUK

Copy link
Copy Markdown
Collaborator

A 45 MB scan compressed to 1.5 MB in Universal Compress but reported no savings in Universal PDF's Export dialog, which greyed the Compressed tab out entirely.

The engines are not the difference. Universal Compress's src/lib/compress/pdf.ts is a port of this repo's compressPdf, down to the raster presets (balanced 1.5×/0.7, maximum 1.0×/0.45) and the same pdf-lib / pdfjs-dist versions. The entry point was.

What was wrong

ExportModal called compressPdf with no quality argument, so it took the 'light' default — a lossless object-stream re-save. On a scan, whose bulk is image data, that is a few percent at best, so the tab it feeds had nothing to offer and disabled itself. Only the landing page's compress pill ever passed 'balanced'.

Changes

Export gets the quality control. The same Light / Balanced / Maximum choice the Compress dialogs have. 'light' stays the default there and only there: an export is an annotated — often signed — document, and rasterising it costs the text layer. That is a trade worth offering, not worth making on someone's behalf, and the dialog says so plainly when a rasterising level is picked. Compression is now its own pass, so changing the level re-compresses the annotated bytes instead of re-baking every annotation and re-rasterising every redaction to arrive at the identical input again.

The lossless yardstick is no longer always paid for. The never-return-a-bigger-file guarantee was measured by saving the whole source document a second time. On a 45 MB scan that is a second 45 MB buffer on top of the source, pdf.js's copy and the parsed document — the slowest step in the function, and one big files were dying on. It now runs only when the lossless pass could plausibly win: repacking removes structural slack, never image data, so a raster result under half the original is a landslide the yardstick cannot overturn. The guarantee itself is unchanged — a text-only PDF still comes back losslessly rather than as 860 KB of JPEG.

Progress reporting. compressPdf reported nothing at all, so a long rasterising run was indistinguishable from a hung dialog. It now takes an onProgress callback, threaded through the export dialog as a bar and through the landing page and both Compress dialogs as a percentage.

Verification

Driven in a real Chromium against the dev server, not only typechecked.

Engine, on an 8-page image-heavy fixture:

level result
Light 16.72 MB → 16.72 MB (0.0% off)
Balanced 16.72 MB → 1.41 MB (91.6% off)
Maximum 16.72 MB → 0.29 MB (98.3% off)
Balanced, text-only fixture falls back to lossless, no bloat

Progress fires per page, is monotonic, and reaches 1 on both the lossless and rasterising paths.

The Export dialog itself, driven through the UI: opens on Light; at Light the scan shows "no savings" with the Compressed tab disabled (the reported bug); switching to Balanced shows a progress bar and then −92% with the tab live; the text-layer warning is visible.

Unrelated, but worth knowing for anyone doing browser-level testing here: npm run dev throws into the ErrorBoundary without VITE_PLATFORM_SUPABASE_URL / VITE_PLATFORM_SUPABASE_ANON_KEY set. Pre-existing, and no env config is committed here.


Generated by Claude Code

Compressing a 45 MB scan in Universal Compress got it to 1.5 MB; the same
file in Universal PDF's Export dialog reported no savings and greyed the
Compressed tab out. The engines are not the difference — Universal
Compress's PDF path is a port of this one, down to the raster presets —
the entry point was.

Export called compressPdf with no quality, taking the 'light' default: a
lossless object-stream re-save. On a scan, whose bulk is image data, that
is a few percent at best, so the tab it feeds had nothing to offer. Only
the landing page's compress pill ever passed 'balanced'. So give Export
the same three-way control the Compress dialogs have. 'light' stays the
default there and only there: an export is an annotated, often signed
document, and rasterising it costs the text layer — a trade worth
offering, not worth making on someone's behalf. It is spelled out in the
dialog when a rasterising level is picked.

Compression is now its own pass, so changing the level re-compresses the
annotated bytes instead of re-baking every annotation to arrive at the
same input again.

Two things behind that, both of which bit the same big file:

The never-return-a-bigger-file guarantee was measured by saving the whole
source document a second time as a lossless yardstick. On a 45 MB scan
that is a second 45 MB buffer on top of the source, pdf.js's copy and the
parsed document, and it is the slowest step in the function. It is now
only paid for when the lossless pass could plausibly win. Repacking
removes structural slack, never image data, so a raster result under half
the original is a landslide the yardstick cannot overturn. The guarantee
is unchanged; a text-only PDF still comes back losslessly rather than as
860 KB of JPEG.

And compressPdf reported no progress at all, so a long rasterising run
looked like a hung dialog — which is how people learn to kill the tab
mid-export. It now takes an onProgress callback, threaded through the
export dialog as a bar and through the landing page and both Compress
dialogs as a percentage.

Verified in a browser against an 8-page image-heavy fixture: 16.72 MB →
16.72 MB at Light, → 1.41 MB at Balanced, → 0.29 MB at Maximum, with the
text-only fixture still falling back to lossless.
@JamesmarkeyUK
JamesmarkeyUK merged commit 47baee8 into main Aug 14, 2026
1 check passed
@JamesmarkeyUK
JamesmarkeyUK deleted the claude/pdf-compression-discrepancy-cunpvc branch August 14, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants