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)
103 lines
3.7 KiB
Python
103 lines
3.7 KiB
Python
"""Verify the e-signature lands exactly on the signature line of the auth form.
|
|
|
|
Renders the authorization PDF, captures the signature-box anchors, creates a
|
|
synthetic drawn signature, stamps it, and asserts the stamped ink falls inside
|
|
the recorded signature box (and on/just-above the signature rule).
|
|
"""
|
|
import base64
|
|
import io
|
|
import os
|
|
import sys
|
|
|
|
_SVC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "workers", "services"))
|
|
sys.path.insert(0, _SVC)
|
|
|
|
import signature_stamper # noqa: E402
|
|
from state_trucking_authorization import ( # noqa: E402
|
|
build_state_trucking_authorization,
|
|
)
|
|
|
|
|
|
def make_signature_png(w=600, h=200) -> bytes:
|
|
"""A black squiggle on transparent background -> base64 PNG."""
|
|
from PIL import Image, ImageDraw
|
|
|
|
im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(im)
|
|
pts = [(20, 150), (120, 40), (220, 160), (320, 50), (430, 150), (560, 60)]
|
|
d.line(pts, fill=(10, 20, 60, 255), width=8, joint="curve")
|
|
buf = io.BytesIO()
|
|
im.save(buf, "PNG")
|
|
return buf.getvalue()
|
|
|
|
|
|
def main() -> int:
|
|
pdf, anchors = build_state_trucking_authorization(
|
|
order_number="CO-TEST1234",
|
|
entity_name="Acme Freight LLC",
|
|
service_name="New York Highway Use Tax (HUT) Registration",
|
|
dot_number="3456789",
|
|
mc_number="MC-987654",
|
|
fein_last4="1234",
|
|
base_state="NY",
|
|
operating_states=["NY", "PA", "OH"],
|
|
signer_name="John Smith",
|
|
signer_title="Owner",
|
|
)
|
|
assert pdf[:4] == b"%PDF", "generator did not emit a PDF"
|
|
signer_anchor = next(a for a in anchors if a["field"] == "signer")
|
|
print(f"signer box: x={signer_anchor['x']:.1f} y={signer_anchor['y']:.1f} "
|
|
f"w={signer_anchor['w']:.1f} h={signer_anchor['h']:.1f}")
|
|
|
|
sig_png = make_signature_png()
|
|
sig_b64 = "data:image/png;base64," + base64.b64encode(sig_png).decode()
|
|
|
|
signed = signature_stamper.stamp_signature(
|
|
pdf,
|
|
anchors,
|
|
signature_type="drawn",
|
|
signature_data=sig_b64,
|
|
signer_field="signer",
|
|
)
|
|
assert signed[:4] == b"%PDF", "stamper did not emit a PDF"
|
|
assert len(signed) > len(pdf), "stamped PDF should be larger (image added)"
|
|
|
|
# Render the signed PDF to an image and confirm dark ink appears inside the
|
|
# signature box and essentially nowhere far outside it.
|
|
try:
|
|
from pdf2image import convert_from_bytes
|
|
except ImportError:
|
|
print("pdf2image not installed — skipping pixel check (PDF structure OK)")
|
|
with open("/tmp/state_trucking_authorization_signed.pdf", "wb") as f:
|
|
f.write(signed)
|
|
print("wrote /tmp/state_trucking_authorization_signed.pdf")
|
|
return 0
|
|
|
|
pages = convert_from_bytes(signed, dpi=150)
|
|
page = pages[0]
|
|
pw, ph = page.size
|
|
# PDF points -> pixels (origin top-left in image, bottom-left in PDF)
|
|
scale = pw / 612.0
|
|
bx = signer_anchor["x"] * scale
|
|
bw = signer_anchor["w"] * scale
|
|
by_top = (792.0 - (signer_anchor["y"] + signer_anchor["h"])) * scale
|
|
bh = signer_anchor["h"] * scale
|
|
|
|
px = page.convert("L").load()
|
|
ink_in_box = 0
|
|
for yy in range(int(by_top) - 4, int(by_top + bh) + 6):
|
|
for xx in range(int(bx) - 4, int(bx + bw) + 4):
|
|
if 0 <= xx < pw and 0 <= yy < ph and px[xx, yy] < 90:
|
|
ink_in_box += 1
|
|
print(f"dark signature pixels inside signature box: {ink_in_box}")
|
|
assert ink_in_box > 200, "signature ink not found on the signature line!"
|
|
|
|
with open("/tmp/state_trucking_authorization_signed.pdf", "wb") as f:
|
|
f.write(signed)
|
|
print("PASS: signature stamped on the signature line")
|
|
print("wrote /tmp/state_trucking_authorization_signed.pdf")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|