Skip to content

fix(compress): stop pass B from destroying content - #207

Merged
nelsonduarte merged 2 commits into
mainfrom
fix/compress-preserve-pass-b
Oct 16, 2026
Merged

nelsonduarte merged 2 commits into
mainfrom
fix/compress-preserve-pass-b

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

PyMuPDF 1.28.2 made Document.scrub() remove invisible text again, and pass B of Compress called it with its defaults. The v1.15.0 and v1.15.1 binaries ship 1.28.2, so they deleted the OCR layer when compressing.

With its defaults scrub() also removed links, form values (reset to their defaults), attachments, JavaScript, replies and popups, metadata and XMP, thumbnails, and visible text overlapping the invisible text. It also applied pending redactions, and it could raise halfway through, leaving a half-cleaned document that was then saved.

Pass B no longer calls scrub() and saves with clean=False. clean=True duplicated glyphs on the pymupdf#5054 construction ((f)Tj 0 0 TD (faeces)Tj became "ffaeces") on PyMuPDF 1.28.0 and 1.28.2.

The value of progress.compress.passB_setup changed in all 8 languages.

Files: app/utils.py, app/translations.json, tests/test_compress_preserves_content.py (new, 27 cases).

Measurements

What Old code This PR
OCR words kept, PDFApps' own OCR output, 18 level and Ghostscript combinations (QA) 0 of 140 in 17 of 18 combinations 140 of 140 in 18 of 18, still invisible
New test cases failing 25 of 27 0 of 27
Output size change, range measured n/a -5.5% to +53%
Output size change, PDFs with OCR n/a +0.3% to +0.5%

Known limitation

With Ghostscript installed, pass A can still produce the winning output, and that output can lose form fields, attachments, links, OCGs, the structure tree and pending redactions. This is left for later increments.

Size

Output can be larger because metadata is now preserved. Measured range: -5.5% to +53%. The +53% was a PDF with a 14 KB JPEG thumbnail embedded in its XMP. On PDFs with OCR, +0.3% to +0.5%.

Verification

  • Full suite: Windows 1013 passed, 2 skipped; Linux 1011 passed, 4 skipped.
  • Mutations: the old code fails 25 tests; scrub() with its defaults fails 25; clean=True alone fails 2.
  • Restoring each scrub() flag on its own makes the test named after that content fail.
  • In CI, test_real_tesseract_ocr_layer_survives skips because Tesseract is not installed on the runner.

🤖 Generated with Claude Code

PyMuPDF 1.28.2 made Document.scrub() remove invisible text again, and
pass B of Compress called it with its defaults. The v1.15.0 and v1.15.1
binaries ship 1.28.2, so they deleted the OCR layer when compressing.
Measured by QA on PDFApps' own OCR output: in 17 of 18 level and
Ghostscript combinations the old code left 0 of 140 words; with this
change all 18 of 18 keep the 140 words, still invisible.

With its defaults scrub() also removed links, form values (reset to
their defaults), attachments, JavaScript, replies and popups, metadata
and XMP, thumbnails, and visible text overlapping the invisible text.
It also applied pending redactions, and it could raise halfway through,
leaving a half-cleaned document that was then saved.

Pass B no longer calls scrub() and saves with clean=False. clean=True
duplicated glyphs on the pymupdf#5054 construction ("(f)Tj 0 0 TD
(faeces)Tj" became "ffaeces") on PyMuPDF 1.28.0 and 1.28.2.

The value of progress.compress.passB_setup changed in all 8 languages.

Tests: 27 new cases in tests/test_compress_preserves_content.py; 25 of
them fail against the old code.

Known limitation: with Ghostscript installed, pass A can still produce
the winning output, and that output can lose form fields, attachments,
links, OCGs, the structure tree and pending redactions. This is left
for later increments.

Size: output can be larger because metadata is now preserved. Measured
range: -5.5% to +53%. The +53% was a PDF with a 14 KB JPEG thumbnail
embedded in its XMP. On PDFs with OCR, +0.3% to +0.5%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread tests/test_compress_preserves_content.py Fixed
…s bound

CodeQL (py/uninitialized-local-variable) flagged `langs` in
_tesseract_or_skip as possibly used before assignment: it is bound
inside the try, and the except only calls pytest.skip, which CodeQL
does not know always raises. The alert is a false positive.

Moving the "eng" check into an else on the try makes the flow explicit:
it runs only when get_languages returned. Behaviour is unchanged: with
Tesseract installed the test still runs and passes, and the three skip
paths (no executable, get_languages failing, no 'eng' data) still skip
with the same messages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Deploying pdfapps with  Cloudflare Pages  Cloudflare Pages

Latest commit: ae04edd
Status: ✅  Deploy successful!
Preview URL: https://c7816f65.pdfapps.pages.dev
Branch Preview URL: https://fix-compress-preserve-pass-b.pdfapps.pages.dev

View logs

@nelsonduarte
nelsonduarte merged commit 47b5407 into main Oct 16, 2026
5 checks passed
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