Files
spring-boot-demo/pdf-invoice
..

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).