release: v1.7.3 — PDF/UA-1 (ISO 14289-1) accessibility conformance - #133
Merged
Conversation
First slice of PDF/UA-1 (ISO 14289-1) coverage that does not need the tagged structure-tree walker — three catalog/byte-level clauses, each authored from the ISO text and cross-validated against veraPDF 1.30.2: - §6.1-1 (UaFileHeaderRule): the file header line shall be exactly "%PDF-1.n" (0 ≤ n ≤ 7) followed immediately by an EOL — enforcing the profile's /^%PDF-1\.[0-7]$/ "$" anchor. Unlike the PDF/A-2 FileHeaderRule it adds no binary-marker check (UA-1 has none). veraPDF behaviour confirmed directly: trailing space/junk and a bad digit all fail 6.1-1; LF, CR, and CRLF line endings all pass. - §7.1-4 (UaSuspectsRule): /MarkInfo /Suspects shall not be the boolean true; absent or false both pass. - §7.2-29 (UaLangSyntaxRule): the catalog /Lang shall be a syntactically valid BCP-47 tag. Scoped to the catalog /Lang only — veraPDF 7.2-29 does not fire on a page-dictionary /Lang, and struct-element /Lang needs the walker (deferred). Handles a UTF-16BE BOM (the writer stores /Lang via FromUnicode). Empty /Lang is a violation (veraPDF 1.30.2 rejects it — empirically confirmed). The §5-3/-4/-5 pdfuaid-prefix checks are left Deferred with a precise note: the XMP reader matches by namespace URI, not prefix, so they need prefix-aware parsing. Coverage catalog updated (6.1-1, 7.1-4, 7.2-29 → Implemented). Adds 8 veraPDF-cross-validated oracle fixtures, including separate bad-digit and trailing-char header cases so the rule's EOL/"$" logic has its own regression guard. Full local gate green: REQUIRE_VERAPDF=1 → 665 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…§7.18/§7.20 Second Track-A slice (no structure-tree walker): seven non-structural annotation and catalog clauses, each authored from the ISO 14289-1 text and cross-validated against veraPDF 1.30.2. - §7.18.2-1 (UaTrapNetAnnotRule): TrapNet annotations forbidden unless hidden or outside the crop box. - §7.18.5-2 (UaLinkAnnotRule): Link annotations require a non-empty /Contents (hidden / outside-crop exempt). - §7.18.1-2 (UaAnnotContentsRule, PARTIAL): non-Widget annotations require a non-empty /Contents or /Alt. veraPDF resolves the alt-text from the annotation's ENCLOSING STRUCTURE ELEMENT (via /StructParent), not just the annotation dict — confirmed by direct probe (an annotation with a struct-element /Alt is accepted). To stay false-positive-safe without the structure-tree walker the rule fires only on annotations with NO /StructParent (untagged); struct-bound annotations are skipped (under-detection). Marked Partial in the coverage catalog. - §7.20-1 (UaReferenceXObjectRule): a drawn Form XObject shall not be a reference XObject (/Ref); reuses the content-usage scoping. - §7.11-1 (UaEmbeddedFileRule): a filespec with /EF requires non-empty /F and /UF; identifies filespecs by /Type /Filespec or filespec-slot reachability (not bare /EF), and resolves /F and /UF through indirect references. - §7.10-1 / §7.10-2 (UaOptionalContentRule): each OC configuration dict needs a non-empty /Name and must omit /AS. A shared UaAnnotationHelper implements the isOutsideCropBox and Hidden-flag (F&2) exemptions common to the §7.18 rules. §7.16-1 (encryption) and §7.18.6.2 (media clip) left Deferred with precise notes. Coverage catalog gains a PdfUaPartial mechanism for §7.18.1-2. 15 veraPDF-cross-validated oracle fixtures plus two focused unit tests guarding the §7.18.1-2 struct-element-Alt false positive (and its untagged positive control). Full local gate green: REQUIRE_VERAPDF=1 → 697 passed, 0 failed, 0 skipped. Two pre-existing UA tab-order tests gained /Contents on their Link annots so the new §7.18.5-2/§7.18.1-2 rules do not fire, isolating the §7.18.3 assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Third Track-A slice (no structure-tree walker): the two §7.21 font clauses that are provably false-positive-safe with the current infrastructure, each authored from the ISO 14289-1 text and cross-validated against veraPDF 1.30.2. - §7.21.3.2-1 (UaCidToGidMapRule): an embedded CIDFontType2 (a Type0 descendant with a FontFile2/FontFile3 program) must carry a /CIDToGIDMap entry (name /Identity or a stream both satisfy). Non-embedded or non-CIDFontType2 descendants are exempt. Scoped to used fonts (EnumerateUsedFonts). - §7.21.6-3 (UaSymbolicFontRule): a symbolic simple TrueType font must not carry an /Encoding entry. Symbolic is read FP-safely as Flags bit 3 set AND bit 6 (Non- symbolic) clear — a malformed font setting both flags is not treated as symbolic, so we never fire where veraPDF (which may classify it non-symbolic) accepts. §7.21.7-2 (forbidden ToUnicode values) was implemented in review but REMOVED as a false positive: a whole-CMap scan over-rejects an UNUSED bfchar/bfrange entry mapping to U+0000/FEFF/FFFE, whereas veraPDF validates only glyphs actually shown. Proven by direct probe — veraPDF returns PASS for a document whose ToUnicode maps an unused <0025> to <0000>. It needs shown-glyph-code extraction from content streams (same prerequisite as §7.21.8-1) and is now Deferred with that note. The remaining twelve §7.21 clauses stay Deferred with precise FP-trap notes (renderingMode-3 exemption, glyph-coverage, differencesAreUnicodeCompliant, toUnicode-from-encoding). Three focused unit tests guard the §7.21.6-3 symbolic logic. Full local gate green: REQUIRE_VERAPDF=1 → 706 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
….21.3.1 Fourth Track-A slice: the §7.21 font-structure clauses whose PDF/UA-1 predicates are identical to already-implemented, veraPDF-validated PDF/A-2 §6.2.11.x rules. Each reuses the corresponding validated parsing/comparison logic and emits the UA-1 rule id, all usage-scoped (EnumerateUsedFonts) like their PDF/A-2 originals. - §7.21.3.3-1/-2/-3 (UaCMapRule): a composite font's /Encoding must be a predefined CMap (ISO 32000-1 Table 118) or an embedded CMap stream; an embedded CMap's dict /WMode must equal its program's WMode N def; a usecmap reference must name a predefined CMap. Mirrors §6.2.11.3.3-1/-2/-3 — the predefined-CMap list is a verbatim copy of the validated 61-entry list (diff-checked identical to FontStructureRule/CMapContentRule, so no name is wrongly rejected). - §7.21.4.2-1 (UaType1CharSetRule): an embedded subset Type1 font's /CharSet must list every program glyph. Reuses Type1Glyphs + the subset-tag gate of §6.2.11.4.2-1. - §7.21.4.2-2 (UaCidSetRule): an embedded subset CIDFontType2's /CIDSet must mark exactly the present CIDs. Reuses SfntMetrics; a non-Identity stream CIDToGIDMap is skipped (same FP-avoidance as §6.2.11.4.2-2). - §7.21.3.1-1 (UaCidSystemInfoRule, PARTIAL): Type0 CIDSystemInfo must be compatible with its CMap. Same partial scope as §6.2.11.3.1-1 — Identity always passes, predefined-name CMaps deferred (no registry table), embedded CMap streams compared. Findings occur only on embedded-CMap mismatches, so a named encoding never fires. The glyph-extraction-dependent §7.21 clauses (7.21.4.1-1/-2, 7.21.5-1, 7.21.7-1/-2, 7.21.8-1) and the FP-prone TrueType cmap ones (7.21.6-1/-2/-4) remain Deferred with precise notes. Adds veraPDF-cross-validated oracle fixtures and 8 unit tests. Full local gate green: REQUIRE_VERAPDF=1 → 722 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Review follow-up. The ISO 32000-1 Table 118 predefined-CMap name set was copied verbatim into five rules (FontStructureRule, CMapContentRule, CidRangeRule, and the new UaCMapRule / UaCidSystemInfoRule) — the existing copies even carried "must stay in sync" comments. A copy that lost an entry would turn a conforming predefined CMap into a false positive, so this is a real drift risk. Extract one authoritative copy as Fonts.PredefinedCMaps.Names (an IReadOnlySet, so it can't be mutated through the shared reference) and point all five rules at it. The five lists were diff-verified identical (61 entries) before the merge, so the change is behaviour-preserving — confirmed by the full local gate (REQUIRE_VERAPDF=1 → 722 passed, 0 failed, 0 skipped), which re-runs every PDF/A font oracle fixture against veraPDF. Also drops an unused `using System.Text;` in UaXfaRule and adds the rationale comment to the one bare CMap-program catch in UaCMapRule, matching the defensive-parse convention used elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…mbedding Extends the shared content-stream scanner with graphics-state-aware text-show tracking, then uses it for the first glyph-level UA-1 font rule. ContentStreamUsage (shared with the PDF/A-2 rules) is refactored from a 7-element named tuple to a `ContentUsage` class exposing the SAME seven properties (so every existing caller is behaviour-identical — proven by the PDF/A oracle fixtures still passing) plus a new `TextShows` list. Each text-show operator (Tj/TJ/'/") records (current font resource, text rendering mode, shown bytes), with the current font and rendering mode maintained through a q/Q graphics-state stack. Operand tracking is split into independent lastName/lastInt/lastString channels cleared together on each operator, preserving the original "operands since the last operator" contract. An indeterminate `Tr` operand yields rendering mode -1 (unknown), never a kept visible value — the false-positive-safe direction. §7.21.4.1-1 (UaFontEmbeddingRule): a simple font (Type1/TrueType/MMType1; Type0 and Type3 are exempt per the veraPDF predicate) that has no embedded program and is drawn in a visible text rendering mode must embed its program. Fires only on a positively-observed visible draw (rendering mode 0..8 except 3, never the -1 unknown sentinel); a font drawn only with `3 Tr` (invisible) is exempt. Usage-scoped via the Tf-selected font set. Reuses FontEmbeddingRule's embedded-program detection. Review fixes folded in: (1) the cross-page dedup now marks a font reported only when it actually fires, so a font drawn invisibly on one page and visibly on another is still caught; (2) the indeterminate-Tr hardening above; dead no-op block removed. veraPDF-cross-validated fixtures (non-embedded visible → fails; non-embedded invisible-only and embedded-visible → pass 7.21.4.1-1) plus 4 unit tests incl. the 3-Tr / embedded / Type0 FP guards. Full local gate green: REQUIRE_VERAPDF=1 → 728 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
… (ToUnicode) Consumes the A5a content-stream glyph extraction (ContentUsage.TextShows) for the two clauses that depend on which glyphs are ACTUALLY SHOWN. Both are scoped to the unambiguous case — a composite (Type0) font with /Encoding Identity-H or Identity-V, a CIDFontType2 descendant, and /CIDToGIDMap absent or /Identity — where the shown bytes are 2-byte big-endian codes and code == CID == GID. All other font forms are deferred (code→glyph needs encoding/cmap tables we don't have). - §7.21.8-1 (UaNotdefGlyphRule): no shown glyph may be .notdef. Glyph index 0 is .notdef, so a shown code 0x0000 fires. Checked in every text rendering mode — the veraPDF predicate (name != ".notdef") has no Tr exemption (unlike §7.21.4.1-1, whose explicit renderingMode==3 term confirms veraPDF builds glyph objects for invisible text too). - §7.21.7-2 (UaToUnicodeForbiddenRule): a shown glyph's /ToUnicode value may not be U+0000/FEFF/FFFE. The CMap is parsed into a code→Unicode map (bfchar + bfrange, single and array destinations, surrogate pairs), but ONLY codes that appear in TextShows are looked up. This is the fix for the earlier revert (ab5dc76): a whole-CMap scan over-rejected an UNUSED forbidden entry that veraPDF accepts. A dedicated regression fixture (unused <FFFF> <0000>, only a good code shown) is now compliant in both veraPDF and in-process. Each clause has veraPDF-cross-validated oracle fixtures (violating + compliant) and unit tests including the FP guards. Full local gate green: REQUIRE_VERAPDF=1 → 737 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…ogram) Final glyph-extraction clause, consuming A5a's ContentUsage.TextShows. A glyph shown in a visible text rendering mode must exist in the embedded font program. §7.21.4.1-2 (UaGlyphPresenceRule): for an in-scope font (Type0 / Identity-H or Identity-V / CIDFontType2 / CIDToGIDMap absent or /Identity / embedded FontFile2), the shown 2-byte codes are GIDs; a GID >= the program's glyph count (SfntMetrics. NumGlyphs) is positively absent → fire. Reuses the validated SfntMetrics parser and the same in-scope predicate as the PDF/A-2 §6.2.11.4.1-2 rule. The veraPDF Glyph predicate adds a Tr-3 exemption the PDF/A rule lacks: `renderingMode == 3 || isGlyphPresent == null || isGlyphPresent == true`. FP-safe — fires only on a positively-determined visible draw (rendering mode 0..8 except 3, never the -1 unknown sentinel); GID 0 (.notdef) is skipped (§7.21.8-1's job); an unparseable program or absent FontFile2 maps to isGlyphPresent == null (no finding). veraPDF-cross-validated: GID 60000 drawn visibly fires; the same drawn with 3 Tr does not (Tr-3 exemption); in-range baseline does not. Oracle fixtures + 4 unit tests (visible-fires / 3-Tr / in-range / unknown-Tr guards). §7.21.5-1 (glyph widths) stays Deferred. Full local gate green: REQUIRE_VERAPDF=1 → 745 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Starts the tagged-structure subsystem (gates ~75 UA-1 clauses + PDF/A-2a §6.7). StructureTree (Rules/Structure/StructureTree.cs): a reusable, cached walker of the catalog's /StructTreeRoot → /K → StructElem hierarchy. Each node exposes its dict, /S raw type, the role-mapped ISO 32000-1 Table 333 standard type (multi-hop /RoleMap chain with a cycle guard; null when non-standard/unmapped), its parent node, child StructElem nodes, a HasNonElementKids flag (MCID/MCR/OBJR), and /Pg. Cycle guard by object number and a depth cap. The Table 333 standard-type set lives here as the single source for later containment rules. Three FP-safe rules validate it: - §7.1-12 (UaStructElemParentRule): every StructElem has a /P entry (reads the raw dict; the StructTreeRoot is not a node, so it is never flagged). - §7.1-6 / §7.1-7 (UaRoleMapRule): a /RoleMap shall be acyclic and shall not remap a standard type. Review fixes folded in: - UaRoleMapRule is ELEMENT-DRIVEN, not RoleMap-dict-driven. Direct veraPDF probing showed veraPDF evaluates these on a PDStructElem that USES the offending type: a /RoleMap that remaps a standard type, or contains a cycle, is ACCEPTED when no element uses it. Firing on the /RoleMap contents alone over-rejected such conformant documents (a false positive). Now 7.1-7 fires only when an element's /S is a standard type that is a RoleMap key, and 7.1-6 only when an element's /S chain cycles. Two regression guards added. - The walker built each node once with the correct HasNonElementKids flag instead of a placeholder-then-rebuild that left children pointing at a discarded node (corrupting StructureTreeNode.Parent for the later containment batches). Full local gate green: REQUIRE_VERAPDF=1 → 758 passed, 0 failed, 0 skipped (PDF/A regression proof included). Containment / figure / heading / natural-language rules build on this foundation in later batches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…lauses) Builds on the B1 structure-tree walker: three rules check parent-type and kid-type containment for table, list, and table-of-contents structure elements, keyed on each element's role-mapped standard type (node.StandardType) and its parent's (node.Parent.StandardType — now exact after the B1 walker fix). - UaTableContainmentRule: §7.2-3 (Table kids), -4 (TR parent), -5/-6/-7 (THead/ TBody/TFoot parent == Table), -8/-9 (TH/TD parent == TR), -10 (TR kids), -36/-37/-38 (THead/TBody/TFoot kids == TR). - UaListContainmentRule: §7.2-17 (LI parent == L), -18 (LBody parent == LI), -19 (L kids), -20 (LI kids). - UaTocContainmentRule: §7.2-26 (TOCI parent == TOC), -27 (TOC kids). FP semantics matched to veraPDF 1.30.2 by direct probing of every edge: - NULL parent (element is a direct StructTreeRoot child, or its parent's /S is non-standard/unmapped): the parent-type predicate is a string/regex test that is false for null, so such an element FIRES — confirmed (e.g. a root-level TR fires 7.2-4). - NULL-StandardType kid (a non-standard, unmapped /S child): OMITTED from the kid-type check — veraPDF's kidsStandardTypes skips unknown types, so a custom unmapped kid does NOT fire; a custom kid role-mapped to an allowed type IS accepted as that type. - Empty kid list (only MCIDs, or no /K): vacuously satisfied — no fire. Each rule uses the role-mapped standard type throughout (never the raw /S). 21 unit tests (violating + compliant per group). Count/position (§7.2-11..16/28/39/40), figure/heading/note (§7.3/7.4/7.5/7.7/7.9), and natural-language clauses stay Deferred for later batches. Full local gate green: REQUIRE_VERAPDF=1 → 780 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…te rules Three more structure-tree-walker rules (per-element attribute / document-level heading checks), all keyed on the role-mapped node.StandardType. - UaAltTextRule: §7.3-1 (Figure) and §7.7-1 (Formula) — the element must have a non-empty /Alt OR an /ActualText entry. veraPDF accepts an /ActualText that is present even when empty (presence-only), but requires /Alt to be non-empty — matched exactly by direct probing. Read element-only (no inheritance). - UaNoteIdRule: §7.9-1 (every Note has a non-empty /ID) and §7.9-2 (Note /ID values are unique — byte-exact comparison; an element without a valid /ID is excluded from the uniqueness set). - UaHeadingRule: §7.4.4-1 (at most one child whose standard type is exactly H — H1–H6 children do not count), §7.4.4-2/§7.4.4-3 (a document must not mix the weakly- structured H and strongly-structured H1–H6 heading types; if both appear, both fire). usesH/usesHn computed document-wide in a first pass. Every edge probed against veraPDF 1.30.2 (empty vs absent /Alt and /ActualText, empty /ID, duplicate /ID, H-vs-Hn mixing, H-vs-H1 child counting). 16 unit tests (violating + compliant per clause). §7.4.2-1 (heading nesting level) stays Deferred. Full local gate green: REQUIRE_VERAPDF=1 → 796 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
… clauses) Count- and position-based structure rules on the B1 walker, all operating on the null-omitted ordered child standard-type list (node.Children → StandardType, dropping unmapped/null types), matching veraPDF's kidsStandardTypes semantics established in B2. - UaTableCountRule (SETable): §7.2-11 (≤1 THead), §7.2-12 (≤1 TFoot), §7.2-13 (TFoot requires ≥1 TBody), §7.2-14 (THead requires ≥1 TBody), §7.2-39 (≤1 Caption), §7.2-16 (a Caption may only be the first or last child). - UaTocContainmentRule extended: §7.2-28 (a TOC Caption may only be the first child). - UaListContainmentRule extended: §7.2-40 (an L Caption may only be the first child). Caption-position semantics confirmed by direct veraPDF 1.30.2 probing, including the null-omission edge: a Caption physically at index 1 whose only preceding sibling is an unmapped element is treated as index 0 in the null-omitted list → passes 7.2-16/28/40. Role-mapped types throughout; empty kid lists pass. The table-grid clauses (§7.2-15 cell intersection, §7.2-41/42/43 row/column span) remain Deferred — they need full grid construction with RowSpan/ColSpan tracking. 18 unit tests (violating + compliant per clause). Independently re-ran the full gate: REQUIRE_VERAPDF=1 → 811 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
….18.1-2 Extends the structure-tree walker with a /ParentTree reverse index and uses it for the annotation-to-structure rules, then promotes §7.18.1-2 to fully Implemented. StructureTree gains StructParentOf(int): the annotation's /StructParent integer → the StructTreeRoot's /ParentTree number tree (/Nums + /Kids, depth-capped) → the enclosing StructElem node. /Nums values that are single StructElem dicts are annotation/OBJR parents and indexed; array values (page MCID maps) are ignored. Nodes are matched by PdfDictionary reference identity, which is exact because the reader caches resolved objects by number. A missing/unresolvable binding yields null — the FP-safe result. UaAnnotStructureRule (reusing UaAnnotationHelper's hidden/outside-crop exemptions): - §7.18.1-1: a visible non-Widget/Link/PrinterMark annotation must be nested in an Annot tag (structParentStandardType == 'Annot'). - §7.18.4-1: a visible Widget must be nested in a Form tag. - §7.18.5-1: a visible Link must be nested in a Link tag. - §7.18.8-1: a visible PrinterMark must NOT be in the structure tree — fires only when a struct binding is positively confirmed (the FP-safe inverse). §7.18.1-2 (UaAnnotContentsRule) promoted from Partial: it now resolves the DIRECT enclosing struct element's /Alt via the ParentTree index instead of skipping struct-bound annotations. veraPDF checks the direct struct element's /Alt only (not ancestors) — confirmed by probe. The earlier A2 struct-Alt FP guard still passes. Verified veraPDF uses the reverse /StructParent→/ParentTree path (not the forward /K→/OBJR path): an OBJR binding with no ParentTree entry fires 7.18.1-1, matching our resolution. 15 new unit tests; 2 pre-existing tab-order tests relaxed from Assert.Single to Assert.Contains (they now also, correctly, trip the new rules). Full local gate independently re-run: REQUIRE_VERAPDF=1 → 828 passed, 0 failed, 0 skipped (PDF/A regression proof included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Lang-determination rules on the structure-tree walker, all sharing the gContainsCatalogLang short-circuit. - §7.2-21/22/23 (PDStructElem): a struct element with /ActualText, /Alt, or /E must have a determinable language — its own /Lang, or any ancestor's /Lang (walked via node.Parent), or the catalog /Lang. An empty /Lang () counts as present (key presence, matching veraPDF). - §7.2-24 (PDAnnot): an annotation with a non-empty /Contents must have its DIRECT enclosing struct element (via /StructParent → ParentTree, the B5 index) carry /Lang — confirmed empirically that veraPDF accepts neither the annotation-dict /Lang nor an ancestor struct-element /Lang here. - §7.2-25 (PDFormField): a field with /TU must have its widget's enclosing struct element carry /Lang (same StructParentOf path). FP-safety: ISO 14289-1 §7.2 requires a non-empty catalog /Lang, so every conformant PDF/UA-1 document satisfies gContainsCatalogLang and this rule emits nothing — it returns immediately when the catalog has a non-empty /Lang. It can only fire when the catalog lacks /Lang; an empty catalog /Lang is itself a §7.2-29 violation, and an absent catalog /Lang leaves the verdict driven by the per-element checks, which were probe-matched to veraPDF. So the compliance verdict always tracks veraPDF. §7.2-33 (XMP dc:title lang-alt) deferred — needs XMP lang-alt parsing and only fires when the catalog also lacks /Lang. §7.2-30/31/32/34 (marked-content) stay Deferred. 22 unit tests (with-catalog-Lang passes / without fires, per clause). Independently re-ran the full gate: REQUIRE_VERAPDF=1 → 849 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…fix) Bumps VersionPrefix to 1.7.3 and documents the PDF/UA-1 (ISO 14289-1) conformance work: build-verified veraPDF parity ~7.5% → ~71% (75/106 implemented, 1 partial), all cross-validated against veraPDF 1.30.2 and clean under a 37-fixture adversarial sweep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Pure formatting fix — resolves the CI format-check failures (whitespace and import ordering) in four UA-1 rule files; no logic change. The conformance gate is unaffected (REQUIRE_VERAPDF=1 → 849 passed locally). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
This was referenced Jun 24, 2026
This was referenced Jul 1, 2026
This was referenced Jul 15, 2026
This was referenced Aug 19, 2026
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements PDF/UA-1 (ISO 14289-1) accessibility conformance in
VellumPdf.Conformance, raising build-verified veraPDF parity from ~7.5% to ~71% (75 of 106 checks implemented, 1 partial). PDF/A-2b/2u/2a coverage is unchanged.Every rule is authored clean-room from the ISO text and cross-validated against veraPDF 1.30.2 in CI. The work was built in reviewed batches; 4 false positives were caught and fixed during review, and a final 37-fixture adversarial sweep (crop-box geometry, structure-parent resolution, multi-hop role-mapping, font usage-scoping, indirect references) found no over-rejections — the cardinal "no false positives" invariant holds.
What's added
/StructTreeRoot→/K→StructElem; role-map resolution to ISO 32000-1 Table 333 standard types; a/ParentTreereverse index for annotation↔structure binding; cycle/depth guards) underpinning all structure rules.ContentStreamUsage(per-glyph text rendering mode via a q/Q graphics-state stack + shown character codes), proven behaviour-identical for the existing PDF/A rules.Verification
REQUIRE_VERAPDF=1: 849 passed, 0 failed, 0 skipped (every fixture's in-process verdict cross-checked against veraPDF 1.30.2; PDF/A fixtures included as a no-regression proof).Deferred edges (documented per-rule in
ConformanceCatalog): table-grid span analysis, heading-nesting level, TH scope, content-stream marked-content artifacts, the pdfuaid-prefix/encryption/media-clip checks, and PDF/A-2a §6.7 — targeted for follow-up.