Overlay Django model data on government PDF forms

The Challenge

Fill official OCDD government forms (OCDDProgressNoteSingleShiftNotFillable021225.pdf) with Django ProgressNoteEntry data:

  • Overlay on real PDF template (not HTML recreation)
  • Works on NixOS dev + Ubuntu Apache2/uWSGI prod
  • No WeasyPrint (GTK/Pango nightmare on NixOS)
  • Visual coordinate tweaking (no parsing tools)
  • Graceful empty field handling

Production File Structure

static/
└── progress_notes/
    └── forms/
        └── OCDDProgressNoteSingleShiftNotFillable021225.pdf
python manage.py collectstatic

Core Implementation

1. build_overlay_pdf() - Dynamic Content Only

def build_overlay_pdf(entry) -> BytesIO:
    pdf = FPDF(format="Letter")
    pdf.add_page()
    pdf.set_margins(left=15, top=15, right=15)

    pdf.set_font("Arial", "", 10)

    if entry.beneficiary_name:
        pdf.set_xy(45, 33)
        pdf.cell(60, 6, entry.beneficiary_name)

    pdf.set_xy(130, 33)
    pdf.cell(40, 6, entry.date_of_service.strftime("%m/%d/%Y"))

    pdf.set_xy(13, 52)
    pdf.cell(80, 6, entry.provider_name or "")

    if entry.start_time:
        pdf.set_xy(158, 52)
        pdf.cell(30, 6, entry.start_time.strftime("%I:%M %p"))

    pdf_bytes = pdf.output(dest="S")
    if isinstance(pdf_bytes, str):
        pdf_bytes = pdf_bytes.encode('latin1', 'replace')

    buf = BytesIO(pdf_bytes)
    buf.seek(0)
    return buf

2. generate_pdf() - Merge + Serve

@login_required
def generate_pdf(request, entry_id):
    entry = ProgressNoteEntry.objects.get(id=entry_id)
    overlay_buf = build_overlay_pdf(entry)

    template_path = staticfiles_storage.path(
        "progress_notes/forms/OCDDProgressNoteSingleShiftNotFillable021225.pdf"
    )

    base_reader = PdfReader(open(template_path, 'rb'))
    overlay_reader = PdfReader(overlay_buf)

    base_page = base_reader.pages
    overlay_page = overlay_reader.pages
    base_page.merge_page(overlay_page)

    writer = PdfWriter()
    writer.add_page(base_page)

    out_buf = BytesIO()
    writer.write(out_buf)
    out_buf.seek(0)

    response = HttpResponse(out_buf, content_type="application/pdf")
    response["Content-Disposition"] = f'attachment; filename="progress_note_{entry_id}.pdf"'
    return response

Positioning Workflow

pdf.set_xy(X, Y)
🔼 Up: Y-5   🔽 Down: Y+5
⬅️ Left: X-5  ➡️ Right: X+5
  1. Generate PDF
  2. Zoom 200% in viewer
  3. Tweak by 5pt → repeat

Install

pip install fpdf2 pypdf

Pure Python. Pixel-perfect forms. Production ready.