Skip to main content

M16 — Security and forms

State: to do — Depends on: M03, M11, M15 — Cryptography placed by ADR 41; scripts and dynamic XFA by ADR 37; versions by ADR 40

Goal​

Open every password-protected document and write protected ones, say precisely what the core cannot open itself; and read, fill, flatten, create and exchange the fields of interactive forms — AcroForms whole, XFA and usage rights detected and kept coherent, Acrobat's standard formats and sums applied without ever running a script.

Eighteen committed documents and five remote ones are encrypted, and every milestone since M03 has had to leave them out: they are refused at opening, so none of them has been round-tripped, stamped, merged, annotated or extracted. Eleven of the seventeen whose password the manifest records open with an empty one — a protection that protects nothing, and that every viewer opens without a word — while the library refuses them all. Forms are the other half of the business: a Cerfa to fill for each client, a subscription form to fill from a spreadsheet, a signed contract whose signature fields must be left ready for signing. The failures to prevent are ordinary and silent: a hybrid XFA form filled through its AcroForm, which Acrobat then shows empty because its XFA data was left stale; a Reader-extended form saved in a way that disables Reader's features with a message the user cannot act on; a numeric field written as 1234.5 where the form's own format says 1 234,50 €; a sum that stays at its old total because nobody ran the calculation; a check box drawn in a ZapfDingbats font that voids the PDF/A claim.

Scope​

In:

  • Decryption under the standard security handler, every revision a valid file can carry: RC4 40-bit (R2), RC4 up to 128 bits (R3), crypt filters with RC4 or AES-128 (R4), AES-256 (R5 read, R6), with EncryptMetadata, the Identity filter, /EFF for embedded files and the /Crypt stream filter; owner and user authentication, passwords as text or as bytes;
  • AES-GCM and the integrity MAC read — ISO/TS 32003 and 32004 — with the framework's AesGcm and HMAC (ADR 41);
  • the public-key security handler detected and reported under a stable code, with the seam through which AdCodicem.Pdf.Signing (M26) supplies the key; unencrypted wrapper documents (ISO 32000-2 §7.6.7) recognized, their payload exposed as an attachment;
  • permissions: decoded, respected by every operation of the library by default, and overridable only by an explicit caller declaration that is reported;
  • encryption on write: AES-256 (R6) by default, AES-128, RC4-128 and RC4-40 on explicit request; crypt-filter options — attachments only, metadata left clear; a full rewrite that keeps, removes or replaces encryption, and an incremental update of an encrypted document under its own key; deterministic output (invariant 6);
  • the form model: every field type, widget, flag, value and option, the calculation order, default resources and appearances, the scripts each field carries, and a JSON listing;
  • filling, with field appearances generated for text, choice and button fields, fonts embedded by default;
  • Acrobat's standard formats (AFNumber, AFPercent, AFDate, AFTime, AFSpecial, AFRange) and simple calculations (AFSimple_Calculate: sum, product, average, minimum, maximum) recognized without executing any script, and every other script reported;
  • XFA detected and classified — static, hybrid, dynamic —, its datasets read, and on fill kept in step or removed, as the caller's policy says;
  • usage rights (/UR3, legacy /UR) interpreted, kept through a save that stays within their grants, and removed with a diagnostic when a save would break them;
  • flattening fields, whole or by name, through M11's flattener;
  • a field creation API — text, check box, radio group, combo box, list box, push button — and signature field placeholders with /Lock and seed values, on an opened document and on one being generated (M08's builder, which M17 uses);
  • FDF, XFDF and JSON import and export of field values — and, through M11's model, of annotations —; and batch filling of one template from many records;
  • validation rules in the form.* family and the security.* rules that need the file key, added to M02's structural profile; M03's version table gains the rows these features need;
  • the command-line tool's encrypt, decrypt, security, fields, fill and export verbs, and flatten --fields.

Out, explicitly:

  • decrypting and encrypting with the public-key handler — M26, in AdCodicem.Pdf.Signing, through the seam defined here (ADR 41); signing a signature field, visible signature appearances, certification — M26; verifying a MAC that ISO/TS 32004 attaches to a signature — M27;
  • writing AES-GCM or a MAC — not planned: Acrobat does not open such files yet, by the survey's account, and PDF/A and every business exchange avoid encryption anyway; the read side exists because invariant 12 asks for it;
  • executing JavaScript, of any kind — never (ADR 37). Scripts are listed, kept on merge (M06), reported on fill and flatten, removed by sanitization (M19); only Acrobat's standard functions are recognized, by their exact call shape;
  • rendering, flattening or filling dynamic XFA — never (ADR 37): detected, reported, its datasets read, the XFA removable; writing a dynamic form's datasets would be filling it without seeing it, and stays out with it;
  • Acrobat's simplified field notation (/** BVCALC … EVCALC **/) — reported like any other script until a real document carries one (below, What it lacks);
  • templates and page spawning (/Names /Pages, /Templates, getTemplate().spawn) — read, kept, reported;
  • barcode fields (Acrobat's /PMD paper forms barcodes) — kept as fields, their appearance left as it is, reported;
  • HTML form controls mapped to fields — M17, on the generation-side API delivered here;
  • scrubbing form values for redaction — M19, through this model;
  • permissions and encryption as sanitization categories — M19; converting a received encrypted document to PDF/A, which forbids encryption — M21, which decrypts through this milestone;
  • rasterizing filled forms — M25; MuPDF and pdf.js are the visual referees until then.

Design​

Where it lives​

In the core, beside the reader and the writer they extend:

Security/ handlers, crypt filters, key derivation, permissions, the key-source seam, the encryption stage
of the writer, AES-GCM and the MAC
Forms/ the field model, filling, appearances, the Acrobat-format recognizer and calculator, XFA, usage
rights, flattening, field creation, FDF, XFDF and JSON, batch filling
TypeResponsibility
PdfCredentialsNone, Password(string), PasswordBytes(ReadOnlySpan<byte>), KeySource(IPdfDecryptionKeySource); on PdfReaderOptions.Credentials
IPdfDecryptionKeySourceThe seam for a handler the core cannot open alone: given the handler's /SubFilter and /Recipients bytes, return the seed and the permissions, or nothing. Implemented by AdCodicem.Pdf.Signing (M26)
PdfSecurityInfodocument.Security: handler, V, R, key length, the method of strings, streams and embedded files, EncryptMetadata, declared permissions, how the caller authenticated (Owner, User, KeySource, None), the MAC's presence and verdict, whether the document is a wrapper and its payload
PdfPermissionsFlags — Print, PrintHighQuality, Modify, Copy, Annotate, FillForms, ExtractForAccessibility, Assemble — decoded from /P for the revision, and their effective value for the authentication
PdfPermissionPolicyRespect (default) or Ignore, on PdfReaderOptions
PdfPermissionExceptionTyped: the operation, the permission, its bit
PdfFormExceptionTyped: a fill, a flattening or a creation the form cannot take — a dynamic XFA form, a refused XFA policy, a name that collides
PdfEncryptionOptionsImmutable: algorithm, user and owner passwords, permissions, EncryptMetadata, scope (Everything, EmbeddedFilesOnly), and an entropy source the caller must name
PdfEntropySystem (the platform's generator) or FromSeed(bytes); no default (invariant 6, below)
PdfAcroFormdocument.Form: the field tree, lazy; Find(name); NeedAppearances, SigFlags, CalculationOrder, default resources and appearance; Xfa; UsageRights
PdfField and its views — PdfTextField, PdfCheckBoxField, PdfRadioGroupField, PdfPushButtonField, PdfComboBoxField, PdfListBoxField, PdfSignatureField, PdfUnknownFieldTyped read of each field type with its flags, value, default value, options and widgets; an unknown /FT is kept and listed
PdfWidgetRectangle, page, flags, appearance states, on-state, /MK (border and background colors, caption, icon, rotation), border style, /DA, /Q
PdfFieldValueText, Number (decimal), Date, Time, State (a button's), Choices, Clear
PdfFieldScriptsPer field and event (keystroke, format, validate, calculate, focus, blur, mouse): Recognized(PdfAcrobatFormat) or Other(length, hash)
PdfAcrobatFormatOne record per recognized function, with its parameters: AFNumber, AFPercent, AFDate, AFTime, AFSpecial, AFSpecialMask, AFRange, AFSimpleCalculate
PdfFillOptionsImmutable: appearances (Generate, NeedAppearances), fonts (EmbedSubstitutes, UseDeclared), XFA (KeepInStep, Remove, Refuse), formats (Apply, Ignore), calculations (Recompute, Leave), read-only fields (Refuse, Override), unknown names (Refuse, Report)
PdfXfaInfoKind (None, Static, Hybrid, Dynamic), packets (names and lengths), template version, the datasets as a bounded XML reader; Remove()
PdfUsageRightsPresent, legacy /UR, the grants of each category, /P, /Msg, and the coverage M04 computed
PdfFormBuilderField creation — AddText, AddCheckBox, AddRadioGroup, AddComboBox, AddListBox, AddPushButton, AddSignatureField — on an opened document's change set or on M08's PdfDocumentBuilder.Form
PdfFormDataFDF, XFDF and JSON: Export, Import (to a PdfFormDataSet of values and annotations), Apply
PdfBatchFillerOne template analyzed once; records filled into one output each or into one volume

Every operation that reads or changes a document takes M03's CancellationToken and IProgress<PdfProgress>, adding the stages Decrypting, Encrypting and Filling, and returns M06's PdfOperationReport, as M09 and M11 do, with the codes listed below.

Opening a protected document​

  • PdfReaderOptions.Credentials is new, and its default, PdfCredentials.None, tries the empty user password, which opens eleven of the seventeen committed documents whose password is recorded. A password given as text is tried in the encoding the revision defines — PDFDocEncoding for R2 to R4, UTF-8 after SASLprep for R5 and R6 —, then in those producers have been seen to use instead — Windows-1252 and UTF-8 bytes for R2 to R4, the string without normalization for R6: a fixed list, each attempt costing one key derivation, and the encoding that matched reported (encryption.password-encoding, information). The owner password is tried before the user password, so that the authentication is the strongest the caller can prove.
  • Failure is typed and immediate. PdfEncryptedException gains a Reason: PasswordRequired (nothing given opens it — the Cabinet's AES file whose password nobody published), PublicKeyHandler (no key source, or one that declined), UnsupportedHandler (a /Filter nobody documents), UnsupportedAlgorithm (V 0 or 3, an unknown R, a crypt-filter method nobody documents), PrimitiveUnavailable (the platform lacks a primitive the revision needs — below).
  • ThrowOnEncrypted keeps its name and changes its meaning, which ADR 30 allows a preview: true, the default, throws when no credential opens the document; false opens the structure without the key, as M04 does to compute signature coverage — strings and streams stay ciphertext, each read reported once (encryption.content-not-decrypted), and nothing that needs the content is attempted.
  • Nothing is read eagerly. Authentication reads /Encrypt and the trailer's /ID; strings are decrypted as their object is parsed, streams as the first stage of their decoding. Opening an encrypted document reads what opening its twin reads — the laziness test of CorpusReadingTests stops skipping them.

The standard security handler​

VRCipherKeyFrom password to keyWritten
12RC440 bitsISO 32000-2 algorithms 2, 4 and 6: MD5 over the padded password, /O, /P, the first /ID elementOn request (Rc4_40), reported weak
23RC440 to 128 bits, by 8Algorithms 2, 5 and 6: MD5 re-hashed 50 times, the owner key through 20 RC4 passesOn request (Rc4_128), reported weak
44RC4 (/V2) or AES-128-CBC (/AESV2), per crypt filter128 bitsAs R3, plus EncryptMetadataOn request (Aes128)
55AES-256-CBC (/AESV3)256 bitsSHA-256 over the password and a salt — Adobe's extension level 3, withdrawnNever; read
56AES-256-CBC (/AESV3)256 bitsAlgorithm 2.B: rounds of SHA-256, 384 and 512 over the UTF-8 password (SASLprep, at most 127 bytes); the file key unwrapped from /UE or /OE; /Perms checkedThe default (Aes256)
67AES-256-GCM (/AESV4)256 bitsAs R6Never; read (below)
  • Per-object keys for R2 to R4 follow algorithm 1: MD5 over the file key, the object's number and generation, and sAlT for AES, truncated to the key length plus five bytes, at most 16. R5 and later use the file key directly.
  • RC4, MD5 and AES are ours as well as the framework's (the maintainer's decision of 2026-09-27, a consequence of ADR 41): the base class library has no RC4, and browser WebAssembly, where M23 runs the core, has no MD5 and no AES. The core carries managed MD5 (M06's, written for /CheckSum), RC4 and AES in CBC and ECB modes, so that every revision but R7 opens in the browser; SHA-2 and HMAC come from the framework, and so do MD5 and AES wherever the platform has them. The managed code is tested against RFC 1321's, RFC 6229's and NIST SP 800-38A's vectors and fuzzed, and it is not constant-time, which the documentation says: it decrypts a file already in the caller's hands, where timing reveals nothing the file does not. AES-GCM (R7) comes from the platform only; where it lacks it, a typed refusal names the primitive (PrimitiveUnavailable). Never a crash.
  • Algorithm 2.B ends by itself. Its loop runs at least 64 rounds and stops once the last byte of the round's output is at most the round number less 32: by the 288th round every byte satisfies that, whatever the input. No bound to classify under ADR 34.
  • /Perms (R6) is decrypted with AES-256 in ECB mode and checked: the adb marker, /P, and the EncryptMetadata byte. A mismatch is a tampered or careless file; the more restrictive of the two readings applies and the difference is reported (encryption.permissions-mismatch, warning).
  • Bounds — the lengths of /O, /U, /OE, /UE, /Perms (32, 48 or 16 bytes as the revision says), /Length (a multiple of 8, 40 to 128 for RC4), a /CF dictionary's size — are checked before any is used; none sizes an allocation. A value only an invalid file carries is refused under UnsupportedAlgorithm, and its reason written where the bound is declared (invariant 12).

What is encrypted, and what is not​

Every string and stream of every indirect object, except:

  • the /Encrypt dictionary itself, direct or indirect — its object number is known before anything is parsed;
  • the /ID strings, and the trailer, and every cross-reference stream;
  • the objects inside an object stream, whose strings are not encrypted a second time: the object stream is;
  • a signature dictionary's /Contents (ISO 32000-2 §7.6.2), which M04 already locates by lexing, and which M26's placeholder patch relies on;
  • streams of /Type /Metadata when EncryptMetadata is false — every such stream, as qpdf reads it, not only the catalog's;
  • whatever a crypt filter named /Identity covers, and a stream whose own /Filter array begins with /Crypt naming another filter in its /DecodeParms.

AES strings and streams begin with a 16-byte IV and end on PKCS#5 padding. A ciphertext shorter than one block, a length not a multiple of 16, or padding that does not check is taken as far as it decrypts and reported (encryption.padding-invalid, repair): producers have written empty strings unencrypted, and viewers show them empty. Decryption of a stream is a stage of M01's decoding pipeline, block by block through pooled buffers, so that M23's piecewise decoding (#48) inherits it unchanged.

AES-GCM and the integrity MAC​

ISO/TS 32003 adds AES-256-GCM as a crypt-filter method on the R6 key derivation; ISO/TS 32004 adds a message authentication code over the file's bytes, keyed from the file key, standalone or attached to a signature. The V, R and method values in the table above are the feature survey's; slice 3 checks them, and every other constant of both specifications, against their text before a line is written.

  • GCM: each string and stream carries a nonce, its ciphertext and a tag. A tag that does not verify means the bytes were altered after encryption: the object's content is withheld — an empty string, a stream that does not decode — and reported (encryption.authentication-failed, warning; security.authentication-failed, error, in validation), never returned as if it were sound.
  • The MAC: verified over the byte range the trailer's authentication dictionary names, with an HMAC keyed by the key the TS derives from the file key and a salt the encryption dictionary adds. A mismatch is reported (encryption.mac-mismatch, warning, and the same validation rule); a MAC attached to a signature is listed and left to M27 (encryption.mac-unverified, information).
  • ADR 41 and the MAC's container. ADR 41 keeps general CMS parsing out of the core, and ISO/TS 32004 wraps the MAC in a CMS structure. Slice 3 first confirms that the standalone MAC can be verified by reading the one fixed profile the specification allows, with System.Formats.Asn1, bounded, refusing anything outside it; if it cannot, verification moves to AdCodicem.Pdf.Signing with M27, the core reports the MAC present and unverified, and ADR 41 is amended to say so.
  • Platforms. AesGcm.IsSupported is false on some — browser WebAssembly among them: a GCM file there is refused with PrimitiveUnavailable.

What the core does not open itself​

  • The public-key handler (/Filter /Adobe.PubSec, /SubFilter adbe.pkcs7.s3, s4 or s5): the core parses the encryption dictionary and its crypt filters, lists the recipients' count and bytes, and asks IPdfDecryptionKeySource for the seed. Given one, it derives the file key itself — SHA-1 for RC4 and AES-128, SHA-256 for AES-256, over the seed, every recipient string and, when metadata is left clear, four bytes of 0xFF — and the document opens like any other. Without one: PdfEncryptedException with PublicKeyHandler, and encryption.public-key-handler naming the subfilter and the recipient count; never a page read as ciphertext. The unit tests implement the seam inside the test project, with EnvelopedCms and the test key of the fixture (below), which proves that the seam is sufficient for M26 before M26 exists.
  • Wrapper documents (ISO 32000-2 §7.6.7, Table 28): an unencrypted file whose /Collection shows one embedded file with /AFRelationship /EncryptedPayload and an /EP dictionary naming the payload's crypt filter and version — Microsoft Purview's protected files are such wrappers. Recognized at opening, reported (encryption.wrapper-document, information, naming the filter), the payload exposed as an attachment through M06's API (document.Security.Payload); M06's assembly reports a wrapper part rather than merging its cover page as content (assembly.wrapper-placeholder), and M15's extraction says the text it finds is the wrapper's.

Permissions​

PdfPermissions decodes /P for the revision: its low 32 bits, whatever the integer's sign — the 1996 IRS form writes 65516, a 16-bit value —; bits 3 to 6 for R2, which has no bits 9 to 12 and whose accessibility and assembly follow its copy and modify bits, as qpdf and pdf.js read them; bits 3 to 6 and 9 to 12 for R3 and later. Bit 10, which ISO 32000-2 deprecates, is read as the text of Table 22 says, checked in slice 2 against it and against qpdf's and pdf.js's readings.

  • Owner authentication grants everything. User authentication grants what /P says. Seven of the committed documents authenticate as owner with their recorded password — the owner password is the user password, or empty.
  • The operations of the library each name the permission they need, and ask one guard, document.Security.Demand(operation):
OperationPermission
Extraction — text, images, exports, search (M15)Copy; extraction for accessibility, tagged structure first, under ExtractForAccessibility
Pages inserted, removed, rotated, extracted; merge, split, outline edits (M06, M07)Assemble (R3 and later), Modify (R2)
Stamps, headers, Bates, normalization (M09); flattening, layers (M11, below)Modify
Annotations added or removed (M11)Annotate
Fields filled, from any source (below)FillForms or Annotate
Fields created or removed (below)Annotate and Modify
Encryption removed or replacedOwner authentication only
  • Respect, the default, throws PdfPermissionException before anything is written or returned. Ignore is a declaration by the caller that it is entitled to do what the file forbids — its owner who lost the owner password, an archive under a legal duty: the operation runs and each permission it overrides is reported (permission.not-respected, warning). ISO 32000-2 §7.6.4.2 says processors "shall respect the intent of the document creator"; a library cannot tell who its caller is, so it asks.
  • Printing is not an operation of the library; the flags are exposed for the caller's own printing path.

Writing encryption​

PdfSaveOptions.Encryption is Keep (the default), Remove, or Apply(PdfEncryptionOptions).

  • Keep writes the input's /Encrypt dictionary as it was and its first /ID element, so that the file key, and every password, stay what they were; it needs no password beyond the one that opened the document, owner or user. An incremental update of an encrypted document is always Keep: its new objects are encrypted under the same key, its trailer repeats /Encrypt and the first /ID element, and the signed revisions stay byte for byte — M03's round trip and M09's stamps now run on encrypted inputs.
  • Remove and Apply need a full rewrite, which M03 refuses on a signed document unless the caller insists, and owner authentication, or Ignore (reported).
  • PdfEncryptionOptions: Aes256 (R6) by default; Aes128 (R4), Rc4_128 (R3) and Rc4_40 (R2) only with AllowLegacyAlgorithms, each reported (encrypt.weak-algorithm), and RC4 refused in 2.0 output, which keeps R6 alone. An owner password is required when the permissions restrict anything — an owner password equal to the user password grants the user everything, which would make the restriction a pretense. R2 to R4 passwords must be representable in PDFDocEncoding (ArgumentException otherwise: readers would not reproduce the bytes); R6 passwords go through SASLprep. EncryptMetadata = false leaves the XMP readable to indexers; EmbeddedFilesOnly writes /StmF and /StrF as /Identity and /EFF as the standard filter — a case file whose annexes alone are protected.
  • Where it happens. The encryption stage belongs to M03's forward-only writer: strings are encrypted as each object is serialized, under its number, which the writer reserved ahead; a stream is compressed, then encrypted, then written behind its indirect /Length, which now counts the IV and the padding; objects bound for an object stream are written clear, and the object stream encrypted as a stream; the cross-reference stream and /Encrypt are written clear.
  • Determinism (invariant 6). R2 to R4 derive the file key from the passwords, /P and the first /ID element, which M03 takes from the input or the caller, never from its own hash — so their output is fixed. R6 wants a random file key and two salts, and AES wants unpredictable IVs. So: every IV is the first 16 bytes of an HMAC-SHA-256 keyed by the file key over the object's number, generation and the string's or stream's index within it — unique, unpredictable without the key, and deterministic; the R6 file key and salts come from PdfEntropy, which the options require: System, the platform's generator, is the caller's explicit choice of varying output, and FromSeed(bytes) reproduces identical bytes run after run. An ADR written with slice 4 records this: randomness in encryption is an input the caller supplies, like a date.
  • Conformance. Every part of PDF/A forbids encryption: under a claim, Apply follows M09's PdfConformancePolicy — Refuse throws PdfConformanceException; RemoveClaim encrypts, removes the claim and reports encrypt.conformance-claim-removed. PDF/UA-1 requires that encryption not stop assistive technology: under a PDF/UA claim, ExtractForAccessibility is always granted, and a caller who clears it meets the same policy.

Output version (ADR 40)​

M03's table of minimum versions gains, checked against the Arlington model's SinceVersion rather than written from memory (ADR 44):

What M16 writesMinimum
RC4 with a 40-bit key (V 1, R 2)1.1
An AcroForm, widgets, /DA, /Q, /MaxLen1.2
/TU, /TM, signature fields1.3
RC4 with a longer key (V 2, R 3); file-select, DoNotSpellCheck, DoNotScroll1.4
Crypt filters (V 4); comb and rich-text flags; RadiosInUnison; CommitOnSelChange; /Lock and /SV1.5
AES-128 (/AESV2); /EFF1.6
AES-256 (V 5, R 6)2.0, or 1.7 with /Extensions /ADBE at level 8, which M03's model writes and unions

PDF 2.0 deprecates NeedAppearances and XFA: the first is never set in 2.0 output, since a 2.0 fill always generates appearances, and the second is carried only as the input had it, never created. M02's version-aware shape rules report what an input carries.

The form model​

  • The field tree is walked lazily from /AcroForm /Fields, iteratively, with a visited set, bounded by the index as M11's walks are — M16 adds no reader limit; a field tree with a cycle exists in the remote corpus, and M06's walker already names fields this way. A terminal field is one whose kids are widgets, or none; a field and its only widget merged in one dictionary is read as both. /FT, /Ff, /V, /DV, /DA and /Q are inherited down the tree, /DA and /Q from the AcroForm dictionary last; /MaxLen too, as viewers read it.
  • Names: the partial name /T, decoded as a text string (the Cerfa forms' Prénom is UTF-16BE), the fully qualified name joined with periods; a partial name that contains a period, or two terminal fields with the same fully qualified name that are not kids of one field, are listed as they are and found by validation. /TU (tooltip, the accessible name) and /TM (the export name) are read.
  • Values: a text field's /V is a text string or a text stream; a button's is a name — for a check box, one of its widgets' on-states, found as the key of /AP /N that is not /Off; for a radio group, the on-state of one kid, mapped through /Opt when the kids' states are indices — which is how Acrobat writes non-ASCII and duplicate export values; a choice field's is a string or an array, with /I for the selected indices and /Opt as strings or as [export display] pairs.
  • Scripts: each field's /AA (keystroke K, format F, validate V, calculate C, and the mouse and focus events) and its /A, each JavaScript source read — a string or a stream, bounded by the object limit — and classified by the recognizer below; document-level scripts (/Names /JavaScript, /OpenAction, the catalog's /AA) counted and listed. Nothing is evaluated.
  • The listing — each field's name, type, flags, value, default value, options, widgets (page, rectangle, flags, on-state, whether an appearance exists), scripts, XFA kind, usage rights — serializes to documented JSON; it is what the fields verb prints.

Filling​

form.Fill(values, options) and field.SetValue(value) record the change in M03's change set; nothing is written until the save.

  • By type: a text field takes Text, or a Number, Date or Time that its recognized format renders (below); /MaxLen refuses a longer value; a comb field without /MaxLen is drawn as a plain field and reported. A check box takes true, false or the name of a state; a radio group the export value of one kid — /V on the field, /AS set to that kid's on-state and to /Off on the others. A combo box takes one of its options, or any text when it is editable; a list box one option, or several when it is multi-select, with /I rewritten sorted.
  • Refused, typed: a read-only field, unless the options override it (reported); a password field, whose value ISO 32000-2 says must never be stored in the file; a file-select field; a signature field — giving it a value is signing, M26's; a field a FieldMDP lock or a DocMDP certification forbids, through M04's guard (below).
  • Rich-text fields (/RV): the plain value is written, and /RV rewritten as a minimal XHTML body holding the same text, so that a viewer that prefers /RV shows the value (fill.rich-text-simplified, information). One committed form has such a field, DD 293's.
  • Kids share a value. A field with several widgets, on several pages, gets one appearance per widget.
  • Encodings: a text string in PDFDocEncoding when it can be, UTF-16BE with its mark otherwise (#36's writer, M03); names for button states, with # escapes for bytes outside the regular characters — a radio export value such as m#00nnlich, in the remote PDFBox form, is written back byte for byte.
  • After a fill, NeedAppearances is removed, since every appearance was generated — PDF/A-2 and 3 forbid it true, and PDF 2.0 deprecates it — unless the caller chose NeedAppearances mode, which writes no appearance, sets it, and is refused under a PDF/A claim.

Field appearances​

Generated for every widget whose field was filled, and on demand (EnsureAppearances) for widgets that lack one — hundreds do in the corpus, blank fields mostly: 185 of the 419 widgets of Cerfa 12156, 97 of the 136 of the 2022 Form 1040. Each is a form XObject written through M08's content builder in M11's appearance frame, numbers in "0.####".

  • Variable text (ISO 32000-2 §12.7.4.3): /DA parsed as the content-stream fragment it is — Tf gives the resource name and size, a color operator the color —; the text laid inside the widget's rectangle less its border and a two-point padding, as Acrobat pads; single-line text centered vertically, multiline text from the top and wrapped by M08's greedy breaking at spaces; /Q for alignment; a comb field in /MaxLen equal cells, one character centered in each; size 0 meaning auto: fitted to the inner height, then shrunk until the line fits, never below 4 points — the constants checked in slice 7 against pdf.js's and qpdf's generators on the corpus. The text sits inside /Tx BMC … EMC, which viewers look for when they regenerate.
  • The frame: /MK /BG filled, /MK /BC stroked with /BS — solid, dashed with its /D array, beveled and inset with their lighter and darker edges, underline —, /MK /R of 90, 180 or 270 degrees applied through the XObject's /Matrix.
  • Choice fields: a combo box shows its value; a list box its options from /TI, the selected ones on the highlight Acrobat draws.
  • Buttons: an existing appearance for a state is kept — filling a check box changes /AS, not its drawing. A missing one is drawn: the /MK /CA character of the ZapfDingbats set — check, circle, cross, diamond, square, star — as our own vector paths, not as text in a ZapfDingbats font that would not be embedded; a push button's caption as text; its icon, /MK /I, kept.
  • Fonts. /DA names a font in /DR, nearly always /Helv, Helvetica, not embedded. By default (EmbedSubstitutes) the text is drawn in M08's embedded, metric-compatible OFL substitute of the font /DA names — Helvetica and Arial by the sans face, Times by the serif, Courier by the monospaced one —, subset in the appearance's own resources, while /DA and /DR are left as the form's author wrote them: a viewer that regenerates the appearance later gets the same metrics, and a PDF/A claim survives the fill. A font /DR embeds is used for the characters its program covers — in full or as a subset: Cerfa 12156's Arial, OmniForm's Verdana —, and M08's per-cluster fallback supplies the rest, reported (fill.font-substituted); a non-embedded font that is not a standard one (Minion, Myriad, TimesNewRomanPSMT, ArialMT) is looked up in the registry by name, then substituted. UseDeclared draws with the declared font as viewers do, and is refused under a PDF/A claim. Text that needs shaping is drawn in logical order and reported by M08 (text.shaping-required), as for M11's appearances.
  • Tagged documents: a filled field's structure does not change; a widget without a Form element, which PDF/UA-1 requires, is reported, not repaired.

Acrobat formats and calculations, without a script​

A script is text the recognizer tokenizes, never evaluates. It recognizes a source consisting of exactly one call to one of Acrobat's standard functions with literal arguments — optional whitespace, one optional semicolon, and nothing else, not a comment, not a second statement:

FunctionArgumentsWhat it means here
AFNumber_Format, AFNumber_Keystrokedecimals, separator style (0 1,234.56, 1 1234.56, 2 1.234,56, 3 1234,56, 4 1'234.56), negative style (minus, red, parentheses, red parentheses), currency style, currency string, currency beforeThe value rendered in the appearance; a text value parsed under the same separator style; red drawn as the text color
AFPercent_Format, AFPercent_Keystrokedecimals, separator style, and the percent sign's position when givenThe value × 100 with %
AFDate_FormatEx, AFDate_KeystrokeEx, AFDate_Format, AFDate_Keystrokea util.printd picture (d, dd, m, mm, mmm, mmmm, yy, yyyy, H, HH, h, hh, M, MM, s, ss, tt), or the index of Acrobat's fixed listA Date rendered in the picture; a text value parsed by it; month names in English, as Acrobat's are
AFTime_FormatEx, AFTime_Format and their keystrokesa picture, or an indexAs dates
AFSpecial_Format, AFSpecial_Keystroke0 zip code, 1 zip + 4, 2 telephone, 3 social security numberDigits placed in the fixed mask
AFSpecial_KeystrokeExa mask (9, A, O, X, *)The value checked against the mask
AFRange_Validategreater-than flag and bound, less-than flag and boundThe value checked against the range
AFSimple_Calculate"SUM", "PRD", "AVG", "MIN", "MAX", and an array of field namesRecomputed after a fill
  • Formats apply to the appearance, never to the value, as in Acrobat: /V keeps 1234.5, the widget shows £1,235 under the HMRC form's AFNumber_Format(0, 0, 0, 0, "£", true). A keystroke or validate function checks the caller's value: under Apply, a value it would reject is refused (fill.value-rejected, naming the function), since Acrobat would not have let a user type it.
  • Calculations run after a fill, once, in /CO order — Acrobat's order, not a dependency order —, each calculated field's sources read as numbers under their own format's separator style, empty as zero, a name that designates a non-terminal field standing for every terminal field beneath it. Arithmetic is decimal, and the result is rounded by the target's format; where it differs from Acrobat's binary floating point, the difference is at most one unit in the last place the format shows, a limit tested and documented.
  • Everything else is reported, never guessed. A custom script, an unrecognized function, a recognized one with a non-literal argument, trailing code: fill.script-not-run (warning) naming the field and the event; a calculated field whose calculation is such a script and whose sources changed is left as it was and reported stale (fill.calculation-stale, warning). The HMRC IHT205 form has seventeen of them, the corpus's test that nothing is guessed; Cerfa 12156's 26 sums are the test that the recognized ones are computed.
  • The referee is pdf.js's own implementation of these functions (src/scripting_api/aform.js, Apache-2.0), run under Node in a container on the same values and arguments (below).

XFA​

  • Detected by /AcroForm /XFA, a stream or an array of packets, and classified by what the file says, not by guesswork:
KindHow it is recognizedIn the corpus
DynamicThe catalog's /NeedsRendering true, or the configuration's dynamicRender required: the PDF page is a placeholder the viewer replacesThe two Canadian forms (remote)
StaticOtherwise, the template's baseProfile="interactiveForms" — XFA foreground: the page content is PDF, XFA describes the fieldsCerfa 14880
HybridOtherwise, a full template rendered statically — dynamicRender forbidden in all four committed forms — beside an AcroForm field tree that mirrors itThe 2022 Form 1040, DD 293, AR-11

The tests run in that order; a file that fits none — XFA with no AcroForm field and no placeholder — is listed as Hybrid with no mirror and reported.

  • Parsed as hostile XML: XmlReader with DTDs prohibited, no resolver, the characters bounded by the stream's own decoding limit, and no recursion, so that depth costs nothing; packets read on demand, the datasets through a forward-only reader. The xmpmeta packet some forms carry inside their XFA is listed and never mistaken for the document's XMP.
  • Datasets read: PdfXfaInfo.Datasets gives the xfa:data subtree; the listing shows each bound value.
  • On fill (PdfFillOptions.Xfa):
    • KeepInStep, the default for static and hybrid forms, writes each filled value into the datasets packet at the node the template binds the field to — the normal binding by named subforms and fields with their occurrence indices, and explicit dataRef bindings of the plain $.a.b[n] shape — text, numbers, check boxes through the template's <items>, choice lists through their saved values. A binding outside those shapes (global, scripted SOM expressions, a <picture> the slice does not convert) makes partial synchronization a lie: the whole XFA is then removed instead, and each unresolved field reported (xfa.removed, warning). The form packet, where Acrobat keeps its own record of the merged state, is left as it was; how Acrobat treats datasets it did not write is exactly what no container can check, and a form filled this way and saved by Acrobat is wanted in the corpus (below).
    • Remove deletes /XFA: the AcroForm becomes the form (xfa.removed).
    • Refuse throws PdfFormException.
    • A dynamic form is never filled: PdfFormException naming ADR 37; removing its XFA leaves the placeholder page as the only content, which is reported (xfa.placeholder-left, warning).
  • XFA scripts (<script>, <calculate>, <validate> in the template — 33 scripts in the AR-11) are counted and reported with every fill (fill.script-not-run), like AcroForm scripts.

Usage rights​

  • /Perms /UR3 (and legacy /UR, whose signature has no byte range — the remote CFIA form) — M04 lists them with their coverage and keeps their transform parameters; M16 reads the grants: /Document (FullSave), /Form (Add, Delete, FillIn, Import, Export, SubmitStandalone, SpawnTemplate, Online), /Annots (Create, Delete, Modify, Copy, Import, Export, Online, SummaryView), /Signature (Modify), /EF (Create, Delete, Modify, Import), /P, /Msg.
  • Whether a save keeps them is decided from M04's classification of the pending change set: an incremental update whose every change is a class the grants cover — FormFill under FillIn, Annotation under Create, Modify or Delete, a new signature under Signature Modify, an embedded file under /EF — leaves the usage-rights signature covering its revision and its grants honored. A full rewrite, or a change of any other class, breaks them: Adobe Reader then disables its extended features with a message the user cannot act on.
  • PdfSaveOptions.UsageRights: RemoveWhenBroken, the default, removes /UR3 and /UR from /Perms in the same save and reports usage-rights.removed (warning) with the change that broke them — what Acrobat's Save a Copy does; Refuse throws before writing; Keep leaves them broken, reported (usage-rights.broken-kept, warning). M09's stamp.usage-rights-broken and M11's annotate.usage-rights-broken now lead to this decision.
  • Removing /Perms /UR3 changes the catalog, which M04 classifies as Other: on a document that is also certified — the Canadian IMM 1344 form — removal would break the certification, so the save is refused unless the caller insists, and the report names both.
  • No container runs Adobe Reader: that an incremental fill within the grants keeps Reader's features is asserted structurally — coverage, classes, grants — and recorded as such; a Reader-saved fill is wanted in the corpus (below).

Signed and certified forms​

Filling is M04's FormFill class: allowed under DocMDP P=2 and P=3, forbidden under P=1; a field a FieldMDP lock names is forbidden to it. M04's write guard applies unchanged — PdfSignatureInvalidationException, or the insisted path with write.signature-invalidated —, and a fill of a signed document is always an incremental update, since M03 refuses a full rewrite of it. Filling and then signing an empty signature field is the ordinary contract workflow: the fill is written as one revision, the signature (M26, or pyHanko in the tests) as the next.

Flattening fields​

form.Flatten(filter, options) hands widgets to M11's flattener — its placement algorithm, its All, Print and Screen modes, its layer policies —, after generating appearances for the filled widgets and for those that have none.

  • The flattened fields leave /Fields, their widgets leave /Annots, and a parent left without kids leaves too; /AcroForm is removed when no field remains, with /NeedAppearances, /XFA, /CO and /SigFlags recomputed otherwise.
  • Hidden widgets are skipped, as M11's modes say: OmniForm's hidden widgets, which pdf.js once drew outside the page, stay out of the content.
  • What is lost is reported: every script of a flattened field and the calculation order (flatten.scripts-removed), the XFA (xfa.removed), usage rights (the save decides, above).
  • Signature fields: a signed one is never flattened — its appearance as page content would show a signature nobody can verify (flatten.signed-field-kept); an unsigned one is kept by default, since it is a placeholder, and flattened only when named.
  • Tagged documents: the field's Form element keeps its place; its OBJR is replaced by the marked content of the flattened appearance, and it gains a PrintField attribute object — /Role (tv, cb, rb, pb), /checked, /Desc from /TU —, the form ISO 32000 defines for a form that is no longer interactive; slice 9 settles the result against veraPDF's PDF/UA-1 profile and records it.
  • Signed and extended documents: flattening changes content — M04's Other, refused under a certification and written as an update after approval signatures, as M11 flattens (flatten.after-signature).

Creating fields and signature placeholders​

PdfFormBuilder works in two contexts:

  • on an opened document: fields and widgets enter M03's change set, widgets placed through M09's page edits and M11's annotation writer; a new field named like an existing one is refused, or added as a widget of it when its type and options agree (AddWidget); M04's guard classifies a new signature field as NewSignature (allowed under P=2) and any other new field as Other;
  • on a document being generated, M08's PdfDocumentBuilder.Form: a widget is written with its page, the field tree — terminal fields, their parents, /Fields, /DR, /SigFlags — at Finish, from object numbers reserved at first use, as M13 writes the structure tree. It is the path M17 takes, and it is forward-only.

What a field takes — immutable options per type: name (a period in it creates the parents), tooltip, mapping name, flags (read-only, required, no-export), value and default value, /MaxLen, comb, multiline, password, scroll and spell-check flags, alignment, font (PdfFieldFont.Standard(...), which names a standard font in /DR and draws the appearance in its embedded substitute, as a fill does, or PdfFieldFont.Embedded(face, Subset | Full) — the second for a viewer to type any character later, at a documented cost in size), size (0 for auto), colors, border, background, rotation, options with export values, and for push buttons a ResetForm action or a SubmitForm action with its URL, format and field list — never a JavaScript action (ADR 37). Each action is checked against the claimed PDF/A part's list of forbidden actions, as veraPDF's rules state it.

  • Appearances are generated at creation by the generator above, or supplied by the caller for any field type — a form XObject for /N of a text or choice field, one per state of a button (its on-state and /Off, and /D when given) — and then written as given, the field's /DA, /Q, /MK and /BS still set so that a viewer can redraw it: M17 paints its fields with M12's painter and hands the appearances over this way. A created field is never left to NeedAppearances.
  • Tagging: on a tagged document, each widget in a Form element holding its OBJR, under the structure element the caller names, or appended after the page's last element and reported (field.tag-appended); /TU required under a PDF/UA claim (a conflict for M09's PdfConformancePolicy otherwise); /Tabs /S on the page.
  • Signature placeholders (AddSignatureField): /FT /Sig, a widget visible — its appearance the caller's form XObject, or a frame with an optional caption, so that the box shows in every viewer before signing — or invisible, with a zero rectangle, which PDF/A exempts from an appearance; /Lock (All, Include or Exclude with field names, and in 2.0 output its /P); /SV seed values — filter, subfilter, digest methods, reasons, legal attestations, MDP, a time-stamp server's URL as text, certificate constraints as bytes the caller supplies —, with their /Ff flags; /SigFlags gains SignaturesExist. No cryptography, and no network: M26 signs the field; pyHanko signs it in the tests.

FDF, XFDF and JSON​

PdfFormData.Export(form, format, output, options) and Import(input, format) → a PdfFormDataSet of values and annotations, which Apply(form, set, fillOptions) fills like any other values.

  • What travels: every terminal field's value by fully qualified name, fields with NoExport left out, empty ones on request; the document's /ID (/ID in FDF, <ids original modified> in XFDF) and a file reference the caller gives (/F, <f href>); with IncludeAnnotations, the markup annotations of M11's model, in the subtypes it authors, mapped to FDF's /Annots and XFDF's <annots> elements — M11 left this exchange here.
  • Import reports each name the form lacks (form-data.field-unknown), each value its field refuses (form-data.value-rejected), and an /ID that is not the document's (form-data.document-mismatch, information), and applies the rest.
  • FDF (ISO 32000-2 §12.7.8) is PDF syntax: read by M01's lexer and parser under its limits, the /FDF dictionary's /Fields walked iteratively with a visited set, hierarchical /Kids and dotted /T both accepted. /JavaScript, /EmbeddedFDFs, /Pages and an encrypted FDF are not followed: each reported (form-data.ignored), never executed or fetched. Written as %FDF-1.2, deterministic, strings encoded as text strings.
  • XFDF (ISO 19444-1, XFDF 3.0): XmlReader, DTDs prohibited, no resolver, its size under the caller's reader limits (ADR 34), read without recursion; nested <field name> elements and dotted names both accepted on import; <value> repeated for a multi-select list, <value-richtext> read as text. Written in UTF-8, nested, in field order.
  • JSON: a schema of ours, published on the site as JSON Schema 2020-12 — a list of fields, in order, each { "name", "type", "value" }, a check box's value true, false or a state's name, a multi-select's an array, null to clear —, read and written with System.Text.Json source generation (AOT). The same shape is the fields listing's, so a listing edited by hand fills the form.

Batch filling​

PdfBatchFiller.Create(template, options) opens the template once, analyzes its fields — types, recognized formats, calculation order, appearance recipes with their fonts resolved and their substitutes subset lazily — and freezes that analysis; records then fill it one at a time or in parallel.

  • Outputs: one file per record, as an incremental update of the template's bytes — the template copied verbatim and one revision appended, which keeps usage rights when the grants allow it and costs a copy and a few objects —, or as a full rewrite, flattened or not; or one volume, through M06's assembly, each record's fields nested under a name of its own (M06's Nest: the record's key, or record-n), or flattened first.
  • Records: IEnumerable<PdfFormDataSet>, from JSON, XFDF, FDF, or — through the tool — CSV whose header names the fields; each record's report kept by its key.
  • Memory follows one record: the template's index and analysis are shared read-only, each worker holds its own lightweight document over the shared source (invariant 8 — a PdfDocument is not thread-safe), and a record's objects are released once its output is written. Which reader structures must become shareable is slice 12's first measurement.
  • Determinism: a record gives the same bytes alone, first, last, or in parallel; /ID per M03's rule or from the caller; no date the caller did not supply.

Validation rules​

Added to M02's structural profile, documented in docs/website/docs/reference/validation-rules.md, and declared in the manifest's findings of every document that earns them. M02's slice 5 opens the security.* family with what needs no key; M16 adds what does:

RuleSeverityMeaning
security.permissions-mismatchWarning/Perms disagrees with /P or with EncryptMetadata
security.authentication-failedErrorAn AES-GCM tag or the ISO/TS 32004 MAC does not verify: the bytes changed after encryption, and readers that check disagree with those that do not
security.revision-deprecatedWarningA document declaring PDF 2.0 encrypted with a revision other than 6
security.wrapper-documentInformationThe document is a wrapper; its content is the payload
form.xfa-out-of-stepErrorA hybrid or static form whose datasets and AcroForm values differ: Acrobat shows one, every other viewer the other
form.xfa-dynamicInformationThe pages are a placeholder; only an XFA processor shows the form
form.value-without-appearanceWarningA field has a value, a widget has no normal appearance, and NeedAppearances is not true: viewers that do not regenerate show nothing
form.field-name-duplicateWarningTwo terminal fields share a fully qualified name without being kids of one field
form.partial-name-periodWarningA partial name contains a period, which ISO 32000-2 forbids
form.radio-value-no-widgetWarningA radio group's or check box's /V names no widget's on-state
form.field-tree-cycleWarningThe field tree returns to a field already in it
form.usage-rightsInformationThe document is Reader-extended: a save outside its grants removes them

Diagnostics and report codes​

Reader diagnostics, in PdfDiagnosticCodes, disjoint from validation rule identifiers (ADR 36): encryption.password-encoding (information), encryption.content-not-decrypted (warning), encryption.padding-invalid (repair), encryption.permissions-mismatch, encryption.authentication-failed, encryption.mac-mismatch (warning), encryption.mac-unverified (information), encryption.public-key-handler (warning), encryption.wrapper-document (information). Every clean encrypted corpus document must open with information entries only.

Report codes, in M06's PdfOperationReport, each documented with what to do about it: permission.not-respected; encrypt.* (weak-algorithm, metadata-clear, conformance-claim-removed, accessibility-granted); fill.* (script-not-run, calculation-stale, value-rejected, value-coerced, format-not-applied, font-substituted, rich-text-simplified, read-only-overridden, appearance-approximated); xfa.* (kept-in-step, removed, placeholder-left); usage-rights.* (removed, broken-kept); flatten.*, extending M11's (scripts-removed, signed-field-kept); field.* (tag-appended, name-collision, action-refused); form-data.* (field-unknown, value-rejected, document-mismatch, ignored).

The command-line tool​

Passwords are never taken as an argument, which process lists and shell histories keep: from a file, from the environment variable --password-env names, or from standard input.

security FILE [--password-file F] [--json] handler, revision, methods, permissions, wrapper, MAC
encrypt FILE -o OUT (--random | --seed-file F) [--algorithm aes-256|aes-128|rc4-128|rc4-40]
[--user-password-file F] [--owner-password-file F] [--allow print,print-high,modify,copy,
annotate,fill,accessibility,assemble] [--metadata clear] [--attachments-only]
decrypt FILE -o OUT [--password-file F] [--ignore-permissions]
fields FILE [--password-file F] [--json] fields, values, scripts, XFA kind, usage rights
fill FILE (--json D | --xfdf D | --fdf D) -o OUT [--flatten] [--xfa keep|remove] [--full-rewrite]
fill FILE --records R.json|R.csv (-o DIR/{key}.pdf | --volume OUT) [--flatten]
export FILE --format xfdf|fdf|json [--annotations] [-o OUT]
flatten FILE --fields [NAME…] M11's verb, extended

Slices​

Each slice ends on a green commit, with its acceptance rows passing, the codes and rules it introduces documented, and the referee harness it adds running in CI.

  1. The standard handler, R2 to R4, and crypt filters. Delivers PdfCredentials, the password encodings, algorithms 1 to 7, RC4, AES-128-CBC, the decryption stage of M01's pipeline, what is and is not encrypted, crypt filters (V 4) with Identity, /Crypt and EncryptMetadata, the typed failures, the new meaning of ThrowOnEncrypted, the managed RC4 and AES-CBC and -ECB beside M06's MD5, and the platform matrix that chooses between them and the framework's. Adds expect.security (handler, revision, key length, methods, permissions) to the manifest and its schema, written by build_corpus.py from qpdf --show-encryption. Proven by unit tests — RFC 1321's MD5, RFC 6229's RC4 and NIST SP 800-38A's AES vectors, run on the managed code and on the framework's alike, keys derived from fixtures whose key qpdf --show-encryption-key reports, every exception of What is encrypted, FsCheck (decryption in chunks of any size equals decryption in one) — and on the corpus: the sixteen committed documents under R2, R3 and R4 open with their recorded password and meet every expectation of their entry; CorpusReadingTests stops expecting a refusal; the OPF documents match their unencrypted siblings; QpdfSecurityRefereeTests compare our decrypted object graph with qpdf --decrypt's, in a container. Leaves AES-256.
  2. AES-256 and permissions. Delivers R5 and R6 — algorithm 2.B, SASLprep, /UE and /OE, /Perms —, PdfPermissions, the policy and the ADR that makes respecting it the library-wide default, Demand and its calls in M06, M07, M09, M11 and M15's operations, and the security.permissions-mismatch rule. Proven by unit tests (RFC 4013's examples, a password of 128 bytes and one of 1 MB, a 16-bit /P, every bit under every revision) and by the AES-256 invoice matching the Chromium invoice it was derived from; every encrypted document's permissions equal qpdf's and pikepdf's; M09's stamp on the AR-11 refused by Modify, extraction from it refused by Copy, both written and reported under Ignore. Leaves PDF 2.0's additions.
  3. What PDF 2.0 adds, and what the core cannot open. Delivers AES-GCM and the MAC — after checking every constant against ISO/TS 32003 and 32004, and settling the MAC container question with ADR 41 —, PrimitiveUnavailable, the public-key detection and IPdfDecryptionKeySource, wrapper documents, M06's assembly.wrapper-placeholder, and the security.authentication-failed and security.wrapper-document rules. Proven by the fixtures of What it lacks — GCM and MAC files and their tampered copies, a public-key file opened through the test project's EnvelopedCms source, a wrapper — each matching its twin, and by pyHanko decrypting the public-key file with the same key in a container. Leaves writing.
  4. Encryption on write. Delivers Keep, Remove and Apply, PdfEncryptionOptions, PdfEntropy, the encryption stage of M03's writer, derived IVs, incremental updates under the input's key, the version rows, the PDF/A and PDF/UA rules, the ADR on randomness, and the encrypt, decrypt and security verbs. Proven by unit tests (FsCheck: any document encrypted then decrypted gives its object graph back, for every algorithm; IVs never repeat within a document; two runs with one seed give identical bytes) and by qpdf, pikepdf, poppler's pdfinfo, pdf.js and MuPDF opening what we encrypt with the same passwords and permissions. Leaves forms.
  5. The form model. Delivers PdfAcroForm, the field views, widgets, values, inheritance, names, scripts classified, the listing and the fields verb, and the rules form.field-name-duplicate, form.partial-name-period, form.radio-value-no-widget, form.field-tree-cycle, form.value-without-appearance. Extends expect.formFields, which M06 filled with names, to name, type and value, from qpdf's --json acroform key. Proven by unit tests (merged field and widget, inheritance at every level, a value as a stream, /Opt indices, a cycle, a tree a hundred thousand deep, a million kids, hostile JavaScript sources) and by qpdf, pikepdf, PyMuPDF and pdf.js's getFieldObjects() listing the 1,269 terminal fields of the 25 committed documents that have any as we do. Leaves XFA and usage rights.
  6. XFA and usage rights, read. Delivers the classification, the bounded packet reader, the datasets, the grants, form.xfa-dynamic, form.xfa-out-of-step and form.usage-rights; adds expect.form (XFA kind, usage-rights grants, script counts) to the manifest. Proven by unit tests (DTDs, entity expansion, a packet of 100 MB, a depth of a million, every grant) and by pdf.js's isPureXfa and pikepdf's reading of /NeedsRendering, the packets and /Perms on the four committed XFA forms, the HMCTS AcroForm's usage rights and the remote dynamic forms. Leaves writing forms.
  7. Filling and appearances. Delivers Fill, SetValue, every type's rules and refusals, the appearance generator, fonts and substitutes, EnsureAppearances, XFA KeepInStep and Remove, the usage-rights policy, fills under signatures through M04's guard, and the fill verb for JSON. Proven by unit tests (snapshots of appearance content bytes for each type, border and rotation; FsCheck: an auto-sized value fits its box for any string and box; a comb of any length), and by the round trip, rendering and referee rows below — MuPDF and pdf.js drawing our appearances alike within the threshold, pdftotext finding the values, pyHanko classifying a fill after signing as form filling. Leaves formats.
  8. Formats and calculations. Delivers the recognizer, the formats in appearances, keystroke and range checks, AFSimple_Calculate in /CO order, stale calculations reported. Proven by unit tests (FsCheck: format then parse gives the number back under every separator style; the recognizer accepts exactly the grammar, over a corpus of near-misses — a comment, two statements, a variable argument) and by AcrobatFormatRefereeTests, which run pdf.js's aform.js under Node in a container on every format and sum the corpus declares, and on generated values around each boundary. Leaves flattening.
  9. Flattening fields. Delivers Flatten through M11's flattener, selection by name, the AcroForm clean-up, signature fields, tagged flattening with PrintField, the loss reports, flatten --fields. Proven by rasterized comparison with the filled page, by qpdf --flatten-annotations=all --generate-appearances on the same fills, pikepdf finding no field, pdftotext finding the values in the page content, and veraPDF's PDF/UA-1 profile on the InDesign form. Leaves field creation.
  10. Creating fields and signature placeholders. Delivers PdfFormBuilder in both contexts, every type, caller-supplied appearances for every type, the actions allowed, tagging, AddSignatureField with /Lock and /SV. Proven by unit tests (collision policies, a field name with periods, every option, a caller's appearances written as given, per state for buttons) and by qpdf, pikepdf and pdf.js listing created fields, veraPDF on PDF/A and PDF/UA inputs, and pyHanko signing each placeholder in a container and finding the signature intact and the lock applied. Leaves exchange.
  11. FDF, XFDF and JSON. Delivers PdfFormData, the three formats both ways, annotations through M11's model, the JSON schema, the export verb and fill --xfdf|--fdf. Proven by unit tests (hostile XML, FDF with a cycle, embedded FDFs, JavaScript; FsCheck: export then import is the identity on values for any form we create) and by pdftk-java's fill_form, generate_fdf and dump_data_fields_utf8 in a container agreeing with us in both directions. Leaves batch filling.
  12. Batch filling, budgets and the remote corpus. Delivers PdfBatchFiller, the shared template analysis, the three output shapes, fill --records, SecurityBenchmarks and FormBenchmarks with MemoryDiagnoser, determinism across every operation of the milestone. Proven by the batch rows, the budgets recorded in status.md, CorpusToolTests, and a green Remote corpus run.

Tests required​

Unit — tests/AdCodicem.Pdf.Tests:

  • Security: every algorithm of ISO 32000-2 §7.6 against a known answer — published vectors for the primitives (RFC 6229, RFC 1321 and NIST SP 800-38A, on the managed code and the framework's; 800-38D; those of the key derivation ISO/TS 32004 names), keys taken from fixtures whose key an independent tool established for the handler; SASLprep on RFC 4013's examples; algorithm 2.B's round count over random inputs, never past 288; FsCheck — encrypt then decrypt is the identity for every algorithm, key length, string and stream length, chunking; IVs distinct across a document; the exceptions of What is encrypted one by one; /P under every revision and sign; every operation's Demand under each authentication and policy.
  • Hostile: an /Encrypt that is a stream, an /R of 99, a /Length of 7, /O of 5 bytes, a /CF naming itself, /StmF naming nothing, AES strings of 15 bytes, a GCM tag cut short, a /Recipients array at the object limit, a key source that throws or returns 1 MB, a wrapper with a thousand embedded files — each ends in a typed exception or a report, inside time and allocation budgets.
  • Forms: inheritance of every inheritable entry; merged fields and widgets; each field type's values and refusals; /Opt with and without pairs; radio states by index; names with UTF-16, PDFDocEncoding and periods; appearance snapshots by content bytes per type, border style, rotation, alignment, comb and auto size; font substitution and fallback; formats — every separator, negative and currency style, every picture token, every special format, leap days and years past 9999 refused —; calculations in /CO order with a source after its target, as Acrobat computes it; the recognizer against its grammar and its near-misses; XFA binding shapes, supported and not; usage-rights grants against every change class; flattening per mode on widgets; creation with every option and in both contexts; FDF, XFDF and JSON both ways.
  • Hostile forms: a field tree with a cycle, a depth of a million, a million kids; /DA of 1 MB; a JavaScript stream at the decoding limit; XFA with a DTD, an entity bomb, a depth of a million; XFDF alike; FDF with /Kids cycles — each a report or a typed exception, bounded in time and memory.
  • Determinism: every write of the milestone twice, identical bytes, under the invariant culture and fr-FR.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container (ADR 27):

  • qpdf — --show-encryption, --decrypt, --check with a password, --json (acroform, encrypt), and --flatten-annotations with --generate-appearances.
  • pikepdf — permissions, the field list, /Perms, XFA packets and datasets (read with lxml), /NeedsRendering.
  • poppler — pdfinfo (encryption, permissions), pdftotext on decrypted, filled and flattened documents — it extracts text drawn in widget appearances, as it does in DILA's notice —, pdftoppm.
  • MuPDF — mutool draw rendering, mutool info.
  • pdf.js, pinned pdfjs-dist under Node — opening with a password, getPermissions(), getFieldObjects(), isPureXfa, rendering with forms enabled; its aform.js run on our formats and sums.
  • PyMuPDF — widgets, their values and types, as a second listing.
  • pdftk-java — fill_form with our XFDF and FDF, generate_fdf, dump_data_fields_utf8.
  • pyHanko — decrypting the public-key fixture; signing our placeholders and existing empty fields; evaluate_modifications() on fills after signatures.
  • veraPDF — PDF/A at the claimed level and PDF/UA-1 after fills, flattening and field creation; encryption of a claiming document refused or its claim removed.

Images are compared in the referee container, as M11 does, at M09's cross-engine threshold.

Acceptance conditions​

"The encrypted documents" are the seventeen committed ones whose password the manifest records: under R2, vendor/us-federal/distiller2-irs-9465-1996-rc4-40.pdf, vendor/us-federal/pdfwriter3-copyright-office-dmca-summary-1998-rc4-40.pdf and the three vendor/opf-format-corpus/ibooks-author*-rc4-40-open-password.pdf; under R3, vendor/uk-ogl/pagemaker-distiller5-hmrc-iht205-form.pdf and the four vendor/opf-format-corpus/openoffice32-writer-rc4-128-*.pdf; under R4 with AES-128, vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf, vendor/jp-nta/indesign-distiller18-nta-gift-tax-vertical.pdf, vendor/pikepdf/livecycle-dod-dd293-aes128-xfa.pdf and the three vendor/opf-format-corpus/pdfmaker9-word-distiller-aes128-*.pdf whose password is empty; under R6, documents/secured/qpdf-invoice-aes256.pdf. "The AcroForms" are the committed documents with fields other than signatures, less the three veraPDF fixtures: documents/form/reportlab-subscription-form.pdf, the IRS Form 9465 and SS-4 (vendor/us-federal/distiller3-irs-ss4-1995-form.pdf), the HMCTS N208 (vendor/uk-ogl/indesign-acrobat-hmcts-n208-form.pdf), the HMRC IHT205, Cerfa 13983 (vendor/fr-licence-ouverte/libreoffice-cerfa-13983-form.pdf), Cerfa 12156 (vendor/fr-licence-ouverte/pdfmaker-acrobat-cerfa-12156-form.pdf), the InDesign PDF/UA-1 form (vendor/pdf-association/indesign15-pdfua1-form.pdf), OmniForm's RD 1924-5 (vendor/us-federal/omniform-usda-rd1924-5-hidden-widgets.pdf) and the I-9 (vendor/us-federal/designer-distiller23-uscis-i9-javascript-form.pdf).

DocumentsBehaviorVerified by
The encrypted documentsEach opens with its recorded password, as owner where the password is the owner's, and meets every expectation of its manifest entry — pages, cleanliness, diagnostics, findings; opening reads no more than a quarter of the fileCorpusReadingTests.Every_corpus_document_opens_as_its_manifest_describes, CorpusReadingTests.Opening_does_not_read_the_content_of
The sameThe object graph we decrypt equals the one qpdf --decrypt writes, object by object, streams by decoded bytesQpdfSecurityRefereeTests.Decryption_matches_qpdf
documents/secured/qpdf-invoice-aes256.pdf and documents/invoice/chromium-invoice-fr.pdf, from which it was derived; the three PDFMaker 9 AES files and vendor/opf-format-corpus/pdfmaker9-word-distiller-fonts-subset.pdf; the four OpenOffice RC4-128 files and vendor/opf-format-corpus/openoffice32-writer-simple.pdf; the three iBooks files and their unencrypted exports ibooks-author11-quartz-lorem-ipsum.pdf, ibooks-author11-quartz-lorem-ipsum-image.pdf, ibooks-author20-quartz-lorem-ipsum-image.pdf; the six others and their decrypted twins — not in the corpus (below)Every page's decoded content streams byte-identical to its twin's — the siblings' were checked equal with pikepdf when this milestone was written — and, for the iBooks exports, which were exported separately, the same text in pdftotextCorpusSecurityTests.Decrypted_content_matches_its_unencrypted_twin
vendor/opf-format-corpus/pdfmaker9-word-distiller-aes128-unknown-open-password.pdfWith no credentials, and with a wrong password, PdfEncryptedException with PasswordRequired, inside the time budget, no content read; never a page of noiseCorpusSecurityTests.An_unknown_password_is_refused_not_guessed
Remote: remote/pdfcpu-issues/adobe-sign-certified-aes128-test-agreement.pdf (identity crypt filter, metadata clear, a certification), remote/canada/livecycle-es10-ircc-imm1344-certified-dynamic-xfa.pdf, remote/canada/livecycle-es9-cfia-fish-export-license-dynamic-xfa.pdf, remote/opf-format-corpus/jhove-hul-109-distiller4-mac-word-transportation-report.pdf, remote/opf-format-corpus/jhove-hul-114-livecycle-es10-ircc-imm5257-certified-xfa.pdfEach opens and meets its expectations; the Adobe Sign file's XMP reads without decryption; its signature's /Contents is left as written, its coverage unchanged, and its change classification — NotAnalyzed in M04 — now given, as pyHanko gives itCorpusSecurityTests.Remote_encrypted_documents_open_and_their_signatures_are_analyzed
Every encrypted documentPermissions equal qpdf --show-encryption's and pikepdf's, including the 1996 form's 16-bit /P 65516 and the R2 files' accessibility, which follows their copy bitQpdfSecurityRefereeTests.Permissions_read_as_qpdf_reads_them
The encrypted documents opened as user: the AR-11 and DD 293 (/P −1052: no copying, no modification, no assembly), the IHT205 and the NTA return (−1068: filling but no annotation), the PDFWriter 3 summary (−12), openoffice32-writer-rc4-128-no-copy.pdf and pdfmaker9-word-distiller-aes128-no-copy.pdf (−1044), the PDFMaker files that forbid accessibility (−1556) or printing (−3080)Under Respect, M15's extraction, M09's stamp, M11's note and M06's page removal each throw PdfPermissionException exactly where /P forbids them, and run where it allows them — filling the IHT205 through bit 9; under Ignore, each runs and reports permission.not-respectedCorpusSecurityTests.Operations_respect_the_permissions_they_need
The earlier milestones' acceptance tests that left encrypted documents out: M03's round trip, M04's classification, M06's listings and its layer row (CorpusAssemblyTests.Layers_keep_their_identity_and_visibility, on the AR-11 original rather than its twin), M08's font rows (CorpusFontTests.Every_embedded_font_program_reports_the_advances_freetype_reads over the 36 programs of the 17 encrypted documents, CorpusFontTests.Standard_14_widths_are_the_producers_widths on the IHT205's widths), M09's stamps (the dynamic XFA forms' stamp.dynamic-xfa row among them), M10's codes, M11's annotations and layers — the AR-11's layer rows on the original rather than its twin —, M15's extractionEach runs on the encrypted documents its permissions allow: the round trip keeps /Encrypt and the first /ID element and qpdf opens the result with the same password; the refusal rows that named M16 become permission rowsCorpusRoundTripTests.Encrypted_documents_round_trip_under_their_own_key, CorpusStampTests.Encrypted_documents_are_stamped_under_their_permissions, and each named test with its exclusion lifted
A public-key-encrypted document and its test key — not in the corpus (below)Without a key source, PdfEncryptedException with PublicKeyHandler and encryption.public-key-handler naming the subfilter and the recipients; never content. With the test project's EnvelopedCms source, the content matches its twin, and pyHanko decrypts it to the same objectsCorpusSecurityTests.Public_key_encrypted_documents_are_reported_never_misread, PyHankoSecurityRefereeTests.The_key_source_seam_opens_what_pyhanko_opens
AES-GCM and ISO/TS 32004 documents, standalone and attached MAC, their twins and their tampered copies — not in the corpus (below)The sound ones open and match their twins; a flipped byte inside a string withholds that object and reports encryption.authentication-failed; one outside every string fails the MAC, encryption.mac-mismatch; the attached MAC is reported unverified; validation reports security.authentication-failed on both tampered filesCorpusSecurityTests.Authenticated_encryption_withholds_what_fails_its_tag
AES-256 R5, R6 under non-ASCII passwords, attachments-only encryption, a /Crypt stream filter, metadata left clear — not in the corpus (below)Each opens with its password, in the encoding reported, and matches its twinCorpusSecurityTests.Every_revision_and_crypt_filter_opens
A wrapper document — not in the corpus (below)encryption.wrapper-document and security.wrapper-document; the payload listed and extractable as an attachment; M06's assembly reports the wrapper instead of merging its cover pageCorpusSecurityTests.Wrapper_documents_are_recognized_and_their_payload_exposed
documents/invoice/chromium-invoice-fr.pdf, libreoffice-invoice-fr.pdf, reportlab-invoice.pdf, documents/report/chromium-report-fr.pdf, documents/contract/chromium-contract-fr.pdf, vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf, documents/invoice/qpdf-invoice-with-facturx-xml.pdf (attachments only)Encrypted under each algorithm, with restricted permissions: qpdf, pikepdf, pdfinfo, pdf.js and MuPDF open each with the same passwords and report the same permissions; qpdf --decrypt gives back the input's object graph; the PDF/UA chapter keeps accessibility granted and passes PDF/UA-1 in veraPDF once decryptedQpdfSecurityRefereeTests.Documents_we_encrypt_open_with_the_same_password_and_permissions
documents/archival/libreoffice-report-pdfa2b.pdf (PDF/A-2b), vendor/zugferd/mustang-zugferd2-en16931-invoice.pdf (PDF/A-3u)Apply throws PdfConformanceException under Refuse; under RemoveClaim the file is encrypted, the claim gone, the loss reportedCorpusSecurityTests.Encrypting_a_pdfa_document_is_refused_unless_the_claim_may_go
The encrypted documents that grant annotation or fillingA note (M11) or a fill written as an incremental update stays encrypted under the same key and /ID; the original bytes are a prefix of the output; qpdf reads the update with the passwordCorpusSecurityTests.An_update_to_an_encrypted_document_stays_under_its_key
The documents authenticated as owner — the AES-256 invoice, the IRS 9465, the iBooks files, the two OpenOffice files behind an open password — and the ten othersRemove gives what qpdf --decrypt gives on the owner-authenticated ones; on the others it is refused under Respect, and written and reported under IgnoreQpdfSecurityRefereeTests.Removing_encryption_needs_the_owner_or_a_declaration
documents/stress/reportlab-journal-1000-pages.pdf, and its AES-256 and RC4-128 derivatives — not in the corpus (below)Encrypting and decrypting hold memory flat as pages grow; decrypting a page allocates nothing per block beyond pooled buffers; budgets recorded in status.mdCorpusSecurityTests.Encryption_holds_its_budget, SecurityBenchmarks
Every document with fields — 1,269 terminal fields in 25 committed documents, and remotely the OPM OF-306 (remote/opm/word365-acrobat-opm-of306-form.pdf)Every field listed with its fully qualified name, type, flags, value, options and widgets as qpdf's --json and pikepdf list it, PyMuPDF and pdf.js's getFieldObjects() agreeing; expect.formFields metCorpusFormTests.Every_field_reads_as_referees_read_it
The committed XFA forms — Cerfa 14880 (vendor/fr-licence-ouverte/livecycle-es9-cerfa-14880-xfa-form.pdf), the 2022 Form 1040 (vendor/us-federal/livecycle-irs-1040-2022-xfa-ur3.pdf), DD 293, AR-11 —; remote, the two Canadian dynamic forms, the IMM 5257, and the filled XFA form inside remote/opf-format-corpus/acrobat9-portfolio-signed-3d.pdf, opened as M07 unpacks itStatic, hybrid, hybrid, hybrid, dynamic, dynamic, and what pdf.js makes of the IMM 5257; pdf.js's isPureXfa true exactly for the dynamic ones; the datasets read equal the packet pikepdf returns, the portfolio member's with its fictitious values; expect.form metCorpusFormTests.Xfa_forms_are_classified_and_their_datasets_read
The AcroForms and the XFA formsEvery script listed by field and event, recognized or not: the IHT205's 36 AFNumber_Format and 36 keystrokes in pounds and its dd/mm/yyyy date recognized, its seventeen custom calculations and its other scripts reported; Cerfa 12156's eight date formats and 26 AFSimple_Calculate sums recognized, its template spawn reported; Cerfa 13983's two date formats; the I-9's AFSpecial_Format zip and telephone recognized and its custom validation and check-box scripts reported; OmniForm's three AFNumber_Format in dollars recognized and its fmt_* functions reported — per field and event, as a referee script over the /JS sources, in pikepdf, counts themCorpusFormTests.Scripts_are_recognized_or_reported_never_run
documents/form/reportlab-subscription-form.pdfFilled — nom "Noailles", prenom "Élodie", societe "Étude Noailles & Associés", courriel, newsletter checked —, saved, reopened: the values read back are the values written, and qpdf, pikepdf, PyMuPDF and pdftk read the same; MuPDF and pdf.js render them within the threshold of each other; after flattening, pdftotext finds them in the page content, the page renders as the filled one did, and pikepdf finds no field and no widgetCorpusFormTests.The_form_document_round_trips_through_fill_save_and_flatten
The AcroForms, each field filled with a value of its typeEvery value reads back, in our reader and in qpdf, pikepdf and PyMuPDF; every widget has an appearance; the documents that had no NeedAppearances still have none; pdftotext finds every text valueCorpusFormTests.Every_acroform_fills_and_reads_back
The same fills on the widgets that had no appearance — 32 of the IRS 9465's 34, 48 of the SS-4's 83, 15 of the N208's 29, 9 of Cerfa 13983's 20, 185 of Cerfa 12156's 419, 110 of OmniForm's 216, 3 of the InDesign form's 9Our appearances render within the threshold of pdf.js's own drawing of the same values, and qpdf's --generate-appearances output for text fields; no ZapfDingbats or other non-embedded font in any appearance we writeCorpusFormTests.Generated_appearances_draw_as_viewers_draw_them
The IHT205's pound amounts, Cerfa 12156's and 13983's dates, the I-9's zip and telephone, OmniForm's dollar amounts, each given typed values and text values; generated values at every boundaryThe appearance shows exactly what pdf.js's aform.js computes for the same value and arguments; /V keeps the unformatted value; a value a keystroke function rejects is refused and namedAcrobatFormatRefereeTests.Formats_are_applied_as_pdfjs_applies_them
Cerfa 12156, its sources filled; the IHT205, its sources filledThe 26 sums recomputed in /CO order equal pdf.js's AFSimple_Calculate on the same values; the IHT205's seventeen calculated fields left as they were and each reported fill.calculation-staleCorpusFormTests.Simple_sums_are_computed_and_other_calculations_reported
The 2022 Form 1040, and DD 293, AR-11 and Cerfa 14880 once decrypted where they are encrypted; the N208 (usage rights, no XFA)Filled incrementally under KeepInStep: the datasets hold each filled value at its bound node, as lxml reads pikepdf's packet, and validation reports no form.xfa-out-of-step; the usage-rights signature still covers its revision (M04) and every later change is FormFill, within the grants. Filled by a full rewrite: /UR3 gone and usage-rights.removed reported. Under Remove: no /XFA, xfa.removed. An unresolvable binding removes the XFA and names the field. Silence fails the testCorpusFormTests.Filling_a_hybrid_or_reader_extended_form_keeps_it_coherent_or_reports_the_loss
Remote: the two Canadian dynamic formsA fill throws PdfFormException naming ADR 37; their datasets read; removing their XFA reports xfa.placeholder-left; for the certified IMM 1344, removing its usage rights is refused, since it would break the certificationCorpusFormTests.Dynamic_xfa_is_read_and_reported_never_filled
A DocMDP P=2 certified form, and one with a FieldMDP lock — not in the corpus, M04's gap (below); vendor/us-federal/itext-govinfo-us-code-certified.pdf (P=1)A fill of an unlocked field is written as an update, and pyHanko finds the certification intact at form-filling level; a locked field refused with PdfSignatureInvalidationException; a field created on the P=1 document refused likewise; each written and reported under AllowInvalidatingSignaturesPyHankoFormRefereeTests.Filling_a_certified_form_stays_within_its_permissions
Cerfa 13983 (empty Signature1 and Signature2), DD 293 and Cerfa 12156 (one empty signature field each); remote, the OPM OF-306 (two)Filled, then signed by pyHanko in a container in its own revision: the signature is intact, and the fill lies inside the signed revisionPyHankoFormRefereeTests.Filled_forms_sign_in_their_empty_fields
The filled AcroForms — not in the corpus, derived by pdftk from recorded values (below)Flattened: the pages render as the filled ones within the threshold, and as qpdf --flatten-annotations=all --generate-appearances renders them; pdftotext finds every value; pikepdf finds no field; OmniForm's hidden widgets appear nowhere; each lost script reportedCorpusFormTests.Flattening_keeps_what_the_fields_showed
vendor/pdf-association/indesign15-pdfua1-form.pdf, filled then flattenedveraPDF's PDF/UA-1 profile reports no failure the input did not have; each former field's Form element holds its content and a PrintField attributeVeraPdfFormRefereeTests.Filling_and_flattening_a_pdfua_form_keeps_it_accessible
The filled AcroForms and their blank originals — the subscription form, the SS-4, the N208, Cerfa 13983, Cerfa 12156, the InDesign form, the I-9XFDF exported from each filled form imports into its blank form and reproduces every value, as qpdf's --json and pdftk's dump_data_fields_utf8 read them; pdftk's fill_form with our XFDF and our FDF gives the same values; our import of pdftk's generate_fdf output gives them too; JSON likewisePdftkFormRefereeTests.Xfdf_exported_from_a_filled_form_refills_the_blank_one
vendor/opf-format-corpus/reader10-openoffice32-annotated-object-streams.pdf (a note and a highlight by AnJackson), M11's annotated setTheir annotations exported to XFDF and FDF and imported into the unannotated document reproduce each annotation's entries as pikepdf lists themCorpusFormDataTests.Annotations_travel_in_xfdf_and_fdf
documents/contract/chromium-contract-fr.pdf (untagged), indesign13-pdfua1-german-book-chapter.pdf (tagged), libreoffice-report-pdfa2b.pdf (PDF/A-2b)A field of every type and a signature placeholder created on each: qpdf, pikepdf and pdf.js list them with their names, types, values and options; veraPDF finds no new PDF/UA-1 or PDF/A-2b failure; pyHanko signs the placeholder, finds the signature intact and the lock appliedCorpusFieldCreationTests.Created_fields_are_listed_signable_and_keep_claims
The subscription form as a template, a thousand recordsOne file per record as an incremental update, a thousand files as full rewrites, one flattened volume: memory flat as records grow, throughput recorded in status.md; a sample of records read by pdftk with its values; each record's bytes the same alone and in parallelCorpusBatchFillTests.A_thousand_records_hold_their_budget, FormBenchmarks
Remote: remote/eu-dss/dss-signed-widget-self-parent-startxref-past-eof.pdf (a widget that is its own /Parent), remote/pdfbox/acrobat-form-nul-in-names-pdfbox6178.pdf (an export value /m#00nnlich)The cycle reported by form.field-tree-cycle, no hang; the radio group filled with its NUL-bearing value and written back byte for byte, pdf.js selecting the same buttonCorpusFormTests.Hostile_field_trees_and_names_end_in_a_report
Every write aboveTwo runs give identical bytesCorpusSecurityTests.Encryption_is_deterministic_under_a_seed, CorpusFormTests.Form_operations_are_deterministic
The same operations through the toolThe AOT binary produces what the API producesCorpusToolTests.Security_and_form_verbs_match_the_api

The remote rows close only on a green Remote corpus run, recorded in status.md with its date.

Corpus​

What the corpus holds​

  • Encryption, committed: eighteen documents — RC4-40 under R2 from Distiller 2 and Acrobat (1998, a 16-bit /P), PDFWriter 3 and Quartz (iBooks Author); RC4-128 under R3 from Distiller 5 (PageMaker) and OpenOffice.org 3.2, with and without an open password and with copying forbidden; AES-128 under R4 from LiveCycle, InDesign and Acrobat 9, the last forbidding copying, accessibility or printing, and one whose open password nobody published; AES-256 under R6 from qpdf. Features encryption-*, encrypted, empty-user-password, open-password, permissions-*, open-password-unknown. Twins: the Chromium invoice for the AES-256 file, and same-content siblings for ten more — the PDFMaker 9 test document, OpenOffice's one-line document, the iBooks exports.
  • Encryption, remote: five more — Adobe Sign's certification under AES-128 with an Identity crypt filter and EncryptMetadata false (identity-crypt-filter, encrypt-metadata-false), two Canadian dynamic XFA forms, two JHOVE files; signatures inside encrypted files (signed-and-encrypted).
  • Forms: 25 committed documents with fields, 1,269 terminal fields — text (comb in five documents, multiline, one rich-text field), check boxes, two radio groups, combo boxes in five, push buttons, signature fields; /DR fonts standard, non-standard and embedded; a calculation order in the IHT205 (17 entries) and Cerfa 12156 (26); AFNumber, AFDate, AFSpecial and AFSimple_Calculate scripts and custom ones; widgets without an appearance in fifteen documents; field tooltips, a merged field and widget, a radio group that cannot be toggled off in the PDF/UA-1 form. Features acroform, calculated-fields, calculation-order, format-keystroke-javascript, field-javascript, widgets-without-appearance-streams, hidden-widgets, field-tooltips, merged-field-widget, unsigned-signature-field.
  • XFA and usage rights: four committed XFA forms — one static (XFA foreground), three hybrid —, all four Reader-extended, plus the HMCTS AcroForm with usage rights; remote, two dynamic forms (one certified, one with legacy /UR), a certified XFA form with usage rights, and a filled XFA form inside a portfolio. Features xfa, xfa-static, xfa-acroform-hybrid, xfa-dynamic, usage-rights-ur3, usage-rights-reapplied, ur-signature-without-byterange.
  • Signatures around forms: the GPO's P=1 certification; empty signature fields in Cerfa 12156, 13983, DD 293 and, remotely, the OF-306; approval-signed documents.
  • Hostile shapes: a field tree with a cycle and a NUL in an export value, both remote.
  • Scale: the 1000-page journal.

What it lacks​

NeedWhyPriorityLikely source
Decrypted twins of the six encrypted documents with no unencrypted sibling: IRS 9465, the PDFWriter 3 summary, the IHT205, the NTA return, AR-11, DD 293"Content matches the twin it was derived from" has nothing to compare with on these; M11 already needs the AR-11's1Derived here: qpdf --decrypt, a recorded transformation in build_corpus.py
A document encrypted with the public-key handler — adbe.pkcs7.s4 and s5, RC4-128, AES-128 and AES-256 —, with its test certificate and key, and its twinThe roadmap's "a public-key-encrypted one is reported, never misread" has no document; M26's decryption needs the same1A public source: pyHanko's test data (MIT), to be screened for a public-key file; failing it, generated here with pyHanko's public-key handler over its fictitious test PKI; an Acrobat certificate-security file as a contribution
AES-GCM (ISO/TS 32003) and MAC (ISO/TS 32004) documents — standalone and attached to a signature —, their twins, and tampered copiesReading them is a deliverable, and invariant 10 wants it proven on a file another implementation wrote1Generated here with an independent writer: pyHanko if its pinned version writes them, iText 9 in a container otherwise, as a producer only; tampered copies derived by a recorded byte flip
AES-256 under R6 with non-ASCII passwords — accented, German, CJK, and one that SASLprep maps —, and under R5SASLprep and the R5 path are untested without them1 for R6, 2 for R5Generated here: pikepdf with Unicode passwords, qpdf --force-R5; an Acrobat-made R6 file with an accented password as a contribution
Filled versions of the committed AcroForms, with fictitious values, filled by an independent tool; the same hybrid forms filled through their AcroForm onlyThe XFDF row needs "a filled corpus form"; flattening needs third-party fills; a pypdf fill that leaves the XFA stale is form.xfa-out-of-step's document1Derived here: pdftk-java's fill_form from a recorded XFDF, and pypdf for the stale hybrids, each recorded in build_corpus.py; a form filled in Acrobat as a contribution (W08, already wanted)
A DocMDP P=2 certification followed by a fill, a FieldMDP lock over some fields, and the same before the fillThe permitted and forbidden fills of a certified form have no document — M04's gap too1Generated here: pyHanko's certify, fill and sign over its fictitious PKI, as M04 plans
Attachments-only encryption, a /Crypt filter on one stream, and EncryptMetadata false committedThe committed corpus has none; the remote Adobe Sign file covers the last only on the nightly run2Generated here by a recorded pikepdf construction checked by qpdf; Acrobat's "encrypt only file attachments" as a contribution
An unencrypted wrapper documentRecognition must be proven on a file shaped as Microsoft Purview writes them, not only on ours2Generated here following ISO 32000-2 Table 28 by a recorded pikepdf construction; a Purview-protected file with fictitious content as a contribution
The 1000-page journal encrypted under AES-256 and RC4-128The memory and throughput budgets of decryption need a large encrypted document2Derived here with pikepdf, recorded
List boxes with multi-select, push buttons with SubmitForm and ResetForm, a rich-text field with a value, password and file-select fields, AFPercent, AFTime, AFRange_Validate, AFSpecial_KeystrokeExNone of these is committed2Generated here: LibreOffice's form export (list boxes, buttons, formatted fields) and a recorded ReportLab or pikepdf construction; Acrobat-made forms as a contribution (W08)
A Reader-extended form filled and saved by Adobe Reader, and an XFA form filled and saved by Acrobat, committedWhat an incremental fill within usage rights looks like, and whether Acrobat re-merges datasets, are asserted structurally only; no container runs Reader2A contribution (W08)
FDF and XFDF files written by Acrobat and by pdftkThe exchange formats have no committed samples; they enter the manifest with M07's fdf and xfdf formats and their block — the fields and values an independent tool reads —, beside the form they belong to2Generated here with pdftk-java; Acrobat's as a contribution
A calculation in Acrobat's simplified field notationRecognizing it is deferred until a real document carries one3A contribution (W08)
Right-to-left and CJK values in text and comb fields of a third-party formThe shaping report is checked on our own fills only3A contribution (W09)
A form of ten thousand fieldsThe fill and appearance budgets need a form larger than Cerfa 12156's 419 widgets3Generated here with ReportLab, recorded

Traps​

  • An empty user password is the common case: eleven of seventeen. Try it first, silently, and report nothing for it — every viewer does the same, and a "clean" document stays clean.
  • Not everything is encrypted: /Encrypt itself, /ID, cross-reference streams, objects inside object streams, a signature's /Contents, metadata when EncryptMetadata is false, and whatever Identity covers. Decrypting one of them turns a sound file into noise.
  • /P is a 32-bit signed integer written by producers that did not all know it: the 1996 IRS form writes 65516. Read the low 32 bits; never compare the whole integer.
  • R2 has no bits 9 to 12: its accessibility and assembly follow its copy and modify bits, as qpdf reads it.
  • Owner authentication is all or nothing. A document whose owner password is its user password gives every caller the owner's rights — and one written by us that way would make its permissions a pretense.
  • Passwords are bytes, not strings: PDFDocEncoding up to R4, UTF-8 after SASLprep from R5; producers got it wrong both ways. SASLprep needs NFKC, which string.Normalize takes from ICU — absent under invariant globalization and in browser WebAssembly: measure it there.
  • The primitives are not everywhere: .NET has no RC4, browser WebAssembly no MD5 and no AES, and AesGcm.IsSupported can be false. The core's managed MD5, RC4 and AES cover the first three; GCM missing is a typed refusal, never a crash.
  • A CBC IV must be unpredictable, and output must be deterministic. Both hold only if the IV comes from a function keyed by the file key; a counter or a hash of public data breaks the first, the system generator the second.
  • AES grows what it encrypts: 16 bytes of IV and up to 16 of padding. A stream's /Length counts them; a forward-only writer only knows it at the end — which is what the indirect /Length is for.
  • Empty strings: producers write them unencrypted, zero bytes long, under AES. Decrypt what can be, report the rest, show it empty — as viewers do.
  • A GCM failure is not a decryption error: it says someone changed the bytes. Returning the plaintext anyway is the one thing that must never happen.
  • Filling a hybrid form through its AcroForm leaves Acrobat showing the XFA's data: stale or empty. Keep the datasets in step or remove the XFA; never leave both disagreeing, and never in silence.
  • Dynamic XFA pages are placeholders. A fill, a stamp or a merge of them is invisible to the only viewer that renders them; removing their XFA leaves "please wait" as the whole document.
  • Usage rights die of any byte they do not expect. A full rewrite always breaks them; an incremental update keeps them only within their grants. And removing them from a certified document is itself a forbidden change.
  • A format is the appearance's, not the value's. /V keeps 1234.5; writing £1,235 there breaks every calculation that reads it.
  • Calculations follow /CO, once, not their dependencies: a sum listed before its source is stale in Acrobat too. Emulate Acrobat, not what would be right.
  • Separator styles change parsing, not only printing: under style 2, 1.234 is one thousand two hundred and thirty-four.
  • A recognized function must be the whole script. AFNumber_Format(2, 0, 0, 0, "", true); foo(); is a custom script that happens to begin with a standard call.
  • ZapfDingbats is not embedded in any committed form's /DR, and every check box drawn in it is a PDF/A failure the next time the form is filled. Draw the symbols as paths.
  • A subset in /DR cannot type new characters, and a viewer regenerating with it shows boxes. Draw with the subset what it has, fall back for the rest, and leave /DA to name what the author chose.
  • /Tx BMC … EMC is not decoration: viewers that regenerate an appearance look for it to know what to replace.
  • A password field's value must never be stored, whatever the caller asks.
  • Check-box on-states are not always Yes: read them from /AP /N. Radio export values may be indices into /Opt, and names may carry #00.
  • A widget can be its own field, a field's /V can be a stream, and /DA is inherited from the AcroForm dictionary when no field says otherwise.
  • FDF is PDF syntax and can carry JavaScript; XFDF and XFA are XML from strangers. Parse all three with every bound on, and execute nothing.
  • Flattening a signed signature field forges a picture of a signature. Keep it interactive.
  • No container runs Acrobat or Reader. What only they can say — that usage rights still hold, that datasets were re-merged — is asserted structurally and recorded as such, never claimed.

Documentation​

  • docs/website/docs/guides/encryption.md — opening protected documents, credentials and encodings, the permission policy, encrypting and its options, entropy and determinism, decrypting, updates of encrypted documents, what the core cannot open and where the signing satellite takes over; the managed MD5, RC4 and AES the core uses where the platform has none, and that they are not constant-time.
  • docs/website/docs/concepts/security-handlers.md — revisions, crypt filters, what is and is not encrypted, AES-GCM and the MAC, wrappers, the key-source seam.
  • docs/website/docs/guides/forms.md — reading, filling, appearances and fonts, formats and sums without scripts, flattening, creating fields, signature placeholders, signed and certified forms.
  • docs/website/docs/concepts/xfa-and-usage-rights.md — the three kinds of XFA and what each operation does to them; usage rights, their grants and when a save removes them.
  • docs/website/docs/guides/form-data-exchange.md — FDF, XFDF and JSON, the JSON schema, batch filling.
  • docs/website/docs/reference/acrobat-formats.md — every recognized function, its arguments, what we render, and each known difference from Acrobat and pdf.js.
  • docs/website/docs/reference/security-and-form-codes.md — every encryption.* diagnostic and every permission.*, encrypt.*, fill.*, xfa.*, usage-rights.*, flatten.*, field.* and form-data.* code.
  • docs/website/docs/reference/validation-rules.md — the security.* and form.* rules added.
  • docs/website/docs/reference/tool/ — security, encrypt, decrypt, fields, fill, export, flatten --fields.
  • docs/website/docs/introduction.md and docs/features/features.json — the encryption and forms features delivered.
  • docs/architecture.md — Security/ and Forms/ in the core, the key-source seam to AdCodicem.Pdf.Signing, the encryption stage of the writer.
  • docs/corpus.md — expect.security, expect.form, expect.formFields as records; where exchange files live.
  • ADRs: randomness in encryption is the caller's input (slice 4); the primitives on platforms that lack them (slice 1); document permissions respected by every operation by default, overridden only by a reported declaration (slice 2); ADR 41 amended if the ISO/TS 32004 MAC cannot be read without a general CMS parser (slice 3).

Exit criteria​

  • Every revision of the standard handler opens, crypt filters included; AES-GCM and the MAC are read; the public-key handler is reported and its seam proven; wrappers are recognized.
  • Permissions are decoded, respected by every operation of M06, M07, M09, M11, M15 and M16, and overridable only by a reported declaration.
  • Encryption is written by every algorithm, deterministically, with its crypt-filter options, as a full rewrite and as an update under the input's key.
  • Forms are read, filled with generated appearances, formatted and summed without a script, flattened, created with signature placeholders, and exchanged as FDF, XFDF and JSON; batch filling works.
  • XFA is classified, kept in step or removed on fill; usage rights are kept within their grants or removed and reported; dynamic XFA is never filled.
  • Every earlier acceptance test that left encrypted documents out runs on them.
  • The version rows are in M03's table and tested against the Arlington model's SinceVersion.
  • The security.* and form.* rules are in the structural profile, documented, and every manifest entry declares what they report on it.
  • The priority-1 gaps above are filled; each remaining gap is recorded in docs/corpus-contributions.md.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a green Remote corpus run recorded in status.md.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones; the security handler, the XFA, XFDF and FDF parsers and the script recognizer are fuzzed, seeded from the corpus, in a nightly campaign.
  • Integration tests confirm decryption, encryption, permissions, fields, appearances, formats, XFA, fills after signatures, flattening and exchange through qpdf, pikepdf, poppler, MuPDF, pdf.js, PyMuPDF, pdftk-java, pyHanko and veraPDF, each in a container.
  • SecurityBenchmarks and FormBenchmarks measure decryption, encryption, filling and batch filling with MemoryDiagnoser; status.md records the budgets.
  • The documentation site publishes the guides, concepts and references above.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).