Skip to main content

M21 — Converting received documents to PDF/A

State: to do — Depends on: M05, M11, M19, M20 — Driven by findings, as ADR 22 drives repair; active content removed as ADR 37 says

Goal​

Turn a received document into PDF/A-2 or PDF/A-3 — at level b, u or a, as far as its content allows — through remedies each justified by a finding of M20's profile, and hand back with it an honest account: what was changed without a visible difference, what now looks different and where, what was removed and what was kept aside as an attachment, and what could not be converted and why. The output claims only a level M20's profile passes on it.

A case file of third-party exhibits is archived, filed with a court or handed to an auditor as PDF/A, and the exhibits never arrive that way: Word's contract schedule leaves Arial out, an HMRC form computes its totals in JavaScript behind an RC4 key, a Cerfa is an XFA form whose usage rights were applied twice, a PDFMaker test document plays a QuickTime movie, the EU's consolidated regulation blends its pages in DeviceRGB under a CMYK output intent while claiming PDF/A-1a, and PDF24's invoice draws a capital E from a glyph its subset does not contain. A converter that rewrites all of that into something veraPDF accepts, without saying that the fonts are substitutes, that the form no longer calculates and that the movie is gone, hands the archivist a document that is valid and wrong. This milestone converts what can be converted, keeps aside what PDF/A will not have on a page, and refuses the rest by name.

Scope​

In:

  • PdfAConverter in the AdCodicem.Pdf.Conformance satellite: analyze (M20's findings for the target), plan (a remedy per finding, each with a class saying what it does to the document), convert, and report — with M20's verdict on the result;
  • targets PDF/A-2b, 2u, 3b and 3u, and 2a and 3a when the input's structure passes M20's level-a rules; an explicit fallback when a level cannot be reached;
  • the remedies — file structure and identification, metadata rebuilt, decryption, an output intent and default color spaces, fonts embedded from the registry or substituted by metric-compatible faces, ToUnicode derived for level u, forbidden image, graphics-state and XObject features removed, LZW re-encoded, actions and scripts and XFA and usage rights removed (M19, M16), appearances generated (M11, M16), annotation flags and optional content configurations corrected, attachments given their MIME type and relationship (part 3) or converted recursively (part 2);
  • content kept aside: under part 3, the media and 3D models a page can no longer play are attached to it as associated files, their annotations flattened to their appearances (M11); the original file attached as Source, and the conversion report as Supplement, when the caller asks;
  • remedy primitives in the core, reused later: a font program embedded into an existing font dictionary, ToUnicode derived from an encoding, default color spaces (shared with M20's merge), a stream re-encoded;
  • signed and certified inputs under M04's classification: a conversion by incremental update that leaves every signed revision as it was, a certification that refuses, and nothing voided in silence;
  • case files: pieces converted, then assembled under M20's Conformance.Auto; a piece that cannot be converted attached rather than dropped;
  • the tool: pdfa analyze, pdfa convert, casefile --pdf-a.

Out, explicitly:

  • PDF/A-1 as a target — part 1 forbids transparency, which only rasterization could flatten, and nobody asks for new part-1 archives; not planned;
  • PDF/A-4 as a target — M28 generates part 4 from our own content; converting received documents to it would be new rows in this milestone's remedy table, and waits for a caller who asks. Not planned;
  • re-encoding a JPEG 2000 image whose channels, bit depth or color specification break part 2's clause — it needs M22's decoder; until then such a document is not convertible, and says so;
  • a text layer for scans so that they reach level u — M22's OCR text layer; a scan converts at level b;
  • tagging an untagged document to reach level a — not in the roadmap (one of its open questions);
  • color conversion — rewriting CMYK as RGB or through ICC transforms is M29's color management; M21 only states, through profiles, what the device color already meant;
  • repairing structural damage — M05's, which conversion calls first and reports separately;
  • removing the bytes of earlier revisions — sanitization, M19, always a full rewrite; a conversion by incremental update leaves them, and its report says so;
  • public-key-encrypted inputs — decrypted by AdCodicem.Pdf.Signing (ADR 41, M26); until then not convertible;
  • dynamic XFA — its content is the XFA; not convertible, never rendered (ADR 37);
  • executing a form's scripts to settle its values before they are removed — never (ADR 37).

Dependencies. The roadmap gives M05 (repair), M11 (appearance generation and flattening), M19 (the sanitization pipeline) and M20 (the findings, the profiles, the targets, default color spaces and merge preservation). The remedies also call M08's font parsers, registry and OFL set, M15's interpreter (codes drawn per font, color used) and M16's decryption, form model, XFA and usage-rights handling, both reached through M19. From earlier: M03's writer and its modes, M04's signature classification, M06's attachments and assembly, M14's XMP model and associated files.

Design​

Where it lives​

PartWhereWhy
PdfAConverter, the remedy registry, the plan and the reportAdCodicem.Pdf.Conformance, Conversion/It is driven by the satellite's findings, as PdfRepair is by the core's
Embedding a program into an existing font dictionary, deriving ToUnicode, default color spaces, re-encoding a stream, correcting an annotation's flagsCore, as public operations on the document, beside the code each edits (fonts, color spaces, filters, annotations)Edits of the object model the writer owns; M20's merge uses default color spaces, M23 re-encoding
Removing actions, scripts, XFA, media and forbidden annotationsCore, M19's pipeline and categoriesADR 37 makes sanitization the one place active content is removed
Decryption, forms, usage rightsCore, M16
Appearances, flatteningCore, M11 and M16

The shape​

PdfAConverter stateless, thread-safe: Analyze(document, options) -> PdfAConversionPlan;
ConvertAsync(plan, output, cancellation, progress) -> PdfAConversionReport
PdfAConversionOptions immutable: Target (PdfA2B by default); LevelFallback (None, ToUnicodeLevel, ToBasicLevel);
Mode (Auto, Incremental, Rewrite); Fonts (the registry, and whether the OFL substitutes
may be used); OutputIntent (the built-in sRGB, or the caller's profile); DefaultSpaceProfiles
(CMYK and gray profiles the caller supplies); Attachments (Keep, Convert, Drop, Refuse);
KeepRemovedMedia; AttachOriginal; AttachReport; Signatures (Refuse, Incremental,
AllowInvalidating); the password or key source (M16); dates and document identifier from
the caller; MaxAttachmentDepth; WhenNotConvertible (Throw, WriteWithoutClaim)
PdfAConversionPlan per finding: the remedy, its class, the objects and pages it touches, what it will change;
the level the plan reaches; what blocks the target — inspectable before a byte is written
PdfAConversionReport entries (code, finding, remedy, class, object, page, before and after where they are data);
the level claimed, or why none; M20's verdict on the result; M05's report when repair ran;
stable JSON
PdfAConversionException when the target cannot be reached and the caller asked to throw; carries the plan
  • Driven by findings (ADR 22). Analyze runs M20's profile for the target level, with the structural profile, and looks up each finding's remedy by rule identifier in the remedy registry. A finding without a remedy is not convertible, with the rule's reason. A change no finding justifies is never made: a test holds every entry of the report to a finding of the plan.
  • The registry covers the catalog. Every rule of the target profiles has an entry — a remedy, or "none" with its reason — and a test fails when M20 adds a rule the registry does not know, so that a new rule never turns into a silent gap. The remedy hints M20 writes into findings are generated from the same registry.
  • The level the plan reaches is the highest at or below the target: a when M20's level-a rules pass on the structure as it will be written, u when every glyph drawn maps to Unicode after the remedies, b otherwise. LevelFallback says how far down the caller accepts; below it, the target is not convertible.
  • Proof before the claim. M20's profile runs over the document with the plan's changes applied — the view M03's writer serializes — before a byte is written, and the claim goes into the XMP only if it passes; otherwise the plan loses the claim and says which rule failed. The integration tests reopen every output and run veraPDF on it; the caller does not pay for a second validation at run time.
  • Modes. Auto converts by incremental update when the input carries approval signatures and no encryption, so that every signed revision stays byte for byte, and by full rewrite otherwise. Incremental and Rewrite force either. An incremental conversion keeps the earlier revisions' bytes, scripts included: the output conforms, since PDF/A judges the revision in force, but it is not sanitized, and the report says so (pdfa-convert.bytes-kept-in-earlier-revision).
  • Idempotent and conservative. Converting a document that already conforms at the target gives an empty plan and, in incremental mode, the input's bytes unchanged; converting an output again gives an empty plan.
  • Streaming. The analysis interprets each page once (M20's single walk), keeping per font a set of the codes drawn — 256 bits for a simple font, a sparse set for a CID font — and per page the color families used; the conversion writes forward (M03), so memory follows the heaviest page and the font programs being subset, never the file.

Classes​

Every remedy states what it does to the document, and the report groups its entries by class:

ClassMeansExamples
LosslessNothing a reader or a viewer sees changesXMP rebuilt, /ID added, LZW re-encoded as Flate, an exact font embedded, an sRGB intent over DeviceRGB, an incorrect CIDSet removed, a resource name shortened
Changes appearanceThe page looks different, on screen or in print, on the pages listedA metric-compatible substitute font, an appearance generated where a viewer drew its own, Interpolate set to false, a transfer function removed
Changes behaviorThe document does less than it didA script, a launch action or additional actions removed; a form that no longer calculates; usage rights removed; encryption and its restrictions removed; an optional content configuration's automatic states removed
Removes contentSomething present is gone from the page, and the report says where it wentA hidden annotation removed; a movie's annotation flattened and its clip attached aside, or dropped under part 2; an attachment dropped under part 2; a PostScript XObject; alternate images
Not convertibleNo remedy reaches the target without inventing or destroying contentA font with no face to embed; a glyph drawn from .notdef; a JPEG 2000 image breaking its clause (until M22); dynamic XFA; a signature sub-filter part 2 forbids, when the signature must stay

The class of a color or appearance remedy is decided by what viewers show, not assumed: the integration tests rasterize every page before and after with poppler and MuPDF, and the pages whose difference passes the threshold must be exactly the pages the report lists under Changes appearance or Removes content.

Remedies​

AreaFindings (M20's families)RemedyClass
File structurepdfa-file, structural errorsM05's repair first when the structural profile reports errors; header and binary comment; /ID derived deterministically as M05 derives it, or the caller's; nothing after %%EOFLossless
Encryptionpdfa-file (/Encrypt)Decrypted through M16 with the caller's password; a public-key handler is not convertible until M26Changes behavior: restrictions gone
Metadatapdfa-metadataXMP built or updated through M14's model: identification, extension schemas for what is not predefined, /Info in step, dates converted exactly (time zone kept, or absent where it was absent); an /Info date that cannot be a date — year zero — removedLossless; a removed date: Removes content
Output intentpdfa-colorFamilies used decide: RGB (and gray) — the built-in sRGB, which is what viewers already assume for DeviceRGB; CMYK — the caller's profile, else not convertible; both — the intent for RGB and /DefaultCMYK on the caller's CMYK profile for the pages that use CMYK; a PDF/X intent (GTS_PDFX) kept, and a GTS_PDFA1 intent added on the same profile objectLossless, as the raster comparison confirms
Device color under a foreign intentpdfa-color/DefaultRGB on sRGB (or /DefaultCMYK on the caller's profile) in the resources of the pages that use it — the EU regulation's DeviceRGB page groups under its CMYK intent, veraPDF's one color failure on it under part 2 (6.2.4.3 test 2)Lossless on screen; confirmed by the raster comparison
Fontspdfa-fontBelowLossless or Changes appearance; Not convertible
Unicode (level u, a)pdfa-font (ToUnicode)Derived from the font's encoding and glyph names (the Adobe Glyph List) for every code drawn; a code that resolves to nothing lowers the levelLossless; extraction gains the mapped codes, listed
.notdef drawnpdfa-fontNone: the glyph does not existNot convertible
Imagespdfa-imageInterpolate false; /Alternates and /OPI removed; a JPEG 2000 image outside its clause is not convertible until M22Changes appearance, Removes content (alternates never shown)
Filterspdfa-file (LZW)Decoded and re-encoded as Flate with the same predictor, the decoded bytes comparedLossless
Graphics statepdfa-graphicsTR and TR2 other than /Default removed; HTP removed; halftones of other types removed; an unknown rendering intent made /RelativeColorimetricChanges appearance (mostly in print)
XObjectspdfa-xobjectA PostScript XObject removed; a reference XObject's /Ref removed, its proxy keptRemoves content
Contentpdfa-content, pdfa-limitAn operator ISO 32000 does not define, outside BX/EX, removed through M19's content pipeline; a resource name longer than 127 bytes renamed; a string or a q nesting past the part's limit is not convertibleLossless; Not convertible
Actions and scriptspdfa-action, pdfa-formThrough M19's categories: JavaScript at every level, Launch, Sound, Movie, ResetForm, ImportData, Hide, SetOCGState, Rendition, Trans, GoTo3DView, named actions other than page navigation, and /AA on the catalog, pages, fields and widgets; each script located, its length and SHA-256 in the report, never its textChanges behavior
Formspdfa-form, pdfa-annotationStatic and hybrid XFA removed through M16, the AcroForm kept with the values it holds; usage rights removed (M16); NeedAppearances true: every widget's appearance regenerated (M16), since the stored ones may be stale, then the flag removed; dynamic XFA not convertibleChanges behavior, Changes appearance; Not convertible
Annotationspdfa-annotationMissing appearances generated (M11); the print flag set on annotations meant to print; hidden, invisible and no-view annotations removed, with a widget's field value in the report; forbidden subtypes (Sound, Movie, Screen, RichMedia, 3D) flattened to their appearance (M11), their media kept aside under part 3Changes appearance, Changes behavior, Removes content
Optional contentpdfa-optional-contentEvery configuration named (Configuration n, deterministically, unless it has a name); /AS removedChanges behavior (a print-only layer no longer switches by itself)
Embedded files, part 3pdfa-attachmentThe MIME type from /Subtype, or detected from the first bytes and the name by a fixed table, or application/octet-stream; AFRelationship from the caller's mapping, Unspecified by default; /AF at the level the file lives on; /F and /UF; /Params dates only where present or suppliedLossless
Embedded files, part 2pdfa-attachmentA PDF converted to part 2 by this converter, recursively, up to MaxAttachmentDepth (3 by default) and never twice for the same bytes; anything else by the caller's policy: dropped (reported), refused, or the target raised to part 3 when the caller allows itLossless, or Removes content
Structure (level a)pdfa-structureNone: a structure that fails level a lowers the level, as the fallback allows. Heuristic tagging is not planned—
Signaturespdfa-signature, M04's classesBelow—

Fonts​

  • Candidates, in order: a program the document itself embeds under the same name elsewhere, when it covers the codes and its widths agree; a face of the caller's registry (M08's PdfFontRegistry) matched by PostScript name, then by family and style; the OFL set's metric-compatible substitutes for Helvetica and Arial, Times and Times New Roman, Courier and Courier New, whose advances M08's acceptance proved equal to the producers' widths. Nothing else: a face that is merely similar would make /Widths lie about the program or move every glyph after the first — and PDF/A requires the two to agree.
  • Checks before embedding, for every code the document draws with the font, collected by the interpreter: the code resolves to a glyph of the candidate — through the encoding and /Differences to a glyph name and the face's cmap for a simple font, through CIDToGIDMap or the ordering for a CID font —; its advance equals /Widths to one unit; the face allows embedding (fsType). A candidate that fails one check is refused, and the report says which check and which code.
  • Identity-H CID fonts without their program are convertible only with the exact face: their CIDs are glyph indices of a file the document no longer has. Symbolic fonts — Symbol, ZapfDingbats, Wingdings — only with a face the caller registers, since the OFL set has no equivalent. A font drawn only in render mode 3 (an OCR layer's) is left as it is: it is not used for rendering, and PDF/A does not ask for it.
  • Writing: the subset of the glyphs drawn, tagged deterministically (M08's rule), attached to the existing descriptor as FontFile2 or FontFile3; the encoding, /Widths and ToUnicode untouched unless level u needs a ToUnicode, so that extraction returns what it returned. An embedded program whose widths disagree with /Widths beyond one unit is not rewritten — changing /Widths moves text — and is not convertible.
  • The report per font: the name, the candidate chosen and why, the largest width difference, and whether glyph shapes changed (any substitute) — listed under Changes appearance with the pages it is drawn on.

Content kept aside​

  • Under a part-3 target, with KeepRemovedMedia (the default): a movie, a sound, a rich-media or 3D stream whose annotation PDF/A forbids is attached to its page as an associated file — its own MIME type, AFRelationship /Supplement, its original name — and the annotation replaced by its appearance, so the page looks the same and the content is still in the file. Scripts are never kept: they are behavior, not content, and the report locates and hashes each.
  • AttachOriginal: the input's exact bytes, as a document-level associated file with AFRelationship /Source — under part 3 only, and the one way a signature that the conversion must void stays verifiable inside the archived copy.
  • AttachReport: the report's JSON, as Supplement, so that the archived copy carries its own account.

Signed and certified inputs​

M04's classification decides, as for every write since M09:

  • A certification (/Perms /DocMDP, any P) forbids a conversion, which changes content: Analyze puts PdfSignatureInvalidationException in the plan and ConvertAsync throws it, naming the signature and its permission. With AllowInvalidating, the document is converted and each voided signature reported (write.signature-invalidated).
  • Approval signatures allow any change. Auto converts by incremental update: each signature still covers exactly its revision, and a validator shows the conversion as a change made after signing, which it is. A signature whose sub-filter part 2 forbids (adbe.x509.rsa_sha1) cannot stay in a PDF/A-2 document: that finding is not convertible unless the caller allows invalidating, and then the signature field is kept without its value and reported.
  • Encrypted and signed: decryption needs a full rewrite, which voids every signature — refused unless the caller allows it.
  • Usage rights (UR3) are removed by M16 whatever the mode, since PDF/A forbids the scripts they unlock.

Case files​

PdfAConverter converts one document; a volume is converted piece by piece and then assembled under M20's Conformance.Auto, which checks every piece against the target and makes one output intent. The tool's casefile verb gains --pdf-a 3b|3u|2b|2u: each piece's report goes into the volume's report, and a piece that cannot be converted is attached, not dropped — its original bytes as an associated file of the volume (Source), and in its place M09's separator page with the piece's number and a statement that the piece is attached in its original form, with the reason. The volume never silently lacks an exhibit.

Report codes​

The report's own vocabulary, like PdfRepairReport's, disjoint from rule identifiers and reader diagnostics: pdfa-convert.repair-applied, encryption-removed, metadata-rebuilt, info-date-removed, output-intent-added, default-color-space-added, font-embedded, font-substituted, font-not-embeddable, to-unicode-derived, level-lowered, stream-re-encoded, image-feature-removed, graphics-state-normalized, xobject-removed, operator-removed, resource-renamed, action-removed, script-removed, xfa-removed, usage-rights-removed, appearance-generated, annotation-flags-set, annotation-removed, annotation-flattened, media-kept-aside, optional-content-configured, attachment-typed, attachment-converted, attachment-dropped, original-attached, report-attached, signature-kept, signature-invalidated, bytes-kept-in-earlier-revision, not-convertible. Each entry carries its class.

Referees​

All in containers (ADR 27), pinned by digest:

RefereeConfirms
veraPDF (2b, 2u, 2a, 3b, 3u, 3a)Every output conforms at the level the report claims
poppler pdftotext (with -bbox), and M15's extractionText before and after is the same, word for word and position for position within a tolerance, except the codes a derived ToUnicode maps, which the report lists
poppler pdftoppm and MuPDF mutool drawPages before and after within the agreed threshold, except exactly the pages the report lists as changed
pdffonts, pikepdfEvery font used for rendering embedded; output intents, default color spaces, /AF and attachments as the report says
qpdf (--check, --show-encryption)Every output sound, and not encrypted
pdfinfo -jsNo script left in the revision in force
pyHankoAfter an incremental conversion, each signature intact over its revision, the conversion seen as a later change
MustangA Factur-X converted to PDF/A-3 is still a valid Factur-X, its XML byte for byte
ExifToolThe rebuilt XMP and its dates

The command-line tool​

pdfa analyze FILE [--target 2b|2u|2a|3b|3u|3a] [--json] prints the plan by class; pdfa convert FILE -o OUT [--target …] [--fallback none|u|b] [--mode auto|incremental|rewrite] [--password P] [--fonts DIR] [--cmyk-profile ICC] [--attach-original] [--attach-report] [--report REPORT.json]; casefile … --pdf-a 3b. Exit codes are M06's — 0 converted at the target, 1 not convertible, 2 usage error, 3 unreadable input, 4 refused by policy (a certification, a permission) — and 6, which this milestone adds to M06's table, for a conversion at a lower level the fallback allowed.

Slices​

Each slice ends on a green commit, with its codes documented and its measurements recorded in docs/status.md.

  1. The plan. Delivers PdfAConverter.Analyze, the options, the remedy registry and its coverage test against M20's catalog, the classes, the level computation and fallback, the plan and report models and their JSON, pdfa analyze; the manifest's conversion expectation — the level veraPDF accepts after conversion, or the reasons a document cannot be converted — and its schema. No writing yet. Proved by unit tests — every finding maps to exactly one remedy or to "not convertible" with a reason; a document that conforms gives an empty plan —; integration: on every non-conforming corpus document, the plan's not-convertible entries equal the reasons recorded for it (a priority-1 gap below), reviewed by hand. Leaves every remedy.
  2. File, metadata, output intent and modes. Delivers M05's repair chained, header, /ID, the XMP built or updated with exact dates, the sRGB intent for RGB and gray documents, both modes, the proof before the claim, and the no-op. Proved by unit tests of the date conversion (zones, no zone, year zero); integration: Chromium's invoice, report and contract and LibreOffice's invoice and report convert to PDF/A-2u that veraPDF accepts, raster and text unchanged; vendor/zugferd/pypdf2-facturx-python-false-pdfa3b.pdf (no /ID, no binary comment, no intent, a bare metadata stream, two file specifications) converts to 3b that veraPDF accepts and Mustang still validates, its XML byte for byte; vendor/verapdf/pdfa2b-content-pass.pdf at 2b is a no-op, byte for byte. Leaves encryption and active content.
  3. Encryption and active content. Delivers decryption through M16, M19's categories for scripts and actions, static and hybrid XFA and usage rights removed through M16, forbidden annotations flattened with their media kept aside, and the report's script entries. Proved by unit tests per category; integration: the encrypted documents with recorded passwords, the JavaScript forms, the QuickTime document and the XFA forms of the acceptance table convert, pdfinfo -js finds nothing, field values read back as before, the movie is an associated file of its page (pikepdf) and the page is unchanged in the raster comparison. Leaves fonts.
  4. Fonts and Unicode. Delivers the core's embedding into an existing dictionary, the candidates and their checks, subsets, ToUnicode derived from encodings, the report per font, the render-mode-3 exemption. Proved by unit tests per candidate path and per refusal; an FsCheck property — for any simple font and any set of codes drawn, the embedded subset holds a glyph for each and its advances equal /Widths to one unit, or the candidate is refused —; integration: ReportLab's invoice (Helvetica), Word 2019's contract schedule (Arial), FOP's signed notice and PDFWriter 4's grade standard convert with every font embedded (pdffonts), words at the same positions (pdftotext -bbox), veraPDF accepting; Calisto MT and PDF24's missing E are refused by name. Leaves color and images.
  5. Color, images, graphics state, XObjects and content. Delivers the output-intent decision table, default color spaces from caller profiles, the PDF/X intent kept beside a PDF/A one, LZW re-encoded, image, graphics-state and XObject features removed, content operators removed through M19's pipeline, resources renamed. Proved by unit tests per decision; integration: the EU's 2015 regulation converts to PDF/A-2a with /DefaultRGB, raster unchanged; the LZW documents re-encode with equal decoded bytes; the derived variants carrying each forbidden feature convert, their changed pages exactly the listed ones. Leaves annotations and forms.
  6. Annotations, forms and optional content. Delivers appearances generated (M11, M16), NeedAppearances resolved, flags set, hidden annotations removed with their values reported, optional content configurations named and /AS removed. Proved by unit tests; integration: vendor/verapdf/pdfa1b-forms-fail.pdf converts to 2b, InDesign's PDF/UA-1 form gets appearances and keeps its PDF/UA-1 claim only because M20's machine rules still pass, OmniForm's hidden widgets are removed and their values listed, the PDFMaker documents with layers convert. Leaves attachments.
  7. Attachments and what is kept aside. Delivers part 3's typing, relationship and /AF, part 2's recursive conversion with its depth and its visited set, the attachment policy, AttachOriginal and AttachReport. Proved by unit tests — the MIME table, a self-including attachment, depth reached —; integration: PDFMaker 9's embedded Quattro Pro spreadsheet kept under 3b with its MIME type and dropped or refused under 2b as the policy says; vendor/verapdf/pdfa3b-embedded-fail.pdf converts to 3b; PDFMaker 10's file-attachment annotation gets its annotation-level /AF; qpdf and pikepdf list every attachment as the report does. Leaves signatures and scale.
  8. Signed inputs, levels, case files and scale. Delivers M04's rules applied to conversion, the forbidden sub-filter, level a and u reached or lowered explicitly, casefile --pdf-a with pieces attached when not convertible, ConversionBenchmarks. Proved by unit tests over the signature policies; integration: FOP's signed notice converted by incremental update keeps its signature valid over its revision in pyHanko; the certified US Code section is refused, then converted with its signature reported void when the caller insists; a case file of corpus documents converts to a volume veraPDF accepts at 3b; the 1000-page journal converts within its memory budget. Leaves the corpus closed.
  9. The corpus closed. Delivers every non-conforming corpus document converted or refused as its recorded expectation says, the text and raster comparisons over all of them, idempotence and determinism, the documentation. Proved by the acceptance conditions below.

Tests required​

Unit —

  • The plan: every rule of the target profiles has a registry entry; a report entry without a finding is impossible; each class assigned as the table says; the level computation for every combination of structure, Unicode and fallback; an empty plan for a conforming document.
  • Metadata: /Info to XMP dates in every form (with and without zone, Z, +01'00', trailing apostrophe), year zero and year one, UTF-16 and PDFDocEncoding strings, custom keys to pdfx: with their extension schema.
  • Color: the decision table's every branch — RGB only, CMYK only, gray only, RGB and CMYK with and without a CMYK profile, a PDF/X intent, several intents —; default spaces written only on the pages that use the family.
  • Fonts: each candidate path; each check and its refusal (a missing glyph, a width off by two units, a restricted fsType); Identity-H without the exact face; a symbolic font; render mode 3 only; a font shared by a thousand pages embedded once.
  • Removal: each action type and each place it lives; XFA static, hybrid and dynamic; usage rights; each forbidden annotation subtype, with and without an appearance to flatten to.
  • Attachments: the MIME table; each relationship; part-2 recursion with a self-including attachment, a cycle across two attachments, the depth reached.
  • Signatures: each policy against a certification, approval signatures, a forbidden sub-filter, encryption.
  • Modes: incremental leaves every earlier byte; rewrite normalizes; the no-op is byte-identical.
  • Hostile: a document of 1,000,000 annotations to flatten (bounded time and memory); an attachment chain nested 1,000 deep; a font dictionary shared by 100,000 pages; an LZW stream whose decoded size is at the reader's limit; a script of 100 MB (hashed as a stream, never loaded whole); a /Differences array of 1,000,000 entries.
  • Properties (FsCheck): idempotence (the plan of an output is empty); monotonicity (a remedy never adds a finding of the target profile); determinism (two conversions give identical bytes).

Integration — in containers (ADR 27): veraPDF on every output at the claimed level; pdftotext and M15's extraction on every input and output; pdftoppm and MuPDF on every page before and after; pdffonts, pikepdf, qpdf and pdfinfo -js on every output; pyHanko on every signed input converted incrementally; Mustang on every Factur-X converted; ExifTool on every rebuilt packet.

Acceptance conditions​

"The non-conforming corpus documents" are: the PDF/A claims veraPDF rejects — vendor/verapdf/pdfa1b-forms-fail.pdf, pdfa2b-actions-fail.pdf, pdfa3b-embedded-fail.pdf, vendor/eu-publications/distiller10-eu-consolidated-regulation-2015.pdf, vendor/opf-format-corpus/imagemagick-false-pdfa1b-jpx.pdf, vendor/zugferd/pypdf2-facturx-python-false-pdfa3b.pdf, and remote remote/eu-dss/pdfmaker11-nbu-sk-qualified-seal-docmdp-fieldmdp.pdf, remote/eu-dss/eboe-fnmt-boe-seal-pkcs7-sha1-then-anf-test-signature.pdf, remote/opf-format-corpus/pdfmaker81-word-va-kernel-systems-guide.pdf —; the committed documents of our generators and of Word, PDF24 and ReportLab, none of which claims PDF/A; and the third-party documents named in the rows below, each chosen for a fault PDF/A forbids. Each has its expected outcome recorded in the manifest — the level veraPDF accepts after conversion, or the reason it cannot be converted — once the schema holds it (below). Rows naming remote documents close only on a green Remote corpus run.

DocumentsBehaviorVerified by
Every non-conforming corpus documentConverts to the level its expectation records, which veraPDF accepts, or is refused with exactly the reasons recorded — never a claim veraPDF rejects, never a silent partial conversionCorpusPdfAConversionTests.Non_conforming_documents_convert_or_say_precisely_why_not (new)
Every document convertedThe words pdftotext and M15's extraction return are the same before and after, at the same positions within the stated tolerance, except the codes a derived ToUnicode maps, which the report lists code by codePdfAConversionRefereeTests.Conversion_leaves_the_text_unchanged (new)
Every document convertedEvery page rasterized by poppler and MuPDF before and after is within the agreed threshold, except exactly the pages the report lists under Changes appearance or Removes contentPdfAConversionRefereeTests.Pages_change_only_where_the_report_says (new)
documents/invoice/chromium-invoice-fr.pdf, documents/report/chromium-report-fr.pdf, documents/contract/chromium-contract-fr.pdf, documents/invoice/libreoffice-invoice-fr.pdf, documents/report/libreoffice-report-fr.pdfPDF/A-2u that veraPDF accepts, with no entry under Changes appearance, Removes content or Not convertible: what a modern producer writes converts without a visible changeCorpusPdfAConversionTests.Producer_output_converts_losslessly (new)
documents/invoice/word-invoice-fr.pdf (tagged), documents/invoice/word-print-driver-invoice-fr.pdf, vendor/uk-ogl/word2019-ccs-contract-schedule.pdf (Arial not embedded, tagged)PDF/A-2a where M20's level-a rules pass on Word's structure, 2u otherwise, as the plan announced; Arial replaced by the OFL set's metric-compatible face, listed under Changes appearance with its pages, every word at the same positionCorpusPdfAConversionTests.Missing_fonts_are_substituted_without_moving_a_word (new)
documents/invoice/reportlab-invoice.pdf, vendor/fr-licence-ouverte/fop-dictao-dila-signed-joafe-notice.pdf, vendor/us-federal/pdfwriter4-usda-dry-whey-standard-2000.pdf, vendor/uk-ogl/pagemaker-distiller5-hmrc-iht205-form.pdfEvery standard 14 font and its Microsoft equivalent embedded as the OFL set's substitute (pdffonts); veraPDF acceptsCorpusPdfAConversionTests.Missing_fonts_are_substituted_without_moving_a_word (new)
vendor/opf-format-corpus/distiller7-pscript5-calisto-mt-not-embedded.pdf (Calisto MT, no face), documents/invoice/pdf24-invoice-fr.pdf (a capital E drawn from a glyph the subset lacks)Not convertible; the report names the font and the missing face, and the code drawn from .notdef on each page; no output claims anythingCorpusPdfAConversionTests.What_cannot_be_converted_is_refused_by_name (new)
The JavaScript forms: pagemaker-distiller5-hmrc-iht205-form (RC4-128, empty user password), vendor/fr-licence-ouverte/libreoffice-cerfa-13983-form.pdf, vendor/fr-licence-ouverte/pdfmaker-acrobat-cerfa-12156-form.pdf, vendor/us-federal/designer-distiller23-uscis-i9-javascript-form.pdf; remote remote/opm/word365-acrobat-opm-of306-form.pdfConverted; pdfinfo -js finds no script; every field's value reads back as before (M16); each removed script located and hashed in the report, under Changes behavior with the statement that the form no longer calculatesCorpusPdfAConversionTests.Scripts_are_removed_and_the_form_keeps_its_values (new)
The XFA forms: vendor/fr-licence-ouverte/livecycle-es9-cerfa-14880-xfa-form.pdf (usage rights applied twice), vendor/us-federal/livecycle-irs-1040-2022-xfa-ur3.pdf, vendor/us-federal/livecycle-uscis-ar11-xfa-form.pdf; remote, dynamic: remote/canada/livecycle-es10-ircc-imm1344-certified-dynamic-xfa.pdf, remote/canada/livecycle-es9-cfia-fish-export-license-dynamic-xfa.pdfStatic and hybrid forms: XFA and usage rights removed, the AcroForm kept, veraPDF accepts; dynamic forms: not convertible, the reason being that the placeholder page is not the formCorpusPdfAConversionTests.Xfa_is_removed_or_refused_as_its_kind_requires (new)
vendor/opf-format-corpus/pdfmaker9-word-distiller-embedded-quicktime.pdf; remote remote/opf-format-corpus/pdfmaker9-word-distiller-embedded-avi.pdfUnder 3b: the Screen annotation replaced by its appearance, the clip an associated file of its page with its MIME type (pikepdf), the page unchanged in the raster comparison; under 2b: the clip dropped and reported under Removes contentCorpusPdfAConversionTests.Media_is_kept_aside_under_part_3 (new)
The encrypted documents with recorded passwords: documents/secured/qpdf-invoice-aes256.pdf, vendor/opf-format-corpus/openoffice32-writer-rc4-128-empty-user-password.pdf, vendor/us-federal/pdfwriter3-copyright-office-dmca-summary-1998-rc4-40.pdf (RC4-40, LZW, a malformed creation date)Decrypted and converted; not encrypted (qpdf --show-encryption); content equal to M16's decryption; the malformed date removed and reported; veraPDF acceptsCorpusPdfAConversionTests.Encrypted_documents_convert_with_their_password (new)
The LZW documents: vendor/us-federal/acrobat3-import-irs-1040-1988-scan.pdf, distiller3-irs-ss4-1995-form.pdf, pdfwriter4-usda-dry-whey-standard-2000.pdfEvery LZW stream re-encoded as Flate with its predictor, the decoded bytes equal; the scan converts at level bCorpusPdfAConversionTests.Lzw_is_re_encoded_losslessly (new)
vendor/eu-publications/distiller10-eu-consolidated-regulation-2015.pdf (PDF/A-1a claim; its page groups blend in DeviceRGB under a CMYK intent); remote remote/ocrmypdf/photoshop-cc2015-pdfx3-cmyk.pdf (a PDF/X-3 CMYK intent)The first: PDF/A-2a with /DefaultRGB on sRGB on the pages that use RGB, the CMYK intent kept, raster unchanged; the second: 2b with a GTS_PDFA1 intent on the PDF/X intent's own profile objectCorpusPdfAConversionTests.Device_color_keeps_its_meaning_under_the_intent (new)
vendor/verapdf/pdfa1b-forms-fail.pdf, vendor/verapdf/pdfa2b-actions-fail.pdf, vendor/verapdf/pdfa3b-embedded-fail.pdf2b, 2b and 3b that veraPDF accepts; the report's entries are exactly the remedies for the clauses veraPDF failedCorpusPdfAConversionTests.VeraPdf_failures_are_remedied_clause_by_clause (new)
vendor/pdf-association/indesign15-pdfua1-form.pdf (widgets without appearances), vendor/us-federal/omniform-usda-rd1924-5-hidden-widgets.pdfAppearances generated; InDesign's PDF/UA-1 claim kept because M20's machine rules pass on the output, never added; OmniForm's hidden widgets removed and their fields' values listedCorpusPdfAConversionTests.Annotations_and_widgets_meet_the_part (new)
vendor/opf-format-corpus/pdfmaker9-word-distiller-embedded-quattro-pro-spreadsheet.pdf, vendor/opf-format-corpus/pdfmaker10-word-file-attachment-annotation.pdf, vendor/bfo/bfo-pdfa2b-embedded-pdf.pdf, documents/invoice/qpdf-invoice-with-facturx-xml.pdfUnder 3b: each attachment with its MIME type, relationship and /AF at its level, listed by qpdf and pikepdf as the report lists it; under 2b: the spreadsheet dropped, refused or the target raised as the policy says, BFO's PDF/A attachment keptCorpusPdfAConversionTests.Attachments_meet_the_part_or_are_accounted_for (new)
vendor/fr-licence-ouverte/fop-dictao-dila-signed-joafe-notice.pdf (approval signature); remote eboe-fnmt-boe-seal-pkcs7-sha1-then-anf-test-signature.pdf (two approval signatures)Converted by incremental update: the signed revisions byte for byte, each signature intact over its revision in pyHanko, the conversion reported by M04 as a later change; veraPDF acceptsCorpusPdfAConversionTests.Approval_signatures_survive_an_incremental_conversion (new)
vendor/us-federal/itext-govinfo-us-code-certified.pdf; remote pdfmaker11-nbu-sk-qualified-seal-docmdp-fieldmdp.pdf (DocMDP P=2)Refused by default, naming the signature and its permission; with AllowInvalidating, converted with each voided signature reported, and the original attached as Source when askedCorpusPdfAConversionTests.Certified_documents_are_never_voided_in_silence (new)
documents/damaged/*M05's repair runs first and its report is part of ours; the output is accepted by veraPDF and its text equals that of the undamaged original the corpus keepsCorpusPdfAConversionTests.Damaged_documents_are_repaired_then_converted (new)
Every conforming document at its own level (pdfa2b-content-pass, pdfa3b-embedded-pass, libreoffice-report-pdfa2b, the Mustang invoices) and every output aboveAn empty plan; in incremental mode the input's bytes unchanged; converting an output again changes nothingCorpusPdfAConversionTests.Conversion_is_idempotent_and_a_no_op_on_conforming_documents (new)
A case file of documents/contract/chromium-contract-fr.pdf, the Chromium invoice, the Word contract schedule, the HMRC form and Calisto MT's page, through casefile --pdf-a 3bA volume veraPDF accepts at 3b; Calisto MT's page attached as Source behind a separator page that says why; each piece's report in the volume'sCorpusPdfAConversionTests.A_case_file_is_archived_with_every_piece_accounted_for (new)
documents/stress/reportlab-journal-1000-pages.pdf (standard 14 fonts, not embedded)Converted within a stated memory budget, independent of page count, time linearCorpusPdfAConversionTests.Conversion_holds_its_memory_budget (new), ConversionBenchmarks (new)
Every output aboveTwo conversions give identical bytes, under the invariant culture and fr-FRCorpusPdfAConversionTests.Conversion_is_deterministic (new)
The same conversions through the toolpdfa convert writes the API's bytes and report, and exits with the documented codeCorpusToolTests.Pdfa_verbs_match_the_api (new)

Corpus​

What the corpus holds​

  • The faults PDF/A forbids, from real producers: fonts left out (*not-embedded*, 27 committed and 20 remote — standard 14, Arial and Times New Roman from Word and PDFMaker, Calisto MT, CJK CID fonts); JavaScript at every level (javascript, document-javascript, field-javascript, 8 committed and 5 remote); LZW (4 and 1); encryption with recorded passwords (18 and 4, RC4-40 to AES-256); a QuickTime and an AVI clip behind Screen annotations, a Flash navigator and 3D models (embedded-video, flash-navigator, embedded-3d-*); XFA, static, hybrid and dynamic, with usage rights (4 and 3); CMYK output intents and images (2 and 3); optional content (8 and 8); widgets without appearances (3 and 4); hidden widgets; attachments at document and annotation level (8 and 10).
  • False claims veraPDF rejects, with the clauses it fails, from the veraPDF fixtures, Distiller 10, ImageMagick, the factur-x Python library, and remote PDFMaker 8.1 and 11 and the re-signed BOE decree.
  • Producers' plain output: Chromium, LibreOffice, Word's Save as PDF and print driver, PDF24, ReportLab — the everyday inputs of a case file.
  • Signed and certified documents: an approval signature by Dictao over FOP, a GPO certification, a DocuSign envelope, and remote the Slovak seal's DocMDP P=2, the BOE's gazette seals, a legacy adbe.x509.rsa_sha1.
  • Damaged documents with their undamaged originals (documents/damaged/*), for repair then conversion.
  • Scale: the 1000-page journal, whose fonts are standard 14 and not embedded.

What it lacks​

NeedWhyPriorityLikely source
For each non-conforming document, its expected outcome: the level veraPDF accepts after conversion, or the reasons it cannot be convertedThe first acceptance row compares with recorded expectations, which must come from the referee and a reviewed reason, not from our converter1Generated here: a manifest field, filled from veraPDF on reviewed outputs, each "not convertible" reason written by hand and reviewed like an unsupported entry
Documents whose missing font is freely available — DejaVu, Noto, Liberation by name — not embeddedThe registry's exact-face path, the one that is lossless, has no document: every missing font in the corpus is proprietary1Derived here: committed documents that embed such a face, the program removed by a recorded pikepdf transformation
A document drawing DeviceRGB and DeviceCMYK with no output intent, and one drawing only DeviceCMYKTwo branches of the color decision table have no document; the one CMYK claim is PDF/A-1a with its intent already chosen1Generated here with ReportLab's CMYK colors
A CMYK ICC profile the tests may embedThe CMYK branches need a profile, which the caller supplies in production — the library ships none, since a CMYK intent states a printing condition only the caller knows — and the tests must have1Meanwhile, the U.S. Web Coated (SWOP) v2 profile of the committed vendor/eu-publications/distiller10-eu-consolidated-regulation-2015.pdf's output intent, read at test time and never copied out, as M29 reads it; whether Adobe's terms allow it inside an output committed to the corpus is to verify first. Then a public source whose terms allow redistribution of the unmodified profile (W19), vendored under tests/corpus/sources/third-party/ with its license and SOURCE, as docs/corpus.md places inputs written by others
Documents carrying each harmless forbidden feature: Interpolate true, TR in a graphics state, /Alternates, /OPI, a PostScript XObject, a reference XObject, a halftone of type 6, an undefined operator outside BX/EX, a 128-byte resource name, a 40,000-byte string in contentTheir remedies and the pages they change must be proven on documents, not on hand-built dictionaries in unit tests2Derived here from a committed document by recorded pikepdf edits
Symbol and ZapfDingbats, and a symbolic TrueType, not embeddedThe symbolic path must refuse without a registered face and succeed with one2Generated here with ReportLab's standard 14 symbol fonts; derived for the TrueType
A PDF that is not PDF/A attached to a document converted to part 2The recursive conversion of attachments has only BFO's PDF/A attachment, which needs none2Derived here: a committed PDF attached to another by a recorded transformation
A non-embedded CJK CID font, committed, with its exact face availableThe Identity-H path with the exact face is proven only on remote files, and never with a face at hand3Generated here with Chromium and a Noto CJK face, the program then removed by a recorded transformation
Real exhibits of the kind case files carry — a scanned letter, an e-mail printed to PDF, a signed contract from a commercial service — that we may commitThe case-file row assembles corpus documents that were not chosen as exhibits2A contribution (W03, W05), or those M18 adds
A signed document whose sub-filter part 2 forbids, that the reader supports todayThe forbidden sub-filter's refusal has only unit tests: the corpus's one adbe.x509.rsa_sha1 file, remote/pdfcpu/acrobat-web-capture8-x509-rsa-sha1-signed.pdf, is recorded as unsupported until M23 (#47), and a milestone cannot accept on a skipped document2A public source among the legacy signature test sets; or the same file once M23 fixes #47 — M23 comes after this milestone, so the row joins M21's acceptance only if #47 is fixed sooner
A rich-media or 3D document we may commitThe part-3 "kept aside" path for 3D and RichMedia is proven on remote files only3The remote corpus already holds Acrobat 9's portfolio; a committed one would come from a public source

Traps​

  • A metric-compatible substitute is not the same font. Positions stay and shapes change; the report says so page by page. A face with other widths would make /Widths lie about the program, or move every glyph — never.
  • Identity-H CIDs are glyph indices of the missing file. No other face can honor them, however similar.
  • Viewers draw DeviceRGB as sRGB. An sRGB intent states what they already assumed; a CMYK intent states a printing condition only the caller knows, and is never guessed.
  • A PDF/X intent is GTS_PDFX, a PDF/A intent GTS_PDFA1, and part 2 wants every intent's profile to be the same object: the PDF/A intent is added on the PDF/X intent's own profile.
  • Hidden annotations cannot stay hidden — PDF/A forbids the flags — and showing them would change the page. They are removed, and what they held is listed.
  • A form's scripts computed its values. After removal the stored values stay and nothing recomputes; the report says the form no longer calculates, and never runs a script to "settle" it first (ADR 37).
  • NeedAppearances true means the stored appearances may be stale: every widget is regenerated, not only the missing ones — or the page shows values the fields do not hold.
  • Removing encryption removes the author's restrictions. A caller archiving under a confidentiality rule needs to know, and the report says so under Changes behavior.
  • An incremental conversion keeps the earlier revision's bytes, scripts included: PDF/A-valid, not sanitized. Only M19's full rewrite removes them.
  • /Info and XMP must agree to the second and the zone under parts 1 to 3; a date written without a zone stays without one, and a date that is not a date (year zero) is removed, never invented.
  • adbe.x509.rsa_sha1 is forbidden from part 2 on: a document cannot keep such a signature and become PDF/A-2.
  • Extraction must not change. A substitute keeps the document's encoding and ToUnicode; a derived ToUnicode changes extraction for codes that had none — listed, never silent.
  • Conversion is not repair: a damaged document goes through M05 first, with its own report, and a document M05 cannot repair is not converted.
  • An attachment can contain itself, directly or through another: recursion is bounded by depth and by a set of the attached streams' hashes.
  • LZW re-encoded as Flate must give the same decoded bytes, predictors and EarlyChange included; a stream is re-encoded only after decoding it whole and without a filter.* diagnostic — filter.checksum-mismatch among them: data that decoded to its end under a zlib checksum that disagrees may be wrong, and a fresh checksum would hide it.
  • A fallback is a promise kept. "2u" is never written when a glyph cannot be mapped; the level goes down only as far as the caller allowed, and the report says why.
  • A PDF/UA claim survives conversion only if M20's machine rules still pass on the output; conversion never adds one.

Documentation​

  • docs/website/docs/guides/converting-to-pdf-a.md (new): targets and levels, the plan and its classes, fonts and color, what is kept aside, signed and encrypted inputs, what cannot be converted and what to do about it.
  • docs/website/docs/guides/archiving-a-case-file.md (new): converting pieces, assembling under Conformance.Auto, pieces attached rather than dropped.
  • docs/website/docs/reference/pdf-a-remedies.md (new, generated from the registry and held to it by a test): every rule, its remedy and its class, or why it has none.
  • docs/website/docs/concepts/pdf-a.md: conversion, and what it never does.
  • docs/website/docs/reference/diagnostics.md: the pdfa-convert.* report codes.
  • docs/website/docs/reference/tool/: pdfa analyze, pdfa convert, casefile --pdf-a.
  • docs/website/docs/introduction.md and docs/features/features.json: the pdfa-conversion entry brought to its state.
  • docs/architecture.md: Conversion/ in the satellite, the remedy primitives in the core.
  • docs/corpus.md: the conversion expectation field, and the derived variants this milestone adds.
  • docs/status.md: the measurements, and how many corpus documents convert, at which level, and why the others do not.

Exit criteria​

  • PdfAConverter analyzes, plans, converts and reports, every remedy justified by a finding and classed.
  • The remedy registry covers every rule of M20's PDF/A-2 and 3 profiles, with a remedy or a reason.
  • Fonts are embedded from the registry or substituted by metric-compatible faces only, with the checks above.
  • Media and 3D are kept aside under part 3; the original and the report can be attached.
  • Signed inputs are converted by incremental update or refused; nothing is voided in silence.
  • casefile --pdf-a archives a volume with every piece converted or attached.
  • 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.
  • ConversionBenchmarks measures the 1000-page journal with MemoryDiagnoser; the budget is recorded.
  • Unit tests cover each remedy, its degenerate and hostile cases; the FsCheck properties hold.
  • Integration tests run veraPDF, poppler, MuPDF, pikepdf, qpdf, pyHanko, Mustang and ExifTool in containers.
  • The documentation site publishes the conversion guide, the case-file guide and the generated remedy table.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).