Every other reader in this pipeline only has to see the document. drawAnswers has to draw on
it, and pdf-lib is both the reader and the writer there — so where pdf.js rescued detection and
extraction by decrypting what pdf-lib could not, nothing rescues the fill step: pdf.js has no
writer, and a document pdf-lib cannot parse is a document it cannot stamp. On the real payer
form this took the shape of getPages() throwing a raw
Expected instance of PDFDict, but got instance of undefined, because the page tree lived
inside a ciphered object stream that pdf-lib skipped.
So the document is decrypted to a plaintext copy first, and the rest of the pipeline runs on
that copy exactly as it does on an ordinary template.
Why this is decryption we are entitled to do
Only the EMPTY user password is ever tried. That is the permissions-only case: the document
opens in any reader with no prompt, and the encryption carries nothing but the author's
/P flags (the Ohio payer form sets /P -1084). Filling a form the payer sent us to fill is
squarely what those flags contemplate, and the plaintext copy never leaves this pipeline —
it exists so the answers can be drawn. A document that wants a real password fails
EncryptedPdfError and is refused: no password is guessed, derived, or brute-forced,
and there is no code path that tries a second one. Two consequences are worth a human's eye
rather than a comment: the filled copy we emit is plaintext, so the author's permission flags
do not survive into it, and "an empty user password is not an access control" is a judgment
call about the author's intent, however standard it is (qpdf and pdftk decrypt this case by
default).
Why @cantoo/pdf-lib
It is a maintained MIT fork of pdf-lib that ports pdf.js's CipherTransformFactory into
pdf-lib's parser, which makes it the only library already reachable from this package that can
decrypt AND re-emit. Plain pdf-lib does not decrypt at all; pdf.js decrypts but its
saveDocument writes an incremental update over the still-encrypted original, so it cannot
hand back a plaintext file. The fork is used for this one job — the drawing engine stays on
plain pdf-lib, and nothing but Uint8Array crosses between them, so the two libraries' types
never meet.
Bytes pdf-lib can WRITE on, not merely open.
Every other reader in this pipeline only has to see the document.
drawAnswershas to draw on it, and pdf-lib is both the reader and the writer there — so where pdf.js rescued detection and extraction by decrypting what pdf-lib could not, nothing rescues the fill step: pdf.js has no writer, and a document pdf-lib cannot parse is a document it cannot stamp. On the real payer form this took the shape ofgetPages()throwing a rawExpected instance of PDFDict, but got instance of undefined, because the page tree lived inside a ciphered object stream that pdf-lib skipped.So the document is decrypted to a plaintext copy first, and the rest of the pipeline runs on that copy exactly as it does on an ordinary template.
Why this is decryption we are entitled to do
Only the EMPTY user password is ever tried. That is the permissions-only case: the document opens in any reader with no prompt, and the encryption carries nothing but the author's
/Pflags (the Ohio payer form sets/P -1084). Filling a form the payer sent us to fill is squarely what those flags contemplate, and the plaintext copy never leaves this pipeline — it exists so the answers can be drawn. A document that wants a real password fails EncryptedPdfError and is refused: no password is guessed, derived, or brute-forced, and there is no code path that tries a second one. Two consequences are worth a human's eye rather than a comment: the filled copy we emit is plaintext, so the author's permission flags do not survive into it, and "an empty user password is not an access control" is a judgment call about the author's intent, however standard it is (qpdf and pdftk decrypt this case by default).Why
@cantoo/pdf-libIt is a maintained MIT fork of pdf-lib that ports pdf.js's
CipherTransformFactoryinto pdf-lib's parser, which makes it the only library already reachable from this package that can decrypt AND re-emit. Plain pdf-lib does not decrypt at all; pdf.js decrypts but itssaveDocumentwrites an incremental update over the still-encrypted original, so it cannot hand back a plaintext file. The fork is used for this one job — the drawing engine stays on plain pdf-lib, and nothing butUint8Arraycrosses between them, so the two libraries' types never meet.