Skip to content

release: v1.7.5 — PDF/A-2 ~92% (non-page content streams, XMP extension schema, 2 FP fixes) - #135

Merged
Tim81 merged 9 commits into
mainfrom
v1.7.5-conformance
Jun 23, 2026
Merged

release: v1.7.5 — PDF/A-2 ~92% (non-page content streams, XMP extension schema, 2 FP fixes)#135
Tim81 merged 9 commits into
mainfrom
v1.7.5-conformance

Conversation

@Tim81

@Tim81 Tim81 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

v1.7.5 — PDF/A-2 conformance deepened + two false-positive fixes

Build-verified veraPDF parity: PDF/A-2b 90.3 → 92.36%, 2u 90.4 → 92.47%, 2a 89.9 → 92.48% (UA-1 ~90% unchanged). Every batch cross-validated against veraPDF 1.30.2 in CI with adversarial false-positive sweeps.

Added — PDF/A-2 across non-page content streams

  • Reusable reachable-content-stream collector: drawn Form XObjects (recursive, cycle-guarded), Type 3 CharProcs of Tf-selected fonts, annotation /AP /N appearances (incl. keyed sub-dicts); reachability pinned empirically to veraPDF.
  • §6.2.2-1 (operator allowlist) and §6.1.10-1 (inline-image filters) now fully Implemented (page + non-page); §6.2.4.2-2 overprint, §6.2.4.4-2 Separation, §6.2.2-2 inherited-resources extended to non-page streams.
  • §6.7.3.3-1 (PDF/A-2a structure-tree presence) Implemented.
  • §6.6.2.3.3-1/-5/-6/-15 (XMP extension-schema RDF container types + prefixes) Implemented, honoring veraPDF's null-prefix leniency.

Fixed — two false positives (a rule must never reject what veraPDF accepts)

  • §6.2.4.3: device colour without an output intent was rejected even when a matching /DefaultRGB|CMYK|Gray colour space was present. Now per-type per veraPDF semantics.
  • §6.2.2-2: a page lacking its own /Resources was flagged for every used name, including names undefined in the inheritance chain (which veraPDF accepts). Now flags only inherited-defined names.

Reader & Conformance remain at Preview maturity (packages ship as stable, non-prerelease 1.x).

Gate: REQUIRE_VERAPDF=1 → 1103 passed; dotnet format clean.

Tim81 and others added 9 commits June 23, 2026 10:40
The catalog listed §6.7.3.3-1 as Deferred citing a structure-tree walker, but
the clause's veraPDF predicate is just containsStructTreeRoot == true — a
document-catalog presence check that LogicalStructureRule (PDF/A-2a-gated, so
FP-safe for 2b/2u) already enforces. Flip it to Implemented and record the
clause-level confirmation: veraPDF fires clause 6.7.3.3 testNumber 1 as the
SOLE failed check on the pdfa2a-no-structure oracle fixture. Only 6.1.6-2
remains Deferred for PDF/A.

Gate: REQUIRE_VERAPDF=1 → 994 passed; dotnet format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…t streams

Add a reusable GetReachableContentStreams collector to ContentStreamUsage
(drawn Form XObjects — recursive, cycle-guarded; all CharProcs of Tf-selected
Type 3 fonts; every annotation /AP /N appearance stream incl. keyed sub-dicts)
and extend ContentStreamOperatorRule (§6.2.2-1) to scan them. §6.2.2-1 moves
Partial -> Implemented.

Reachability policy empirically pinned against veraPDF 1.30.2: Form XObjects
validated only when drawn (undrawn excluded); Type 3 CharProcs validated for
every Tf-selected font regardless of which glyph is shown (the unshown-CharProc
case fires in veraPDF too — scope matches, no false positive); appearance
streams validated regardless of annotation visibility. Appearance /N detection
probes ResolveStream first so an indirect keyed sub-dictionary (Widget
checkbox/radio) is walked, not dropped.

Cross-validated fixtures (drawn/undrawn/nested/cyclic forms, inline-image-in-
form, direct + keyed annotation appearances). Gate REQUIRE_VERAPDF=1 -> 1012
passed; dotnet format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…tent streams

Extend InlineImageFilterRule to scan inline images in drawn Form XObjects,
Tf-selected Type 3 CharProcs, and annotation /AP /N appearance streams via the
shared GetReachableContentStreams collector (same refactor shape as the §6.2.2-1
extension: per-blob ScanBytes run over page content and every reachable stream).
§6.1.10-1 moves Partial -> Implemented.

Cross-validated fixtures for forbidden (/F /LZWDecode) and permitted (/F /AHx)
filters in drawn/nested forms and annotation appearances, plus the adversarial
case where ID..EI binary sample data contains the literal bytes /F /LZWDecode
(confirms SkipInlineImageData prevents the dict parser from misreading samples).
Gate independently re-run REQUIRE_VERAPDF=1 -> 1026 passed; dotnet format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…streams

Run the overprint/OPM graphics-state interpreter over drawn Form XObjects,
Tf-selected Type 3 CharProcs, and annotation /AP /N appearance streams (via the
shared GetReachableContentStreams collector), each scanned in isolation against
its own /Resources. Extracts InterpretStream from EvaluatePage; null-/Resources
streams are skipped (names unresolvable -> under-detection, FP-safe).

Stays Partial by design: veraPDF threads graphics state across Do boundaries
(probe N3-B: page sets op/OPM/ICCBased-CMYK, form merely fills -> veraPDF fires;
isolated per-stream scanning under-detects this). Isolated scanning is a strict
subset of veraPDF's view -> can only under-detect, never over-detect. Catalog
note updated to record the residual inherited-state gap.

Cross-validated fixtures (self-contained form violation, OPM-0 guard, no-
/Resources guard) + 4 in-process FP-sweep facts; 8-case adversarial sweep found
zero false positives. Gate REQUIRE_VERAPDF=1 -> 1036 passed; format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…streams

Feed Separations used in drawn Form XObjects, Tf-selected Type 3 CharProcs, and
annotation /AP /N appearance streams into the document-wide Separation
consistency comparison (via the shared GetReachableContentStreams collector).
Extracts ProcessResourceScope from the page loop and adds a small defensive
cs/CS scanner (ScanSelectedColorSpaces) for non-page stream bytes; per-stream
names resolve against that stream's own /Resources, null-/Resources skipped,
seenCsObjects dedups Separations shared across page and forms.

Stays Partial: image /ColorSpace and nested alternate spaces remain unwalked.
FP-safe by pool growth — adding non-page used-Separations only grows the
comparison pool toward what veraPDF already compares (our set stays a subset),
so any inconsistency we detect veraPDF also detects. Probes N4-A/B/C confirm
veraPDF compares page-vs-drawn-form Separations and ignores undrawn ones.

Cross-validated fixtures (page/form inconsistent -> fail; identical -> pass) +
3 FP-sweep facts; sweep found zero false positives. Gate independently re-run
REQUIRE_VERAPDF=1 -> 1043 passed; format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…ed-name FP

Extend the inherited-resource check (§6.2.2-2) to non-page content streams:
a resource-less drawn Form XObject / Type 3 CharProc / annotation appearance
that uses a named resource defined in the page's resource scope is flagged
(it relies on inherited resources, which §6.2.2 forbids). Scoped precisely to
veraPDF's inheritedResourceNames model — empirically pinned (probes N5/A1/B1/B2):
veraPDF fires ONLY when the used name IS defined in the ancestor scope; an
undefined name does not fire.

That same finding exposed a pre-existing FALSE POSITIVE in the page-level path:
EvaluatePage reported EVERY used name when a page lacked its own /Resources,
without checking the inherited scope — so a page using a name undefined in the
whole inheritance chain was over-rejected (veraPDF accepts it, probe A1). Fixed
to resolve the inherited /Resources and report only names actually defined there.

Nested-form case (inner resource-less form, name in page) confirmed NOT an FP
(veraPDF fires too, probe B1); outer-defined-only stays silent in both (B2).
§6.2.2-2 stays Partial (Pattern scn/SCN + Properties BDC/DP names still out of
scope). New fixtures + facts for every probed case; gate independently re-run
REQUIRE_VERAPDF=1 -> 1061 passed; format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…+ per-type matching

OutputIntentRule's device-colour check fired whenever any device colour was used
without a PDF/A output intent, ignoring /Default* colour spaces entirely — a
FALSE POSITIVE: veraPDF accepts DeviceRGB/CMYK/Gray used with a matching
/DefaultRGB|CMYK|Gray colour space and no output intent (probed, 2026-06-23).

Rework to veraPDF's per-type semantics: DeviceRGB satisfied by /DefaultRGB OR an
RGB-profile output intent; DeviceCMYK by /DefaultCMYK OR a CMYK intent; DeviceGray
by /DefaultGray OR ANY intent. Intent colour space read from the DestOutputProfile
ICC header data-colour-space field (offset 16). Per-type device-colour detection
added additively to ContentStreamUsage (UsesDeviceRgb/Cmyk/Gray; UsesDeviceColour
unchanged for existing consumers) + PreflightContext.DocumentDeviceColourTypes().

FP-safe by construction: fires only on a POSITIVELY-determined mismatch (intent
present but its colour space read and not the needed one); any uncertainty
(unreadable/non-standard ICC colour space, malformed profile) stays silent.
Document-wide Default*/usage aggregation under-detects the cross-page case
(FP-safe) — 6.2.4.3-2/-3/-4 stay Partial (image/pattern/XObject device colour
still undetected). 8 cross-validated fixtures (FP regression guards + true
positives + correct-match) + sweep; gate independently re-run REQUIRE_VERAPDF=1
-> 1077 passed; format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…prefixes

Extend ExtensionSchemaRule to validate the RDF container TYPE (rdf:Bag vs rdf:Seq)
and namespace prefixes of the pdfaExtension schema, flipping four clauses to
Implemented:
- 6.6.2.3.3-1: pdfaExtension:schemas must be rdf:Bag + 'pdfaExtension' prefix
- 6.6.2.3.3-5: pdfaSchema:property must be rdf:Seq + null|'pdfaSchema' prefix
- 6.6.2.3.3-6: pdfaSchema:valueType must be rdf:Seq + null|'pdfaSchema' prefix
- 6.6.2.3.3-15: pdfaType:field must be rdf:Seq + null|'pdfaType' prefix

6.6.2.3.3-8 and -17 (scalar valueType field prefix) get the prefix check too but
STAY Partial — veraPDF also requires the value type be a defined Text type
(isValueTypeDefined), which needs the value-type registry not yet built; firing
only on wrong prefix is a strict subset of veraPDF's condition (FP-safe).

Key veraPDF nuance probed and honored (2026-06-23): the container elements
(-5/-6/-15) accept a NULL prefix (default namespace) as well as the canonical one,
while -1/-8/-17 require the literal named prefix. Each check fires only on a
subset of veraPDF's firing conditions; only documents that declare an extension
schema are affected. 10 cross-validated fixtures + 6 facts + sweep; gate
independently re-run REQUIRE_VERAPDF=1 -> 1103 passed; format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
…on schema, 2 FP fixes)

VersionPrefix 1.7.4 -> 1.7.5; CHANGELOG 1.7.5 entry; README conformance parity
~90% -> ~92% for PDF/A-2b/2u/2a (UA-1 ~90% unchanged). Reader/Conformance kept
at Preview maturity (packages themselves ship stable, non-prerelease).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RghjbtTZ9hn9P8A1JEgXVq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant