release: v1.7.4 — PDF/UA-1 ~90%, PDF/A-2a ~90% conformance - #134
Merged
Conversation
…column span) Builds a cell-occupancy grid for each Table on the structure-tree walker and checks the four grid clauses, with the placement algorithm derived empirically from veraPDF 1.30.2. - §7.2-15 (cell intersection): place each TH/TD at the first free column in its row (skipping slots already occupied by a row-spanning cell from an earlier row), fill RowSpan×ColSpan slots, and flag both cells when a slot collides. - §7.2-41 (column row-span consistency): every column must cover the same number of rows; a hole fires. - §7.2-42/-43 (row column-span consistency): every row must have the same effective width; a wider row fires 7.2-42, a narrower one 7.2-43. Rows are gathered from direct TR children and from THead/TBody/TFoot wrappers; cell spans are read from the `/A` attribute (single dict, array of dicts, or `[dict revision]` array) and only an `/O /Table` owner contributes RowSpan/ColSpan (default 1). Role-mapped cell/row types throughout; null-type kids omitted. The skip-occupied placement was the key veraPDF behaviour to match (a rowspan from an earlier row pushes the next cell right rather than overlapping). FP-safe: a valid table yields a uniform grid, so the rules fire only on genuine irregularities — 13 unit tests include valid-rowspan, colspan-header, THead/TBody, and /A-array guards that must NOT fire (each veraPDF-confirmed exit 0). Full local gate green: REQUIRE_VERAPDF=1 → 862 passed, 0 failed, 0 skipped; dotnet format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…uage Completes the language-determination family with the two remaining gContainsCatalogLang-dominated clauses. - §7.2-2 (UaOutlineLangRule): a document outline requires a determinable natural language. Fires only when the catalog has an /Outlines dict with at least one item (/First present) AND the catalog has no /Lang. - §7.2-33: a metadata language-alternative array (any rdf:Alt with an x-default item, not just dc:title — confirmed against veraPDF) requires a determinable language. Fires only when such an x-default lang-alt exists AND the catalog has no /Lang. XmpReader gains an additive HasXDefaultLangAlt(XDocument) helper. FP-safety: both share the gContainsCatalogLang short-circuit — a conformant PDF/UA-1 document always has a non-empty catalog /Lang, so the rule returns immediately and emits nothing. It can only fire when the catalog lacks /Lang (itself a §7.2-lang / §7.2-29 violation), so the compliance verdict always tracks veraPDF; the precise triggers (outline presence; XMP x-default presence) were probe-matched to veraPDF. 9 unit tests (with-catalog-Lang passes / without fires, per clause). Full local gate green: REQUIRE_VERAPDF=1 → 871 passed, 0 failed, 0 skipped; dotnet format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…e-mapped) A structure element whose /S is non-standard must be role-mapped to an ISO 32000-1 Table 333 standard type. UaNonStandardTypeRule fires when a walked StructElem has a /S (RawType != null) that does not resolve to a standard type (StandardType == null — i.e. non-standard and unmapped, or its RoleMap chain does not terminate at a standard type). FP-safety hinges on StructureTree.StandardTypes exactly matching veraPDF's standard-type determination, so this was empirically verified before implementing: all 47 of our standard types were probed (veraPDF fires 7.1-5 for NONE of them → set is not too big), and 10 extended/PDF-2.0 candidates (Aside, Title, Sub, FENote, Em, Strong, …) were probed (veraPDF fires 7.1-5 for ALL of them → it does not treat them as standard, so our set is missing none). The set is therefore a complete superset of veraPDF's determination — no over-rejection. The shared StandardTypes set was NOT modified. Oracle fixture (custom type role-mapped to a standard type → compliant) + 4 unit tests (custom unmapped fires; role-mapped / multi-hop role-mapped / standard baseline do not). Full local gate green: REQUIRE_VERAPDF=1 → 877 passed, 0 failed, 0 skipped; dotnet format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…header Two structure-tree rules, each clause-level cross-validated against veraPDF 1.30.2 (the oracle's boolean verdict alone cannot prove clause agreement, so an independent adversarial pass confirmed veraPDF fires the SAME clause on every negative case and accepts every valid variant): - §7.4.2-1 (UaHeadingNestingRule): document-wide depth-first scan of H1-H6; fires when a heading level skips upward (n > previousLevel + 1). Matches veraPDF's flat pre-order algorithm — first-heading-H2, sibling-section, and per-section-reset cases all corroborated. - §7.5-1/-2 (UaTableHeaderRule): a TD has a connected header when its Table has any scoped TH (/A /O=Table /Scope) or the TD's /Headers resolve to real TH /IDs. 7.5-1 when neither holds; 7.5-2 when /Headers reference unknown IDs. Implicit-grid tables (TH header row, no /Scope, no /Headers) are rejected by veraPDF on clause 7.5 — confirmed across 2x2/3-col/multi-row shapes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Reuses the structure-tree walker for four previously-deferred PDF/A-2a Level A clauses, each cross-validated against veraPDF 1.30.2 at the clause level (the oracle's boolean verdict alone cannot prove clause agreement): - §6.7.3.4-1/-2/-3 (A2aStructureTypeRule): element-scoped — a non-standard /S type must role-map to a Table-333 standard type (-1), its RoleMap chain must be acyclic (-2), and a standard /S type must not be remapped to a non-standard type (-3). Narrower than UA-1 7.1-7 (which fires on any standard remap), so it is a distinct rule, not an alias. - §6.7.4-1 (A2aLangSyntaxRule): catalog and struct-elem /Lang values must be an empty string or a valid RFC 3066 / BCP-47 tag. §6.7.3.4-3 resolves the FULL /RoleMap chain, matching veraPDF: a standard type remapped through a non-standard intermediate that ultimately reaches a standard type (/P -> /Foo -> /Span) is accepted. An immediate-target-only check was a confirmed false positive (veraPDF passes that document); a multi-hop guard fixture locks the corrected behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Adds a marked-content interpreter to the content-stream scan (BMC/BDC/EMC nesting, MC tags, Lang/ActualText/Alt/E properties, MCID, per-text-show language context) and implements the four §7.2 marked-content natural-language clauses on top, each clause-level cross-validated against veraPDF 1.30.2. Language for a text item / marked-content sequence is determinable when: catalog /Lang is present (short-circuit), or the enclosing MC sequence carries a BDC /Lang, or the owning struct element (resolved via MCID → /ParentTree page-MCID array → StructElem) or any of its ancestors carries a /Lang. The struct-element path (new StructureTree.StructNodeForMcid + HasLangInHierarchy) is essential for FP-safety: a UA-1 doc with no catalog /Lang but /Lang on the struct element wrapping the text is accepted by veraPDF, and an earlier content-stream-only version over-rejected it — a confirmed false positive now guarded by a regression fixture. Adversarially verified at the clause level: artifact / no-MCID text correctly fires 7.2-34 (veraPDF treats it as a SETextItem subject to the check), a /Span with /ActualText and no MCID correctly fires 7.2-30 (no MCID gating), and an /Artifact BDC carrying its own /Lang is silent in both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Two marked-content nesting rules on the committed interpreter, each clause-level cross-validated against veraPDF 1.30.2: - §7.1-1 (SEMarkedContent, Artifact): an Artifact BDC must not be nested inside tagged content — fires when an ancestor BDC carries an MCID that resolves to a struct element in the page /ParentTree. - §7.1-2 (SEMarkedContent): tagged content (a BDC whose own MCID resolves in the /ParentTree) must not be nested inside an Artifact — fires when an ancestor BDC carries the Artifact tag. The interpreter gains HasArtifactAncestor + AncestorMcid (computed from the marked-content stack at push time, propagating the nearest tagged ancestor's MCID through intermediate no-MCID sequences). Both rules fire only when an MCID genuinely resolves in the ParentTree; an unresolvable MCID is skipped, so the rules over-reject nothing veraPDF accepts. FP guards (top-level Artifact, Artifact-in-Artifact, top-level tagged content, dangling MCID) confirmed silent in both engines. §7.1-3 (SESimpleContentItem) is deferred: its scope includes path-painting operators that ContentStreamUsage does not yet model, so a text-show-only implementation could not be proven complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Adds the Adobe Glyph List (embedded resource, extracted from veraPDF 1.30.2's bundled copy for exact parity; BSD-3-Clause, attributed in NOTICE) and a small AOT-safe verbatim-membership resolver, and implements the three TrueType cmap/encoding clauses, each clause-level cross-validated against veraPDF: - §7.21.6-1: a non-symbolic TrueType embedded program must contain non-symbol cmap entries (nrCmaps > 0, or > 1 when a Symbol (3,0) subtable is present). - §7.21.6-2: a non-symbolic TrueType must use MacRoman/WinAnsi encoding; with a /Differences array, every glyph name must be VERBATIM in the AGL and the program must contain a Microsoft Unicode (3,1) cmap. veraPDF's differencesAreUnicodeCompliant requires raw AGL membership — uniXXXX/uXXXXX and period-suffix names fail (empirically confirmed, both engines agree). - §7.21.6-4: a symbolic TrueType program must have exactly one cmap subtable or include the Microsoft Symbol (3,0) encoding. SfntMetrics now exposes cmap subtable count + (3,0)/(3,1) presence. All rules are usage-scoped (fonts selected via Tf) and fire only when the program is embedded and parseable; ambiguous Symbolic/NonSymbolic flag combinations are skipped (FP-safe). Adversarially verified: absent /Encoding, /Encoding dict without /BaseEncoding, and uniXXXX/period-suffix Differences names all fire in both veraPDF and the rule (true positives, not FPs). §7.21.7-1 stays deferred: veraPDF derives Unicode from encoding for standard- encoded simple fonts, so a glyph-/ToUnicode requirement would over-reject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…IDFontType2) Implements the glyph-width-consistency check for the one font form where code→glyph-id resolution is exact and needs no encoding/cmap guessing: composite Type0 fonts with /Encoding /Identity-H or /Identity-V, a descendant CIDFontType2, /CIDToGIDMap absent or /Identity, and an embedded /FontFile2 — so the shown 2-byte codes are CIDs and GIDs directly (same proven scope as UaGlyphPresenceRule). The dictionary width comes from the CIDFont /W array or /DW default; the program advance from hmtx via SfntMetrics.AdvanceWidth1000. veraPDF predicate (object Glyph, 7.21.5-1): renderingMode == 3 || widthFromFontProgram == null || widthFromDictionary == null || |widthFromFontProgram - widthFromDictionary| <= 1. The rule fires at a conservative > 2 threshold: the rule's Math.Round and veraPDF's rounding of the same scaled advance can differ by up to 1 unit, so a ±2 margin guarantees the rule never over-rejects a font veraPDF accepts at the ±1 rounding boundary (cost: an exact-2-unit mismatch is not flagged — FP-safe). Usage-scoped (fonts selected via Tf) and Tr-3 / Tr-(-1) exempt, matching veraPDF. Simple fonts, non-Identity encodings, stream /CIDToGIDMap, and CFF are deferred — their code→glyph-id resolution is not proven exact against veraPDF. Clause-level verified: /W-removed mismatch fires 7.21.5-1; matching widths, unused-font mismatch, and Tr-3-only rendering stay silent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
The marked-content interpreter resolved an MCID only from the inline form `/Tag << /MCID n >> BDC`, never the named-reference form `/Tag /Name BDC` (where /Name points at a /Resources /Properties entry carrying /MCID). veraPDF resolves both, so named-reference BDCs left MarkedContentSequence.Mcid null. That was a latent false positive in the committed §7.2-34 rule: in a document with no catalog /Lang whose text is tagged via a named-reference BDC and whose language is on the owning struct element, veraPDF accepts (it resolves MCID → struct-element /Lang) but the rule fired 7.2-34 because the struct-/Lang resolution was skipped when Mcid was null. Surfaced while pinning §7.1-3's content-item operator set. ContentStreamUsage now builds a per-page name → MCID map from /Resources /Properties (inherited-resource aware) and populates Mcid for named-reference BDCs; the existing AncestorMcid propagation flows through unchanged. A miss leaves Mcid null exactly as before, so the change can only add a resolution, never remove one (FP-safe). Clause-level verified: the named-reference lang case is now silent (new oracle fixture, veraPDF-confirmed) while a genuine no-/Lang violation still fires; the §7.1-1/-2 fires it newly enables on named-reference Artifact nesting match veraPDF (previously under-detected, no new FP); §7.2-30/31/32 unaffected. The §7.1-3 deferral note is updated — named-reference resolution is no longer its blocker; only the painting/image/shading content-item operator tracking remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Completes the §7.1 artifact/content cluster. The interpreter now emits a
content item per real-content operator — text-show (Tj/TJ/'/"), painting path
ops (S/s/f/F/f*/B/B*/b/b*), inline images (ID…EI), image XObject Do, and sh —
capturing the marked-content context at emit time (effective MCID via the
nearest enclosing BDC, inline or named-reference, plus artifact ancestry).
Color/state ops, path construction without a paint, clip-only (W n), and
form-XObject Do deliberately do not emit — the operator set is probe-verified
against veraPDF 1.30.2.
UaSimpleContentItemRule fires §7.1-3 only when a content item has NO enclosing
BDC MCID at all AND is not inside an Artifact — definitively untagged real
content. This is intentionally a strict subset of veraPDF's predicate
(isTaggedContent == true || parentsTags.contains('Artifact')): any present MCID
is treated as tagged without resolving it through the /ParentTree, so a
malformed/partial ParentTree can never produce a false positive. It under-
detects the dangling-MCID case (FP-safe) and never fires on a document veraPDF
accepts. Page content streams only.
Verified across a 9-document FP battery (tagged text inline + named-reference
BDC, artifact decorations, image figure, path in tagged/artifact context,
color/clip ops, form Do, artifact-only, the writer's own UA-1 output) — all
silent in both engines; a bare painting op outside any BDC fires 7.1-3 in both
(veraPDF clause-confirmed oracle fixture).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Bump VersionPrefix 1.7.3 → 1.7.4; CHANGELOG 1.7.4 entry; README coverage updated (UA-1 ~71%→~90%, 2a ~87%→~90%). Build-verified veraPDF parity: PDF/A-2b 90.3%, 2u 90.4%, 2a 89.9%, PDF/UA-1 90.1% (95 impl + 1 partial of 106). 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
Deepens
VellumPdf.Conformanceto ~90% build-verified veraPDF parity for PDF/UA-1 (95 of 106 checks implemented + 1 partial, up from ~71%) and ~90% for PDF/A-2a (up from ~87%). 12 commits; no public API changes.Build-verified coverage (
ConformanceCatalog.Coverage): PDF/A-2b 90.3%, 2u 90.4%, 2a 89.9%, PDF/UA-1 90.1%.What's added
/Tag << /MCID n >>) and named-reference (/Tag /Namevia/Resources /Properties) forms, artifact ancestry, and a per-operator content-item model. Underpins the §7.1/§7.2 marked-content rules.NOTICE); glyph-width consistency for Identity-H CIDFontType2 (§7.21.5-1)./Langsyntax (§6.7.4-1).Verification
REQUIRE_VERAPDF=1: 994 passed, 0 failed, 0 skipped (every oracle fixture cross-validated against veraPDF 1.30.2);dotnet format --verify-no-changesclean./Langresolution, a glyph-width rounding-boundary margin, and a latent §7.2-34 named-reference-BDC FP). Two pre-release adversarial sweeps — content-stream/marked-content rules (48 valid-document fixtures incl. named-reference + content-item cross-interactions) and the font rules (24 shapes incl. the rounding boundary) — found zero over-rejections; an earlier structure-rule sweep was also clean.Release
Merging this and pushing the
v1.7.4tag triggers the Release workflow (NuGet + GitHub Release). Left for human merge + tag.https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq