437 lines
16 KiB
Python
437 lines
16 KiB
Python
"""
|
||
generate_pdfs.py
|
||
----------------
|
||
Generates realistic sample PDFs for adobe-to-docusign migration testing.
|
||
|
||
Each PDF mirrors the form fields described in the matching *-formfields.json
|
||
so that tab positions map to visible labels on the document.
|
||
|
||
Adobe rect coordinates are top-left origin; DocuSign yPosition is bottom-left.
|
||
Formula: docusign_y = PAGE_HEIGHT - adobe_top - adobe_height
|
||
To place a *label* just above a field: label_y = PAGE_HEIGHT - adobe_top + 2
|
||
"""
|
||
|
||
import base64
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from reportlab.lib.pagesizes import letter
|
||
from reportlab.pdfgen import canvas
|
||
|
||
W, H = letter # 612 × 792 pt
|
||
|
||
SAMPLE_DIR = Path(__file__).parent.parent / "sample-templates"
|
||
SAMPLE_DIR.mkdir(exist_ok=True)
|
||
|
||
|
||
def save_b64(pdf_path: Path) -> Path:
|
||
b64_path = pdf_path.with_suffix(".pdf.b64")
|
||
b64_path.write_text(base64.b64encode(pdf_path.read_bytes()).decode())
|
||
return b64_path
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helper: draw a labelled field box at an Adobe-style rect
|
||
# ---------------------------------------------------------------------------
|
||
def draw_field(c: canvas.Canvas, label: str, rect: dict, page_h: float = H,
|
||
field_hint: str = ""):
|
||
left = rect["left"]
|
||
top = rect["top"]
|
||
width = rect["width"]
|
||
height = rect["height"]
|
||
# Convert Adobe top-origin to ReportLab bottom-origin
|
||
rl_y = page_h - top - height
|
||
|
||
# Label just above the box
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawString(left, rl_y + height + 3, label + (" [" + field_hint + "]" if field_hint else ":"))
|
||
|
||
# Field box
|
||
c.setStrokeColorRGB(0.5, 0.5, 0.5)
|
||
c.setFillColorRGB(0.97, 0.97, 1.0)
|
||
c.rect(left, rl_y, width, height, fill=1, stroke=1)
|
||
c.setFillColorRGB(0, 0, 0)
|
||
|
||
|
||
def draw_radio_option(c: canvas.Canvas, value: str, rect: dict, page_h: float = H):
|
||
left = rect["left"]
|
||
top = rect["top"]
|
||
size = min(rect["width"], rect["height"])
|
||
rl_y = page_h - top - size
|
||
c.setStrokeColorRGB(0.3, 0.3, 0.3)
|
||
c.setFillColorRGB(1, 1, 1)
|
||
c.circle(left + size / 2, rl_y + size / 2, size / 2, fill=1, stroke=1)
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0, 0, 0)
|
||
c.drawString(left + size + 3, rl_y + 1, value)
|
||
|
||
|
||
def draw_checkbox(c: canvas.Canvas, label: str, rect: dict, page_h: float = H):
|
||
left = rect["left"]
|
||
top = rect["top"]
|
||
size = min(rect["width"], rect["height"])
|
||
rl_y = page_h - top - size
|
||
c.setStrokeColorRGB(0.3, 0.3, 0.3)
|
||
c.setFillColorRGB(1, 1, 1)
|
||
c.rect(left, rl_y, size, size, fill=1, stroke=1)
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0, 0, 0)
|
||
c.drawString(left + size + 5, rl_y + 1, label)
|
||
|
||
|
||
def draw_signature_block(c: canvas.Canvas, label: str, rect: dict, page_h: float = H):
|
||
left = rect["left"]
|
||
top = rect["top"]
|
||
width = rect["width"]
|
||
height = rect["height"]
|
||
rl_y = page_h - top - height
|
||
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawString(left, rl_y + height + 3, label + ":")
|
||
c.setStrokeColorRGB(0.2, 0.4, 0.8)
|
||
c.setFillColorRGB(0.93, 0.96, 1.0)
|
||
c.rect(left, rl_y, width, height, fill=1, stroke=1)
|
||
c.setFillColorRGB(0.2, 0.4, 0.8)
|
||
c.setFont("Helvetica-Oblique", 8)
|
||
c.drawString(left + 4, rl_y + 4, "Sign here")
|
||
c.setFillColorRGB(0, 0, 0)
|
||
|
||
|
||
def section_header(c: canvas.Canvas, text: str, y: float):
|
||
c.setFont("Helvetica-Bold", 11)
|
||
c.setFillColorRGB(0.1, 0.2, 0.5)
|
||
c.drawString(72, y, text)
|
||
c.setStrokeColorRGB(0.1, 0.2, 0.5)
|
||
c.line(72, y - 3, W - 72, y - 3)
|
||
c.setFillColorRGB(0, 0, 0)
|
||
|
||
|
||
# ===========================================================================
|
||
# 1. Employee Onboarding Form (2 pages)
|
||
# ===========================================================================
|
||
def generate_onboarding():
|
||
path = SAMPLE_DIR / "onboarding-sample.pdf"
|
||
c = canvas.Canvas(str(path), pagesize=letter)
|
||
|
||
# ---- Page 1: Employee fills ----
|
||
c.setFont("Helvetica-Bold", 18)
|
||
c.setFillColorRGB(0.1, 0.2, 0.5)
|
||
c.drawCentredString(W / 2, H - 60, "Employee Onboarding Form")
|
||
c.setFont("Helvetica", 10)
|
||
c.setFillColorRGB(0.4, 0.4, 0.4)
|
||
c.drawCentredString(W / 2, H - 78, "Please complete all required fields before your first day.")
|
||
c.setFillColorRGB(0, 0, 0)
|
||
|
||
section_header(c, "Personal Information", H - 110)
|
||
|
||
fields_page1 = json.loads((SAMPLE_DIR / "onboarding-template-formfields.json").read_text())
|
||
|
||
for f in fields_page1:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 1:
|
||
continue
|
||
rect = locs[0]["rect"]
|
||
ft = f["type"]
|
||
label = f["fieldName"].replace("Employee", "Employee ").strip()
|
||
|
||
if ft == "TEXT_FIELD":
|
||
draw_field(c, label, rect)
|
||
elif ft == "DATE":
|
||
draw_field(c, label, rect, field_hint="MM/DD/YYYY")
|
||
elif ft == "DROPDOWN":
|
||
draw_field(c, label, rect, field_hint=", ".join(f.get("items", [])))
|
||
elif ft == "CHECKBOX":
|
||
draw_checkbox(c, "Health & Dental Benefits", rect)
|
||
elif ft == "RADIO":
|
||
# Draw label above first radio
|
||
if locs:
|
||
first_rect = locs[0]["rect"]
|
||
rl_y = H - first_rect["top"] - first_rect["height"]
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawString(first_rect["left"], rl_y + first_rect["height"] + 3, "Commute Option:")
|
||
c.setFillColorRGB(0, 0, 0)
|
||
items = f.get("items", [])
|
||
for i, loc in enumerate(locs):
|
||
val = items[i] if i < len(items) else str(i)
|
||
draw_radio_option(c, val, loc["rect"])
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 1 of 2 — Employee Onboarding Form")
|
||
c.showPage()
|
||
|
||
# ---- Page 2: HR section + signatures ----
|
||
c.setFont("Helvetica-Bold", 14)
|
||
c.setFillColorRGB(0.1, 0.2, 0.5)
|
||
c.drawCentredString(W / 2, H - 50, "Employee Onboarding Form — Signatures")
|
||
c.setFillColorRGB(0, 0, 0)
|
||
|
||
section_header(c, "HR Notes (Internal)", H - 90)
|
||
|
||
for f in fields_page1:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 2:
|
||
continue
|
||
rect = locs[0]["rect"]
|
||
ft = f["type"]
|
||
if ft == "TEXT_FIELD":
|
||
draw_field(c, f["fieldName"], rect, field_hint="HR use only")
|
||
elif ft == "SIGNATURE":
|
||
draw_signature_block(c, f["fieldName"], rect)
|
||
|
||
section_header(c, "Acknowledgement", H - 260)
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.2, 0.2, 0.2)
|
||
disclaimer = (
|
||
"By signing below, the employee confirms that all information provided is accurate "
|
||
"and that they have read and understood the company policies. HR Representative "
|
||
"countersigns to approve onboarding completion."
|
||
)
|
||
text = c.beginText(72, H - 280)
|
||
text.setFont("Helvetica", 9)
|
||
text.setFillColor((0.2, 0.2, 0.2))
|
||
# Simple word wrap
|
||
words = disclaimer.split()
|
||
line, lines = [], []
|
||
for w in words:
|
||
line.append(w)
|
||
if len(" ".join(line)) > 72:
|
||
lines.append(" ".join(line[:-1]))
|
||
line = [w]
|
||
if line:
|
||
lines.append(" ".join(line))
|
||
for ln in lines:
|
||
c.drawString(72, text.getY(), ln)
|
||
text.moveCursor(0, 12)
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 2 of 2 — Employee Onboarding Form")
|
||
|
||
c.save()
|
||
b64 = save_b64(path)
|
||
print(f"✅ Generated {path.name} + {b64.name}")
|
||
|
||
|
||
# ===========================================================================
|
||
# 2. NDA Template (2 pages)
|
||
# ===========================================================================
|
||
def generate_nda():
|
||
path = SAMPLE_DIR / "nda-sample.pdf"
|
||
c = canvas.Canvas(str(path), pagesize=letter)
|
||
|
||
# ---- Page 1: Agreement text ----
|
||
c.setFont("Helvetica-Bold", 18)
|
||
c.setFillColorRGB(0.1, 0.2, 0.5)
|
||
c.drawCentredString(W / 2, H - 60, "Non-Disclosure Agreement")
|
||
c.setFont("Helvetica", 10)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawCentredString(W / 2, H - 80, "Confidential — Between Employee and Company")
|
||
|
||
section_header(c, "Agreement Parties", H - 110)
|
||
|
||
# EmployeeName field (page 1, top=220)
|
||
fields = json.loads((SAMPLE_DIR / "nda-template-formfields.json").read_text())
|
||
for f in fields:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 1:
|
||
continue
|
||
rect = locs[0]["rect"]
|
||
draw_field(c, "Employee Full Name", rect)
|
||
|
||
# Boilerplate body text
|
||
body = [
|
||
"This Non-Disclosure Agreement ('Agreement') is entered into as of the date signed",
|
||
"below ('Effective Date') by and between the Employee identified above and Acme Corp",
|
||
"('Company'), collectively referred to as the 'Parties'.",
|
||
"",
|
||
"1. CONFIDENTIAL INFORMATION",
|
||
" The Employee agrees not to disclose, publish, or make available any Confidential",
|
||
" Information (as defined herein) to any third party without the prior written",
|
||
" consent of the Company.",
|
||
"",
|
||
"2. OBLIGATIONS",
|
||
" The Employee shall use the Confidential Information solely for the purpose of",
|
||
" performing their duties with the Company and shall protect the information with",
|
||
" at least the same degree of care used for their own confidential information.",
|
||
"",
|
||
"3. TERM",
|
||
" This Agreement shall remain in effect for a period of two (2) years from the",
|
||
" Effective Date and shall survive termination of employment.",
|
||
"",
|
||
"4. GOVERNING LAW",
|
||
" This Agreement shall be governed by the laws of the State of California.",
|
||
]
|
||
y = H - 260
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.15, 0.15, 0.15)
|
||
for line in body:
|
||
c.drawString(72, y, line)
|
||
y -= 13
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 1 of 2 — Non-Disclosure Agreement")
|
||
c.showPage()
|
||
|
||
# ---- Page 2: Signature ----
|
||
c.setFont("Helvetica-Bold", 14)
|
||
c.setFillColorRGB(0.1, 0.2, 0.5)
|
||
c.drawCentredString(W / 2, H - 50, "Non-Disclosure Agreement — Execution Page")
|
||
|
||
section_header(c, "Signatures", H - 100)
|
||
c.setFont("Helvetica", 10)
|
||
c.setFillColorRGB(0.2, 0.2, 0.2)
|
||
c.drawString(72, H - 130, "By signing, the Employee acknowledges reading and agreeing to all terms above.")
|
||
|
||
for f in fields:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 2:
|
||
continue
|
||
draw_signature_block(c, "Employee Signature", locs[0]["rect"])
|
||
|
||
# Date line
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawString(72, H - 500, "Date: _______________________________")
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 2 of 2 — Non-Disclosure Agreement")
|
||
|
||
c.save()
|
||
b64 = save_b64(path)
|
||
print(f"✅ Generated {path.name} + {b64.name}")
|
||
|
||
|
||
# ===========================================================================
|
||
# 3. Sales Agreement (3 pages)
|
||
# ===========================================================================
|
||
def generate_sales_contract():
|
||
path = SAMPLE_DIR / "sales-contract-sample.pdf"
|
||
c = canvas.Canvas(str(path), pagesize=letter)
|
||
|
||
fields = json.loads((SAMPLE_DIR / "sales-contract-formfields.json").read_text())
|
||
|
||
# ---- Page 1: Terms + PurchasePrice field ----
|
||
c.setFont("Helvetica-Bold", 18)
|
||
c.setFillColorRGB(0.1, 0.25, 0.1)
|
||
c.drawCentredString(W / 2, H - 60, "Sales Agreement")
|
||
c.setFont("Helvetica", 10)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawCentredString(W / 2, H - 78, "Buyer and Seller — Binding Contract")
|
||
|
||
section_header(c, "Transaction Details", H - 110)
|
||
|
||
for f in fields:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 1:
|
||
continue
|
||
rect = locs[0]["rect"]
|
||
draw_field(c, "Purchase Price (USD)", rect, field_hint="e.g. 5000.00")
|
||
|
||
body_p1 = [
|
||
"This Sales Agreement ('Agreement') is entered into as of the date of execution",
|
||
"by and between the Buyer and the Seller identified on the signature page.",
|
||
"",
|
||
"1. SALE OF GOODS",
|
||
" Seller agrees to sell and Buyer agrees to purchase the goods described in",
|
||
" Schedule A (attached) for the Purchase Price stated above.",
|
||
"",
|
||
"2. PAYMENT TERMS",
|
||
" Payment is due in full within 30 days of signing unless otherwise agreed",
|
||
" in a separate written addendum signed by both parties.",
|
||
"",
|
||
"3. DELIVERY",
|
||
" Seller shall deliver the goods within 14 days of receipt of payment.",
|
||
" Risk of loss transfers to Buyer upon delivery.",
|
||
"",
|
||
"4. WARRANTIES",
|
||
" Seller warrants that it has clear title to the goods and that the goods",
|
||
" conform to the specifications in Schedule A.",
|
||
"",
|
||
"5. DISPUTE RESOLUTION",
|
||
" Disputes shall be resolved by binding arbitration under AAA Commercial Rules.",
|
||
]
|
||
y = H - 210
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.15, 0.15, 0.15)
|
||
for line in body_p1:
|
||
c.drawString(72, y, line)
|
||
y -= 13
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 1 of 3 — Sales Agreement")
|
||
c.showPage()
|
||
|
||
# ---- Page 2: Schedule A ----
|
||
c.setFont("Helvetica-Bold", 14)
|
||
c.setFillColorRGB(0.1, 0.25, 0.1)
|
||
c.drawCentredString(W / 2, H - 50, "Schedule A — Goods Description")
|
||
section_header(c, "Item Detail", H - 90)
|
||
body_p2 = [
|
||
"Item: [To be completed by Seller]",
|
||
"Quantity: [To be completed by Seller]",
|
||
"Description: [To be completed by Seller]",
|
||
"Unit Price: [To be completed by Seller]",
|
||
"",
|
||
"Condition: New / Used / Refurbished (circle one)",
|
||
"",
|
||
"Special Terms:",
|
||
" _________________________________________________________",
|
||
" _________________________________________________________",
|
||
" _________________________________________________________",
|
||
]
|
||
y = H - 130
|
||
c.setFont("Helvetica", 10)
|
||
c.setFillColorRGB(0.15, 0.15, 0.15)
|
||
for line in body_p2:
|
||
c.drawString(72, y, line)
|
||
y -= 18
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 2 of 3 — Sales Agreement")
|
||
c.showPage()
|
||
|
||
# ---- Page 3: Signatures ----
|
||
c.setFont("Helvetica-Bold", 14)
|
||
c.setFillColorRGB(0.1, 0.25, 0.1)
|
||
c.drawCentredString(W / 2, H - 50, "Sales Agreement — Execution Page")
|
||
section_header(c, "Authorized Signatures", H - 90)
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.2, 0.2, 0.2)
|
||
c.drawString(72, H - 120, "Each party signing below agrees to be bound by all terms and conditions of this Agreement.")
|
||
|
||
for f in fields:
|
||
locs = f.get("locations", [])
|
||
if not locs or locs[0]["pageNumber"] != 3:
|
||
continue
|
||
label = "Buyer Signature" if f["fieldName"] == "BuyerSign" else "Seller Signature"
|
||
draw_signature_block(c, label, locs[0]["rect"])
|
||
|
||
# Printed name / date lines
|
||
c.setFont("Helvetica", 9)
|
||
c.setFillColorRGB(0.3, 0.3, 0.3)
|
||
c.drawString(200, H - 440, "Printed Name: ________________________ Date: __________")
|
||
c.drawString(420, H - 440, "Printed Name: ________________________ Date: __________")
|
||
|
||
c.setFont("Helvetica", 8)
|
||
c.setFillColorRGB(0.5, 0.5, 0.5)
|
||
c.drawCentredString(W / 2, 30, "Page 3 of 3 — Sales Agreement")
|
||
|
||
c.save()
|
||
b64 = save_b64(path)
|
||
print(f"✅ Generated {path.name} + {b64.name}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
generate_onboarding()
|
||
generate_nda()
|
||
generate_sales_contract()
|
||
print("\nAll sample PDFs generated.")
|