I ran one signed PDF through five compressors. Every single output failed signature validation. The part that bothers me most is not the broken signatures. It is the two tools that deleted the signature outright, because those files look perfectly normal afterwards.
The test file
I didn't want to use a real contract, so I built a fake one: a one-page agreement that literally says "fictional, for testing only", with a 1800×1200 synthetic image on it. I made a throwaway root CA and a fictional signer with openssl, then signed the file with pyHanko. The signed file is 931,980 bytes.
To check signatures I used two validators, poppler's pdfsig and pyHanko's own validation, both configured to trust only my test root. On the original file, pdfsig says "Signature is Valid" and pyHanko reports INTACT:TRUSTED,UNTOUCHED.
The signature dictionary has /ByteRange [0 923749 931275 705]. That is two byte ranges: the first 923,749 bytes, and 705 bytes starting at offset 931,275. The gap between them holds the signature value. Together the two ranges reach the very last byte of the file. Change one byte inside them, or shift them by inserting anything earlier, and the hash no longer matches.
Five tools, two ways to fail
I ran the file through qpdf (plain rewrite and object-stream packing), Ghostscript with /ebook, the ebook preset of ImgIng's PDF compressor, iLovePDF's recommended level and PDF24's default. Every output had a single %%EOF (the original has two) and different leading bytes. As far as I can tell, all of them rewrite the whole file instead of appending to it.
| Tool | Output bytes | Signature dict | pdfsig |
|---|---|---|---|
| qpdf rewrite | 930,748 | kept | Illegal values in ByteRange array |
| qpdf object streams | 930,178 | kept | Illegal values in ByteRange array |
| Ghostscript /ebook | 39,876 | gone | does not contain any signatures |
| ImgIng ebook | 64,903 | kept | Illegal values in ByteRange array |
| iLovePDF recommended | 37,513 | gone | does not contain any signatures |
| PDF24 default | 50,877 | kept | Illegal values in ByteRange array |
pyHanko agreed every time. The four files that kept the dictionary were judged INVALID. For the qpdf rewrite it spelled it out: "The signature does not cover the entire file."
Failure mode one: a broken signature. qpdf, ImgIng and PDF24 copied the old signature dictionary into the new file, ByteRange numbers and all. The bytes underneath had changed, so the numbers now point at the wrong places. The ImgIng output still claims a range ending at byte 931,980 while the file is only 64,903 bytes long. Anyone who validates this file gets an error.
Failure mode two: no signature at all. Ghostscript and iLovePDF dropped the signature field entirely. The iLovePDF output doesn't even have SigFlags any more. Nothing errors when you open it. If the visible "signed by" stamp is part of the page drawing, a reader may still see it and assume the document is signed. I'd call this the worse outcome, because the person receiving it has no reason to check.
Local or uploaded made no difference
I used ImgIng (https://imging.ai/) for the browser-based run, in Chromium 149 on an M4 Mac. That makes three tools on my machine (qpdf, Ghostscript and ImgIng, which does the compression in the browser tab) and two that upload the file to their own servers first (iLovePDF and PDF24, whose pages say files are deleted after up to 2 hours and after "a short period of time"). For a contract that is real, where the file goes is worth thinking about on its own. For the signature it changed nothing. Local or remote, every tool rewrote the bytes. I don't blame them much. A compressor's job is to change bytes, and a signature's job is to detect changed bytes.
One gap in my testing: I did not open the outputs in Acrobat or Foxit, so I can't tell you what their signature panels show. Everything above is from pdfsig and pyHanko only.
What I do now
If you receive a PDF that is supposed to be signed, run pdfsig file.pdf before trusting it. "does not contain any signatures" on a document you expected to be signed is the red flag that a compressor can leave behind.
If you are the one compressing, do it before signing, and never after. Appending changes at the end (incremental updates) does keep the original signature bytes intact; I tested a second signature appended that way, and pyHanko accepted it. But appending can only make a file bigger, so it doesn't help with compression at all.
