Short answer: quarantine bad OCR before signing, normalize page orientation and image quality first, then let a human-owned contract template produce the signed artifact and audit record.
Stop here.
A freight carrier may send a phone scan, a skewed TIFF, or a PDF whose text layer is only decorative. Garbage extraction is rarely a mysterious language-model problem. It is usually geometry, resolution, contrast, or a PDF rendering decision that nobody recorded. The signing service must treat extracted text as an input with a confidence boundary, never as proof that the source page was read correctly. That boundary deserves a concrete state in the workflow: pending review is a valid business result, while silently guessing at a party name or freight rate is not. In practice, the image, its metadata, and the OCR configuration form a small evidence bundle. Keep that bundle addressable by contract ID so a later reviewer can reproduce the decision without asking which worker happened to process the upload. This is especially important when a dispatcher replaces a page after a carrier calls about a missing surcharge, because the replacement must create a new hash and review event rather than mutate yesterday's evidence.
The decision record: who owns the contract template?
For a logistics platform, template ownership is the first architectural decision. If legal operations own templates in a controlled repository, the service can pin a template version, render a deterministic PDF, and sign that exact byte stream. If each carrier sends its own form, the service should preserve the source file, extract fields into a review queue, and record that the submitted layout—not your generated template—was signed. Mixing these modes is how an audit trail loses its meaning.
The invariants are straightforward: the original scan is immutable; every orientation or enhancement transform has a hash; a reviewer can see the page used for each field; and the signature event references one final document digest. OCR text can be re-run, but the evidence chain cannot silently change.
| Choice | Works well when | Failure boundary |
|---|---|---|
| Owned, versioned template | Operations need repeatable bills of lading or carrier agreements | A layout change requires approval and a new version |
| Submitter-owned form | Partners have mandated forms or regional variants | OCR fields need review when layouts drift |
| Hybrid mapping | A small stable header surrounds partner pages | Mapping rules become a second template system |
I prefer the first option for recurring freight contracts, with an explicit exception path for partner forms. The catch is governance: a template editor now needs review, rollback, and access logs. It is not suitable when legal cannot commit to a canonical layout; keep the source-form workflow in that case.
How do you debug OCR garbage text, page orientation, and scan quality?
Start with the page image, not the OCR output. Render each PDF page at a known scale, inspect its width-to-height ratio, and calculate basic image signals before choosing a recognizer. A 90-degree rotation can turn a perfectly legible label into nonsense while leaving a plausible-looking confidence score. A 120 dpi fax-like scan has a different problem from a sharp 300 dpi grayscale page with a dark background.
The following gate is intentionally boring. It makes the decision observable and keeps transformations reproducible.
from dataclasses import dataclass
from hashlib import sha256
from pathlib import Path
from PIL import Image, ImageOps
@dataclass
class PageCheck:
digest: str
width: int
height: int
mean: float
orientation: str
needs_review: bool
def inspect_page(path: str) -> PageCheck:
raw = Path(path).read_bytes()
digest = sha256(raw).hexdigest()
image = Image.open(path).convert("L")
width, height = image.size
normalized = ImageOps.autocontrast(image)
mean = sum(normalized.getdata()) / (width * height)
orientation = "portrait" if height >= width else "landscape"
# A low mean or very small raster is a review signal, not an automatic rejection.
needs_review = min(width, height) < 1400 or mean < 45 or mean > 235
return PageCheck(digest, width, height, mean, orientation, needs_review)
This check does not pretend to infer text. It records facts that an operator can challenge. Correct orientation using the document's rotation metadata when it is trustworthy; otherwise use a bounded set of quarter-turns and select the result with the recognizer's layout confidence, then retain all candidates and the selected angle. Deskew small angles, crop scanner borders, and convert to a consistent color space. Do not overwrite the upload.
A useful failure code is OCR_REVIEW_ORIENTATION, not a generic 500. Another is OCR_REVIEW_QUALITY for clipped margins, bleed-through, or a page whose character height is too small. Those codes route to a person before signing and become searchable audit events.
The signing path and its failure boundaries
The server should make a contract ID before OCR begins. Store the upload under that ID, append an event for each transform, and keep the extracted fields separate from the source object. A reviewer confirms parties, dates, rates, and the template version; only then does the signer receive a digest-bound PDF.
def sign_freight_contract(upload, template, signer, audit):
source_hash = audit.store_immutable(upload.bytes, kind="source_scan")
checks = [inspect_page(page) for page in render_pages(upload.bytes)]
if any(check.needs_review for check in checks):
audit.event("OCR_REVIEW_QUALITY", source_hash)
return {"status": "needs_review", "contract_id": upload.id}
fields = extract_fields(checks)
audit.event("OCR_FIELDS_EXTRACTED", {"contract_id": upload.id, "fields": fields})
pdf = render_template(template.version, fields)
final_hash = sha256(pdf).hexdigest()
signature = signer.sign(pdf, digest=final_hash)
audit.event("CONTRACT_SIGNED", {"digest": final_hash, "template": template.version})
return {"status": "signed", "signature": signature, "digest": final_hash}
The critical boundary is between extraction and authorization. OCR may suggest a rate of 8.50 where the scan says 85.00; the reviewer, not a retry loop, resolves that ambiguity. Keep idempotency on the contract ID and final digest so a network retry cannot create two signature events.
I once assumed a rotated page would be obvious from width and height. It was not: a landscape table was legitimately landscape, while the next portrait page had an embedded rotation flag. The correction was to inspect rendered pixels and metadata together, then log the chosen transform. Your mileage may vary with mixed-language forms, so calibrate thresholds against a small, labeled sample instead of treating 1400 pixels as a universal law.
What should the audit record retain after a scan is fixed?
Retain the original bytes, transformed-page hashes, OCR engine configuration, orientation choice, confidence values, reviewer identity, template version, and final signed digest. ISO 32000-2 describes the PDF format and its object structure; it does not make an unreliable text layer authoritative. The audit system should therefore link evidence to the rendered page and cryptographic digest, not merely to a string in a PDF text object.
Retention policy matters in logistics because a contract can outlive the application that created it. Encrypt source scans, restrict raw-image access, and define deletion windows that legal and regional privacy requirements accept. If a partner disputes a surcharge, you need to show which pixels were reviewed and which version of the template generated the signature.
Rejected option: sign the uploaded PDF after OCR
Signing the upload directly looks fast, and it preserves the partner's form, but it leaves template ownership ambiguous and makes field corrections hard to prove. It is valid when an external authority requires that exact PDF and the signer is explicitly attesting to the original artifact. It is a poor default for a platform that standardizes recurring carrier agreements.
A second tempting shortcut is to “improve” every page automatically. Aggressive thresholding can erase faint ink; forced portrait rotation can damage wide manifests. Keep transforms conservative, expose the before-and-after image to review, and fail closed when the evidence is unclear.
The decision rule is therefore narrow: own and version templates when you need repeatable contracts; preserve submitter forms when their layout is mandated; in both cases, block signing until orientation, scan quality, extracted fields, and the final digest are auditable.
Top comments (0)