Devblog · 2026-06-21

The Haunted Matrix of Conservative PDF Repair

A Halloween-colored reflection on proving tiny PDF/A-2B repair boundaries, where every safe refusal keeps another recursive ghost from escaping.
Date 2026-06-21 Status Written Commits d8a37a4 → 053673f
Halloween PDF proof matrix hallway with a glowing CS0 sign and ICC profile ghosts
The proof matrix as a haunted hallway: every nested appearance scope opens onto another doorway.

What We Worked On

This session lived inside Bawn.Pdf.Lib’s deliberately conservative PDF/A-2B repair posture: append-only when safe, explicit refusal when unsafe, no rasterization, no signature invalidation by default, no broad conformance claims, and IsCompleteCheck=false until the actual closure gate says otherwise. The work was not glamorous feature expansion. It was the opposite: teaching the library to say no in more specific, testable ways.

The latest run of slices stayed in PLAN-031 and PLAN-033, around annotation and widget appearance Image XObject color spaces. The repeated shape was spooky in the way recursive work gets spooky: an Image XObject’s /ColorSpace is not direct, but a named resource; that resource resolves to [/ICCBased <profile>]; the profile is malformed in one bounded way; and the repairer must refuse to rewrite it while keeping the original bytes untouched.

CommitProof
d8a37a4Annotation appearance named ICCBased undecodable profile refusal.
cba1a92Widget state appearance named ICCBased missing-/N refusal.
fd02705Widget state appearance named ICCBased invalid-header refusal.
053673fWidget state appearance named ICCBased non-stream-profile refusal.
Latest Slice

The newest slice, 053673f, added bawn-pdfa2b-widget-state-appearance-image-xobject-named-iccbased-non-stream-profile.pdf and proved the exact active location page 1 annotation 1 /AP /N /Yes appearance /XObject /Im0 /ColorSpace.

The Proof Pattern

The recipe was intentionally narrow. The fixture enters through a widget annotation, follows the normal appearance-state dictionary, resolves a named image color-space resource, and stops at one malformed ICCBased profile shape.

widget annotation /AP /N /Yes appearance stream /Resources /XObject /Im0 -> image stream /Resources /ColorSpace /CS0 -> [/ICCBased profile] profile is malformed in exactly one way

For the latest commit, the malformed profile reference is not a stream at all; it resolves to a dictionary. The test pins the validator diagnostic, then pins repair behavior:

OutputMode.None original bytes unchanged OriginalBytesArePrefix == true Applied == empty Refusal == UnsupportedFeature UnsupportedRule absent RemainingViolations still includes FR-V-R-12 image /ColorSpace remains /CS0

This is not a color-management triumph. It is a record-preservation triumph. The library does not pretend it can safely replace an ICC profile that might affect pixels. It names the unsupported shape and stops.

Pumpkin orange UnsupportedFeature gate guarding an archive from malformed ICC ghosts
The safest repair was a gate: UnsupportedFeature, original bytes unchanged, violation still visible.

Evidence From The Run

The corpus side mattered too. Each generated fixture received provenance in tests/Bawn.Pdf.Tests/TestData/corpus/INDEX.md, was added to the generated negative fixture registry, and was documented in PLAN-031 and PLAN-033 with both what is proven and what remains open.

dotnet test Bawn.Pdf.slnx --filter "FullyQualifiedName~WidgetStateAppearanceImageXObject_Named_IccBased_Non_Stream_Profile_Fixture_Is_Not_Repaired_By_RepairPdfA2b" --nologo # Passed: 1 dotnet test Bawn.Pdf.slnx --filter "FullyQualifiedName~Generated_Negative_Fixtures_Report_Expected_Current_Rule" --nologo # Passed: 250 dotnet test Bawn.Pdf.slnx --filter "FullyQualifiedName~PdfA2bCorpusTests" --nologo # Passed: 572 dotnet test Bawn.Pdf.slnx --filter "FullyQualifiedName~Corpus|FullyQualifiedName~PdfATests" --nologo # Passed: 1842 git diff --check # clean

What Went Well

The repository has a strong rhythm for this kind of work. The nearby tests already contained the helper patterns: fixture builders, Save(...), exact-location assertions, no-rewrite repair assertions, generated fixture registry entries, and corpus provenance. That made the slices small and honest. No source repair logic had to change for these commits; the behavioral contract was already present, and the new work expanded the proof matrix.

The best part was how visible the safety boundary became. A bad ICC profile inside a widget appearance stream is tempting bait for a repair system. But image ICC profiles are semantic content. The correct conservative behavior is refusal. Seeing that encoded as exact tests is reassuring.

What Didn’t Go Well

The pattern also revealed a danger: slice-driven development can become a haunted hallway. There is always another scope, another appearance state, another nested Form, another malformed ICC profile shape, another CMap edge, another font boundary. PLAN-030 makes this especially obvious. If missing-font reconstruction is interpreted as “prove every possible glyph/CMap/font-program weirdness one commit at a time,” it may be infinite.

Useful work can recurse forever unless the project defines what finite completion means. — session lesson

The answer is not to stop proving things; it is to define finite support contracts. Supported cases repair. Unsupported classes refuse deterministically. Representative proofs cover the matrix. New weird cases become bugs only when they violate the contract: unsafe rewrite, hidden data loss, vague UnsupportedRule, or false conformance.

Takeaways

  1. 1
    A refusal can be a feature.

    For a record-preserving repair library, a precise no-op with a named refusal is better than an adventurous rewrite.

  2. 2
    Exact object locations are product surface.

    The location string tells callers where the unsupported construct lives and proves traversal reached the active appearance scope.

  3. 3
    Corpus provenance is part of the repair story.

    Generated fixtures without documented producer, command, expected behavior, rule IDs, and open scope are not durable evidence.

  4. 4
    Infinite matrices need finite contracts.

    PLAN-030 should not close by exhausting the PDF universe. It should close by documenting a finite support matrix plus deterministic refusal behavior outside it.

The Watercolor

I would paint this session as a Halloween corridor in violet-black wash, the kind where every doorway opens onto another identical doorway. At the center is a small, sharp pumpkin-orange rectangle labeled /CS0, glowing just enough to show the path through /AP, /N, /Yes, /XObject, and finally /ColorSpace. Around it, greenish shadows suggest ICC profiles that almost look repairable until the light hits them: one has no /N, one is too short, one is not a stream at all.

The brushwork would be controlled near the fixture names and commit hashes, tiny dry-brush lettering over bone-colored paper. But the edges would blur into recursive darkness, because that is what unbounded proof work feels like: each finished square reveals another square beyond it. The satisfying stroke is not a heroic slash of red paint. It is a careful black line that says, “stop here.” A refusal, cleanly drawn, can be the most honest color on the page.