Introduction & Approaches

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. Before writing any code you have to decide how the document is going to be described. The same invoice can be produced by placing text and tables with an API, by filling a visual template, by converting HTML, or by transforming XML with a stylesheet. Each choice fixes who can change the layout later (a developer, a report designer or a web developer), how large documents behave, and, often forgotten, which licence your own service ends up under. This page compares the four approaches, shows the same invoice line written in three of them, and ends with a decision flowchart. The later pages of this section go deep into each library; they use the Bookshelf Billing scenario of the section landing page.

The four approaches

Programmatic layout

You call an API that creates pages, paragraphs, tables and images, or even draws on the page with graphics operators. Libraries: iText Core (AGPLv3 or commercial), Apache PDFBox (Apache-2.0) and OpenPDF (LGPL and MPL, a fork of the old iText 4 code line). You get full control and the best performance, and the layout lives in Java code, so changing it is a developer task. It is the right choice when the structure is dynamic (a variable number of sections, computed page breaks, signatures, forms, barcodes drawn at exact positions).

Template reports

You design the document visually, as bands (title, page header, detail, summary) and elements in an XML file, and the engine fills it with data and exports it. Libraries: JasperReports Library (LGPL-3.0) with its designer Jaspersoft Studio, and Eclipse BIRT (EPL-2.0). A report designer can change the layout without touching Java, and the engine already knows grouping, subtotals, subreports, crosstabs, charts and paging. It is the right choice for list-oriented, data-driven reports such as the monthly sales report.

HTML-to-PDF

The document is HTML and CSS, rendered by a converter. Options: iText pdfHTML (add-on of iText, AGPLv3 or commercial), OpenHTMLtoPDF and Flying Saucer (both LGPL, pure Java, no JavaScript), and a headless browser such as Chromium, driven through Playwright or Puppeteer or wrapped as a service by Gotenberg. Web developers can work in a language they already know and reuse templates such as Thymeleaf, but pagination, headers and footers, fonts and conformance (PDF/A, tagged PDF) are weaker than in the first two approaches, and a browser adds a process, memory and a larger attack surface to operate.

XSL-FO

The content is XML, transformed by a stylesheet into formatting objects and rendered by Apache FOP (Apache-2.0, version 2.11 on Maven Central as of 2026-10-09). It is mature and standards-based (XSL-FO 1.1) and paginates well, but the skill base is small and the project moves slowly, so it is mainly met in existing publishing pipelines. New projects seldom start here.

Approach Strengths Weaknesses Typical Bookshelf Billing use

Programmatic

Total control, speed, signatures, forms, PDF/A and PDF/UA support, barcodes

Layout changes are code changes; verbose for long tabular reports

Invoices, onboarding form, signed documents

Template reports

Visual design, grouping, subreports, charts, crosstabs, data-source adapters

Learning curve of the report model; limited for free-flow prose

Monthly sales report from PostgreSQL

HTML-to-PDF

Web skills and templates, quick start, rich CSS

Pagination and conformance limits; browser operations

Marketing-style documents, e-mail attachments rendered from existing HTML

XSL-FO

Standards-based, strong pagination

Small ecosystem, XSLT skills needed

Legacy publishing pipelines only

The same invoice line in three styles

Each example below produces a one-line invoice for two copies of a book at 39.90, with the line total of 79.80. They were compiled and run with Java 21 syntax against the library versions of the baseline date, and each writes a PDF file in the working directory. The data is an invented placeholder.

Programmatic: iText

Maven coordinates (as of 2026-10-09, itext-core is a POM aggregator and therefore declared with the type pom):

<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>itext-core</artifactId>
  <version>9.8.0</version>
  <type>pom</type>
</dependency>

The bouncy-castle-adapter artifact is only needed once you sign or encrypt, which later pages do. The layout is built from objects: a Table with percentage column widths and one Cell per value.

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;

public class ItextInvoiceLine {

    record InvoiceLine(String title, int quantity, BigDecimal unitPrice) {
        BigDecimal total() {
            return unitPrice.multiply(BigDecimal.valueOf(quantity));
        }
    }

    public static void main(String[] args) throws IOException {
        var line = new InvoiceLine("Refactoring, 2nd edition", 2, new BigDecimal("39.90"));
        Path target = Path.of("invoice-itext.pdf");

        try (var pdf = new PdfDocument(new PdfWriter(Files.newOutputStream(target)));
             var document = new Document(pdf)) {
            document.add(new Paragraph("Invoice B-2026-0001"));
            var table = new Table(UnitValue.createPercentArray(new float[] {6, 1, 2, 2}))
                    .useAllAvailableWidth();
            table.addCell(new Cell().add(new Paragraph(line.title())));
            table.addCell(new Cell().add(new Paragraph(String.valueOf(line.quantity()))
                    .setTextAlignment(TextAlignment.RIGHT)));
            table.addCell(new Cell().add(new Paragraph(line.unitPrice().toPlainString())
                    .setTextAlignment(TextAlignment.RIGHT)));
            table.addCell(new Cell().add(new Paragraph(line.total().toPlainString())
                    .setTextAlignment(TextAlignment.RIGHT)));
            document.add(table);
        }
    }
}

The example derives from the iText jump-start tutorial and the installation guide of the iText knowledge base.

Template: JasperReports

Maven coordinates (as of 2026-10-09). The engine needs the PDF backend (jasperreports-pdf, based on OpenPDF), the default fonts, and a Java compiler for the report expressions (jasperreports-jdt, the Eclipse compiler):

<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-pdf</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-fonts</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-jdt</artifactId>
  <version>7.0.8</version>
</dependency>

The layout is not in Java but in a template, src/main/resources/invoice-line.jrxml, in the v7 format, where every element is an <element kind="..."> and the file declares no namespace. The amounts in the template are computed by an expression written with the $F{...} (field) and $P{...} (parameter) syntax.

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport name="invoice-line" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <parameter name="invoiceNumber" class="java.lang.String"/>
  <field name="title" class="java.lang.String"/>
  <field name="quantity" class="java.lang.Integer"/>
  <field name="unitPrice" class="java.math.BigDecimal"/>
  <variable name="lineTotal" class="java.math.BigDecimal">
    <expression><![CDATA[$F{unitPrice}.multiply(java.math.BigDecimal.valueOf($F{quantity}))]]></expression>
  </variable>
  <title height="30">
    <element kind="textField" width="515" height="24">
      <expression><![CDATA["Invoice " + $P{invoiceNumber}]]></expression>
    </element>
  </title>
  <detail>
    <band height="20">
      <element kind="textField" x="0" width="300" height="20">
        <expression><![CDATA[$F{title}]]></expression>
      </element>
      <element kind="textField" x="300" width="60" height="20" hTextAlign="Right">
        <expression><![CDATA[$F{quantity}]]></expression>
      </element>
      <element kind="textField" x="360" width="75" height="20" hTextAlign="Right">
        <expression><![CDATA[$F{unitPrice}]]></expression>
      </element>
      <element kind="textField" x="435" width="80" height="20" hTextAlign="Right">
        <expression><![CDATA[$V{lineTotal}]]></expression>
      </element>
    </band>
  </detail>
</jasperReport>

Java only compiles the template, fills it with data and exports the result. The data source is a collection of maps, which avoids a subtle trap that was found while testing this page: the bean data source reads fields through JavaBean getters (getTitle()), so a Java record, whose accessors are title(), makes the fill fail with "Error retrieving field value from bean". The parameters map must also be mutable, since the engine adds its own entries to it, so a Map.of(...) has to be wrapped in a new HashMap<>(...).

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JasperInvoiceLine {

    public static void main(String[] args) throws JRException, IOException {
        List<Map<String, ?>> lines = List.of(Map.of(
                "title", "Refactoring, 2nd edition",
                "quantity", 2,
                "unitPrice", new BigDecimal("39.90")));

        try (InputStream template = JasperInvoiceLine.class.getResourceAsStream("/invoice-line.jrxml");
             var out = Files.newOutputStream(Path.of("invoice-jasper.pdf"))) {
            JasperReport report = JasperCompileManager.compileReport(template);
            var print = JasperFillManager.fillReport(
                    report, new HashMap<>(Map.of("invoiceNumber", "B-2026-0001")), new JRMapCollectionDataSource(lines));

            var exporter = new JRPdfExporter();
            exporter.setExporterInput(new SimpleExporterInput(print));
            exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
            exporter.exportReport();
        }
    }
}

The example derives from the JasperReports data source sample and the project repository. The sample site answers 403 to command-line clients, so open it in a browser.

HTML-to-PDF: pdfHTML

Maven coordinates (as of 2026-10-09). pdfHTML is an iText add-on, so it brings iText Core along and carries the same AGPLv3 or commercial licence:

<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>html2pdf</artifactId>
  <version>6.3.4</version>
</dependency>

The layout is HTML and CSS in a Java text block, which is the natural place for it when the markup is a literal. In a real service the markup would come from a template engine such as Thymeleaf.

import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class HtmlInvoiceLine {

    public static void main(String[] args) throws IOException {
        String html = """
                <html>
                  <head>
                    <style>
                      table { width: 100%; border-collapse: collapse; }
                      td { border-bottom: 1px solid #999; padding: 4px; }
                      td.num { text-align: right; }
                    </style>
                  </head>
                  <body>
                    <h1>Invoice B-2026-0001</h1>
                    <table>
                      <tr><td>Refactoring, 2nd edition</td><td class="num">2</td>
                          <td class="num">39.90</td><td class="num">79.80</td></tr>
                    </table>
                  </body>
                </html>
                """;
        try (var out = Files.newOutputStream(Path.of("invoice-html.pdf"))) {
            HtmlConverter.convertToPdf(html, out, new ConverterProperties());
        }
    }
}

The example derives from the pdfHTML API documentation and the pdfHTML product page. Replacing pdfHTML by OpenHTMLtoPDF keeps the same shape (markup in, OutputStream out) but swaps the licence for the LGPL, at the price of no JavaScript and a smaller CSS subset. With Gotenberg the same HTML is posted to a container, as described in the Chromium HTML route documentation, and the PDF comes back in the response.

Choosing between iText and JasperReports

The two libraries are not rivals so much as tools for different documents, and a service like Bookshelf Billing uses both.

  • Pick iText when the document is a composition: an invoice with a variable layout, a contract with signature fields, a form, a document that must be signed, tagged or conform to PDF/A, or one where you draw barcodes and stamps at exact positions. The same API also reads, merges, splits and signs existing files.

  • Pick JasperReports when the document is a report: many rows of the same shape, groups with subtotals, charts, a crosstab, and an author who is not the developer. The layout is a file that can be versioned and edited in Jaspersoft Studio.

  • Pick HTML-to-PDF when the layout already exists as web content and a designer owns it, and the document does not need strict conformance. Prefer a pure-Java converter when you cannot run a browser, and a headless browser when the CSS must match what a user sees on screen.

  • Pick PDFBox when you mainly need to read, merge, fill forms in and sign existing files under the Apache licence, and OpenPDF when you have legacy code written for the old com.lowagie API and cannot take the AGPL. OpenPDF 2.x keeps the com.lowagie packages; OpenPDF 3.x renames them to org.openpdf, so a 3.x upgrade is itself a migration.

  • Pick FOP only when a XSL-FO pipeline already exists.

Mixing is common: fill a Jasper report to produce the body, then post-process the bytes with iText or PDFBox to merge, stamp, fill forms and sign. JasperReports has no native signing, so signing is always a post-processing step.

flowchart TD A["New PDF requirement"] --> B{"Is the layout owned by a report designer or by a web designer?"} B -->|"Report designer"| C{"Rows of one shape, groups, charts, crosstabs?"} B -->|"Web designer"| H{"Can you run a headless browser?"} B -->|"Developer"| P["Programmatic layout"] C -->|"Yes"| J["JasperReports: LGPL-3.0, safe in SaaS if unmodified"] C -->|"No"| P H -->|"Yes, CSS must match the screen"| G["Chromium via Playwright or Gotenberg"] H -->|"No"| O["OpenHTMLtoPDF or Flying Saucer: LGPL, no JavaScript"] P --> L{"Closed-source or SaaS and AGPL is not acceptable?"} L -->|"No: open source, or AGPL acceptable"| I["iText Core under AGPLv3"] L -->|"Yes, but budget available"| IC["iText with a commercial licence"] L -->|"Yes, no budget"| PB["PDFBox Apache-2.0 or OpenPDF LGPL"] J --> S["Need signing, forms or PDF/A?"] I --> S IC --> S PB --> S S -->|"Yes"| T["Post-process the bytes with iText, PDFBox or EU DSS"] S -->|"No"| Z["Deliver the PDF"] T --> Z

Licensing and what it means for SaaS

The choice of library is also a choice of licence, and this is where most teams are surprised. The text that follows is a summary for engineers, not legal advice; read the licences and ask your counsel before shipping.

iText

iText Core 9.8.0 is released under the GNU Affero General Public License v3 or under a commercial licence from Apryse (the vendor of iText). The AGPL is a strong copyleft licence whose section 13 extends the obligation to offer source code to users who interact with the program over a network. In the reading of iText and Apryse, a service that links iText and lets customers download generated PDFs is such a program, so the source of the whole service has to be offered to those users under the AGPL, unless you buy a commercial licence. That is the key consequence for a SaaS: the usual "we never distribute binaries" argument that works for the GPL does not apply. The vendor explains the two options in its AGPLv3 licence page and in the knowledge-base article What is the difference between AGPL and commercial. Open-source and internal tools can use the AGPL edition; a proprietary SaaS product normally needs the commercial one.

iText add-ons

pdfHTML (html2pdf) and the other open add-ons follow the same AGPLv3 or commercial model. Others, such as pdfCalligraph (complex scripts), pdfXFA, pdfOptimizer, pdfOffice and pdfRender, are commercial only, and are resolved from the vendor’s own repository (https://repo.itextsupport.com/) instead of Maven Central. Check the licence of every add-on before you write it into the build.

JasperReports

The library is released under the LGPL-3.0 (see the project’s licence page). The LGPL lets proprietary software use the library unmodified, as an ordinary dependency, without opening the application source; only changes to the library itself have to be shared. That is why JasperReports is the usual route for a closed-source SaaS that needs reports. The edition split matters, too: in 7.0.8 the Community Edition cannot produce tagged PDF (PDF/UA and PDF/A "a" levels), which needs the commercial JasperReports Library Professional, and the free JasperReports Server was withdrawn in January 2024, so the embeddable library is what remains free.

PDFBox, FOP, OpenHTMLtoPDF, OpenPDF

PDFBox and FOP use the Apache-2.0 licence, which has no copyleft obligations for your service. OpenHTMLtoPDF and Flying Saucer are LGPL. OpenPDF is LGPL and MPL, and its 3.0 line renames the packages from com.lowagie to org.openpdf (3.0.5 on Maven Central as of 2026-10-09).

The old books and the licence

A frequent source of confusion is the older literature. Books written for iText 1.x and 2.x, such as iText in Action, describe the com.lowagie.text API, which was then available under the MPL and LGPL. iText 5 and later moved to the AGPL, and iText 7 and later are a complete rewrite with a different API, so code from those books does not compile against iText 9. The community fork that stayed on the old licence is OpenPDF, which is why it exists. When you see a very old snippet, check which library and which licence it assumes before copying the idea.

Java 21 idioms in the examples

The reference books on this subject were written for Java 1.3 to 5: no generics, no var, no records, and resources closed by hand in finally blocks. Every example in this section is written for Java 21, the long-term-support release the rest of the site uses, and relies on these idioms:

  • try-with-resources (Java tutorial) closes the PdfDocument, the Document and the output stream in the right order, even if an exception is thrown. In iText, closing the layout Document also closes the underlying PdfDocument; the two are declared together here so both are closed in a known order.

  • Text blocks (JEP 378) keep a literal HTML or JRXML snippet readable, without escaped quotes and \n sequences.

  • Records (JEP 395) model immutable values such as an invoice line. A record fits iText code, where you read the values yourself, but not JasperReports bean data sources, which expect getters, as the example above shows.

  • var for local variables whose type is obvious from the right-hand side, and List.of and Map.of for small immutable collections, remembering that JasperReports mutates the parameters map.

  • Money is a BigDecimal, never a double, and is formatted at the edge, not accumulated as a string.

The official reference for these language features is the Java 21 language guide on records and text blocks.