Skip to content
kleashPublic

About

Template-driven PDF generation for financial institutions — upload a template bundle, POST JSON, get a PDF. Dual implementation: Jinja2/WeasyPrint and FreeMarker/Apache FOP.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Financial Document PDF Service

Template-driven PDF generation for financial institutions, with late-bound data. Developers upload a template bundle, applications post JSON data, and get a PDF back.

15 document types, every one shipped in both stacks — 7 designed from published specimen documents, 8 reconstructed from real bank statements — as two complete implementations of the same feature:

python/   Jinja2  → HTML/CSS → WeasyPrint → pypdf     (recommended)
java/     FreeMarker → XSL-FO → Apache FOP → PDFBox

Run python3 tools/check-bundles.py at any time for the live inventory: it verifies both stacks ship every type, that their data files are byte-identical, that every stated total equals its line items, and that samples exist.

Template Sample Stress test
banking-statement 6-page wealth statement, mailing cover, ledgers 120-row ledger → 11 pages
invoice 2-page institutional fee invoice, remittance stub 92 line items → 5–6 pages
announcement-letter merger-notice letter, key-dates table 15+ paragraph letter → 4–5 pages
contract 4-page fixed-rate note, amortization schedule 25+ sections → 11–12 pages
deposit-receipt 1-page branch deposit receipt (authored by Claude Haiku following the guide, as a live authorability test) 50 items → 2 pages
adverse-action-notice credit-decline notice: reasons table, score panel, ECOA rights (Haiku-authored, reviewed) 25 reasons → 4 pages
loan-approval-letter approval letter with verified amortization schedule (Haiku-authored, reviewed) 120-month schedule → 7 pages

Reconstructions — real-world statement layouts rebuilt from genuine documents (see references/reconstructions/), proving the engine handles production bank output, not just clean-room designs: relationship-summary (gradient masthead, hexagon infographic, icon panels), card-statement (multi-card ledger sections), remittance-advice (form-grid advice with brand band), consolidated-statement (dual-brand, banded multi-account).

Both stacks render the same JSON for every type — the data contract is shared, only the layout implementation differs. All six original requirements hold across all types: tables, images, headers/footers, font customisation, background images, and stamping.


Ground truth

Nothing here was invented from imagination. references/ holds genuine specimen documents published by real institutions — Wells Fargo and Bank of America sample statements, a CFPB credit-card statement, university and utility invoices, a Factur-X e-invoice, Morgan Stanley and OCC letters, a bank merger customer notice, Fannie Mae and SBA note forms, a Chase deposit account agreement — plus a LAYOUT-SPEC.md per type distilled from reading every page of them: anatomy, tables, typography, furniture, and the canonical data model. The templates were built against those specs and reviewed page-by-page against those PDFs. (The rendered branding is the fictional "Meridian Bank, N.A." — the references ground the layout, not the identity.)

references/<type>/canonical_sample_data.json is the single source for each type's sample_data.json in both stacks; the copies are byte-identical and CI should keep them that way.


Which to use

Python, unless you're constrained to the JVM. Templates are HTML/CSS, so any front-end developer can edit them; iteration is ~1 s; the ecosystem is familiar.

Java (FOP) if you're on the JVM. Note this is not the openhtmltopdf route I'd normally suggest for Java — for the statement layout it's the wrong tool. That design needs four independent page regions, and openhtmltopdf is CSS 2.1 with thin @page margin-box support. XSL-FO has fo:region-end as a first-class page region.

Python (WeasyPrint) Java (FOP)
Cold render ~1.0 s ~1.5–3 s (JVM start; the service reuses the JVM)
Template authoring HTML + CSS XSL-FO
Side regions running elements + margin boxes native
PDF bookmarks not supported fo:bookmark-tree
border-radius native CSS fox: extension only

Quick start

Python

cd python
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

.venv/bin/python render.py templates/invoice out/invoice.pdf
.venv/bin/python render.py templates/contract out/c.pdf --data mydata.json
.venv/bin/python render.py templates/announcement-letter out/l.pdf --stamp "DUPLICATE COPY"

.venv/bin/python watch.py templates/invoice --png     # live dev loop
.venv/bin/uvicorn service:app --reload                # HTTP service

WeasyPrint needs Pango, Cairo, HarfBuzz and GDK PixBuf at the system level — use the included Dockerfile rather than fighting a bare VM.

Java

cd java
mvn package
mvn dependency:build-classpath -Dmdep.outputFile=target/classpath.txt

java -cp "target/classes:$(cat target/classpath.txt)" com.statement.Main \
     templates/invoice out/invoice.pdf

  --data f.json    render your own data
  --stamp "TEXT"   watermark every page
  --lint           static bundle checks only
  --fo             dump the intermediate XSL-FO

# HTTP service (upload / render / preview), same surface as the Python one:
TEMPLATE_ROOT=templates_store PORT=8080 \
java -cp "target/classes:$(cat target/classpath.txt)" com.statement.Service

The service API (both stacks)

POST /templates   upload a bundle zip → linted + smoke-rendered + versioned,
                  409 on duplicate name+version
GET  /templates   list templates and versions
POST /render      {"template","version","data","stamp"?} → PDF
POST /preview     render an unpublished bundle, nothing stored
GET  /healthz     liveness + worker count

Upload transport differs: the Python service takes multipart (curl -F bundle=@b.zip), the Java service takes the zip raw (curl -H 'Content-Type: application/zip' --data-binary @b.zip), and its /preview takes {"bundle": <base64 zip>, "data": {...}}. Rendering is CPU-bound in both; each keeps renders on a bounded worker pool.


Layout

references/          ground truth: 12 specimen PDFs + LAYOUT-SPEC.md and
                     canonical data per document type
tools/rasterize.py   PDF → per-page PNGs; the visual-inspection half of QA
tools/render-samples.sh  regenerate samples/ from committed templates + data
tools/fonts/         Liberation Sans/Serif/Mono (SIL OFL) + DejaVu (carries
                     ₹ U+20B9, used as a per-glyph fallback — cookbook F13)
TEMPLATE-AUTHORING.md  the bundle-authoring recipe (written to be followable
                     by a developer pairing with a small model — see §8)

python/
  render.py          Jinja2 → WeasyPrint → pypdf; sandboxed URL fetcher
  service.py         FastAPI: upload / render / preview, worker pool
  watch.py           re-render on save, ~1 s loop
  templates/{banking-statement,invoice,announcement-letter,contract}/
    template.html styles.css manifest.json
    sample_data.json stress_data.json assets/ fonts/

java/
  src/main/java/com/statement/
    Renderer.java              FreeMarker → FOP → PDFBox; sandboxed resolver
    Service.java               HTTP surface (JDK HttpServer, zero extra deps)
    ZipBundles.java            zip-slip/bomb-safe bundle extraction
    Stamper.java               PDFBox overlay pass, page-geometry aware
    BundleLinter.java          layer 1: static checks
    StrictEventListener.java   layer 2: escalate FOP warnings to failures
    PdfInvariants.java         layer 3: blank pages, golden page count
    Main.java                  CLI
  test/
    SecurityCheck.java         SSRF, traversal, missing asset, escaping
    TrapCheck.java             re-introduces each FOP trap, asserts it's caught
    service-smoke.sh           end-to-end API round-trip (8 checks)
  templates/<type>/     15 bundles, mirroring python/templates/
    *.ftlx fop.xconf manifest.json sample_data.json stress_data.json

samples/<type>/      rendered evidence per document type (see samples/README.md)

Verified, not assumed

Every claim below was measured, not inferred. tools/check-bundles.py re-verifies the mechanical ones on demand (15 types, both stacks, byte-identical data, totals equal to their line items, samples present):

  • Every page of every sample and stress render visually inspected (rasterized and reviewed) against the reference documents — by the authors, then again by independent adversarial reviewers with edit rights
  • All arithmetic recomputed from the JSON and matched to the rendered text to the cent (totals chains, tax, amortization schedule, fee-change tables)
  • Extracted text proofread: no typos, no None/null, no unrendered template droppings, consistent labels
  • Stress renders paginate correctly in both stacks: column headers repeat, closing totals appear exactly once, Page X of Y correct, no blank pages
  • Stamping applies to every page, at every page size (the overlay is sized to the document's geometry — a hardcoded A4-landscape stamp clipped on Letter-portrait bundles until the reviewers caught it)
  • expected_pages is enforced for sample-data renders (upload validation, CLI default) and correctly not for caller data, whose length varies
  • Zero renderer warnings on clean builds; --lint clean on all four Java bundles
  • Java service round-trip: upload → 201, duplicate → 409, render → PDF, missing required field → 422, unknown version → 404, preview → PDF, zip-slip archive → 400
  • Security suites pass in both stacks: SSRF, path traversal, missing asset, auto-escaping, zip slip
  • Java trap suite: 6 traps re-introduced, 6 caught, 0 missed

Read these

  • guide/COOKBOOK.md — element recipes, the full gotcha catalog, tips — written for a developer pairing with a small model, backed by two verified snippet galleries in guide/snippets/
  • TEMPLATE-AUTHORING.md — how to build a new bundle, end to end
  • guide/HAIKU-PROMPTS.md — verified copy-paste prompts for driving a small model to reconstruct each of the eight real statement layouts, with the fill-in table and the follow-up prompts that fix common defects
  • guide/AUTHORABILITY-REPORT.md — measured results of having Claude Haiku author templates from these guides: what small models get right, what they fabricate, and the guide changes each defect produced
  • references/<type>/LAYOUT-SPEC.md — what makes each document type look real
  • python/README.md — workflow, bundle format, API, nine WeasyPrint gotchas the templates encode
  • java/README.md — FOP vs openhtmltopdf, honest comparison against CSS
  • java/FOP-TRAPS.md — the three-layer defence against FOP's silent failures

Before you ship

Branding. The logos are placeholder wordmarks and all sample data is fictional. Drop in your own assets and keep real account numbers out of sample_data.json — it's committed and runs as the upload smoke test.

Add visual regression. Rasterise each page (tools/rasterize.py) and diff against golden PNGs in CI. It's the only defence against the "renders fine, looks wrong" class — in particular a table wider than its page, which FOP clips silently with no event and no exception.

Auth. Neither service ships authentication; both are meant to sit behind your gateway. Add TLS and an auth layer before exposing them beyond localhost.


Project

License Apache 2.0 — see LICENSE. Third-party fonts and reference documents are attributed in NOTICE.
Contributing CONTRIBUTING.md — the checks to run, the house rules, and how to add a template.
Data provenance PRIVACY.md — everything rendered here is fictional, and how that is enforced rather than asserted.
Trademarks Layout specs describe reconstructions as modelled on named institutions' document layout families. These are descriptive references only and imply no affiliation or endorsement. All marks belong to their owners.

Run the full check suite before any change lands:

python3 tools/check-bundles.py                 # data contracts, arithmetic, coverage
tools/render-samples.sh                        # regenerate samples/ from templates
cd java && mvn package && ./test/service-smoke.sh

About

Template-driven PDF generation for financial institutions — upload a template bundle, POST JSON, get a PDF. Dual implementation: Jinja2/WeasyPrint and FreeMarker/Apache FOP.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages