How to Convert HTML to PDF in Java Using Playwright and Thymeleaf
Need to convert HTML into polished PDFs in your Java project? This tutorial will show you how to use Playwright for Java together with Thymeleaf templates to generate clean, professional-looking PDFs. Whether you're creating invoices, reports, or other dynamic business documents, this solution allows you to render HTML to PDF with pixel-perfect precision using a headless browser. With easy-to-follow code examples and a straightforward setup, it's a practical and modern approach to HTML to PDF generation in Java.
What is Playwright for Java?
Playwright for Java is a powerful browser automation library that provides a unified API to control Chromium, Firefox, and WebKit browsers. Originally developed by Microsoft for Node.js, Playwright has been successfully ported to the Java ecosystem – giving Java developers access to reliable tools for browser automation, testing, and even high-quality PDF generation.
Playwright's ability to run a full browser instance ensures accurate rendering of complex CSS and JavaScript, unlike traditional Java PDF libraries.
Explore our guides for other programming languages:
Key Features of Playwright for PDF Generation
When implementing HTML to PDF generation in Java, Playwright provides several powerful features:
- Chromium-Based PDF Generation: PDF generation is supported only in Chromium for consistent results.
- Headless Mode: Runs without a visible UI for efficient server-side processing.
- Full JavaScript Execution: Executes JavaScript, enabling dynamic content in generated PDFs.
- Precise Rendering: Uses a real browser to faithfully replicate screen rendering in the PDF, including CSS, web fonts, SVGs, and more.
- Customizable PDF Output: Offers options for page size, margins, headers/footers, background rendering, and more via the API.
- Automatic Resource Management: Handles loading of images, fonts, styles, and external assets automatically.
PDF generation is currently only supported when using Chromium in Playwright. Firefox and WebKit do not support the page.pdf()
function.
What is Thymeleaf?
Thymeleaf is a modern server-side Java template engine for generating dynamic HTML content. It works well with frameworks like Spring, allowing you to create clean, maintainable HTML templates rendered on the server.
Why Combine Playwright and Thymeleaf for PDF Generation in Java?
To create professional, dynamic PDFs from HTML in Java applications, combining Thymeleaf and Playwright offers an efficient and reliable approach.
Here's why this pairing works so well:
- Thymeleaf generates dynamic HTML templates that are well-structured and easy to maintain.
- Playwright renders these HTML templates in a real Chromium browser for accurate PDFs.
- This method produces pixel-perfect PDFs from HTML in Java, faithfully capturing CSS styles, fonts, and JavaScript-driven content.
- The combination offers a powerful and flexible solution for high-quality PDF generation in Java.
Step-by-Step Implementation: PDF Invoice Generator
Let's build a complete solution for generating PDF invoices using Playwright and Thymeleaf templates.
Step 1: Set Up Your Java Development Environment
To work effectively with Playwright, it's important to have a properly configured development environment. First, ensure you have the necessary tools and prerequisites:
- IDE: IntelliJ IDEA, Eclipse, or any Java IDE of your choice → we use IntelliJ IDEA.
- JDK: Java 11 or newer → this tutorial uses Java 23.
- Build Tool: Maven or Gradle → we'll use Maven in this guide.
Step 2: Create a New Java Project and Add Required Dependencies
Next let's set up a Java project with the necessary dependencies.
Our solution depends on these primary external libraries:
- Playwright for Java - The browser automation library for PDF generation.
- Thymeleaf - A modern server-side Java template engine for HTML.
- SLF4J - For logging functionality throughout the application.
Update your pom.xml
file to include these dependencies:
<dependencies>
<!-- Playwright for browser automation and PDF generation -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.52.0</version>
</dependency>
<!-- Thymeleaf for HTML templating -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>
<!-- SLF4J API for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<!-- SLF4J implementation -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.13</version>
</dependency>
</dependencies>
After updating the pom.xml
, install the Playwright browser binaries manually:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"
Step 3: Set Up Your Project Structure
A well-organized directory structure is essential for maintaining and scaling your application. We'll divide our code into logical components:
- Models for our data structures (invoice data).
- Templates for our Thymeleaf HTML templates.
- Services for core functionality (template rendering and PDF generation).
- Output as the destination for generated files.
Create the following directory structure:
playwright-pdf-generator/
│
├── src/
│ ├── main/
│ │ ├── java/org/example/playwright/
│ │ │ ├── App.java # Main application entry point
│ │ │ ├── model/ # Data models
│ │ │ │ └── InvoiceModel.java # Invoice data structure
│ │ │ │
│ │ │ └── service/ # Service classes
│ │ │ ├── TemplateService.java # Template rendering service
│ │ │ └── PdfService.java # PDF generation service
│ │ │
│ │ └── resources/
│ │ ├── templates/ # Template files
│ │ │ └── invoice.html # Invoice template
│ │
│ └── test/ # Test files
│
├── output/ # Generated PDFs
│
└── pom.xml # Maven configuration
Step 4: Create the Invoice Data Model
The data model represents our business object – an invoice – with all its properties and relationships.
Create the file src/main/java/org/example/playwright/model/InvoiceModel.java
:
InvoiceModel.java
package org.example.playwright.model;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
// Main model class representing an invoice
public class InvoiceModel {
// Basic invoice properties
private String invoiceNumber;
private LocalDate invoiceDate;
private LocalDate dueDate;
// Company information
private String companyLogo;
private String companyName;
private String companyAddress;
private String companyEmail;
private String companyPhone;
// Client information
private String clientName;
private String clientAddress;
private String clientEmail;
private String clientPhone;
// Financial details - tax and currency settings
private BigDecimal taxRate;
private String currency = "$";
// Invoice items - collection of products/services
private List<InvoiceItem> items = new ArrayList<>();
// Additional information
private String notes;
// Calculates subtotal by summing all item totals before tax
public BigDecimal getSubTotal() {
return items.stream()
.map(InvoiceItem::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Calculates the tax amount based on subtotal and tax rate
public BigDecimal getTaxAmount() {
return getSubTotal()
.multiply(taxRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP))
.setScale(2, RoundingMode.HALF_UP);
}
// Calculates the final invoice total
public BigDecimal getTotal() {
return getSubTotal().add(getTaxAmount());
}
// Getters and Setters for all properties
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 getCompanyLogo() {
return companyLogo;
}
public void setCompanyLogo(String companyLogo) {
this.companyLogo = companyLogo;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getCompanyAddress() {
return companyAddress;
}
public void setCompanyAddress(String companyAddress) {
this.companyAddress = companyAddress;
}
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 getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getClientAddress() {
return clientAddress;
}
public void setClientAddress(String clientAddress) {
this.clientAddress = clientAddress;
}
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 BigDecimal getTaxRate() {
return taxRate;
}
public void setTaxRate(BigDecimal taxRate) {
this.taxRate = taxRate;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
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;
}
// Inner class representing a single line item
public static class InvoiceItem {
private String description;
private int quantity;
private BigDecimal unitPrice;
// Default constructor
public InvoiceItem() {
}
// Convenience constructor for creating items
public InvoiceItem(String description, int quantity, BigDecimal unitPrice) {
this.description = description;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// Calculates the total price for this item (quantity × unitPrice)
public BigDecimal getTotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
// Getters and Setters for item properties
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
}
}
This model represents the structure of our invoice data, including properties for company and client details, invoice items, and calculated properties for totals, taxes, and other information.
Step 5: Create a Template with Thymeleaf
Now, let's create the HTML template using Thymeleaf. Thymeleaf is a modern server-side Java template engine that produces HTML that can be correctly displayed in browsers.
Create the file src/main/resources/templates/invoice.html
:
Thymeleaf template (invoice.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="'Invoice ' + ${invoice.invoiceNumber}">Invoice</title>
<style>
/* Color variables */
:root {
--primary-color: #3D46BD;
--secondary-color: #475569;
--text-color: #1e293b;
--light-bg: #f1f5f9;
--border-color: #cbd5e1;
--table-row-color: #fafafa;
}
@page {
size: A4;
margin: 0;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.5;
font-size: 12px;
}
/* Main container */
.invoice-container {
max-width: 800px;
margin: 10px auto;
padding: 30px 60px;
}
/* Header section */
.invoice-header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.logo-container img {
max-height: 100px;
max-width: 300px;
}
.invoice-title {
color: var(--primary-color);
font-size: 36px;
font-weight: 700;
margin: 0 0 15px;
text-align: center;
}
.invoice-details {
text-align: left;
font-size: 13px;
}
.invoice-details p {
margin: 5px 0;
}
.label {
font-weight: 600;
}
.divider {
height: 2px;
background: var(--primary-color);
margin: 20px 0 10px;
}
/* Billing parties section */
.parties-container {
display: flex;
justify-content: space-between;
margin-bottom: 35px;
}
.party-section {
width: 48%;
}
.party-title {
font-size: 15px;
text-transform: uppercase;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 10px;
}
.party-details {
padding: 15px;
background-color: var(--light-bg);
border-radius: 6px;
border-left: 4px solid var(--primary-color);
font-size: 13px;
}
.party-details p {
margin: 5px 0;
}
.party-details .name {
font-weight: 600;
font-size: 14px;
}
/* Items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.items-table th {
background-color: var(--light-bg);
padding: 12px 15px;
text-align: left;
font-size: 13px;
font-weight: 600;
border-bottom: 2px solid var(--primary-color);
}
.items-table td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.items-table tr:nth-child(even) {
background-color: var(--table-row-color);
}
.items-table th.text-right {
text-align: right;
}
.text-right {
text-align: right;
}
/* Summary section */
.summary-section {
display: flex;
font-size: 13px;
justify-content: flex-end;
margin-bottom: 30px;
}
.summary-table {
width: 350px;
border-spacing: 0;
font-size: 13px;
}
.summary-table td {
padding: 8px 15px;
}
.summary-table .value {
text-align: right;
font-weight: 500;
}
.summary-table .subtotal-row td {
border-bottom: 1px solid var(--border-color);
}
.summary-table .total-row td {
background-color: var(--primary-color);
color: white;
font-weight: 600;
}
/* Notes section */
.notes-section {
padding: 20px;
background-color: var(--light-bg);
border-radius: 6px;
border-left: 4px solid var(--primary-color);
}
.notes-title {
font-weight: 600;
margin-bottom: 10px;
font-size: 13px;
}
/* Footer */
.footer {
margin-top: 30px;
text-align: center;
color: var(--secondary-color);
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.footer p {
margin: 5px 0;
}
.thank-you {
font-size: 13px;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="invoice-container">
<!-- Header section -->
<div class="invoice-header">
<!-- Company logo -->
<div class="logo-container">
<img th:src="${invoice.companyLogo}" th:alt="${invoice.companyName} + ' Logo'" alt="Company Logo"/>
</div>
<!-- Invoice title and basic details -->
<div class="invoice-title-section">
<h1 class="invoice-title">INVOICE</h1>
<div class="invoice-details">
<p>
<span class="label">Invoice Number:</span>
<span th:text="${invoice.invoiceNumber}">INV-12345</span>
</p>
<p>
<span class="label">Date Issued:</span>
<span th:text="${#temporals.format(invoice.invoiceDate, 'yyyy-MM-dd')}">2025-05-20</span>
</p>
<p>
<span class="label">Due Date:</span>
<span th:text="${#temporals.format(invoice.dueDate, 'yyyy-MM-dd')}">2025-06-19</span>
</p>
</div>
</div>
</div>
<!-- Separator between sections -->
<div class="divider"></div>
<div class="parties-container">
<!-- Company information -->
<div class="party-section">
<h2 class="party-title">BILLED FROM</h2>
<div class="party-details">
<p class="name" th:text="${invoice.companyName}">Company Name</p>
<p th:text="${invoice.companyAddress}">Company Address</p>
<p th:text="${invoice.companyEmail}">Company Email</p>
<p th:text="${invoice.companyPhone}">Company Phone</p>
</div>
</div>
<!-- Client information -->
<div class="party-section">
<h2 class="party-title">BILLED TO</h2>
<div class="party-details">
<p class="name" th:text="${invoice.clientName}">Client Name</p>
<p th:text="${invoice.clientAddress}">Client Address</p>
<p th:text="${invoice.clientEmail}">Client Email</p>
<p th:text="${invoice.clientPhone}">Client Phone</p>
</div>
</div>
</div>
<!-- List of items in the invoice -->
<table class="items-table">
<thead>
<tr>
<th>DESCRIPTION</th>
<th>QUANTITY</th>
<th>UNIT PRICE</th>
<th class="text-right">AMOUNT</th>
</tr>
</thead>
<tbody>
<!-- Dynamically generated rows for each invoice item -->
<tr th:each="item : ${invoice.items}">
<td th:text="${item.description}">Item description</td>
<td th:text="${item.quantity}">1</td>
<td>
<span th:text="${invoice.currency}">$</span>
<span th:text="${#numbers.formatDecimal(item.unitPrice, 1, 2)}">0.00</span>
</td>
<td class="text-right">
<span th:text="${invoice.currency}">$</span>
<span th:text="${#numbers.formatDecimal(item.total, 1, 2)}">0.00</span>
</td>
</tr>
</tbody>
</table>
<!-- Summary with totals -->
<div class="summary-section">
<table class="summary-table">
<!-- Subtotal row -->
<tr class="subtotal-row">
<td class="label">Subtotal</td>
<td class="value">
<span th:text="${invoice.currency}">$</span>
<span th:text="${#numbers.formatDecimal(invoice.subTotal, 1, 2)}">0.00</span>
</td>
</tr>
<!-- Tax calculation row -->
<tr class="tax-row">
<td class="label">
Tax (<span th:text="${invoice.taxRate}">0</span>%)
</td>
<td class="value">
<span th:text="${invoice.currency}">$</span>
<span th:text="${#numbers.formatDecimal(invoice.taxAmount, 1, 2)}">0.00</span>
</td>
</tr>
<!-- Final total amount -->
<tr class="total-row">
<td class="label">TOTAL</td>
<td class="value">
<span th:text="${invoice.currency}">$</span>
<span th:text="${#numbers.formatDecimal(invoice.total, 1, 2)}">0.00</span>
</td>
</tr>
</table>
</div>
<!-- Additional notes -->
<div class="notes-section" th:if="${invoice.notes != null and !invoice.notes.isEmpty()}">
<div class="notes-title">NOTES</div>
<div th:text="${invoice.notes}">Invoice notes</div>
</div>
<!-- Footer -->
<div class="footer">
<div class="thank-you">Thank You for Your Business!</div>
<p>If you have any questions about this invoice, please contact us.</p>
<p th:text="${invoice.companyEmail}">[email protected]</p>
</div>
</div>
</body>
</html>
The template includes:
- Professional styling with CSS.
- Thymeleaf variable expressions (
${...}
) for dynamic content. - Structured layout for company and client information.
- Table for product/services items.
- Summary section with calculations.
- Notes section for additional information.
- Sample footer.
Step 6: Create the Template Engine Service
Now let's create the service that renders our Thymeleaf templates.
Create src/main/java/org/example/playwright/service/TemplateService.java
:
TemplateService.java
package org.example.playwright.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
// Service for rendering Thymeleaf templates into HTML
public class TemplateService {
private static final Logger logger = LoggerFactory.getLogger(TemplateService.class);
// Core Thymeleaf engine for processing templates
private final TemplateEngine templateEngine;
// Sets up Thymeleaf engine
public TemplateService() {
templateEngine = new TemplateEngine();
// Set up template loading from classpath
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(true);
templateEngine.setTemplateResolver(templateResolver);
logger.info("Template service initialized");
}
// Renders template with data model
public String renderTemplate(String templateName, String variableName, Object model) {
try {
// Create context with the model data
Context context = new Context();
context.setVariable(variableName, model);
return templateEngine.process(templateName, context);
} catch (Exception e) {
logger.error("Error rendering template: {}", e.getMessage(), e);
throw new RuntimeException("Failed to render template: " + templateName, e);
}
}
}
Key features of this service include:
- Template Engine Initialization: Sets up the Thymeleaf engine with appropriate configuration.
- Template Resolution: Configures how templates are located and loaded.
- Context Creation: Builds the context with variables needed for rendering.
- Error Handling: Provides clear error messages if template rendering fails.
The service is designed to be reusable with any model type, making it flexible for different document types.
Step 7: Create the PDF Generation Service
The PDF Service is the core component for generating documents. It leverages Playwright to render HTML into high-quality PDFs using actual browser technology.
Create src/main/java/org/example/playwright/service/PdfService.java
:
PdfService.java
package org.example.playwright.service;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Margin;
import com.microsoft.playwright.options.WaitUntilState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
// Service for converting HTML to PDF using Playwright
public class PdfService implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(PdfService.class);
// Core Playwright components
private Playwright playwright;
private Browser browser;
private boolean initialized = false;
// Initializes Playwright with headless Chromium
public void initialize() {
if (initialized) {
return;
}
try {
playwright = Playwright.create();
browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
initialized = true;
logger.info("Playwright initialized successfully");
} catch (Exception e) {
logger.error("Failed to initialize Playwright: {}", e.getMessage(), e);
throw new RuntimeException("Failed to initialize Playwright", e);
}
}
// Converts HTML to PDF and saves it to the specified path
public String generatePdfFromHtml(String html, String outputPath, Page.PdfOptions options) {
if (!initialized) {
initialize();
}
// Create output directory if it doesn't exist
Path outputFilePath = Paths.get(outputPath);
try {
Files.createDirectories(outputFilePath.getParent());
} catch (Exception e) {
logger.error("Failed to create output directory: {}", e.getMessage(), e);
throw new RuntimeException("Failed to create output directory", e);
}
// Render HTML and generate PDF
try (Page page = browser.newPage()) {
page.setContent(html, new Page.SetContentOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
Page.PdfOptions pdfOptions = options != null ? options : getDefaultPdfOptions();
pdfOptions.setPath(Paths.get(outputPath)); // Set output path
page.pdf(pdfOptions);
String absolutePath = outputFilePath.toAbsolutePath().toString();
logger.info("PDF generated successfully: {}", absolutePath);
return absolutePath;
} catch (Exception e) {
logger.error("PDF generation error: {}", e.getMessage(), e);
throw new RuntimeException("Failed to generate PDF", e);
}
}
// Returns default PDF generation options
private Page.PdfOptions getDefaultPdfOptions() {
Page.PdfOptions options = new Page.PdfOptions();
options.setFormat("A4");
options.setPrintBackground(true);
Margin margin = new Margin();
margin.setTop("20px");
margin.setRight("20px");
margin.setBottom("20px");
margin.setLeft("20px");
options.setMargin(margin);
return options;
}
// Closes all Playwright resources
@Override
public void close() {
if (browser != null) {
browser.close();
}
if (playwright != null) {
playwright.close();
}
initialized = false;
logger.info("Playwright resources closed");
}
}
This PDF service uses Playwright to convert HTML content to PDF. Key features include:
- Resource Management: Implements
AutoCloseable
for proper cleanup of Playwright resources. - Browser Initialization: Handles browser setup with appropriate configuration.
- PDF Configuration: Allows customization of PDF output with format, margins, and other options.
- Error Handling: Provides detailed error information if PDF generation fails.
- File System Management: Creates output directories as needed.
The service is designed to be efficient by reusing the browser instance across multiple PDF generation requests.
Step 8: Implement the Main Application
Now let's tie everything together in the main application.
Update src/main/java/org/example/playwright/App.java
:
App.java (main application)
package org.example.playwright;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.Margin;
import org.example.playwright.model.InvoiceModel;
import org.example.playwright.service.PdfService;
import org.example.playwright.service.TemplateService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
// Main application class for generating PDF invoices with Playwright
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
logger.info("Starting HTML to PDF conversion with Playwright");
try (PdfService pdfService = new PdfService()) {
// Initialize services
pdfService.initialize();
TemplateService templateService = new TemplateService();
// Create sample invoice data
InvoiceModel invoice = createSampleInvoice();
logger.info("Created sample invoice with number: {}", invoice.getInvoiceNumber());
// Set up output directory
String outputDir = "output";
Paths.get(outputDir).toFile().mkdirs();
// Generate unique filename with timestamp
String timestamp = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
+ "-" + System.currentTimeMillis();
String pdfFileName = "Invoice_" + timestamp + ".pdf";
String pdfOutputPath = Paths.get(outputDir, pdfFileName).toString();
// Render HTML from template
String html = templateService.renderTemplate("invoice", "invoice", invoice);
logger.info("Template rendered successfully");
// Configure PDF options
Page.PdfOptions pdfOptions = getPdfOptions();
// Generate PDF file
String generatedPdfPath = pdfService.generatePdfFromHtml(html, pdfOutputPath, pdfOptions);
logger.info("PDF generation completed successfully!");
logger.info("PDF saved to: {}", generatedPdfPath);
} catch (Exception e) {
logger.error("Error: {}", e.getMessage(), e);
}
}
// Configure PDF generation options
private static Page.PdfOptions getPdfOptions() {
Page.PdfOptions pdfOptions = new Page.PdfOptions();
pdfOptions.setFormat("A4");
pdfOptions.setPrintBackground(true);
// Create margin settings
Margin margin = new Margin();
margin.setTop("30px");
margin.setRight("30px");
margin.setBottom("30px");
margin.setLeft("30px");
pdfOptions.setMargin(margin);
// Add page numbering footer
pdfOptions.setDisplayHeaderFooter(true);
pdfOptions.setFooterTemplate(
"<div style='width: 100%; text-align: center; font-size: 10px; font-family: Arial; padding: 5px;'>" +
"Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>");
return pdfOptions;
}
// Creates a sample invoice with test data
private static InvoiceModel createSampleInvoice() {
// Generate invoice number with current year and random digits
String invoiceNumber = "INV-" + LocalDate.now().getYear() + "-" + (1000 + new Random().nextInt(9000));
InvoiceModel invoice = new InvoiceModel();
// Invoice basic information
invoice.setInvoiceNumber(invoiceNumber);
invoice.setInvoiceDate(LocalDate.now());
invoice.setDueDate(LocalDate.now().plusDays(30));
// Company details
invoice.setCompanyLogo("https://img.pdfbolt.com/logo-template-example.png");
invoice.setCompanyName("FutureTask Solutions");
invoice.setCompanyAddress("404 Java Street, Codebase City");
invoice.setCompanyEmail("[email protected]");
invoice.setCompanyPhone("+1 (234) 567-8901");
// Client information
invoice.setClientName("CheckedException Ltd.");
invoice.setClientAddress("0 Catch Block Avenue, Trytown");
invoice.setClientEmail("[email protected]");
invoice.setClientPhone("+1 (234) 567-8901");
// Financial details
invoice.setCurrency("$");
invoice.setTaxRate(new BigDecimal("8.5"));
// Payment information
invoice.setNotes("Payment due within 30 days. Late payments subject to 1.5% monthly interest.");
// Invoice line items
List<InvoiceModel.InvoiceItem> items = new ArrayList<>();
items.add(new InvoiceModel.InvoiceItem("Enterprise Java Development", 20, new BigDecimal("125")));
items.add(new InvoiceModel.InvoiceItem("API Integration Services", 2, new BigDecimal("1500")));
items.add(new InvoiceModel.InvoiceItem("Database Optimization", 1, new BigDecimal("950")));
items.add(new InvoiceModel.InvoiceItem("Cloud Deployment Setup", 1, new BigDecimal("750")));
items.add(new InvoiceModel.InvoiceItem("Technical Documentation", 5, new BigDecimal("75")));
invoice.setItems(items);
return invoice;
}
}
The main application orchestrates the entire PDF generation process:
- Services Initialization: Sets up the template and PDF services.
- Data Preparation: Creates a sample invoice with example data.
- Output Configuration: Sets up directories and filenames.
- Template Rendering: Converts the invoice data into HTML using Thymeleaf.
- PDF Configuration: Sets page size, margins, headers, footers, and other PDF options.
- PDF Generation: Creates the final PDF document using Playwright.
- Error Handling: Provides error reporting if any step fails.
All operations use try-with-resources for proper resource management.
Step 9: Run the Application
Now you're ready to run the application and generate your invoice PDF!
When successful, you'll see output confirming each step of the process, and the generated PDF will be saved in the output
directory.
Here's what your generated PDF will look like:
Playwright PDF Configuration Options
Playwright provides extensive options for customizing your generated PDFs.
Here are the most useful configuration settings:
Option | Description | Example Values |
---|---|---|
format | Standard paper size. | "A4" "Letter" "Legal" etc. |
width/height | Custom page dimensions. | "8.5in" "11in" |
margin | Space around content. | "top": "20px" "bottom": "20px" "right": "20px" "left": "20px" |
printBackground | Include background colors/images. | true false |
landscape | Page orientation. | true (landscape) false (portrait) |
scale | Content scaling factor. | 1.0 (100%)0.75 (75%) |
pageRanges | Specific pages to include. | "1-4, 7, 11-18" |
displayHeaderFooter | Show headers and footers. | true false |
headerTemplate | HTML for page headers. | "<div>Company Confidential</div>" |
footerTemplate | HTML for page footers. | "<div>Page <span class='pageNumber'></span></div>" |
preferCSSPageSize | Use CSS @page size. | true false |
Here's how to apply these options in code:
// Create PDF options with customization
Page.PdfOptions pdfOptions = new Page.PdfOptions()
.setFormat("Letter")
.setLandscape(true)
.setPrintBackground(true);
// Set custom margins
Margin margin = new Margin()
.setTop("45px")
.setRight("30px")
.setBottom("25px")
.setLeft("30px");
pdfOptions.setMargin(margin);
// Set header and footer
pdfOptions.setDisplayHeaderFooter(true);
pdfOptions.setHeaderTemplate(
"<div style='width: 100%; text-align: center; font-size: 10px;'>CONFIDENTIAL</div>");
pdfOptions.setFooterTemplate(
"<div style='width: 100%; text-align: center; font-size: 10px;'>" +
"Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>");
pdfOptions.setScale(0.8);
Alternative Approaches for HTML to PDF Conversion
While implementing Playwright directly in your Java application provides excellent control over the PDF generation process, it also comes with maintenance costs. You need to manage browser instances, handle resource consumption, and ensure proper scaling in production environments.
HTML to PDF API Services
For many developers, a more streamlined approach is to use specialized HTML to PDF API services. These cloud-based solutions handle all the complexities of browser management and rendering while exposing simple REST APIs for integration.
PDFBolt, for example, provides an HTML to PDF conversion service that actually uses Playwright under the hood, but abstracts away all the infrastructure challenges. This gives you the same high-quality rendering without having to maintain browser instances yourself.
With an API-based approach, you can:
- Simplify your tech stack by offloading browser management.
- Scale PDF generation without worrying about server resources.
- Maintain the same high-fidelity rendering (including CSS and JavaScript).
- Focus on your core application logic rather than PDF generation infrastructure.
This approach is particularly beneficial when:
- You're working with serverless or microservice architectures.
- PDF generation workloads are inconsistent or unpredictable.
- You want to minimize dependencies in your deployment environment.
- Development speed and simplicity are priorities for your team.
For high-volume PDF generation or when you need to optimize for specific enterprise requirements, integrating an HTML to PDF API can provide the same rendering quality as a self-hosted Playwright solution while reducing operational complexity.
Check out the PDFBolt API Documentation for implementation examples, API reference, and customization options for HTML to PDF conversion.
Conclusion
In this comprehensive guide, you've learned how to create a robust PDF generation solution using Playwright for Java and Thymeleaf templates. This solution is ideal for generating invoices, reports, statements, certificates, and other business documents that require a professional appearance and reliable output.
For situations where maintaining browser instances isn't ideal, or when scaling to handle high-volume document generation, consider using a dedicated HTML to PDF API service, which handles all the infrastructure and optimization for you.
Now you have all the knowledge you need to confidently implement robust PDF generation in your Java applications.
Broken layouts? Not today. Grab a coffee, relax, and let Playwright take the stage. 🎭