Capture-to-form signature placement so the customer's drawn or typed signature lands right on the signature rule of the actual form, not in a sidecar page. - migration 085: esign_records.signature_anchors (JSONB exact PDF coords, lower-left origin, points) + signed_document_minio_key - signature_stamper.py: signature_box() anchors; anchors_from_acroform() pulls the signature field /Rect from a real AcroForm (e.g. MCS-150 certifySignature); stamp_signature() overlays PNG (auto-trimmed so ink rests on the rule) or typed name, scaled to actual page size - state_trucking_authorization.py: renders the Limited Authorization to File PDF and returns (pdf_bytes, anchors) - esign_stamp.py: stamp_esign_document() downloads unsigned PDF, stamps, uploads _signed.pdf, sets signed_document_minio_key (idempotent) - dot_esign.py: extract certifySignature anchor for MCS-150/closeout forms so the federal perjury cert is signed on the line - state_trucking.py: authorization gate — first run emails signing link and PAUSES; resumes with client_approved after signing - job_server handle_esign_completed: stamp then re-dispatch - tests: test_signature_placement.py (custom form), and test_mcs150_signature_placement.py (official AcroForm) both assert the signature lands inside the recorded signature box (verified visually)
117 lines
4 KiB
Python
117 lines
4 KiB
Python
"""Verify the captured signature lands on the official MCS-150 signature line.
|
|
|
|
Fills the real MCS-150 AcroForm, extracts the ``certifySignature`` anchor from
|
|
the form's own field rectangle, stamps a synthetic drawn signature, renders the
|
|
signature page, and asserts a meaningful number of dark pixels fall inside the
|
|
recorded signature box (i.e. the signature is on the rule, not floating).
|
|
|
|
Requires: reportlab, pypdf, pillow, and (for rendering) pdftoppm.
|
|
Run from the repo root with a venv that has those installed.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import importlib.util
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
def _load(name: str, rel: str):
|
|
spec = importlib.util.spec_from_file_location(name, os.path.join(REPO, rel))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
return mod
|
|
|
|
|
|
def _synthetic_signature_png() -> str:
|
|
from PIL import Image, ImageDraw
|
|
|
|
im = Image.new("RGBA", (600, 140), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(im)
|
|
d.line(
|
|
[(20, 110), (120, 30), (220, 110), (340, 40), (480, 100), (580, 50)],
|
|
fill=(10, 20, 60, 255),
|
|
width=6,
|
|
)
|
|
buf = io.BytesIO()
|
|
im.save(buf, "PNG")
|
|
return base64.b64encode(buf.getvalue()).decode()
|
|
|
|
|
|
def main() -> int:
|
|
sys.path.insert(0, REPO)
|
|
mcs = _load("mcs150_pdf_filler", "scripts/document_gen/templates/mcs150_pdf_filler.py")
|
|
stamper = _load("signature_stamper", "scripts/workers/services/signature_stamper.py")
|
|
|
|
intake = {
|
|
"legal_name": "ADAMS LUMBER INC",
|
|
"dot_number": "1157913",
|
|
"entity_type": "corporation",
|
|
"carrier_operation": "authorized_for_hire",
|
|
"interstate_intrastate": "interstate",
|
|
"hazmat": "no",
|
|
"power_units": "5",
|
|
"drivers": "6",
|
|
"annual_miles": "250000",
|
|
"cargo_types": ["general"],
|
|
"signer_name": "Mark Adams",
|
|
"signer_title": "President",
|
|
"address_state": "OR",
|
|
"email": "m@a.com",
|
|
}
|
|
pdf_path = mcs.fill_mcs150(intake, order_number="TEST-MCS150-SIG")
|
|
pdf_bytes = open(pdf_path, "rb").read()
|
|
|
|
anchors = stamper.anchors_from_acroform(pdf_bytes, {"certifySignature": "signer"})
|
|
assert anchors, "no signature anchor extracted from MCS-150 AcroForm"
|
|
box = anchors[0]
|
|
print("signature anchor:", box)
|
|
|
|
signed = stamper.stamp_signature(
|
|
pdf_bytes,
|
|
anchors,
|
|
signature_type="drawn",
|
|
signature_data=_synthetic_signature_png(),
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
signed_path = os.path.join(td, "signed.pdf")
|
|
open(signed_path, "wb").write(signed)
|
|
page_no = box["page"] + 1
|
|
prefix = os.path.join(td, "render")
|
|
subprocess.run(
|
|
["pdftoppm", "-png", "-f", str(page_no), "-l", str(page_no), "-r", "150", signed_path, prefix],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
# pdftoppm zero-pads the page number; find the produced PNG.
|
|
png = next(
|
|
os.path.join(td, f) for f in sorted(os.listdir(td)) if f.startswith("render") and f.endswith(".png")
|
|
)
|
|
from PIL import Image
|
|
|
|
im = Image.open(png).convert("L")
|
|
W, H = im.size
|
|
px = im.load()
|
|
sc = 150 / 72.0
|
|
bx, by, bw, bh = box["x"], box["y"], box["w"], box["h"]
|
|
dark = 0
|
|
for X in range(int(bx * sc), int((bx + bw) * sc)):
|
|
for Y in range(int(H - (by + bh) * sc), int(H - by * sc)):
|
|
if 0 <= X < W and 0 <= Y < H and px[X, Y] < 120:
|
|
dark += 1
|
|
print("dark signature pixels inside MCS-150 signature box:", dark)
|
|
assert dark > 500, f"signature not on the line (only {dark} dark px in box)"
|
|
|
|
print("PASS: signature stamped on the MCS-150 signature line")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|