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:
PdfAConverterin theAdCodicem.Pdf.Conformancesatellite: 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,
ToUnicodederived 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 asSupplement, when the caller asks; - remedy primitives in the core, reused later: a font program embedded into an existing font dictionary,
ToUnicodederived 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
| Part | Where | Why |
|---|---|---|
PdfAConverter, the remedy registry, the plan and the report | AdCodicem.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 flags | Core, 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 annotations | Core, M19's pipeline and categories | ADR 37 makes sanitization the one place active content is removed |
| Decryption, forms, usage rights | Core, M16 | |
| Appearances, flattening | Core, 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).
Analyzeruns 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.
LevelFallbacksays 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.
Autoconverts 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.IncrementalandRewriteforce 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:
| Class | Means | Examples |
|---|---|---|
| Lossless | Nothing a reader or a viewer sees changes | XMP 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 appearance | The page looks different, on screen or in print, on the pages listed | A metric-compatible substitute font, an appearance generated where a viewer drew its own, Interpolate set to false, a transfer function removed |
| Changes behavior | The document does less than it did | A 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 content | Something present is gone from the page, and the report says where it went | A 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 convertible | No remedy reaches the target without inventing or destroying content | A 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
| Area | Findings (M20's families) | Remedy | Class |
|---|---|---|---|
| File structure | pdfa-file, structural errors | M05'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 %%EOF | Lossless |
| Encryption | pdfa-file (/Encrypt) | Decrypted through M16 with the caller's password; a public-key handler is not convertible until M26 | Changes behavior: restrictions gone |
| Metadata | pdfa-metadata | XMP 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 — removed | Lossless; a removed date: Removes content |
| Output intent | pdfa-color | Families 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 object | Lossless, as the raster comparison confirms |
| Device color under a foreign intent | pdfa-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 |
| Fonts | pdfa-font | Below | Lossless 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 level | Lossless; extraction gains the mapped codes, listed |
.notdef drawn | pdfa-font | None: the glyph does not exist | Not convertible |
| Images | pdfa-image | Interpolate false; /Alternates and /OPI removed; a JPEG 2000 image outside its clause is not convertible until M22 | Changes appearance, Removes content (alternates never shown) |
| Filters | pdfa-file (LZW) | Decoded and re-encoded as Flate with the same predictor, the decoded bytes compared | Lossless |
| Graphics state | pdfa-graphics | TR and TR2 other than /Default removed; HTP removed; halftones of other types removed; an unknown rendering intent made /RelativeColorimetric | Changes appearance (mostly in print) |
| XObjects | pdfa-xobject | A PostScript XObject removed; a reference XObject's /Ref removed, its proxy kept | Removes content |
| Content | pdfa-content, pdfa-limit | An 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 convertible | Lossless; Not convertible |
| Actions and scripts | pdfa-action, pdfa-form | Through 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 text | Changes behavior |
| Forms | pdfa-form, pdfa-annotation | Static 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 convertible | Changes behavior, Changes appearance; Not convertible |
| Annotations | pdfa-annotation | Missing 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 3 | Changes appearance, Changes behavior, Removes content |
| Optional content | pdfa-optional-content | Every configuration named (Configuration n, deterministically, unless it has a name); /AS removed | Changes behavior (a print-only layer no longer switches by itself) |
| Embedded files, part 3 | pdfa-attachment | The 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 supplied | Lossless |
| Embedded files, part 2 | pdfa-attachment | A 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 it | Lossless, or Removes content |
| Structure (level a) | pdfa-structure | None: a structure that fails level a lowers the level, as the fallback allows. Heuristic tagging is not planned | — |
| Signatures | pdfa-signature, M04's classes | Below | — |
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/Widthslie 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
/Differencesto a glyph name and the face'scmapfor a simple font, throughCIDToGIDMapor the ordering for a CID font —; its advance equals/Widthsto 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
FontFile2orFontFile3; the encoding,/WidthsandToUnicodeuntouched unless level u needs aToUnicode, so that extraction returns what it returned. An embedded program whose widths disagree with/Widthsbeyond one unit is not rewritten — changing/Widthsmoves 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 withAFRelationship /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, asSupplement, 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, anyP) forbids a conversion, which changes content:AnalyzeputsPdfSignatureInvalidationExceptionin the plan andConvertAsyncthrows it, naming the signature and its permission. WithAllowInvalidating, the document is converted and each voided signature reported (write.signature-invalidated). - Approval signatures allow any change.
Autoconverts 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:
| Referee | Confirms |
|---|---|
veraPDF (2b, 2u, 2a, 3b, 3u, 3a) | Every output conforms at the level the report claims |
poppler pdftotext (with -bbox), and M15's extraction | Text 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 draw | Pages before and after within the agreed threshold, except exactly the pages the report lists as changed |
| pdffonts, pikepdf | Every 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 -js | No script left in the revision in force |
| pyHanko | After an incremental conversion, each signature intact over its revision, the conversion seen as a later change |
| Mustang | A Factur-X converted to PDF/A-3 is still a valid Factur-X, its XML byte for byte |
| ExifTool | The 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.
- 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. - 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.pdfat 2b is a no-op, byte for byte. Leaves encryption and active content. - 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 -jsfinds 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. - Fonts and Unicode. Delivers the core's embedding into an existing dictionary, the candidates and their
checks, subsets,
ToUnicodederived 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/Widthsto 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. - 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. - Annotations, forms and optional content. Delivers appearances generated (M11, M16),
NeedAppearancesresolved, flags set, hidden annotations removed with their values reported, optional content configurations named and/ASremoved. Proved by unit tests; integration:vendor/verapdf/pdfa1b-forms-fail.pdfconverts 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. - 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,AttachOriginalandAttachReport. 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.pdfconverts 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. - 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-awith 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. - 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:
/Infoto 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 topdfx: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
/Differencesarray 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.
| Documents | Behavior | Verified by |
|---|---|---|
| Every non-conforming corpus document | Converts 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 conversion | CorpusPdfAConversionTests.Non_conforming_documents_convert_or_say_precisely_why_not (new) |
| Every document converted | The 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 code | PdfAConversionRefereeTests.Conversion_leaves_the_text_unchanged (new) |
| Every document converted | Every 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 content | PdfAConversionRefereeTests.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.pdf | PDF/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 change | CorpusPdfAConversionTests.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 position | CorpusPdfAConversionTests.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.pdf | Every standard 14 font and its Microsoft equivalent embedded as the OFL set's substitute (pdffonts); veraPDF accepts | CorpusPdfAConversionTests.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 anything | CorpusPdfAConversionTests.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.pdf | Converted; 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 calculates | CorpusPdfAConversionTests.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.pdf | Static 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 form | CorpusPdfAConversionTests.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.pdf | Under 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 content | CorpusPdfAConversionTests.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 accepts | CorpusPdfAConversionTests.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.pdf | Every LZW stream re-encoded as Flate with its predictor, the decoded bytes equal; the scan converts at level b | CorpusPdfAConversionTests.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 object | CorpusPdfAConversionTests.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.pdf | 2b, 2b and 3b that veraPDF accepts; the report's entries are exactly the remedies for the clauses veraPDF failed | CorpusPdfAConversionTests.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.pdf | Appearances 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 listed | CorpusPdfAConversionTests.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.pdf | Under 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 kept | CorpusPdfAConversionTests.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 accepts | CorpusPdfAConversionTests.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 asked | CorpusPdfAConversionTests.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 keeps | CorpusPdfAConversionTests.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 above | An empty plan; in incremental mode the input's bytes unchanged; converting an output again changes nothing | CorpusPdfAConversionTests.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 3b | A 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's | CorpusPdfAConversionTests.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 linear | CorpusPdfAConversionTests.Conversion_holds_its_memory_budget (new), ConversionBenchmarks (new) |
| Every output above | Two conversions give identical bytes, under the invariant culture and fr-FR | CorpusPdfAConversionTests.Conversion_is_deterministic (new) |
| The same conversions through the tool | pdfa convert writes the API's bytes and report, and exits with the documented code | CorpusToolTests.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
| Need | Why | Priority | Likely source |
|---|---|---|---|
| For each non-conforming document, its expected outcome: the level veraPDF accepts after conversion, or the reasons it cannot be converted | The first acceptance row compares with recorded expectations, which must come from the referee and a reviewed reason, not from our converter | 1 | Generated 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 embedded | The registry's exact-face path, the one that is lossless, has no document: every missing font in the corpus is proprietary | 1 | Derived 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 DeviceCMYK | Two branches of the color decision table have no document; the one CMYK claim is PDF/A-1a with its intent already chosen | 1 | Generated here with ReportLab's CMYK colors |
| A CMYK ICC profile the tests may embed | The 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 have | 1 | Meanwhile, 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 content | Their remedies and the pages they change must be proven on documents, not on hand-built dictionaries in unit tests | 2 | Derived here from a committed document by recorded pikepdf edits |
| Symbol and ZapfDingbats, and a symbolic TrueType, not embedded | The symbolic path must refuse without a registered face and succeed with one | 2 | Generated 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 2 | The recursive conversion of attachments has only BFO's PDF/A attachment, which needs none | 2 | Derived here: a committed PDF attached to another by a recorded transformation |
| A non-embedded CJK CID font, committed, with its exact face available | The Identity-H path with the exact face is proven only on remote files, and never with a face at hand | 3 | Generated 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 commit | The case-file row assembles corpus documents that were not chosen as exhibits | 2 | A contribution (W03, W05), or those M18 adds |
| A signed document whose sub-filter part 2 forbids, that the reader supports today | The 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 document | 2 | A 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 commit | The part-3 "kept aside" path for 3D and RichMedia is proven on remote files only | 3 | The 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
/Widthslie 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 intentGTS_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).
NeedAppearancestrue 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.
/Infoand 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_sha1is 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 derivedToUnicodechanges 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
EarlyChangeincluded; a stream is re-encoded only after decoding it whole and without afilter.*diagnostic —filter.checksum-mismatchamong 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 underConformance.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: thepdfa-convert.*report codes.docs/website/docs/reference/tool/:pdfa analyze,pdfa convert,casefile --pdf-a.docs/website/docs/introduction.mdanddocs/features/features.json: thepdfa-conversionentry 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
-
PdfAConverteranalyzes, 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-aarchives 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 corpusrun recorded instatus.md. -
ConversionBenchmarksmeasures the 1000-page journal withMemoryDiagnoser; 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).