Take an invoice PDF that already carries AES-256 encryption and ask the PDFium Component for Delphi and C++Builder (PDFiumPas) to stamp it PDF/A for archival retention, or to sign it with PAdES, through an incremental update rather than a full rewrite. The library will not get there by patching the encrypted bytes directly: its six conformance marker injectors detect an existing /Encrypt entry and pass the source through byte-for-byte unchanged, and its PAdES signer raises an exception rather than emit a signature that no validator will accept
That is a different question from auditing a PDF you did not create for hidden risk, which is its own read-only exercise. This article is about the write side of the same trust boundary: what your own code is allowed to do to a file whose bytes are already locked behind someone else's password, the moment that code tries to add anything to it after the fact
What does ISO 32000-1 require when you update an encrypted PDF?
ISO 32000-1 §7.5.6 requires an incremental update's trailer to repeat every entry from the previous trailer except /Prev, and Table 15 lists /Encrypt among the entries a trailer can carry. Drop it from the new trailer and a conforming reader has no reason to doubt the omission: the newest trailer is authoritative, so a reader that finds no /Encrypt there decides the whole file is unencrypted and tries to parse the older, still-enciphered body as plain bytes. Keep /Encrypt in the new trailer but write the update's own objects as plaintext, and the failure just moves one step later: the reader correctly detects encryption, runs every object it touches through the file's cipher, including the new ones that were never encrypted to begin with, and gets noise back for content that was perfectly legible before decryption touched it. Either mistake produces a file that looks like a normal, well-formed incremental update at the byte level, right up until a conforming reader opens it
Six marker injectors, one v2.14.2 encryption gate
PDFiumPas ships six byte-level marker injectors, one for each ISO PDF subset it can label: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), and PDF/VT-1 (ISO 16612-2). Each one takes the bytes PDFium's own FPDF_SaveAsCopy already wrote and layers a second, smaller incremental update on top of them: a new XMP metadata stream, a catalog dictionary edit that points at it, and for the print-oriented subsets an OutputIntent and ICC profile. As of v2.14.2, every one of InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, and InjectPdfVTMarkers reads the source trailer first, and if it reports an existing /Encrypt entry, copies the source through to the destination stream unmodified and returns immediately. No XMP, no OutputIntent, no catalog edit — the caller gets the original file back, byte for byte
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Permitted to be encrypted is not the same as safe to inject into
PDF/E-1 and PDF/R-1 both explicitly allow their host document to be encrypted at the specification level, which reads like an exemption until you look at what actually has to happen on disk. ISO 24517-1 §6.3 permits encryption for PDF/E-1, and ISO 23504-1 §6.2.3 permits it for PDF/R-1 provided the header declares %PDF-2.0. Neither clause says anything about whether a byte-level post-processor can safely add a plaintext object to that encrypted container, and it cannot, for the same §7.5.6 reasons that apply to every other subset. PDFiumPas's own compliance validators for these two profiles, ValidatePdfECompliance and ValidatePdfRCompliance, record the presence of /Encrypt deliberately without flagging it as a defect, which is correct for a read-only validator that never writes a byte. It is also an easy pattern to skim past and assume the sibling injector needs no separate guard, when the injector is the one function in the pair that actually has to refuse
Does SaveAsPdfX silently decrypt your document?
Yes, whenever you go through the public convenience methods instead of calling an injector directly. Every one of TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, and SaveAsPdfVT renders the current document to a temporary stream with SaveAs(Tmp, saRemoveSecurity) before handing those bytes to its matching injector. saRemoveSecurity maps to PDFium's own FPDF_REMOVE_SECURITY flag, so the temporary copy the injector receives was never encrypted in the first place, and the injector's /Encrypt guard never has a reason to trigger. The output carries your PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, or PDF/VT-1 markers, but it is no longer protected by whatever password opened the source
That trade-off is invisible until someone downstream opens the "protected" archival copy without a password and notices it just works. The fix is not a different method call; PDFiumPas has no saAddSecurity counterpart to pair with saRemoveSecurity, because the underlying PDFium engine was never built to write new encryption, only to remove it. If both properties matter for one file, encryption has to be a separate step you own, applied after the conformance markers, not folded into the same SaveAsPdfA call
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
What happens when you sign an encrypted PDF with PAdES?
PDFiumPas refuses outright, rather than silently dropping the request the way a marker injector does. TPdf.SignPades and SignPadesToStream both route through an internal SignPadesBytes, and the first thing it does after parsing the source trailer is check for /Encrypt. If the entry is present, it raises EPadesCrypto with the message "SignPadesBytes: the source document is encrypted; remove encryption before signing" instead of proceeding any further. InjectPadesDssMarkers, the function that embeds certificates, OCSP responses, and CRLs for long-term validation, applies the identical check for the identical reason, with its own message: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
The reasoning here is stricter than the marker injectors' pass-through, and deliberately so. A silent pass-through is safe for a PDF/A stamp because skipping it leaves you with the same valid PDF you started with, just unlabelled. Signing cannot fail that quietly: a signature that was silently never added looks, to any calling code that only checks a boolean result, exactly like a signature that was added successfully. EPadesCrypto descends from the ordinary Exception class, so catching it is normal exception handling, not a special control-flow convention you have to learn
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Sequencing conformance stamps, signatures, and encryption
The practical fix is ordering, not a different library. Apply PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, or PDF/VT-1 markers first, add any PAdES signature next, and only then run whatever step in your pipeline actually owns encryption, whether that is a dedicated PDF writer, a signing appliance, or your own AES implementation. PDFiumPas's incremental-update layer fits naturally in the middle of that sequence, appending small, targeted objects onto a file that is otherwise finished, and encryption belongs at the end precisely because it is the one operation in the chain PDFiumPas itself cannot perform or reverse
None of this changes how PDFiumPas reads the trailer and cross-reference data that every incremental update depends on, which is its own source of subtlety once xref streams enter the picture; validating a PDF's object and xref streams covers how that same trailer-reading path handles PDF 1.5+ compressed structures. And once a document is ready for something stronger than a conformance stamp, signing a PDF with a PAdES B-B signature in Delphi is where SignPades takes over from exactly the point where this article leaves off
The marker injectors and SignPades methods described here ship as part of the PDFium Component for Delphi and C++Builder, alongside the rendering and read-only inspection PDFium provides natively