M20 — PDF/A levels and conformance profiles
State: to do — Depends on: M02, M13, M14, M15 — Profiles in a satellite by ADR 36; conformance preserved by ADR 17; one rule engine by ADR 21
Goal
Say of any document whether it conforms to the PDF/A part and level, or the PDF/UA part, it claims or the caller asks about — rule by rule, in the terms of the standard and of the Matterhorn Protocol, with what only a person can judge set apart rather than guessed at — and keep that verdict true through everything the library writes: PDF/A-2 generated at levels b, u and a, a claim carried through a merge by making the merged document conform rather than by dropping it, and a PDF/A-1 file kept PDF/A-1 by every operation that writes to it.
A claim is not a verdict. The corpus holds PDF/A claims veraPDF rejects from Distiller, ImageMagick, PDFMaker 8.1
and 11, the factur-x Python library and a re-signed gazette decree; a PDF/UA claim whose pdfuaid says
conformance B, a property the standard does not have; and an Acrobat Image Conversion page damaged on purpose
whose claim veraPDF still upholds. Until this milestone the library generates PDF/A-3 (M14) and PDF/UA-1 (M13),
keeps claims only while every part of a merge makes the same one (M06), and refuses what a claim forbids (M09, M11,
M16) — but it cannot say whether a claim it is handed is true. An archive ingesting exhibits, a court portal's
pre-check, an accessibility audit and an e-invoicing gateway all ask that question first, and they ask it
together with their own ("no JavaScript, fonts embedded, nothing larger than A3"): one report, one vocabulary,
one engine (ADR 21).
Scope
In:
- the public rule API ADR 36 kept internal (core,
AdCodicem.Pdf.Validation): rules, the subjects they inspect, profiles composed from rules and from other profiles, rule descriptors with their references to the standards, verdicts per conformance level, and a review outcome for the conditions only a person can judge; M02's structural rules and M14'sfacturx.*container checks moved onto it without a change to what they report; - caller-defined rules and policies in families of their own (
x-…), written in code or declared in a JSON policy file with a closed vocabulary and a published schema, with a policy verdict separate from severity; - the
AdCodicem.Pdf.Conformancesatellite (ADR 36): conformance claims read from XMP; profiles for PDF/A-1a, 1b, 2a, 2b, 2u, 3a, 3b, 3u, 4, 4e and 4f and PDF/UA-1; a rule catalog with each rule's clause and test references, mapped to veraPDF's numbering; PDF/UA-1 mapped to ISO 14289-1's clauses and to the Matterhorn Protocol 1.1's failure conditions, human conditions included as review items; the implementation of the core's conformance-checker seam; DI registration; - PDF/A-2b, 2u and 2a generation in the core, beside M14's part 3, from the builder and from HTML, and the dual claim PDF/A-2a and PDF/UA-1;
- conformance actively preserved through a merge (M06): a target claim computed or given, every part verified against it, output intents made one, color of the parts whose profile differs kept exact through default color spaces, metadata and extension schemas united, associated files brought to part 3's rules — and otherwise the loss reported with the part and the rule that caused it;
- writes that keep a PDF/A-1 file PDF/A-1: one table of part 1's constraints, consulted by the writer (M03), the fonts (M08), stamps (M09), annotations (M11), fills (M16) and merges (M06);
- a key newer than the version a file declares (#123, moved from M02 on 2026-09-29): read from the Arlington
model's
SinceVersionwith its extension wrappers, so that PDF/A-3 brings/AFinto PDF 1.7, and judged where a claim bounds the version — PDF/A-1 on PDF 1.4 — rather than in the structural profile, where a newer key still conforms (ADR 45) and the rule flagged 72 to 85 of 271 sound documents; - the tool:
validate --profile,validate --policy,html2pdf --pdf-a 2b|2u|2a.
Out, explicitly:
- converting a non-conforming document into PDF/A — M21, driven by this milestone's findings; each finding's remedy hint names what M21 will do, so that a report reads as a conversion plan;
- PDF/UA-2 and Well-Tagged PDF profiles, PDF Declarations, PDF/A-4, 4e and 4f generation — M28, on the public API delivered here. M20 validates PDF/A-4, 4e and 4f;
- PDF/X and PDF/VT — M29; PDF/E-1, which PDF/A-4e superseded — not planned;
- checks that need a rendered page — color contrast (Matterhorn checkpoint 04), flicker (03), whether color alone carries meaning — are review items, never a machine verdict; flattening transparency to reach PDF/A-1 needs rasterization and is not planned (M25 renders; nothing converts);
- the cryptographic content of signatures under PDF/A-2 to 4 (the CMS object's certificates and digest
algorithm): CMS lives in
AdCodicem.Pdf.Signing(ADR 41); those clauses are contributed as rules by that satellite in M26 and M27, through this milestone's API. M20 checks what needs no cryptography: the byte range (M04) and the sub-filter; - editing a received document's structure (alternative text on a third party's figure) and heuristic tagging of untagged documents — neither is in the roadmap; the second is one of its open questions;
- recording a person's review in the file — PDF Declarations' claim data, M28;
- executing JavaScript to decide what a form shows — never (ADR 37): scripts are found, located and reported.
Dependencies. The roadmap gives M02 (the engine), M13 (the structure and what PDF/UA-1 asks of it), M14 (the
XMP model, the PDF/A-3 targets and their requirement table, PdfConformancePolicy's use for PDF/A) and M15: the
content rules — color used, glyphs drawn, operators, q depth, render modes — need its content interpreter,
and the level-a and PDF/UA rules its public structure reader. From earlier: M03's writer and its version policy,
M04's signature coverage, M06's merge and its rules for claims, M08's font parsers, ToUnicode and the OFL set, M09's
stamps, M11's annotations and layers, and M16's forms, fills and encryption, all under the claims this milestone
judges.
Design
Where it lives
| Part | Where | Why |
|---|---|---|
| The public rule API, subjects and the single walk, the report's verdicts and review items | Core, Validation/ (namespace AdCodicem.Pdf.Validation) | ADR 36's engine: M20 makes it public rather than moving it; the core's PdfRepair (M05) keeps consuming findings |
PdfConformanceLevel, the claim reader, IPdfConformanceChecker | Core, Validation/ | The merge (M06), the PDF/A-2 attachment rule and M14's RaiseClaimToPartThree ask whether a document conforms, and the core cannot reference the satellite |
| PDF/A-2 targets, the part-1 constraint table, merge preservation, default color spaces | Core, beside M14's PDF/A-3 targets | The writer, the content builder, the fonts and the assembly enforce them; no dependency is needed |
| The ICC profile header reader and the JPEG 2000 box reader the rules use | Core, internal | M14 and M12.5 need the same facts on what they write; bounded readers of headers, never decoders |
Profiles, the rule catalog, the Matterhorn mapping, review conditions, policies and their JSON form, PdfConformanceValidator | AdCodicem.Pdf.Conformance | Hundreds of rules and their reference data are no business of an application that never validates a claim (ADR 36) |
| The veraPDF differential harness | tests/AdCodicem.Pdf.TestSupport | Shared by this milestone's integration tests and by every later one that claims a level |
The satellite depends on AdCodicem.Pdf alone, and is AOT- and trimming-compatible: its catalog is static
data, its policy parser uses System.Text.Json with source generation, and nothing is discovered by reflection.
The public rule API
IValidationRule public: Descriptor; Start(ValidationContext) -> IValidationPass
IValidationPass one document's run of a rule: Visit(in ValidationSubject, ref PdfFindingSink);
Finish(ref PdfFindingSink). A stateless rule returns one shared pass
PdfValidationRuleDescriptor immutable: Id, Severity, Title, Subjects, References, RemedyHint, AppliesTo
(PDF versions; conformance levels), NeedsContent
PdfRuleReference a citation: specification ("ISO 19005-2", "ISO 14289-1", "Matterhorn 1.1"),
clause, test number, failure condition; several per rule
ValidationSubjectKind [Flags] Document, Trailer, Metadata, OutputIntent, Page, Font, Image, XObject,
ColorSpace, GraphicsState, Annotation, Field, Action, OptionalContent,
EmbeddedFile, StructureElement, TextRun, ColorUse, ImageUse, OperatorUse, Signature
ValidationSubject readonly ref struct: kind, the typed read-only view, its location; a content
subject's spans (a glyph run's codes) point into the interpreter's buffers and are
valid only during the call
ValidationContext read-only: the document, its reader options, its version, its claims, and lazily
built shared views — pages, fonts, the XMP model (M14), the structure tree (M15),
the form (M16), attachments (M06), signatures (M04)
PdfFindingSink ref struct over the report: Report(rule, location, message arguments); counts every
finding, keeps what the capacity allows, never allocates for one it does not keep
ValidationProfile Create(name, version, rules), Combine(profiles…): rules deduplicated by identifier,
ordered by the profile's own order; Structural stays built in
- One walk, whatever the number of rules. The engine visits each subject once, in a fixed order — the
trailer, the catalog, the metadata, the output intents, then each page in order with its resources, its
content (through M15's interpreter, whose device turns operators into
TextRun,ColorUse,ImageUseandOperatorUsesubjects), its annotations; then the form, the structure tree (iteratively), the embedded files and the signatures — and hands it to the passes that subscribed to its kind, through dispatch arrays built once per profile. An object reached twice (a font shared by a thousand pages) is visited once as a resource, through a bitmap over object numbers. Content is interpreted once for every rule that needs it; a profile none of whose rules needs content never interprets any. - Rules are stateless; passes carry a document's state — the heading sequence, the codes used per font, the output intents seen — and are dropped when the run ends. The validator stays stateless and thread-safe (ADR 36).
- Findings aggregate by rule and subject. A font that references
.notdeften thousand times is one finding with a count of 10,000 and its first location, not ten thousand findings: a report stays readable and bounded (invariant 4), and veraPDF's "failed checks" count is comparable. - Locations gain a content position: the content stream object, the offset in its decoded data, and the operator's index, so that a finding inside a page's content points at the operator.
- Cancellation and progress follow M03's convention:
Validate(document, cancellation, progress)checks the token per subject without allocating and reports pages done. - Severity in a conformance profile. ADR 45, amending ADR 36, set the bar for
Errorat whether the reader can vouch that it reads the document as written, which is the structural profile's question. A conformance profile asks another: does the document meet the part it is checked against? Its rules reportErrorfor a requirement of the part that is not met,Warningfor a recommendation, andInformationfor a fact the verdict does not use — one severity per identifier still. Slice 1's ADR records this, at the next free number, as an amendment of ADR 36, beside the review outcome and the caller families below; the alternative weighed there is a fourth severity, rejected because it would put "how wrong" and "wrong against what" on one scale. - Caller families. A family that begins with
x-belongs to callers (x-acme-ingest.javascript-present); the identifier grammar of ADR 36 already admits it, no built-in family ever takes the prefix, and a test holds both. A caller rule whose identifier is not in anx-family is refused when the profile is built, as is a second rule with an identifier already present. - What moves onto the API. M02's structural rules, unchanged:
CorpusValidationTestsmust report exactly what it reported, in the same order. M14'sfacturx.*container findings become rules of afactur-x-containerprofile the FacturX satellite builds withValidationProfile.Create— the first consumer outside this milestone's satellite, and the proof that the API serves one. M19'saction.*andhidden.*families and M16'sform.*rules follow, being core rules already.
Verdicts and the review outcome
PdfConformanceLevel a value: standard (PDF/A, PDF/UA), part, level letter, revision — PdfA1A … PdfA4F,
PdfUA1; parsed from and written as the claim's text ("PDF/A-2b", "PDF/UA-1")
PdfConformanceVerdict Level; Outcome; the failed rule identifiers; the rules not run and why; the number of
review items
PdfConformanceOutcome Conforms, ConformsPendingReview, DoesNotConform, Indeterminate
PdfReviewItem Condition (a Matterhorn failure condition, or a clause), Location, Question (our
wording), Evidence (bounded text: the alternative text, the heading's text), Suspicion
(None, Low, High)
PdfValidationReport + Verdicts (one per level the profile checks), ReviewItems (bounded by their own
capacity, counted whether kept or not), Coverage (rules not run, and why)
- A target and a level are two views of one claim.
PdfConformanceTarget(M13, M14) is what a writer is asked to produce, a combination (PdfA2A | PdfUA1);PdfConformanceLevelis one identification, read from a file or checked.PdfConformanceTarget.Levelsgives a target's levels as values of this type, so that the verdict on an output is computed for exactly the levels its target named, and no third list of levels exists. DoesNotConformwhen a rule of the level's families reportsError, or when the structural profile does: a structural error is a conformance failure whatever the file claims, and a file the reader had to rebuild failspdfa-filebesides.Indeterminatewhen an applicable rule could not run: content skipped by the caller's option (M02 made content rules skippable), an object cut at a reader limit (ADR 34), an encrypted document opened without its password, a signed PDF/A-2 whose CMS clauses wait for the signing satellite. The verdict names each rule and the reason — neverConformson a check that did not happen. A fault found without the missing check still makes the verdictDoesNotConform: an/Encryptentry fails PDF/A whatever the password.ConformsPendingReviewfor PDF/UA when no machine rule fails and at least one human condition applies — which is every document with content, since whether the reading order makes sense is a person's question. A PDF/UA claim is never reportedConformsby a machine.- Review items list each applicable human condition where it applies: every
Figure,Formulaand link with its alternative text as evidence ("is this an accurate description?"), every artifact on a page with real content, the reading order once per page, the table headers once per table, headings once per document. Heuristics raise the suspicion, never a finding (ADR 15): an alternative text equal to a file name, to "image", or identical on every figure; a paragraph in bold at a larger size where a heading could be; an artifact whose text is not a page number, a date or a repeated header. - Several levels in one run: a profile may combine PDF/A-2a and PDF/UA-1, as veraPDF checks several flavors at once; each level gets its verdict, and a rule shared by both reports once.
Claims
PdfConformanceClaims.Read(document) (core) reads the XMP of the revision in force through M14's model:
pdfaid:part, conformance, rev, amd and corr, in element or attribute form; pdfuaid:part, rev,
amd; and lists pdfd:declarations for M28 without judging them. What the standards do not define is reported,
not repaired: a lowercase level letter, a part 4 without rev, a pdfuaid:conformance (the DoD form's), two
identifications that disagree, a claim only in a superseded revision's packet. Reading reports them as claim.*
diagnostics — the account of what was read, as M14's xmp.* codes are — and the conformance profiles judge the same
defects as findings (pdfa-metadata.identification-invalid, pdfua-metadata.identification-invalid). A claim is the
level a document says; PdfConformanceValidator.Validate(document) checks the claimed levels by
default, and any level the caller names.
The satellite: profiles and the rule catalog
| Profile | Standard | What it adds to the structural profile | Referee flavor |
|---|---|---|---|
pdf-a-1b, pdf-a-1a | ISO 19005-1:2005 with its corrigenda | File structure on PDF 1.4 (no object or cross-reference streams), no transparency, no JPEG 2000, no embedded files, no optional content, CharSet and CIDSet for subsets; level a: structure and Unicode | veraPDF 1b, 1a; Apache PDFBox Preflight as a second opinion on 1b |
pdf-a-2b, 2u, 2a | ISO 19005-2:2011 | Transparency and JPEG 2000 under their rules, optional content configured, embedded files only when themselves PDF/A-1 or 2; level u: every glyph mapped to Unicode | veraPDF 2b, 2u, 2a |
pdf-a-3b, 3u, 3a | ISO 19005-3:2012 | Part 2, with any embedded file reached from /AF with its relationship and MIME type | veraPDF 3b, 3u, 3a |
pdf-a-4, 4f, 4e | ISO 19005-4:2020 | On PDF 2.0: pdfaid:rev, the document information dictionary reduced, page-level output intents, no levels b, u or a; 4f: any embedded file as an associated file; 4e: 3D and RichMedia as its annex allows | veraPDF 4, 4f, 4e |
pdf-ua-1 | ISO 14289-1:2014, Matterhorn Protocol 1.1 | Tagged content, role mapping, headings, tables, lists, notes, language, metadata, fonts, annotations, forms, navigation, security; human conditions as review items | veraPDF ua1 |
ConformanceProfiles.For(level) returns one; ConformanceProfiles.ForClaims(document) the combination of what
the document claims.
The catalog is data: for each rule its identifier, severity, title, the parts it applies to, its
PdfRuleReferences — one per part, since the same requirement is clause 6.3.4 in part 1 and 6.2.11.4.1 in part 2,
and veraPDF numbers its tests within each clause —, its remedy hint, and whether it needs content. One identifier
for one requirement, whatever the part: pdfa-font.not-embedded runs in every PDF/A profile. The families:
| Family | Covers |
|---|---|
pdfa-file | Header and binary comment, trailer /ID, no /Encrypt, nothing after the last %%EOF but an end of line, cross-reference and stream syntax, F, FFilter and FDecodeParms, LZW, object and cross-reference streams under part 1, a reader repair |
pdfa-metadata | The identification, the packet's form (no bytes or encoding in the xpacket header, unfiltered, typed), /Info against XMP (parts 1 to 3), /Info reduced (part 4), extension schemas for what is not predefined (parts 1 to 3) |
pdfa-color | Output intents — GTS_PDFA1, one profile object, the ICC version and device class a part allows, the profile's color space and N —, device color against the intent and default color spaces, ICCBased, Separation and DeviceN alternates |
pdfa-image | Interpolate, /Alternates, /OPI, bit depth (16 bits under part 1), JPEG 2000's channels, bit depth and color specification |
pdfa-graphics | TR, TR2, HTP, halftone types, rendering intents; transparency (forbidden in part 1, its color space and blend modes from part 2) |
pdfa-xobject | PostScript and reference XObjects, Subtype2 /PS |
pdfa-content | Operators ISO 32000 defines, BX/EX sections, q nesting, inline image filters |
pdfa-font | Embedding (not for a font used only with render mode 3), embeddability, widths against the program, CharSet and CIDSet, CIDToGIDMap, a symbolic TrueType's cmap, .notdef referenced, ToUnicode for levels u and a |
pdfa-annotation | Subtypes allowed, flags, CA (part 1), appearance dictionaries, NeedAppearances |
pdfa-action | Forbidden action types, named actions, /AA on the catalog, pages, fields and widgets |
pdfa-form | XFA, field additional actions |
pdfa-optional-content | Its absence under part 1; configuration names and no /AS from part 2 |
pdfa-attachment | Their absence under part 1; PDF/A only under parts 2 and 4; /AF, AFRelationship, /F and /UF, the MIME type under parts 3 and 4f |
pdfa-structure | Level a: /MarkInfo, the structure tree, standard or role-mapped types, language, alternative and actual text |
pdfa-limit | The implementation limits each part states |
pdfa-signature | The byte range covering the file but /Contents, the sub-filters allowed (parts 2 to 4) |
pdfua-content, pdfua-structure, pdfua-metadata, pdfua-language, pdfua-heading, pdfua-table, pdfua-list, pdfua-note, pdfua-figure, pdfua-font, pdfua-annotation, pdfua-form, pdfua-navigation, pdfua-optional-content, pdfua-attachment, pdfua-xobject, pdfua-security | ISO 14289-1 §7.1 to §7.21, one family per clause group |
Sourcing. Each rule is written from the text of the standard, in our own words, with its references; veraPDF's validation profiles are the referee, not the source — their license (the repository states GPL-3.0 or later, or MPL-2.0 or later) is read and recorded in slice 2's ADR, at the next free number, beside how the Matterhorn Protocol's identifiers are used, as M14's ADR records the EN 16931 artifacts. What the referee gives is a checklist: a test, reading veraPDF's profiles at the pinned version from the referee's container, fails when a (specification, clause, test) veraPDF checks has neither a rule of ours nor a recorded reason in the catalog ("needs a rendered page", "left to the signing satellite", "veraPDF's reading, disputed: …").
Readers the rules need, all bounded and total: the ICC profile header and tag table (version, class, color
space, the size field checked against the stream's decoded length); JPEG 2000's header boxes (ihdr, colr,
bpcc, res ), read without decoding a codestream (decoding is M22's); M08's font parsers for embedded programs'
widths, cmaps and glyph sets; M14's XMP model; M15's interpreter and structure reader; M16's form model; M04's
signature coverage.
PDF/UA-1 and the Matterhorn Protocol
- Every PDF/UA-1 rule cites its ISO 14289-1 clause and the Matterhorn Protocol 1.1 failure conditions it decides,
by identifier (
01-005, and so on). The protocol types each failure condition as machine-checkable or requiring a person; the mapping,MatterhornConditions, is data held to the protocol's published table: a test fails when a machine condition maps to no rule without a recorded reason, or a human condition to no review question. The table is obtained from the PDF Association at version 1.1, and its terms of use read and recorded in slice 2's sourcing ADR, before a condition is encoded; this specification assumes no count of its conditions. The protocol's 31 checkpoints — real content tagged, role mapping, flickering, color and contrast, sound, metadata, the document dictionary, OCR, appropriate tags, character mappings, natural language, stretchable characters, graphics, headings, tables, lists, mathematical expressions, page headers and footers, notes and references, optional content, embedded files, article threads, digital signatures, non-interactive forms, XFA, security, navigation, annotations, actions, XObjects and fonts — each get a section of the generated reference, with what we check, what we ask a person, and what we cannot see. - Machine conditions checked here: content neither tagged nor an artifact, and artifacts inside tagged
content (through the interpreter's marked-content subjects); standard types reached by the role map, no
circular mapping, no remapping of a standard type;
dc:title,DisplayDocTitle,pdfuaid;/Langpresent and valid wherever text needs it; figures and formulas with alternative text; headings not mixingHandHn; tables'THandTDregular and headers resolvable; list structure; notes with unique/IDs;/Tabs /Son pages with annotations; annotations in the structure with/Contents; links inLinkwith anOBJR; widgets inForm; the accessibility permission bit (§7.16); no dynamic XFA (§7.15); form XObjects with MCIDs drawn once; fonts embedded and mapped to Unicode (§7.21),.notdefnot drawn. - Human conditions become review items: is an artifact really an artifact; is real content really content; does the reading order make sense; is the alternative text accurate; is the heading a heading; is color or contrast carrying meaning; do table headers describe their cells; is the language right for the passage.
- Evidence from the structure, never from the paint. Where the structure and the page disagree (the AbleDocs scan draws its OCR text under the page image), the rules judge the structure and the review item says what the page shows.
Policies
PdfValidationPolicy immutable: Name (an x- family), Version, the profiles it runs, the caller's rules, and
Blocking — which findings make the policy fail: by severity, by identifier or family,
by conformance outcome
PdfPolicyVerdict Passed / Failed, with the blocking findings and the verdicts that decided it
PdfValidationPolicy.FromJson(stream) the declarative form, source-generated, unknown keys refused
The declarative vocabulary is closed, each check compiled to a rule in the policy's family:
| Key | Check | Reads |
|---|---|---|
requireConformance | The document conforms to one of the listed levels (a verdict, not a claim); acceptPendingReview for PDF/UA | This milestone's profiles |
requireClaim | It claims one of them | Claims |
forbid | Any of: javascript, launch-actions, external-links, embedded-files, xfa, encryption, multimedia, 3d, hidden-text, hidden-layers, unsigned-signature-fields | M15's inventory, M19's families |
fontsEmbedded | Every font used for rendering embedded | pdfa-font rules |
maxPageSize, minPageSize | Every effective CropBox within the size (A3, or millimeters) | M06's page model |
maxPages, maxFileSize | Counts | The index |
requireTagged, requireLanguage, requireTitle | As named | M15's structure reader, XMP |
minImageResolution | Every image placed at or above the resolution | M15's inventory |
blocking | error, warning, a list of identifiers or families, does-not-conform | — |
The schema (validation-policy.schema.json) is published on the site and held to the parser by a test, as the
corpus manifest's schema is held to its model. A policy's verdict never changes a rule's severity: the policy
decides what blocks, the rule what it found.
PDF/A-2 generation
PdfConformanceTarget (M14) gains PdfA2B, PdfA2U and PdfA2A, each combinable with PdfUA1. Part 3 is part 2
with any embedded file, so M14's requirement table serves unchanged but for its last rows:
| Area | Under part 2 (ISO 19005-2) | Where | When it cannot be met |
|---|---|---|---|
| Embedded files | Only a PDF/A-1 or PDF/A-2 document (§6.8), with /F and /UF; no /AF required | M06's attachments API under the target | An attachment that is not a PDF, or a PDF that does not conform: a conflict |
| Verification of an embedded PDF | Through IPdfConformanceChecker when the caller registers one (the satellite does, in DI); otherwise its claim is trusted and pdfa.attachment-claim-unverified says so | The core, through the seam | A PDF with no claim: a conflict |
| Identification | pdfaid:part 2, level B, U or A | M14's XMP model | — |
| Level a | M13's structure, as for 3a; the dual claim with PDF/UA-1 writes pdfuaid's extension schema, as PDFlib's invoice does | M13, M14 | M13's conflicts |
M14's RaiseClaimToPartThree now asks the checker whether the input really conforms to part 2 before raising it,
and reports what it found; without a checker it still trusts and says so.
Writing to a PDF/A-1 document
Parts 2 to 4 relaxed much of what part 1 forbids, and every operation the library offers on a received document
must know it. M14's requirement table becomes data — PdfAConstraintSet (internal), one per part and level —
and each writer consults the set of the input's claim:
| Constraint of part 1 | Who keeps it | By default |
|---|---|---|
| No cross-reference or object streams | M03's writer (already) | A classic table |
No transparency: CA and ca 1, no soft mask, /BM /Normal, no transparency group | M09's stamps and M11's annotations (already), M06's page copies | A mark with opacity refused, as today |
| No JPEG 2000, no 16-bit image | M07's image pages, M09's image stamps, M12's images when merged in | Refused; a JPEG or PNG passes |
| No embedded files; no optional content | M06's attachments and merge of /OCProperties, M11's print-only layers | Refused |
CharSet for Type 1 subsets, CIDSet for CID subsets | M08's embedding, whenever the target document claims part 1 | Written, from the glyphs actually in the subset |
| No LZW | M03's writer (Flate only) | — |
Annotations: CA 1, the print flag, an appearance with /N only | M11, M16's widgets | Written so |
| Actions: the types part 1 forbids, whose list is not part 2's | M11's links, M16's field actions | Refused |
| PDF 1.4's implementation limits (a string of 65,535 bytes, a name of 127, 8,388,607 objects, 8 DeviceN colorants) | The writer and the content builder count | A conflict |
| XMP: part 1's predefined schemas, extension schemas for the rest | M14's model | Generated |
A conflict follows PdfConformancePolicy, as everywhere: Refuse (the default) throws PdfConformanceException
naming the clause of part 1; RemoveClaim writes and reports the operation's own *.conformance-claim-removed.
An update to a signed PDF/A-1 file — the BOE gazettes, remote — follows M04 and M09's rules as it would without the
claim: an incremental update, each signature still covering its revision.
Conformance through a merge
M06 keeps a claim only while every part makes the same one, and removes it otherwise. M20 makes the output conform:
- The target.
PdfAssemblyOptions.ConformanceisKeep(M06's rule, the default),Auto, or an explicit level.Autocomputes the greatest level every part claims, on the order b < u < a within a part and 1 < 2 < 3 across parts — a PDF/A-1b and a PDF/A-2u give 2b; a PDF/A-2b with an embedded XML and a PDF/A-3u give 3b — and PDF/UA-1 only if every part claims it. An explicit level may include a part that claims nothing, if the checker finds it conforming — never for PDF/UA-1, whose human conditions no machine can settle. - Verification. With a checker registered, each part is checked against the target before a byte is written,
and the plan lists each verdict; a part that does not conform removes the claim, and the report names the part
and the rules that failed (
assembly.conformance-claim-removed, with rule identifiers). Without a checker, claims are trusted andassembly.claim-unverifiedsays so. - Output intents. One
GTS_PDFA1intent, one profile object — part 2 requires every intent's profile to be the same indirect object, not equal bytes. Parts whose profile decodes to the same bytes share it. A part whose profile differs in the same color family keeps its color exact: its pages and every resource dictionary of its content get/DefaultRGB(orDefaultCMYK,DefaultGray) as anICCBasedspace on its own profile, so that its device color is interpreted as it was (assembly.output-intent-defaulted). A part in another family (a CMYK intent beside an RGB one) is handled the same way when its profile allows, and otherwise is a conflict. - Metadata. A new packet from the base part's, the target identification, extension schemas united across
parts (M14's model),
/Infoin step. - Associated files brought to part 3's rules when the target is part 3; under part 2, attachments that are not PDF/A are a conflict; under part 1 there are none.
- Structure. M06's tree merge; level a and PDF/UA-1 need every part tagged, as M06 already requires.
- Proof. The output is checked against the target before the claim is written (the checker again, on the written file); a failure removes the claim and reports it. A claim the library has reason to doubt is never left in the file (invariant 7).
Diagnostics and findings
In PdfDiagnosticCodes, disjoint from rule identifiers (ADR 36):
| Code | Severity | Meaning |
|---|---|---|
claim.malformed | Warning | An identification the standards do not define: a lowercase level, a part 4 without rev, pdfuaid:conformance; read as far as it goes |
claim.conflicting | Warning | Two identifications that disagree in one packet, or /Info and XMP |
claim.superseded-only | Information | A claim found only in an earlier revision's packet |
pdfa.attachment-claim-unverified | Information | Under part 2, an embedded PDF's claim trusted, no checker registered |
assembly.claim-unverified | Information | A merge kept a claim without verifying its parts |
assembly.claim-extended | Information | A part that did not claim the target joined it, having passed the checker |
assembly.output-intent-defaulted | Information | A part's color kept exact through a default color space on its own profile |
A rule that could not run is not a diagnostic — the reader did nothing — but an entry of the report's coverage,
with its reason (content-skipped, limit-reached, not-decrypted, left-to-signing), and it makes the verdict
Indeterminate. The conformance rules' identifiers, severities and references are the catalog's, published in the
generated reference (below). The report's JSON gains verdicts, reviewItems and coverage, stably ordered.
Referees
All in containers (ADR 27), pinned by digest, their versions recorded in docs/status.md:
| Referee | Confirms |
|---|---|
veraPDF (--flavour 1a … 4f, ua1; machine-readable report) | Each verdict, and each failed (specification, clause, test), on every corpus document and every output |
| Apache PDFBox Preflight | A second opinion on PDF/A-1b, where veraPDF and our profile disagree |
| pikepdf | Output intents, default color spaces, CIDSet and CharSet against the embedded program, attachments and /AF, the structure tree of merged volumes |
qpdf (--check, --json) | Every output sound; cross-reference form under part 1 |
poppler (pdfinfo -struct-text, pdffonts, pdfinfo -js) | Structure order after merges; fonts embedded; scripts present, for policy facts |
| ExifTool | The rebuilt XMP packet and its extension schemas |
The command-line tool
validate FILE [--profile structural|pdf-a-1b|…|pdf-ua-1|claims] [--policy policy.json] [--json] prints the
verdicts, the findings with their references, and the review items. Its exit codes are M06's — 0 when nothing
blocks, 1 when the document does not conform or the policy fails, 2 for a usage error, 3 for an input that cannot be
read — and 5, which this milestone adds to M06's table for every verb, when nothing fails and the verdict waits: a
review pending (ConformsPendingReview) or a rule that could not run (Indeterminate).
html2pdf --pdf-a 2b|2u|2a [--pdf-ua]. merge --conformance keep|auto|pdf-a-2b.
Slices
Each slice ends on a green commit, with its codes documented and its measurements recorded in docs/status.md.
- The public rule API, on the structural profile. Delivers the ADR (public engine, severity in conformance
profiles, review items,
x-families),IValidationRuleand its passes, subjects and the single walk, dispatch arrays, aggregation, content locations,ValidationProfile.CreateandCombine, verdicts and coverage in the report, cancellation and progress; M02's structural rules moved onto it. Proved by unit tests of registration, refusals (a duplicate identifier, a caller rule outsidex-), dispatch (each subject visited once whatever the rules), aggregation and bounds; an FsCheck property — for any sequence of findings the report counts every one and keeps at most its capacity, deterministically —;CorpusValidationTestsunchanged, finding for finding and in order;ValidationBenchmarkswithin 10 % of its last recorded figures. Leaves conformance. - The satellite, claims, the catalog and the referee. Delivers the package,
PdfConformanceLevel, the claim reader and its diagnostics, the catalog's shape and references, the sourcing ADR, the veraPDF harness (container, report parsing, the checklist test),IPdfConformanceCheckerand its DI registration, thepdfa-fileandpdfa-metadatafamilies for all parts, and the manifest's fields for the corpus gaps below — a list of every claim a document makes, each with veraPDF's verdict, its failed (specification, clause, test) set and veraPDF's version, and a recorded disagreement with its reason —, with their schema,docs/corpus.mdandCorpusManifestSchemaTests, replacingclaimsConformance,conformanceValidand M13'sclaimsUaanduaConformanceValid, every entry migrated in the same change; the claim pattern admits PDF/X (1a, 3, 4, 4p), PDF/VT (1, 2, 2s) and the Well-Tagged PDF declarations, which M28 and M29 fill, each with its own referee's verdict. Proved by unit tests over hand-made packets (every identification form and defect); integration: on every committed document, the claims read equal those ExifTool reads; on every document with a PDF/A claim, the failures veraPDF reports in the file-structure and metadata clauses are exactly ours, in both directions. Leaves the other families. - PDF/A-2b, 2u and 2a generation. Delivers the targets, the part-2 attachment rule through the checker, the
dual claim,
RaiseClaimToPartThreeverified,html2pdf --pdf-a 2b|2u|2a. Proved by unit tests per row and per refusal, and with NSubstitute that the checker is asked once per embedded PDF; integration: veraPDF's2b,2u,2aandua1pass the reference documents, and our own profiles, as far as slice 2 built them, report nothing — each later slice runs its families over these documents first, since what we produce is judged first (ADR 17). Leaves the remaining families. - Fonts and Unicode. Delivers
pdfa-fontfor all parts: embedding (render mode 3 excepted, through the interpreter'sTextRunsubjects),fsType, widths against the program to one unit,CharSetandCIDSet,CIDToGIDMap, symboliccmaps,.notdef,ToUnicodefor levels u and a. Proved by unit tests per rule — a document that triggers it, one that does not, one legal but unusual (the Ricoh scan's non-embedded font drawn only in invisible text) —; integration: the font clauses' failures equal veraPDF's on every claimed document and on the2bdifferential set (below). Leaves color and content. - Color, images, graphics state, XObjects and content. Delivers the ICC header reader, the JPEG 2000 box
reader,
pdfa-color,pdfa-image,pdfa-graphics,pdfa-xobject,pdfa-content,pdfa-limit. Proved by unit tests, with hostile ICC and JP2 headers (a size that lies, a tag table past the end, a million tags); integration: veraPDF's failures in these clauses equal ours, including the EU's 2015 regulation — veraPDF 1.30 fails its part-1 claim on 6.2.3.3 test 1 (its page groups blend in DeviceRGB under a CMYK intent, three checks) and 6.4 test 3 (a transparency group on each page) — and ImageMagick's JPEG 2000 under part 1. Leaves the interactive families. - Annotations, actions, forms, optional content, attachments and signatures. Delivers the families of those
names,
pdfa-signature's byte range and sub-filters, and the rules left to the signing satellite recorded as such. Proved by unit tests; integration: veraPDF's verdicts on its own fixtures (pdfa1b-forms-fail,pdfa2b-actions-fail,pdfa3b-embedded-failand their passing twins) and on BFO's embedded PDF/A, clause for clause. Leaves level a and part 4. - Level a and part 4. Delivers
pdfa-structurefor 1a, 2a and 3a on M15's structure reader, and thepdf-a-4,4fand4eprofiles — the part-4 identification,/Inforeduced, page-level intents, embedded files by level, 3D and RichMedia under 4e. Proved by unit tests; integration: veraPDF agrees on every 1a, 2a and 3a claim in the corpus, onvendor/verapdf/pdfa4-metadata-pass.pdf, onpdf20-version-mismatch.pdf, whose PDF/A-4 claim the manifest does not record today, and on the remote PDF/A-4f invoice. Leaves writing. - Writes that keep PDF/A-1. Delivers
PdfAConstraintSetfor every part, consulted by M03, M06, M07, M08, M09, M11 and M16;CharSetandCIDSetwritten by M08 under part 1. Proved by unit tests per constraint and per operation; an FsCheck property — for any subset M08 writes,CIDSethas exactly the bits of the CIDs present —; integration: every operation of the matrix below on every committed PDF/A-1 document, then veraPDF upholds the claim, qpdf finds a classic table, pikepdf findsCIDSetequal to the subset's glyphs. Leaves merges. - Conformance through a merge. Delivers
PdfAssemblyOptions.Conformance, the target computation, part verification, one output intent object, default color spaces, united metadata, part-3 attachments, the final check. Proved by unit tests, an FsCheck property on the target — never above any part's verified level, andKeepnever above M06's —; integration: the merge pairs of the acceptance table pass veraPDF at the level the plan announced, and each refused merge names the part and the rules veraPDF also fails. Leaves PDF/UA. - PDF/UA-1's machine rules. Delivers the
pdfua-*families with ISO 14289-1 and Matterhorn references,MatterhornConditionsand its coverage test. Proved by unit tests per rule; integration: on the PDF/UA claims of the corpus and on every tagged document, veraPDF'sua1failures equal ours, clause and test. Leaves the human conditions. - The review outcome. Delivers review items for every human condition that applies, their evidence and
suspicions,
ConformsPendingReview, and the dual verdict PDF/A-2a with PDF/UA-1 in one run. Proved by unit tests over the applicability of each condition; integration: the three committed PDF/UA-1 documents veraPDF upholds come outConformsPendingReviewwith a review item for every figure, formula and link their structure holds — counted independently by a pikepdf walk —; suspicions on the derived variants with poor alternative text. Leaves policies. - Policies, the tool, and the corpus closed. Delivers
PdfValidationPolicy, its JSON form and schema,validate --profileand--policy, the generated reference pages,ConformanceBenchmarks, and every disagreement with veraPDF fixed or recorded in the manifest with its reason. Proved by unit tests of the parser (unknown keys, bad sizes, empty blocking) and of each vocabulary key; integration: the corpus policy's verdicts equal the facts pdfinfo, pdffonts, qpdf and veraPDF report; the tool's output equals the API's.
Tests required
Unit —
- The API: dispatch by subject kind (a font shared by 1,000 pages visited once as a resource); passes created and
dropped per document; aggregation per rule and subject with the first location; the report's capacity and
review capacity counted separately; verdict computation for every combination of failed, not run and review
items;
x-families enforced both ways; duplicate identifiers refused;Combineordering; cancellation checked per subject; progress per page. - Claims: each identification form (element, attribute, several
rdf:Descriptions), every defect listed underclaim.*, claims in a superseded packet, a document with PDF/A and PDF/UA claims both. - Each conformance rule: a document that triggers it, one that does not, and one where it must stay silent because the situation is legal but unusual — M02's triad; for rules shared by several parts, each part's reference.
- PDF/A-2 generation: every row of the part-2 table, the checker asked through its seam (NSubstitute), trusted when absent and reported; the dual claim's XMP.
- Part-1 writes: each constraint, for each operation that could break it;
CharSetlists exactly the glyph names of a Type 1 subset. - Merges: the target computation over every pair of levels; output intents shared by object; default color spaces written only for parts whose profile differs; united extension schemas; the final check removing a claim and saying why.
- PDF/UA-1: each machine rule; the Matterhorn mapping's coverage both ways; each human condition's applicability; each suspicion heuristic, with the cases where it must stay silent (an alternative text that merely resembles a file name).
- Policies: every vocabulary key, the blocking modes, the schema held to the parser, a policy's verdict independent of rule severities.
- Hostile: a role map with a 100,000-entry cycle; a structure tree 1,000,000 elements deep (iterative); a page with
1,000,000 annotations (report bounded, time linear); an ICC profile whose size field says 4 GB; a JPEG 2000 box
that claims to extend past its stream; an XMP packet with 100,000 extension schemas; a content stream of
10,000,000 operators with
qnested 100,000 deep (time linear, one finding); a font with 1,000,000 widths. - Properties (FsCheck): the engine is total — any document the reader opens gives a report, never an exception (run over the fuzzing campaign's mutated corpus); the verdict is monotonic — adding a failing finding never improves it; the merge target never exceeds a verified part's level; two runs give identical reports.
Integration — in containers (ADR 27): veraPDF on every corpus document for the claimed levels and for the 1b,
2b and ua1 differential flavors, and on every output of this milestone; PDFBox Preflight where veraPDF and we
disagree on part 1; pikepdf, qpdf, poppler and ExifTool on every output.
Acceptance conditions
"The reference documents" are M12's renderings of sources/invoice-fr.html, report-fr.html and
contract-fr.html and M13's accessibility reference, committed as documents/*/adcodicem-*. "Every claimed
document" is every corpus entry with expect.claimsConformance — 30 committed and 19 remote today, from part 1 to
part 4 — plus the PDF/UA claims the manifest does not yet record (below). "The differential set" is every committed
document, whatever it claims, validated under 2b and ua1 by veraPDF and by us: a verdict on files that never
claimed anything is where a validator shows whether it is right or merely strict. Rows naming remote documents
close only on a green Remote corpus run.
| Documents | Behavior | Verified by |
|---|---|---|
| The reference documents under PDF/A-2b, 2u and 2a, and under 2a with PDF/UA-1 | veraPDF reports no error at each level and under ua1; our profiles report no finding and the UA verdict ConformsPendingReview | PdfA2RefereeTests.Reference_documents_pass_pdf_a_2_at_every_level (new) |
| M14's PDF/A-3 outputs committed to the corpus | Our 3b, 3u and 3a profiles agree with veraPDF on each, clause for clause | CorpusConformanceTests.Our_own_outputs_are_judged_as_verapdf_judges_them (new) |
Every claimed document — committed: the veraPDF fixtures (pdfa1b-annotations-pass, pdfa1b-forms-fail, pdfa2b-content-pass, pdfa2b-actions-fail, pdfa3b-embedded-pass, pdfa3b-embedded-fail, pdfa4-metadata-pass), bfo-pdfa2b-embedded-pdf, the EU publications (distiller10-eu-consolidated-regulation-2015 rejected, pdflib-oj-exchange-rates-greek, antenna-house-oj-exchange-rates-2019, 3heights-eu-consolidated-regulation-2026), OpenOffice.org 3.2's three PDF/A-1a, PDFMaker 9's PDF/A-1b, Acrobat 11's three Image Conversion pages, imagemagick-false-pdfa1b-jpx, the Factur-X and ZUGFeRD invoices of vendor/zugferd/ and vendor/docentric/, pdflib-pps-kraxi-pdfa2a-pdfua1-invoice, documents/archival/libreoffice-report-pdfa2b.pdf; remote: the BOE gazettes and the re-signed decree, the Slovak seal, ricoh-3heights-scan-issue5747, ghostscript10-pdfa1b-type1c, outside-in-pdfa1a-broken-loca-issue17671, pdfmaker81-word-va-kernel-systems-guide, the remote/zugferd-corpus/ and remote/mustang/ invoices, the PDF/A-4f one among them | Our verdict for the claimed level equals expect.conformanceValid, and the set of failed (specification, clause, test) equals veraPDF's, in both directions; each disagreement is fixed, or recorded in the manifest with its reason — "an opinion of our own" on Antenna House's malformed font XMP and on the two Image Conversion pages damaged on purpose, since the corpus sources already flag both | CorpusConformanceTests.Our_verdict_agrees_with_verapdf_on_every_claim (new) |
The claims veraPDF cannot parse: vendor/opf-format-corpus/pdfmaker9-word-distiller-one-byte-missing.pdf (a byte missing after the header), remote/opf-format-corpus/jhove-hul-35-atypon-pdfplus-journal-article.pdf | DoesNotConform, with the pdfa-file rules naming the damage the reader repaired — a verdict of our own, recorded as such in the manifest | CorpusConformanceTests.Claims_veraPDF_cannot_parse_get_a_verdict_of_our_own (new) |
The differential set, under 2b and ua1 | Our failed clauses and tests equal veraPDF's on every document; the disagreements recorded, each with its reason, and their count in status.md | ConformanceDifferentialTests.Our_failed_clauses_are_verapdfs_on_every_document (new) |
The PDF/UA claims: committed pdflib-pps-kraxi-pdfa2a-pdfua1-invoice, indesign13-pdfua1-german-book-chapter, indesign15-pdfua1-form, vendor/fr-licence-ouverte/libreoffice-cerfa-13983-form.pdf (LibreOffice's claim, with JavaScript formats), designer-distiller23-uscis-i9-javascript-form, livecycle-irs-1040-2022-xfa-ur3 (a hybrid XFA form), livecycle-dod-dd293-aes128-xfa (pdfuaid:conformance B, under AES-128); remote: the two AbleDocs documents and the InDesign CS6 brochure | Our UA verdict matches veraPDF's machine verdict — ConformsPendingReview where veraPDF finds no failure, DoesNotConform with the same clauses otherwise —; the DoD form's claim reported claim.malformed | CorpusPdfUaTests.Ua1_verdicts_agree_with_verapdf_on_every_claim (new) |
| The three committed PDF/UA-1 claims veraPDF upholds, and the remote AbleDocs scan (480 formulas) | A review item for every Figure, Formula and Link in the structure, with its alternative text as evidence, and one per human condition that applies to the document — the counts equal an independent pikepdf walk's; the scan's review items say the OCR text lies under the page image | CorpusPdfUaTests.Human_conditions_become_review_items_where_they_apply (new) |
vendor/opf-format-corpus/pdfmaker9-word-distiller-aes128-no-accessibility-extraction.pdf | The security rule of §7.16 fails, and veraPDF's ua1 agrees | CorpusPdfUaTests.Accessibility_permission_is_checked (new) |
The committed PDF/A-1 documents veraPDF upholds — pdfa1b-annotations-pass, the three openoffice32-* PDF/A-1a, pdfmaker9-word-distiller-pdfa1b-test-document, acrobat11-image-conversion-pdfa1b-image, antenna-house-oj-exchange-rates-2019 — under each operation: an exhibit stamp and a Bates number (M09) in the default face, a note and a link (M11), a metadata edit, a page extracted (M06), each merged with another of them | veraPDF upholds the same claim after each; a classic table and no object stream; CIDSet or CharSet equal to each new subset's glyphs; a highlight with opacity, a print-only layer and a file attachment each refused, naming the clause of part 1 | CorpusPdfA1Tests.Writing_to_a_pdf_a_1_file_keeps_it_pdf_a_1 (new) |
Remote PDF/A-1 under the same operations: the BOE's 2015 law and 2026 decree (signed), ricoh-3heights-scan-issue5747, ghostscript10-pdfa1b-type1c | The same, and on the signed gazettes each signature still covers exactly its revision (M04, pyHanko) | CorpusPdfA1Tests.Writing_to_a_pdf_a_1_file_keeps_it_pdf_a_1 (new) |
Merges: pdflib-oj-exchange-rates-greek with 3heights-eu-consolidated-regulation-2026 (PDF/A-2a, one sRGB profile differently encoded); pdfa1b-annotations-pass (Adobe RGB) with pdfa2b-content-pass (Apple RGB) under Auto; pdfa3b-embedded-pass with libreoffice-report-pdfa2b; openoffice32-writer-simple-pdfa1a with openoffice32-writer-lorem-ipsum-pdfa1 | The level the plan announced — 2a, 2b with the second part's pages on /DefaultRGB, 3b, 1a — passes veraPDF; one output intent profile object; the attachments of the part-3 output keep /AF | CorpusConformanceMergeTests.Merging_pdf_a_documents_keeps_a_claim_verapdf_upholds (new) |
Merges with a part that does not conform: pdfa2b-content-pass with pdfa2b-actions-fail; openoffice32-writer-simple-pdfa1a with imagemagick-false-pdfa1b-jpx; libreoffice-report-pdfa2b with vendor/zugferd/pypdf2-facturx-python-false-pdfa3b.pdf | The claim removed; the report names the part and the rules — the same clauses veraPDF fails on that part alone — and never a claim veraPDF rejects | CorpusConformanceMergeTests.A_part_that_does_not_conform_removes_the_claim_precisely (new) |
pdflib-pps-kraxi-pdfa2a-pdfua1-invoice with pdflib-oj-exchange-rates-greek | PDF/A-2a kept; PDF/UA-1 removed and reported, since one part never claimed it — never extended by a machine | CorpusConformanceMergeTests.Pdf_ua_is_never_extended_to_a_part_that_did_not_claim_it (new) |
| The FacturX satellite's container checks, moved onto the public API | On every hybrid M14 names, exactly the findings M14's acceptance recorded, in the same order | CorpusFacturXTests.Container_defects_are_reported_exactly (M14's, unchanged) |
| Every committed document, under the structural profile after slice 1 | Exactly the findings M02 recorded, in the same order | CorpusValidationTests.Every_document_produces_exactly_its_declared_findings (M02's, unchanged) |
| Every committed document under a corpus policy — no JavaScript, fonts embedded, not encrypted, page size at most A3, a PDF/A claim that conforms | The policy's verdict on each equals the facts the referees report: pdfinfo -js, pdffonts, qpdf --show-encryption, pdfinfo -box, veraPDF | ValidationPolicyTests.A_policy_decides_as_the_referees_facts_say (new) |
The 1000-page journal under 2b with ua1, and the remote 9,302-page United States Code | Memory within a stated budget, content interpreted once, time linear in pages; recorded in status.md | CorpusConformanceTests.Validating_the_largest_documents_stays_within_its_budget (new), ConformanceBenchmarks (new) |
| Any corpus document, any profile | Two runs give identical reports | CorpusConformanceTests.Conformance_validation_is_deterministic (new) |
| The same validations through the tool | validate prints the API's report and exits with the documented code | CorpusToolTests.Validate_verb_matches_the_api (new) |
Corpus
What the corpus holds
- PDF/A claims with veraPDF's verdict (
expect.claimsConformance,conformanceValid): 49 entries — part 1 from OpenOffice.org 3.2, PDFMaker 9, Acrobat 11 Image Conversion, Antenna House, Distiller 10, ImageMagick, and remote Ghostscript 10, 3-Heights over a Ricoh scan, Oracle Outside In, the BOE gazettes, PDFMaker 8.1 and 11; part 2 from PDFlib, 3-Heights, BFO, LibreOffice and the veraPDF fixtures; part 3 from Mustang, iText and PDFBox, Aspose, Apache FOP, GnuAccounting, the factur-x Python library, and remote intarsys, Symtrax (3a), Konik, 4s4u, iText 9, WeasyPrint (3u); part 4 from the veraPDF fixture and WeasyPrint's remote PDF/A-4f invoice. Nine are rejected, two cannot be parsed by veraPDF at all, and our own LibreOffice PDF/A-2b report has no verdict recorded yet. - Conformance fixtures (
conformance-fixture): the veraPDF pass and fail pairs for part 1 (annotations, forms), part 2 (content, actions), part 3 (embedded files), part 4 (metadata),pdf20-version-mismatch.pdf(a part-4 failure the manifest records without its claim), and BFO's PDF/A-2b carrying a PDF. - PDF/UA claims (
pdfua-1-claim,pdfua1-claim,pdfua-claim,pdfua-1-claim-xmp,pdfua-claim-invalid-conformance-b): seven committed — PDFlib, InDesign 13 and 15, LibreOffice's Cerfa, Designer's USCIS I-9, the IRS 1040 hybrid XFA, the DoD form under AES-128 — and three remote from AbleDocs and InDesign CS6. - Tagged documents without a claim (
tagged,tagged-structure,tagged-structure-tree): 98 tagged in all, 56 of them committed, from Word, PDFMaker, InDesign, Distiller, LiveCycle, FineReader, OmniPage, LibreOffice, PDFlib, Antenna House — theua1differential's material — with the anomalies M02 names: role maps to themselves, custom types, a/MarkInfopointing at the page tree, structure without/MarkInfo,StructParentswithout a tree, empty/Lang,/Langof(English), a duplicated MCID in PDF 2.0. - What the rules read, in every shape producers wrote it: output intents in sRGB, Adobe RGB, Apple RGB, CMYK
and PDF/E (
*-output-intent), fonts embedded and not in every type,CIDSet, JPEG 2000 in JP2 and raw codestreams, transparency groups and soft masks, optional content, JavaScript at every level, XFA, embedded files and associated files with every relationship, signatures of every kind (M04's). - Scale: the 1000-page journal, and remote the United States Code, the 82-page tagged scan, the 48-update file.
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
veraPDF's failed rules — specification, clause, test — for each document, under its claimed levels and under 2b and ua1 for every document, with veraPDF's version; and a field recording a disagreement with its reason | The acceptance compares rule by rule, not only pass or fail, and says that a disagreement is recorded "in the manifest"; conformanceValid holds one boolean | 1 | Generated here: build_corpus.py --committed-only and --remote run veraPDF's container, once the schema has the fields (docs/corpus.md) |
Every claim a document makes, PDF/A and PDF/UA together, with each verdict — PDFlib's invoice claims both, the manifest one; six of the seven committed PDF/UA claims are recorded only as features, and LibreOffice's Cerfa's not at all; pdf20-version-mismatch.pdf's PDF/A-4 claim is not recorded | The PDF/UA rows cannot be checked against recorded expectations, and one PDF/A-4 fixture escapes the claimed-document row | 1 | Generated here: a list-valued claim field in the schema, filled by build_corpus.py from the XMP and veraPDF |
| Pass and fail fixtures for each rule of each part and level and of PDF/UA-1 — one pair per (clause, test) veraPDF checks | Nine fixtures cannot show that a hundred-odd rules per part fire where they should and only there; the veraPDF corpus exists for exactly this and encodes the verdict in each file name | 1 | A public source: the veraPDF corpus (CC BY 4.0, already vendored in part), a selection committed; the whole of it through a pinned archive in the remote corpus (ADR 33) if the selection outgrows what should be committed |
| Our own PDF/A-2b, 2u, 2a and 2a with PDF/UA-1 renderings of the reference documents, committed | "What we produce must be as readable as what we consume": M21, M26 and M28 accept on them | 1 | Generated here, by this milestone, recorded in build_corpus.py |
| PDF/A-2u, PDF/A-4e, and a committed PDF/A-3a and PDF/A-4f, from third-party producers | Four levels are judged on fixtures or remote files only; 2u has no document at all | 2 | Generated here with WeasyPrint's PDF/A variants (a Python producer, its version and the variants it offers checked when run); the veraPDF corpus's part-4 folders; a contribution (W12) |
| The other 33 files of BFO's PDF/A-2 suite | A second fixture author for part 2, with pass and fail cases written independently of veraPDF's | 2 | A public source: BFO's suite (CC BY 3.0), committed |
| The Isartor test suite (PDF/A-1b) | The historical reference suite for part 1, which PDFBox Preflight was built against — the second opinion's own calibration | 2 | The remote corpus: its terms permit use and forbid redistribution (docs/corpus-sources.md), which ADR 32 admits once they are read again |
| A PDF/A-2b document with a CMYK output intent that veraPDF upholds | The merge's /DefaultCMYK path and the cross-family conflict are checked only against the one CMYK claim, which veraPDF rejects | 2 | Generated here: Ghostscript's pdfwrite in PDF/A-2 mode with a CMYK profile whose terms allow embedding; or a public source |
| A PDF/A-1b and a PDF/A-2b form, fields and all, that veraPDF upholds | Fills on a PDF/A-1 file (CharSet in the appearance's font) are otherwise checked only on the failing fixture | 2 | Generated here: LibreOffice's PDF/A export of an ODT form, if its fields survive the PDF/A option; else a contribution (W08) |
| A signed PDF/A-2 or 3 document that veraPDF judges | The pdfa-signature rules and the rules left to the signing satellite have no document; every signed claim in the corpus is part 1, which has no signature clause | 2 | Generated here once M26 signs; before that, a public source among the PAdES test sets with a PDF/A claim (remote if their terms require) |
| Tagged documents with deliberately poor alternative text, a heading drawn as a bold paragraph, an artifact holding real content | The suspicion heuristics need documents where they must fire and where they must stay silent | 3 | Derived here from committed tagged documents by recorded edits (pikepdf) |
| The PDF/UA Reference Suite 1.1's members no pass screened | More PDF/UA-1 claims from other producers, each with the suite's own statement of what it shows | 3 | A public source, each member screened for personal data first; remote where the suite's CC BY cannot be shown to cover the content |
Traps
- veraPDF is the referee, not the standard. It upholds Antenna House's claim over malformed font XMP, and
Acrobat's over an image whose
/Heightwas altered; a rule that agrees with it everywhere may be copying its mistakes. A disagreement is written down with the clause's text beside it, never absorbed into a rule. - The same requirement has a different clause in each part: font embedding is 6.3.4 in part 1 and 6.2.11.4.1 in part 2, and veraPDF numbers tests within clauses. Our identifiers are stable; the references carry the numbers.
- A font used only for invisible text is not "used for rendering". The Ricoh scan's non-embedded CID font draws only render mode 3 OCR text, and veraPDF upholds its PDF/A-1b claim. Deciding needs the interpreter, per font and per render mode.
- Several output intents must share one profile object, not equal bytes in two objects: a merge that copies each part's intent breaks part 2 even when the profiles are identical.
- A default color space changes nothing visible when it holds the part's own profile — and everything, when it holds the wrong one. It is written only from the part's output intent, never from a guess.
- Default color spaces are looked up in the current resource dictionary. A form XObject, a pattern, a Type 3
glyph or an annotation appearance with resources of its own does not see the page's
/DefaultRGB: each of the part's resource dictionaries gets the entry, or its device color escapes the remapping. pdfaid:conformanceisA,BorU, uppercase, and absent for plain part 4; part 4 needspdfaid:rev. The DoD form'spdfuaid:conformancedoes not exist in PDF/UA at all.- Part 1's predefined XMP schemas are those of 2004. A property that later XMP made standard needs an extension schema under part 1 and not under part 2, so a rule that checks "undescribed schemas" is part-aware.
- Transparency is detected, not assumed. A page
/Groupwith/S /Transparencyfails part 1 even with nothing transparent on it; anSMaskof/Nonedoes not. .notdefis found only by drawing: a code maps to it through the font's encoding, itscmapand itsCIDToGIDMap, and PDF24's invoice maps a capital E to a glyph its subset lacks. The rule needs the interpreter, and the finding names the code.- A claim in an earlier revision is not a claim. The XMP of the revision in force decides; an update that drops
pdfaidremoves the claim, and one that adds it adds a claim the earlier bytes must still satisfy. - The structural profile's errors are conformance failures, its warnings are not. A validator that failed a PDF/A claim for a stale hint table would contradict every archive's experience.
- A PDF/UA verdict from a machine is never "conforms". Every document with content has a question only a
person can answer — does the reading order make sense —; reporting
Conformswould be a claim the library cannot support (invariant 7). - Review items can outnumber findings a thousand to one — 480 formulas in one scan. They are bounded, counted, grouped by condition in the report's summary, and never promoted to findings.
- Rule identifiers are public API from the day they ship (ADR 36), and a conformance family holds dozens. Name them for the requirement, not for veraPDF's test number, which moves between its versions.
- A policy is not a severity. Letting a caller's policy make a warning an error would give one identifier two severities; the policy says what blocks, and the report stays the same for everyone.
- A caller's rule runs inside our walk. It must not resolve what the context already resolved, nor allocate per subject; the documentation says so, and the benchmark includes a caller rule.
- Merging a PDF/A-1 part into a part-2 output is not free: part 2 checks things part 1 never did (
.notdef, optional content configurations). The part is checked against the target, not against its own claim.
Documentation
docs/website/docs/concepts/conformance.md(new): claims and verdicts, the four outcomes, review items and why a machine never settles PDF/UA, what is checked, what needs a person, what waits for another satellite.docs/website/docs/guides/validating-pdf-a-and-pdf-ua.md(new): validating a received document, reading the report, several levels in one run, the tool's exit codes.docs/website/docs/guides/validation-policies.md(new): policies in JSON and in code,x-families, blocking, the published schema.docs/website/docs/guides/writing-validation-rules.md(new): the public API — subjects, passes, the sink, performance rules for callers.docs/website/docs/reference/conformance-rules.md(new, generated from the catalog and held to it by a test): every rule, its severity, the parts it applies to, its clause and test references, its remedy.docs/website/docs/reference/matterhorn-mapping.md(new, generated): each checkpoint and failure condition, the rule or review question that answers it, or why none can.docs/website/docs/concepts/pdf-a.md(M14's): PDF/A-2 levels, attachments under part 2, the merge'sConformanceoption, what writing to a PDF/A-1 file keeps.docs/website/docs/concepts/validation.mdanddocs/website/docs/reference/validation.md(M02's): the engine made public, subjects, verdicts.docs/website/docs/reference/diagnostics.md: theclaim.*,pdfa.*andassembly.*codes added.docs/website/docs/reference/tool/:validate --profileand--policy,html2pdf --pdf-a 2*,merge --conformance.docs/website/docs/reference/validation-rules.md: the public API's contract for identifiers, the conformance families, thex-reservation, and a link to the generated reference.docs/website/docs/introduction.mdanddocs/features/features.json: thepdfa-profilesentry brought to its state.docs/architecture.md: the satellite's layout, the checker seam, the public engine and the single walk.docs/adr/: the ADR on the public engine (amending ADR 36) and the ADR on how the rules are sourced.docs/releasing.md: the new package and its API baseline (#42).docs/corpus.md: the new manifest fields, veraPDF's machine-readable report and PDFBox Preflight as referees.docs/status.md: the measurements, the agreement with veraPDF (documents and rules, disagreements recorded), #39 closed for veraPDF.
Exit criteria
- The rule engine is public, with subjects, passes, verdicts, review items and
x-families; its ADR is written; M02's rules and M14's container checks run on it with unchanged results. -
AdCodicem.Pdf.Conformanceships profiles for PDF/A-1a to 4f and PDF/UA-1; every (clause, test) veraPDF checks maps to a rule of ours or to a recorded reason; the sourcing ADR is written. - Every Matterhorn machine condition maps to a rule or a recorded reason, every human condition to a review question.
- PDF/A-2b, 2u, 2a and the dual 2a with PDF/UA-1 are generated from the builder and from HTML.
- Every operation that writes to a PDF/A-1 file keeps it PDF/A-1, or refuses, or removes the claim and says so.
- A merge keeps a verified claim through one output intent, default color spaces and united metadata, or names the part and the rules that removed it.
- Policies work in code and in JSON, with a published schema held to the parser.
- 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; every disagreement with veraPDF is fixed or recorded with its reason. -
ConformanceBenchmarksmeasures the 1000-page journal under2bwithua1, withMemoryDiagnoser, and a caller rule in the profile; the budget is recorded. - #42 is fixed before the package ships, if an earlier satellite has not fixed it.
- #123 is settled in a profile's terms: a key newer than the declared version reported where a claim bounds the version, extension-aware, one finding per document, or closed with the reason it is not.
- Unit tests cover each rule, its degenerate and hostile cases; the FsCheck properties hold.
- Integration tests run veraPDF, PDFBox Preflight, pikepdf, qpdf, poppler and ExifTool in containers.
- The documentation site publishes the concepts, the guides, and the generated rule and Matterhorn references.
- Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).