Bhinish Dhiman
Why merging an encrypted PDF can give you blank pages, measured
Merging a password-protected PDF through upstream pdf-lib with ignoreEncryption produced a four-page file in which all three protected pages were blank and only 49 of 373 characters survived, because the page content was copied while still encrypted. The same merge through @cantoo/pdf-lib with the password gave four pages, 373 characters and nothing blank.
The symptom is well known: you merge a bank statement into a cover letter, no error appears, and the statement’s pages come out white. We reproduced it in code and counted what was lost.
What PDF encryption actually encrypts
PDF encryption applies a cipher — AES-256 under ISO 32000-2 — to the file’s strings and streams, and only to those. The object structure stays in the clear: a reader can see that there are three pages, what size they are and which content stream each one uses, without a password. What it cannot do without the password is turn those content streams from ciphertext into drawing instructions.
That split is the whole mechanism of the blank-page failure. A library can walk the structure, copy the pages, and never notice that the bytes it copied are still encrypted.
The measurement
We built a three-page statement with 324 characters of known text and encrypted it with AES-256 and a user password, using the same encrypt() call as our Protect PDF tool. We then merged it after a one-page, 49-character cover letter by two routes: upstream pdf-lib 1.17.1 loaded with ignoreEncryption: true, and @cantoo/pdf-lib 2.11.1 loaded with the password — which is what src/lib/pdf/ops.ts does. Each result was opened in pdf.js 6.3.289, its text extracted, and every page rendered at half scale and scanned for any non-white pixel. Run under Node 24 on 1 October 2026.
| Route | Pages out | Characters extracted | Blank pages | Encrypt dictionary in output |
|---|---|---|---|---|
Upstream pdf-lib, ignoreEncryption: true |
4 | 49 | 3 | No |
| @cantoo/pdf-lib, password supplied | 4 | 373 | 0 | No |
Both routes report success. Both produce a file that opens without a password. One of them has thrown the statement away.
Why the pages are blank rather than garbled
Upstream pdf-lib refuses an encrypted file by default, with a message that names the escape hatch: “Input document to PDFDocument.load is encrypted. You can use PDFDocument.load(..., { ignoreEncryption: true }).” Tools that want to appear to support encrypted input take that hatch.
With the flag set, the library parses the structure it can read and copies three page objects, each pointing at a content stream of AES ciphertext. The new document has no encryption dictionary, so a reader treats those streams as plain Flate-compressed data, inflates them, gets noise, and stops drawing. pdf.js logs Trying to parse invalid object and Invalid object ref: 14 0 R and moves on. The 49 characters that survived are the cover letter, which was never encrypted.
The in-place variant is worse
We also loaded the encrypted file with ignoreEncryption, changed one metadata field, and saved it without copying anything. That output could not be opened at all: pdf.js reported Incorrect Password when given the correct password. The save had rewritten the file’s structure around ciphertext whose keys no longer match, which is a document nobody can recover, not a document with a blank page.
The fork does the decryption
@cantoo/pdf-lib is a fork of pdf-lib that adds a password option to load() and an encrypt() method. Given the password it decrypts every stream as it parses, so copyPages moves plaintext, and the merged output holds all 373 characters with every page rendering. Without the password it throws exactly as upstream does — it does not guess, and there is no route that removes a password you do not know.
The check to run on any merge tool
Merge a protected file and extract the text afterwards, with our PDF to text tool or anything else. If the characters from the protected document are absent, the tool copied ciphertext. A blank page is not a rendering glitch to be fixed by another viewer; the content did not survive the merge.