DEV Community

DarianReed1254
DarianReed1254

Posted on

Auditable Monthly Report Archiving with PDF Text Extraction and OCR Verification

Short answer: extract embedded PDF text first, but make the decision per page rather than per file; send only pages that fail structural and content checks through OCR, then archive the untouched source, the rendered report, every derived text artifact, and a signed manifest that binds their hashes together. For a fintech monthly report, the least complex defensible design is a deterministic extraction path with OCR as a recorded fallback, because search convenience must never blur which bytes were approved and which text was inferred later.

The page says monthly_archive_unverifiable, not OCR latency high. The on-call sees a report identifier, the source hash, a signature-verification result, the number of pages with usable embedded text, the number routed to OCR, and the archive commit state. That is enough to decide whether money-facing evidence is safe, delayed, or genuinely missing. A page about a vendor timeout alone is noise if the archive can retry without losing its evidence chain; a green OCR dashboard is worse than useless when the signed manifest was never committed.

Dashboards can wait.

What should page the on-call?

Page on an expiring archive deadline combined with an inability to produce a verifiable evidence bundle. Do not page merely because one extraction method failed. The system has two reading paths precisely because mixed archives are untidy: a born-digital report can contain selectable text on pages 1 through 17, a scanned approval on page 18, and an image-only appendix after that.

The earlier signal should be a durable state transition: rendered to evidence_pending, followed by a bounded delay with no valid manifest. Record extraction coverage, OCR fallback count, signature result, hash mismatches, queue age, and final archive confirmation as separate fields. A single success ratio folds distinct failures together and leaves the responder guessing.

This distinction changes alert severity. A temporary OCR failure for two appendix pages is retryable work. A mismatch between the source PDF hash named in a signed manifest and the object retrieved from storage is an evidence-integrity failure, even if every dashboard claims that text extraction completed in 40 milliseconds. The latter deserves a page immediately. The former should normally create a ticket or consume its retry budget first.

The instrumentation change is small but consequential: measure the business invariant, not the tool invocation. Emit an archive record only after the report signature has been checked according to policy, immutable object identifiers are known, and hashes for the source, rendered PDF, extracted text, OCR output, and manifest are available. Attach a stable report ID and processing-attempt ID to every event, while keeping account data and extracted document content out of labels.

Should you use an OCR API or PDF text extraction?

PDF text extraction reads the document's existing text objects and mappings. It is usually the right first path for born-digital pages because it avoids recognition error and can retain character information already present in the file. Yet successful extraction is not proof that the result is useful: missing or incorrect character maps can produce empty output, scrambled reading order, replacement characters, or text that looks plausible while columns have been interleaved. ISO 32000-2 defines the PDF format; it does not promise that every producer encoded a clean semantic reading order.

OCR starts from rendered pixels. That makes it necessary for scans and photographed pages, and useful as an independent check on a suspicious page, but it introduces a derived interpretation. Small decimal points, parentheses around negative amounts, faint stamps, and table boundaries matter in financial reports. A text layer saying 1000 where the image says 10.00 is searchable and wrong.

Neither path wins outright.

The limitations are concrete. Embedded-text extraction is not suitable for image-only scans and cannot repair a misleading text layer. OCR is a poor default for clean born-digital pages when exact characters and reading order are already available, and a remote OCR API may also be unsuitable when residency rules forbid sending report pixels outside an approved boundary. A local engine keeps data inside that boundary but transfers capacity planning, model updates, language packs, and failure isolation to the operating team. The trade-off is therefore accuracy and custody against compute and operational load, evaluated per page; it is not a feature contest.

So the router needs observable tests, not a rule such as “any text means extraction succeeded.” Render every page at a controlled resolution and evaluate the extracted layer against conservative signals: character count relative to page area, proportion of invalid or replacement characters, expected report markers, bounding boxes that fall within the page, and whether reading order produces recognizable table headers. Treat these as routing evidence, not as proof of correctness. Pages below the threshold go to OCR; pages close to it can run both paths and be flagged for review when material fields disagree.

Keep four things distinct:

  1. The original input document, byte for byte.
  2. The generated monthly report that was presented for approval.
  3. Derived text, with page number, method, engine configuration, and confidence metadata.
  4. A signed manifest binding hashes, identities, timestamps, and processing lineage.

Never replace the source PDF with an OCR-enhanced copy and call that preservation. The enhanced file may be useful for search, but it is a new object. The archived source remains the evidence; extracted text is an index into it.

That boundary is non-negotiable.

Bind the monthly report to a signed audit record

A digital signature can authenticate signed byte ranges and expose later changes, but validation is a policy decision as well as a cryptographic calculation. The verifier needs the signer's certificate chain, validation time, trust anchors, algorithm policy, and revocation evidence where the policy requires it. Long-term validation also needs preserved evidence that can be evaluated after certificates expire. ETSI's PAdES profiles describe PDF-signature formats for this purpose, while RFC 3161 specifies a timestamp protocol that can establish that data existed at a particular time.

Do not sign only the extracted text. Sign, or include in a signed manifest, cryptographic digests for every archived object plus the relationship among them. Otherwise an operator can verify a blob without proving which report, extraction attempt, or accounting period it belonged to. Canonical serialization matters too: if two services serialize the same logical manifest differently, their hashes differ. RFC 8785 defines a JSON canonicalization scheme when JSON is the chosen representation.

The Go example below deliberately stops at building the canonical evidence payload. Key custody, certificate validation, timestamp acquisition, and signature creation belong behind reviewed interfaces and policy; a hand-written crypto shortcut in article code would create exactly the ambiguity an audit trail is supposed to remove.

package evidence

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

type Artifact struct {
    Role      string `json:"role"`
    MediaType string `json:"media_type"`
    SHA256    string `json:"sha256"`
}

type PageDecision struct {
    Page   int    `json:"page"`
    Method string `json:"method"` // "embedded_text", "ocr", or "dual_review"
    Reason string `json:"reason"`
}

type Manifest struct {
    SchemaVersion string         `json:"schema_version"`
    ReportID      string         `json:"report_id"`
    Period        string         `json:"period"`
    Artifacts     []Artifact     `json:"artifacts"`
    Pages         []PageDecision `json:"pages"`
}

func Digest(data []byte) string {
    sum := sha256.Sum256(data)
    return hex.EncodeToString(sum[:])
}

func ValidateDecision(d PageDecision) error {
    if d.Page < 1 {
        return fmt.Errorf("page must be positive")
    }
    switch d.Method {
    case "embedded_text", "ocr", "dual_review":
        return nil
    default:
        return fmt.Errorf("unsupported extraction method %q", d.Method)
    }
}
Enter fullscreen mode Exit fullscreen mode

Hash the exact bytes placed in the archive, not an in-memory object before a later library rewrites metadata or linearizes the PDF. Use SHA-256 or another algorithm accepted by your organizational signature policy, and version the manifest schema. The example's Period is an identifier such as 2026-09, not a claim about when signing occurred; trusted signing and timestamp fields should use precise UTC instants and explicit validation semantics.

Test the failures that dashboards conceal

Build the test corpus around page pathology. Include born-digital text with correct mappings, text with broken mappings, image-only scans, rotated pages, mixed orientations, faint stamps, multi-column tables, ligatures, unusual fonts, password-protected inputs, an incremental update after signing, and a file whose visible report differs from its searchable text layer. Synthetic fixtures cover known branches, but a de-identified and access-controlled corpus from actual document producers is needed to expose their recurring quirks. Do not publish that corpus or send it to an external processor without a data-handling review.

Acceptance tests should assert more than “non-empty text.” Compare material fields against reviewed ground truth, verify page association, and confirm that the archived source hash matches the manifest. Then tamper with one artifact at a time and prove verification fails. Remove the OCR result and confirm that search degrades while source verification still works; remove the manifest and confirm the archive is not declared complete.

Break it on purpose.

Deployment deserves the same suspicion. Pin renderer and OCR versions because changes can alter pixels, recognition, ordering, and therefore hashes of derived artifacts. Roll out against a shadow sample, compare routing decisions and material-field disagreements, and retain the old processor long enough to explain deltas. A queue consumer should make archive commits idempotent: retries may produce another processing attempt, but they must not silently overwrite the accepted evidence bundle.

There is a boring operational constraint here. OCR consumes substantially more CPU than reading an existing text layer, and rendering untrusted PDFs expands the attack surface. Isolate parsers and renderers, enforce memory and page limits, reject unsupported encryption states explicitly, and keep network access off workers unless a reviewed dependency requires it. Cost follows the same boundary: measure pages rendered and recognized, storage growth, review volume, and retry work, but do not let a lower processing bill justify weaker verification.

Capacity is policy too.

Tune thresholds against audit risk, not queue comfort

The router threshold creates two kinds of error. Too permissive, and corrupt embedded text reaches the index without OCR or review. Too strict, and healthy pages take the slower recognition path, increasing queue age and generating more opportunities for recognition differences. There is no universal confidence number because document producers, languages, scans, fonts, and material fields differ.

Start with labeled page classes and choose thresholds separately for them. A cover page with a report ID has a different risk profile from a dense holdings table. Track false accepts, false fallbacks, dual-path disagreements on material fields, and reviewer reversals. Revisit the threshold when a renderer, extraction library, OCR engine, or upstream report template changes. A global “98% success” metric cannot tell an on-call which class regressed or whether the remaining 2% contains every signed approval page.

The final alert should therefore read like an action: the monthly evidence bundle is approaching its archive deadline, signature policy is satisfied or failed for a named reason, and these exact artifacts are absent or inconsistent. It should link to identifiers and verification output, not document contents. The responder can retry a derived-text job, quarantine a mismatched object, or escalate a trust-policy failure without reconstructing the pipeline from six dashboards.

Getting the threshold wrong has a human cost. If every low-confidence scan wakes someone, the alert teaches the team to distrust it; if the system waits for the whole monthly batch to fail, one altered report can hide behind aggregate success. Page only when the evidence invariant is threatened within an actionable window. Everything else belongs in metrics, review queues, or daylight tickets.

Further reading

Top comments (0)