Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Kg9RWSTJzyz6KP9X
pdf-invoice
Companion code for Generate PDF Invoices in Spring Boot: OpenPDF vs iText vs Flying Saucer (HTML to PDF).
Spring Boot 4.1.2, JDK 25, OpenPDF 3.0.5, iText Core 9.8.0 (AGPL 3 or commercial), Flying Saucer 10.7.0, Thymeleaf 3.1.5, Apache PDFBox 3.0.8 (tests only). No database, no Docker.
One invoice, rendered by three libraries, with Marathi and Hindi text in Noto Sans Devanagari (SIL OFL 1.1) embedded:
| Engine name | Code | What it is |
|---|---|---|
openpdf |
OpenPdfInvoiceWriter |
(a) OpenPDF's programmatic API, defaults |
itext |
ITextInvoiceWriter |
(b) iText Core 9.8.0, programmatic API, defaults |
flyingsaucer |
FlyingSaucerInvoiceWriter, templates/invoice.html |
(c) Thymeleaf template to XHTML to Flying Saucer, defaults |
openpdf-jdk, openpdf-jdk-actualtext |
OpenPdfInvoiceWriter with Shaping.JDK |
OpenPDF's GlyphLayoutManager: text laid out by the JDK's own shaper |
openpdf-fop |
OpenPdfInvoiceWriter with Shaping.FOP |
OpenPDF's optional Apache FOP glyph engine (only with mvn -Pfop) |
flyingsaucer-shaped |
FlyingSaucerInvoiceWriter with shaped=true |
Flying Saucer with OpenPDF's static LayoutProcessor switched on |
InvoiceController serves GET /invoices/{engine}.pdf with StreamingResponseBody (default) or a buffered byte[] (?mode=buffered), a Marathi
Content-Disposition file name, ?probe=true for an invoice that is only a shaping test string, and ?failAtEnd=true to see what a failure after rendering looks like.
Bench (profile bench) times N invoices per engine in one JVM.
export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
java -jar target/pdf-invoice-1.0.0.jar # then: curl -o invoice.pdf localhost:8080/invoices/openpdf.pdf
./scripts/run-all.sh # tests + every capture below; rewrites output/ (about 4 minutes)
scripts/run-all.sh needs poppler (pdftoppm, pdftotext, pdffonts, pdfinfo) and Python with Pillow built with libraqm (for the reference line in the comparison image).
Everything in output/ was produced by that script; the numbers in 06-bench.txt are indicative (shared 2-vCPU sandbox, one run per line).
Captured output
| File | What it holds |
|---|---|
output/00-versions-and-licences.txt |
licence declarations read from the POMs, sentences from iText's AGPL page, latest GA versions |
output/01-text-extraction.txt |
PDFBox text extraction of each engine's invoice (PdfContentTest) |
output/02-endpoints.txt |
status and headers for every engine, streamed vs buffered, a failure after rendering |
output/03-pdffonts-and-producer.txt |
pdffonts per engine and the Producer line each library writes |
output/04-glyph-counts.txt |
glyph codes written into the page, per engine (GlyphCountTest) |
output/05-layoutprocessor-trap.txt |
what the static LayoutProcessor does to other OpenPDF writers in the same JVM |
output/06-bench.txt |
time per invoice, PDF size, subset vs whole font |
output/png/shaping-comparison.png |
the probe string drawn by each engine under a Pillow/libraqm reference line |
output/png/invoice-*.png |
the first 800 pixel rows of each engine's invoice at 110 dpi |
output/pdf/*.pdf |
the PDFs themselves (probe-*.pdf are the shaping probe invoices) |
Not covered here
iText's pdfCalligraph add-on (proprietary licence, needs a licence key, so it was not run), PDF/A and PDF/UA conformance, digital signatures, multi-page invoices and page headers/footers,
and Microsoft Word / Adobe Acrobat / mobile viewers (rendering was checked with poppler's pdftoppm only).
Fonts
src/main/resources/fonts/ holds static Regular and Bold instances of Noto Sans Devanagari, cut from the variable font with scripts/make_fonts.py
(source and licence in the script's header and in OFL.txt).