Skip to main content

How to Generate PDF from HTML in Java Using Flying Saucer

· 16 min read
Michał Szymanowski
Michał Szymanowski
PDFBolt Co-Founder

Convert HTML to PDF in Java using Flying Saucer and Thymeleaf – step-by-step guide

Looking for a reliable way to convert HTML to PDF in Java – without relying on headless browsers or heavyweight libraries? In this tutorial, you'll learn how to use Flying Saucer (xhtmlrenderer) together with Thymeleaf templates to generate clean, professional PDFs directly in your Java application. Flying Saucer is a lightweight, pure Java library that renders XHTML and CSS into PDF format – no external rendering engines required. It’s ideal for reports, invoices, and other dynamic documents where fast and reliable PDF generation is essential.

What is Flying Saucer?

Flying Saucer (also known as xhtmlrenderer) is a pure Java library designed to render well-formed XML/XHTML and CSS for display and printing. Unlike browser-based PDF generation tools, Flying Saucer offers a lightweight, server-friendly approach for PDF generation.

Originally developed as an open-source project, Flying Saucer has been widely adopted in enterprise Java applications where consistent PDF output and low resource consumption are critical.

License

Flying Saucer is released under LGPL v2.1 or later, which permits its use in both open-source and commercial projects, provided you comply with the license terms.

For full details, see the LICENSE file on GitHub.

Key Features of Flying Saucer for PDF Generation

When implementing HTML to PDF generation in Java applications, Flying Saucer offers several distinct advantages:

  • Pure Java Implementation: No external dependencies on browsers or native libraries.
  • CSS 2.1 Support: Comprehensive support for CSS 2.1 specifications with select CSS 3 features.
  • XHTML Compliance: Requires well-formed XHTML for reliable rendering.
  • Font Management: Excellent support for custom fonts and typography control.
  • Memory Efficient: Lower memory footprint compared to browser-based solutions.
  • Embedded Images: Supports embedded images through data URLs or resource loading.
  • Page Control: Advanced page break control and print-specific CSS support.
CSS Limitations

Flying Saucer supports CSS 2.1 and selected CSS 3 features. Modern CSS features like Flexbox, Grid, and advanced selectors are not supported.

→ Design your templates with these limitations in mind.

Why Choose Flying Saucer for HTML-to-PDF in Java?

  • Lightweight and Pure Java: No bundled browser binaries or external processes – simpler deployment and lower overhead.
  • Predictable Performance: Consistent render times at scale, with no browser startup delays.
  • Security and Compliance: All processing happens inside the JVM – easier to audit and smaller attack surface.
  • Print-CSS Control: Full support for paged-media features (@page rules, page-break-*) and other print-specific CSS.
When Flying Saucer Might Not Be Enough
  • You need modern CSS support (Flexbox, Grid).
  • JavaScript execution is required.
  • Visual fidelity to web rendering is critical.
  • Scalable solution for high-volume applications.

Step-by-Step Implementation: HTML to PDF Invoice Generator with Flying Saucer

Let's build a complete invoice generation system using Flying Saucer and Thymeleaf.

Step 1: Development Environment Setup

Before diving into the implementation, ensure your development environment meets the requirements:

RequirementDetails
JDKJava 17 or higher (required since Flying Saucer 9.6.0).
Build ToolMaven (in this tutorial) or Gradle.
IDEIntelliJ IDEA, Eclipse or VS Code with Java extensions.

Step 2: Maven Dependencies Configuration

Set up a new Maven project with the necessary dependencies for Flying Saucer PDF generation.

Update your pom.xml with these essential dependencies:

<dependencies>
<!-- Flying Saucer: core + PDF (OpenPDF) -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-core</artifactId>
<version>9.12.0</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.12.0</version>
</dependency>

<!-- Thymeleaf for HTML templating -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>

<!-- Optional: HTML validation and cleaning -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.20.1</version>
</dependency>
</dependencies>

Essential Dependencies Explained:

  • flying-saucer-core: The core XHTML/CSS rendering engine that processes your HTML and CSS.
  • flying-saucer-pdf: Integrates with OpenPDF to generate the final PDF file (replaces older iText5 module).
  • thymeleaf: Powerful template engine for building dynamic, data-driven HTML templates.
  • jsoup (optional): Lightweight HTML parser for cleaning and manipulating HTML before rendering.

Step 3: Project Structure Organization

Organize your project with a clean, maintainable structure:

flying-saucer-pdf-generator/

├── src/
│ ├── main/
│ │ ├── java/org/example/flyingsaucer/
│ │ │ ├── FlyingSaucerApp.java # Main application
│ │ │ ├── model/ # Data models
│ │ │ │ └── InvoiceModel.java # Invoice data structure
│ │ │ │
│ │ │ └── service/ # Service classes
│ │ │ ├── TemplateService.java # Template rendering
│ │ │ └── PdfGenerationService.java # PDF generation
│ │ │
│ │ └── resources/
│ │ └── templates/ # Thymeleaf templates
│ │ └── invoice.html # Invoice template

├── output/ # Generated PDFs

└── pom.xml # Maven configuration

Step 4: Invoice Data Model Implementation

Design a comprehensive data model to represent invoice information.

Click to view Invoice Model (InvoiceModel.java)
package org.example.flyingsaucer.model;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

// Invoice model for PDF generation
public class InvoiceModel {
// Invoice identification
private String invoiceNumber;
private LocalDate invoiceDate;
private LocalDate dueDate;

// Company information (from)
private String companyName;
private String companyLogo;
private String companyAddress;
private String companyCity;
private String companyPostalCode;
private String companyCountry;
private String companyEmail;
private String companyPhone;

// Client information (to)
private String clientCompany;
private String clientAddress;
private String clientCity;
private String clientPostalCode;
private String clientCountry;
private String clientEmail;
private String clientPhone;

// Financial settings
private String currencySymbol = "$";
private BigDecimal taxRate = BigDecimal.ZERO;
private String taxLabel = "Tax";

// Invoice items
private List<InvoiceItem> items = new ArrayList<>();

// Additional information
private String notes;

// Calculated fields
public BigDecimal getSubTotal() {
return items.stream()
.map(InvoiceItem::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
}

public BigDecimal getTaxAmount() {
if (taxRate.equals(BigDecimal.ZERO)) {
return BigDecimal.ZERO;
}
return getSubTotal()
.multiply(taxRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP))
.setScale(2, RoundingMode.HALF_UP);
}

public BigDecimal getTotal() {
return getSubTotal().add(getTaxAmount()).setScale(2, RoundingMode.HALF_UP);
}

// Getters and Setters
public String getInvoiceNumber() { return invoiceNumber; }
public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; }

public LocalDate getInvoiceDate() { return invoiceDate; }
public void setInvoiceDate(LocalDate invoiceDate) { this.invoiceDate = invoiceDate; }

public LocalDate getDueDate() { return dueDate; }
public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }

public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }

public String getCompanyLogo() { return companyLogo; }
public void setCompanyLogo(String companyLogo) { this.companyLogo = companyLogo; }

public String getCompanyAddress() { return companyAddress; }
public void setCompanyAddress(String companyAddress) { this.companyAddress = companyAddress; }

public String getCompanyCity() { return companyCity; }
public void setCompanyCity(String companyCity) { this.companyCity = companyCity; }

public String getCompanyPostalCode() { return companyPostalCode; }
public void setCompanyPostalCode(String companyPostalCode) { this.companyPostalCode = companyPostalCode; }

public String getCompanyCountry() { return companyCountry; }
public void setCompanyCountry(String companyCountry) { this.companyCountry = companyCountry; }

public String getCompanyEmail() { return companyEmail; }
public void setCompanyEmail(String companyEmail) { this.companyEmail = companyEmail; }

public String getCompanyPhone() { return companyPhone; }
public void setCompanyPhone(String companyPhone) { this.companyPhone = companyPhone; }

public String getClientCompany() { return clientCompany; }
public void setClientCompany(String clientCompany) { this.clientCompany = clientCompany; }

public String getClientAddress() { return clientAddress; }
public void setClientAddress(String clientAddress) { this.clientAddress = clientAddress; }

public String getClientCity() { return clientCity; }
public void setClientCity(String clientCity) { this.clientCity = clientCity; }

public String getClientPostalCode() { return clientPostalCode; }
public void setClientPostalCode(String clientPostalCode) { this.clientPostalCode = clientPostalCode; }

public String getClientCountry() { return clientCountry; }
public void setClientCountry(String clientCountry) { this.clientCountry = clientCountry; }

public String getClientEmail() { return clientEmail; }
public void setClientEmail(String clientEmail) { this.clientEmail = clientEmail; }

public String getClientPhone() { return clientPhone; }
public void setClientPhone(String clientPhone) { this.clientPhone = clientPhone; }

public String getCurrencySymbol() { return currencySymbol; }
public void setCurrencySymbol(String currencySymbol) { this.currencySymbol = currencySymbol; }

public BigDecimal getTaxRate() { return taxRate; }
public void setTaxRate(BigDecimal taxRate) { this.taxRate = taxRate; }

public String getTaxLabel() { return taxLabel; }
public void setTaxLabel(String taxLabel) { this.taxLabel = taxLabel; }

public List<InvoiceItem> getItems() { return items; }
public void setItems(List<InvoiceItem> items) { this.items = items; }

public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }

// Represents a single invoice line item
public static class InvoiceItem {
private String description;
private Integer quantity;
private BigDecimal unitPrice;
private String notes; // Optional - used with th:if

public InvoiceItem() {}

public InvoiceItem(String description, Integer quantity, BigDecimal unitPrice) {
this.description = description;
this.quantity = quantity;
this.unitPrice = unitPrice;
}

public InvoiceItem(String description, Integer quantity, BigDecimal unitPrice, String notes) {
this.description = description;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.notes = notes;
}

public BigDecimal getTotal() {
if (quantity == null || unitPrice == null) {
return BigDecimal.ZERO;
}
return unitPrice.multiply(BigDecimal.valueOf(quantity)).setScale(2, RoundingMode.HALF_UP);
}

// Getters and Setters
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }

public BigDecimal getUnitPrice() { return unitPrice; }
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }

public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
}
}

Key Model Features:

  • BigDecimal precision: Ensures accurate financial calculations without floating-point errors.
  • Automatic calculations: Subtotal, tax, and total amounts are computed automatically.
  • Flexible structure: Supports multiple invoice items with individual pricing.
  • Validation-ready: Proper null checks prevent calculation errors.

Step 5: Flying Saucer-Compatible HTML Template

Flying Saucer requires well-formed XHTML and supports CSS 2.1. Design your template accordingly for optimal PDF output.

Click to view Invoice Template (invoice.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8"/>
<title th:text="'Invoice ' + ${invoice.invoiceNumber}">Invoice</title>
<style>
@page {
size: A4;
margin: 0;
}

* {
margin: 0;
padding: 0;
}

body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
line-height: 1.6;
color: #2d3748;
}

/* Header */
.invoice-header {
background: #319795;
color: white;
padding: 30px 55px 25px;
}

.header-content {
display: table;
width: 100%;
}

.company-section {
display: table-cell;
width: 60%;
}

.invoice-section {
display: table-cell;
width: 40%;
text-align: right;
}

.company-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}

.company-logo {
width: 100px;
height: auto;
margin-left: 55px;
}

.invoice-title {
font-size: 32px;
font-weight: bold;
margin-bottom: 10px;
}

.invoice-number {
display: inline-block;
font-size: 14px;
font-weight: bold;
background-color: rgba(255, 255, 255, 0.2);
padding: 8px 15px;
border-radius: 20px;
margin-bottom: 15px;
}

.header-dates {
font-size: 13px;
text-align: right;
line-height: 1.7;
}

/* Content */
.invoice-content {
padding: 15px 50px;
}

/* Billing cards */
.billing-container {
display: table;
width: 100%;
margin-bottom: 15px;
}

.billing-card {
display: table-cell;
width: 48%;
background-color: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px 20px;
}

.billing-spacer {
display: table-cell;
width: 4%;
}

.billing-title {
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
color: #319795;
margin-bottom: 5px;
}

.billing-name {
font-size: 13px;
font-weight: bold;
margin-bottom: 4px;
}

.billing-details {
color: #4a5568;
line-height: 1.5;
}

/* Items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
vertical-align: center;
}

.items-table thead tr {
background: #2d3748;
color: white;
}

.items-table th {
padding: 15px 12px;
text-align: left;
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
}

.items-table td {
padding: 12px;
border-bottom: 1px solid #e2e8f0;
}

.item-description {
font-weight: 600;
}

.items-table .text-right {
text-align: right;
}

/* Summary */
.summary-container {
display: table;
width: 100%;
}

.summary-spacer {
display: table-cell;
width: 60%;
}

.summary-card {
display: table-cell;
width: 40%;
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 10px 20px;
}

.summary-table {
width: 100%;
border-collapse: collapse;
}

.summary-table td {
padding: 8px 0;
border-bottom: 1px solid #e2e8f0;
}

.summary-label {
font-size: 13px;
font-weight: 600;
}

.summary-value {
text-align: right;
font-size: 13px;
font-weight: 600;
}

.summary-total .summary-label,
.summary-total .summary-value {
font-size: 14px;
font-weight: bold;
color: #319795;
border-bottom: none;
}

/* Notes */
.notes-section {
margin-top: 20px;
background-color: #f7fafc;
border-left: 4px solid #319795;
border-radius: 4px;
padding: 20px;
}

.notes-title {
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
color: #319795;
margin-bottom: 10px;
}

.notes-content {
color: #4a5568;
line-height: 1.6;
}

/* Footer */
.invoice-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #e2e8f0;
text-align: center;
}

.footer-thank-you {
font-size: 14px;
font-weight: bold;
color: #319795;
margin-bottom: 10px;
}

.footer-contact {
font-size: 11px;
color: #718096;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="invoice-header">
<div class="header-content">
<div class="company-section">
<div class="company-name" th:text="${invoice.companyName}">ABC Company Ltd</div>
<img class="company-logo" th:src="${invoice.companyLogo}" alt="Company Logo"/>
</div>
<div class="invoice-section">
<div class="invoice-title">INVOICE</div>
<div class="invoice-number" th:text="${invoice.invoiceNumber}">INV-2025-001</div>
<div class="header-dates">
<div><strong>Issue Date: </strong>
<span th:text="${#temporals.format(invoice.invoiceDate, 'dd.MM.yyyy')}">20.05.2025</span>
</div>
<div><strong>Due Date: </strong>
<span th:text="${#temporals.format(invoice.dueDate, 'dd.MM.yyyy')}">20.06.2025</span></div>
</div>
</div>
</div>
</div>

<div class="invoice-content">
<div class="billing-container">
<div class="billing-card">
<div class="billing-title">Bill From</div>
<div class="billing-name" th:text="${invoice.companyName}">ABC Company Ltd</div>
<div class="billing-details">
<div th:text="${invoice.companyAddress}">123 Business Street</div>
<div>
<span th:text="${invoice.companyCity}">City</span>,
<span th:text="${invoice.companyPostalCode}">12345</span>,
<span th:text="${invoice.companyCountry}">Country</span>
</div>
<div th:text="${invoice.companyEmail}">[email protected]</div>
<div th:text="${invoice.companyPhone}">+1 (555) 123-4567</div>
</div>
</div>
<div class="billing-spacer"></div>
<div class="billing-card">
<div class="billing-title">Bill To</div>
<div class="billing-name" th:text="${invoice.clientCompany}">Client Company Ltd</div>
<div class="billing-details">
<div th:text="${invoice.clientAddress}">456 Client Avenue</div>
<div>
<span th:text="${invoice.clientCity}">Client City</span>,
<span th:text="${invoice.clientPostalCode}">67890</span>,
<span th:text="${invoice.clientCountry}">Country</span>
</div>
<div th:text="${invoice.clientEmail}">[email protected]</div>
<div th:text="${invoice.clientPhone}">+1 (555) 987-6543</div>
</div>
</div>
</div>

<table class="items-table">
<thead>
<tr>
<th style="width: 50%;">Description</th>
<th style="width: 15%;" class="text-right">Quantity</th>
<th style="width: 20%;" class="text-right">Unit Price</th>
<th style="width: 15%;" class="text-right">Amount</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${invoice.items}">
<td>
<div class="item-description" th:text="${item.description}">Professional Services</div>
</td>
<td class="text-right" th:text="${item.quantity}">10</td>
<td class="text-right">
<span th:text="${invoice.currencySymbol}">$</span><span
th:text="${#numbers.formatDecimal(item.unitPrice, 1, 2)}">100.00</span>
</td>
<td class="text-right"><strong>
<span th:text="${invoice.currencySymbol}">$</span><span
th:text="${#numbers.formatDecimal(item.total, 1, 2)}">1,000.00</span>
</strong></td>
</tr>
</tbody>
</table>

<div class="summary-container">
<div class="summary-spacer"></div>
<div class="summary-card">
<table class="summary-table">
<tr>
<td class="summary-label">Subtotal:</td>
<td class="summary-value">
<span th:text="${invoice.currencySymbol}">$</span><span
th:text="${#numbers.formatDecimal(invoice.subTotal, 1, 2)}">1,000.00</span>
</td>
</tr>
<tr th:if="${invoice.taxRate > 0}">
<td class="summary-label">
<span th:text="${invoice.taxLabel}">Tax</span> (<span th:text="${invoice.taxRate}">10.0</span>%):
</td>
<td class="summary-value">
<span th:text="${invoice.currencySymbol}">$</span><span
th:text="${#numbers.formatDecimal(invoice.taxAmount, 1, 2)}">100.00</span>
</td>
</tr>
<tr class="summary-total">
<td class="summary-label">TOTAL:</td>
<td class="summary-value">
<span th:text="${invoice.currencySymbol}">$</span><span
th:text="${#numbers.formatDecimal(invoice.total, 1, 2)}">1,100.00</span>
</td>
</tr>
</table>
</div>
</div>

<div class="notes-section" th:if="${invoice.notes}">
<div class="notes-title">Notes</div>
<div class="notes-content" th:text="${invoice.notes}">Thank you for your business.</div>
</div>

<div class="invoice-footer">
<div class="footer-thank-you">Thank You for Your Business!</div>
<div class="footer-contact">
<div>Questions about this invoice? Contact us!</div>
<div th:text="${invoice.companyEmail}">[email protected]</div>
</div>
</div>
</div>
</body>
</html>

Template Design Principles:

  • CSS 2.1 compatibility: Uses table-based layouts and standard CSS properties.
  • Print optimization: @page rules for paper size and margins.
  • Professional styling: Clean, business-appropriate design with proper typography.
  • Responsive elements: Conditional rendering based on data availability.
Adding Headers and Footers

Use @page rules to style headers, footers, and margins in your printed PDF:

@page {
size: A4;
margin: 0.5in 0;
@top-center {
content: "Confidential Document";
font-size: 10px;
color: #666;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-size: 10px;
padding: 0 0.5in;
}
}

Step 6: Template Service Implementation

Implement the service for rendering Thymeleaf templates into HTML with proper configuration and error handling.

Click to view Template Service (TemplateService.java)
package org.example.flyingsaucer.service;

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

// Service for rendering Thymeleaf templates to HTML strings
public class TemplateService {
private final TemplateEngine templateEngine;

public TemplateService() {
this.templateEngine = createTemplateEngine();
System.out.println("TemplateService initialized successfully");
}

// Renders a template with the provided data model
public String renderTemplate(String templateName, String variableName, Object model) {
try {
Context context = new Context();
context.setVariable(variableName, model);

String renderedHtml = templateEngine.process(templateName, context);
System.out.println("Template rendered successfully: " + templateName);

return renderedHtml;
} catch (Exception e) {
System.err.println("Template rendering failed: " + templateName + " - " + e.getMessage());
throw new RuntimeException("Template rendering failed: " + templateName, e);
}
}

// Creates and configures the Thymeleaf template engine
private TemplateEngine createTemplateEngine() {
TemplateEngine engine = new TemplateEngine();

ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix("templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(true);

engine.setTemplateResolver(resolver);
return engine;
}
}

Service Responsibilities:

  • Template engine configuration: Sets up Thymeleaf with appropriate resolvers and caching.
  • Context management: Handles variable binding for template processing.
  • Error handling: Provides meaningful error messages for debugging.
  • Performance optimization: Enables template caching for production use.

Step 7: PDF Generation Service

Implement the core service that uses Flying Saucer to convert HTML to PDF with error handling and XHTML conversion.

Click to view PDF Generation Service (PdfGenerationService.java)
package org.example.flyingsaucer.service;

import com.lowagie.text.DocumentException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

// Service for generating PDF documents from HTML using Flying Saucer
public class PdfGenerationService {

// Generates a PDF from HTML content and saves it to the specified path
public String generatePdf(String htmlContent, String outputPath) {
try {
createOutputDirectory(outputPath);
String xhtmlContent = convertToXhtml(htmlContent);
generatePdfFromXhtml(xhtmlContent, outputPath);

String absolutePath = Paths.get(outputPath).toAbsolutePath().toString();
System.out.println("PDF generated: " + absolutePath);

return absolutePath;
} catch (Exception e) {
System.err.println("PDF generation failed: " + e.getMessage());
throw new RuntimeException("Failed to generate PDF", e);
}
}

// Validates if the provided HTML is well-formed
public boolean validateHtml(String htmlContent) {
try {
Jsoup.parse(htmlContent, "", Parser.xmlParser());
return true;
} catch (Exception e) {
System.err.println("HTML validation failed: " + e.getMessage());
return false;
}
}

// Converts HTML to well-formed XHTML using JSoup
private String convertToXhtml(String htmlContent) {
try {
Document document = Jsoup.parse(htmlContent, "", Parser.xmlParser());

document.outputSettings()
.syntax(Document.OutputSettings.Syntax.xml)
.escapeMode(org.jsoup.nodes.Entities.EscapeMode.xhtml);

return document.html();
} catch (Exception e) {
System.err.println("HTML to XHTML conversion failed: " + e.getMessage());
throw new RuntimeException("HTML to XHTML conversion failed", e);
}
}

// Generates PDF from XHTML using Flying Saucer
private void generatePdfFromXhtml(String xhtmlContent, String outputPath)
throws DocumentException, IOException {

try (OutputStream outputStream = new FileOutputStream(outputPath)) {
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(xhtmlContent);
renderer.layout();
renderer.createPDF(outputStream);
}
}

// Creates the output directory if it doesn't exist
private void createOutputDirectory(String outputPath) throws IOException {
Path path = Paths.get(outputPath);
Path parentDir = path.getParent();

if (parentDir != null && !Files.exists(parentDir)) {
Files.createDirectories(parentDir);
}
}
}

Core Functionalities:

  • HTML to XHTML conversion: Uses JSoup to ensure well-formed XML structure.
  • PDF rendering: Leverages Flying Saucer for PDF output.
  • Directory management: Automatically creates output directories.
  • Validation: Checks HTML structure before processing.

Step 8: Create the Main Application

Implement the main application that ties all components together with sample data generation.

Click to view Main Application (FlyingSaucerApp.java)
package org.example.flyingsaucer;

import org.example.flyingsaucer.model.InvoiceModel;
import org.example.flyingsaucer.service.PdfGenerationService;
import org.example.flyingsaucer.service.TemplateService;

import java.math.BigDecimal;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Random;

// Main application demonstrating HTML to PDF conversion using Flying Saucer
public class FlyingSaucerApp {

public static void main(String[] args) {
System.out.println("Starting Flying Saucer PDF generation");

try {
// Initialize services
TemplateService templateService = new TemplateService();
PdfGenerationService pdfService = new PdfGenerationService();

// Create sample invoice data
InvoiceModel invoice = createSampleInvoice();
System.out.println("Generated sample invoice: " + invoice.getInvoiceNumber());

// Render HTML template
String htmlContent = templateService.renderTemplate("invoice", "invoice", invoice);

// Validate HTML before PDF generation
if (!pdfService.validateHtml(htmlContent)) {
System.out.println("HTML validation warnings - proceeding anyway");
}

// Generate unique output filename
String timestamp = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String filename = String.format("Invoice_%s_%d.pdf", timestamp, System.currentTimeMillis());
String outputPath = Paths.get("output", filename).toString();

// Generate PDF
String generatedPdfPath = pdfService.generatePdf(htmlContent, outputPath);

System.out.println("PDF generation completed successfully!");

} catch (Exception e) {
System.err.println("Application error: " + e.getMessage());
System.exit(1);
}
}

// Creates a sample invoice with realistic data
private static InvoiceModel createSampleInvoice() {
InvoiceModel invoice = new InvoiceModel();

// Generate unique invoice number
String invoiceNumber = "INV" + LocalDate.now().getYear() + "-" +
String.format("%04d", new Random().nextInt(9999) + 1);

// Basic invoice information
invoice.setInvoiceNumber(invoiceNumber);
invoice.setInvoiceDate(LocalDate.now());
invoice.setDueDate(LocalDate.now().plusDays(30));

// Company information
invoice.setCompanyName("Galactic Java Works");
invoice.setCompanyLogo("https://img.pdfbolt.com/logo-light-example.png");
invoice.setCompanyAddress("42 Orbit Way");
invoice.setCompanyCity("Byteville");
invoice.setCompanyPostalCode("42424");
invoice.setCompanyCountry("Cosmos");
invoice.setCompanyEmail("[email protected]");
invoice.setCompanyPhone("+99 (000) 424-2424");

// Client information
invoice.setClientCompany("Flying Incognito");
invoice.setClientAddress("73 Eclipse Plaza");
invoice.setClientCity("Nebulon");
invoice.setClientPostalCode("13131");
invoice.setClientCountry("Galaxy");
invoice.setClientEmail("[email protected]");
invoice.setClientPhone("+99 (123) 456-7890");

// Financial settings
invoice.setCurrencySymbol("$");
invoice.setTaxRate(new BigDecimal("9.5"));
invoice.setTaxLabel("Sales Tax");

// Invoice items
invoice.setItems(Arrays.asList(
new InvoiceModel.InvoiceItem(
"Software Development Services",
8,
new BigDecimal("150.00")
),
new InvoiceModel.InvoiceItem(
"Database Design and Implementation",
1,
new BigDecimal("2500.00")
),
new InvoiceModel.InvoiceItem(
"API Integration Services",
3,
new BigDecimal("750.00")
),
new InvoiceModel.InvoiceItem(
"Quality Assurance Testing",
12,
new BigDecimal("100.00")
),
new InvoiceModel.InvoiceItem(
"Project Management",
1,
new BigDecimal("1200.00")
)
));

// Additional information
invoice.setNotes(
"Please include the invoice number on payment. Thank you for choosing "
+ invoice.getCompanyName() + "."
);
return invoice;
}
}

Application Flow:

  • Service initialization: Creates template and PDF generation services.
  • Data preparation: Generates sample invoice data.
  • Template processing: Renders Thymeleaf template with invoice data.
  • PDF generation: Converts HTML to PDF and saves to output directory.

Step 9: Running and Testing Your Application

Execute your Flying Saucer PDF generation application:

  1. Run the application.
  2. Check output: Look for generated PDF in the output/ directory.

Preview of the Generated Invoice

Here's a preview of the generated invoice PDF: Invoice PDF generated using Flying Saucer in Java

CSS Best Practices for Flying Saucer – Complete Reference

CategoryRecommendationImplementation
Document StructureUse well-formed XML/XHTML (required – renderer rejects malformed HTML).Closed tags: <br /> not <br>
CSS StandardsFollow CSS 2.1 specification strictly.Valid W3C CSS only.
Character EncodingAlways use UTF-8 to prevent special character issues.<meta charset="utf-8"/>
Media TypesSeparate screen/print styles (essential for PDF output).@media print { ... }
@media screen { ... }
PDF Page SetupDefine page size and margins.@page { size: letter; margin: 1in; }
Page BreaksControl pagination for multi-page documents.page-break-before: always
Font RegistrationAdd custom fonts programmatically (required for PDF embedding).renderer.getFontResolver().addFont("font.ttf", true)
Font EmbeddingEmbed fonts in PDF CSS.-fs-pdf-font-embed: embed
Table PaginationEnable header repetition across pages.-fs-table-paginate: paginate
PerformanceUse simple selectors for faster rendering..class over div > p.class:nth-child(2)
ImagesOptimize size and format to reduce memory usage.Proper dimensions, web-optimized formats.
Avoid JavaScriptNo client-side scripting (not supported).Server-side processing only.
Avoid CSS3 EffectsNo animations/transitions (print-focused renderer).Static styling only.

Other PDF Generation Solutions in Java

Each approach serves different use cases – choose based on your requirements for web-standards support, maintenance overhead, and scalability.

HTML to PDF API Services for Enterprise Applications

While Flying Saucer excels at XML/CSS rendering with precise control, modern enterprise applications often need more. For production workloads that require scalability, up-to-date web standards, and minimal maintenance, consider an HTML to PDF API service like PDFBolt.

Flying Saucer vs. API Services

FactorFlying SaucerHTML to PDF API (PDFBolt)
Web standardsCSS 2.1 + select CSS3 featuresFull modern web stack
JavaScript supportNoneFull support
MaintenanceSelf-managed dependenciesZero maintenance
ScalingManual infrastructureAutomatic
Cost modelOpen sourceSubscription
Best forControlled XML/CSS environmentsModern web applications

Conclusion

Flying Saucer offers a powerful and lightweight HTML to PDF Java library that enables developers to generate high-quality PDF documents. It’s an ideal solution for building Java-based PDF invoice generators, reporting tools, or any system that requires CSS-styled PDF output using Thymeleaf templates.

By leveraging standards-compliant CSS 2.1 for print styling, combined with Flying Saucer’s fast and predictable rendering engine, you can build scalable and secure PDF generation workflows in Java. Whether you're working on a microservice that exports PDF invoices or a backend service for automated document generation, Flying Saucer handles the job efficiently and reliably.

If you need advanced layout features, JavaScript rendering, or modern CSS support (like Flexbox or Grid), consider a cloud-based HTML to PDF API such as PDFBolt.

Ready to generate some PDFs? Flying Saucer is cleared for takeoff! 🛸