When building business systems, requirements like “we want to export invoices as PDFs” or “make reports downloadable” come up almost without fail.
There are several libraries for generating PDFs in Java, but when implementing this in Spring Boot, the two things you’ll struggle with first are library selection and Japanese font support. In this article, I’ll walk through implementing an invoice PDF generator using Thymeleaf templates with OpenPDF and openhtmltopdf (the successor to Flying Saucer).
Note that the version numbers in this article (OpenPDF 1.3.34 / openhtmltopdf 1.0.10) are current as of writing. Check the GitHub Releases of each project for the latest versions.
When choosing a library, check the license first
These are the usual candidates:
- iText 7 is feature-rich, but it’s offered under AGPL or a commercial license, so closed-source business systems generally need a commercial contract.
- OpenPDF is a successor forked from iText 4, with a dual license of LGPL 3.0 / MPL 2.0. It’s easy to embed in commercial systems.
- openhtmltopdf (a fork of Flying Saucer) is a high-level library that generates PDFs from HTML/CSS, internally using PDFBox or OpenPDF.
Apache PDFBox and JasperReports are also options, but PDFBox is too low-level for reports, and JasperReports has a learning curve with its proprietary template format (.jrxml).
Here’s a rough guide for choosing:
- Use OpenPDF if you want fine-grained layout control in code.
- Use openhtmltopdf if you want to design reports with HTML and CSS.
- If you’re already using Thymeleaf, openhtmltopdf is by far the easiest.
This article focuses on the practical “Thymeleaf + openhtmltopdf” combination, with a supplementary look at OpenPDF’s low-level API.
Adding dependencies
For Maven, add the following:
<dependencies>
<!-- OpenPDF (only if using the low-level API) -->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.34</version>
</dependency>
<!-- HTML→PDF -->
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-core</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>1.0.10</version>
</dependency>
</dependencies>
Thymeleaf works out of the box if you have spring-boot-starter-thymeleaf. For Thymeleaf basics, see How to use Thymeleaf with Spring Boot for reference.
Creating a PDF at a low level with OpenPDF
Before getting to the main topic, let’s take a quick look at the “write directly in code, without HTML” pattern. It’s useful when the layout is extremely simple, or when you want to embed a small bit of output into existing code.
import com.lowagie.text.Document;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import java.io.ByteArrayOutputStream;
public byte[] createSimplePdf() throws Exception {
try (var baos = new ByteArrayOutputStream()) {
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
PdfWriter.getInstance(document, baos);
document.open();
document.add(new Paragraph("請求書"));
PdfPTable table = new PdfPTable(3);
table.addCell("品名");
table.addCell("単価");
table.addCell("金額");
table.addCell("開発作業");
table.addCell("10,000");
table.addCell("100,000");
document.add(table);
document.close();
return baos.toByteArray();
}
}
The old com.lowagie.text.Table API is treated as legacy, so in the current OpenPDF you should use PdfPTable. With this code as-is, Japanese characters will appear as tofu (□□□). OpenPDF only ships with ASCII-range fonts by default, so you need to explicitly embed a font to display Japanese.
Embedding a Japanese font
Commonly used Japanese fonts that are easy to use commercially include IPAexGothic and Noto Sans JP. Place the .ttf file under src/main/resources/fonts/ and load it.
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import org.springframework.core.io.ClassPathResource;
private Font japaneseFont() throws Exception {
var resource = new ClassPathResource("fonts/ipaexg.ttf");
BaseFont base = BaseFont.createFont(
resource.getURL().toString(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);
return new Font(base, 10);
}
Specifying BaseFont.EMBEDDED embeds the font in the PDF so it displays correctly even on environments without that font installed. Subset embedding (embedding only the characters actually used) is the default, so the file size doesn’t bloat too much.
Thymeleaf + openhtmltopdf for HTML→PDF
Now for the main topic. If you’re building reports for real work, being able to write in HTML and CSS is overwhelmingly easier. You can even tweak the design while previewing in a browser.
First, prepare a Thymeleaf template at src/main/resources/templates/invoice.html.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<style>
@font-face {
font-family: 'IPAexGothic';
src: url('fonts/ipaexg.ttf');
}
body { font-family: 'IPAexGothic', sans-serif; font-size: 10pt; }
h1 { font-size: 18pt; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #333; padding: 4px; }
.total { text-align: right; margin-top: 12px; }
tr { page-break-inside: avoid; }
</style>
</head>
<body>
<h1>請求書</h1>
<p>請求番号は <span th:text="${invoice.number}"></span> です。</p>
<p><span th:text="${invoice.customer}"></span> 御中</p>
<table>
<thead>
<tr><th>品名</th><th>数量</th><th>単価</th><th>金額</th></tr>
</thead>
<tbody>
<tr th:each="item : ${invoice.items}">
<td th:text="${item.name}"></td>
<td th:text="${item.quantity}"></td>
<td th:text="${item.unitPrice}"></td>
<td th:text="${item.amount}"></td>
</tr>
</tbody>
</table>
<p class="total">合計 <span th:text="${invoice.total}"></span> 円</p>
</body>
</html>
Note that you must write it in XHTML-compliant form. Use self-closing tags like <br/> instead of <br>. Also, openhtmltopdf supports CSS3 Paged Media (@page, page-break-*, etc.) but does not support flexbox or grid layouts. It’s safer to build reports primarily with table-based layouts.
In the service layer, render the HTML as a string with Thymeleaf and pass it to openhtmltopdf.
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@Service
@RequiredArgsConstructor
public class InvoicePdfService {
private final TemplateEngine templateEngine;
public byte[] generate(Invoice invoice) throws IOException {
Context ctx = new Context();
ctx.setVariable("invoice", invoice);
String html = templateEngine.process("invoice", ctx);
try (var baos = new ByteArrayOutputStream()) {
var fontUrl = new ClassPathResource("fonts/ipaexg.ttf").getURL();
String baseUri = new ClassPathResource("templates/").getURL().toString();
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();
builder.useFont(() -> fontUrl.openStream(), "IPAexGothic");
builder.withHtmlContent(html, baseUri);
builder.toStream(baos);
builder.run();
return baos.toByteArray();
}
}
}
By registering the font with useFont, combined with @font-face on the CSS side, embedding is reliable. The baseUri is the base used to resolve relative paths for images and fonts, so if you forget it, images won’t display.
Letting users download via a REST API
Return the generated PDF as the HTTP response. The basic CRUD structure is the same as in Spring Boot REST API CRUD Tutorial, but the key points are returning a byte array and attaching Content-Disposition.
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@RestController
@RequiredArgsConstructor
public class InvoiceController {
private final InvoicePdfService pdfService;
private final InvoiceRepository invoiceRepository;
@GetMapping("/invoices/{id}/pdf")
public ResponseEntity<byte[]> download(@PathVariable Long id) throws IOException {
Invoice invoice = invoiceRepository.findById(id).orElseThrow();
byte[] pdf = pdfService.generate(invoice);
String filename = URLEncoder.encode("請求書.pdf", StandardCharsets.UTF_8);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + filename)
.body(pdf);
}
}
When the filename contains Japanese characters, specify it in the filename*=UTF-8''... format per RFC 5987. This ensures the file downloads with the correct name in both Chrome and Firefox. For more on file downloads, see How to handle MultipartFile in Spring Boot as well.
Stream large PDFs instead of buffering
If you load a hundreds-of-pages report fully into memory as byte[], concurrent access will quickly pressure the heap. Use StreamingResponseBody to write directly to the OutputStream.
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@GetMapping("/reports/large.pdf")
public ResponseEntity<StreamingResponseBody> large() {
StreamingResponseBody body = out -> {
var fontUrl = new ClassPathResource("fonts/ipaexg.ttf").getURL();
String baseUri = new ClassPathResource("templates/").getURL().toString();
String html = renderLargeReportHtml();
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();
builder.useFont(() -> fontUrl.openStream(), "IPAexGothic");
builder.withHtmlContent(html, baseUri);
builder.toStream(out);
builder.run();
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.body(body);
}
Since openhtmltopdf accepts toStream(OutputStream), you can pipe it directly to the response stream. If generation takes time, consider making it asynchronous with @Async, then sending an email notification or uploading to S3 when complete.
Things to check during implementation
Here are a few things to suspect first when things don’t work after running.
When Japanese characters become tofu, it’s almost always because the font isn’t registered with useFont, or the font-family name in @font-face doesn’t match the CSS side. If page breaks occur in the middle of a table row, adding page-break-inside: avoid; in CSS will improve it. If CSS doesn’t behave as expected, check whether you’re using unsupported features like flexbox, and consider replacing them with table layouts.
Also, if you reference older articles, leftover com.itextpdf dependencies can sneak in an indirect AGPL dependency. Run mvn dependency:tree once to confirm — it’ll give you peace of mind.
Summary
For PDF generation in Spring Boot, combining OpenPDF and openhtmltopdf — both license-safe — is a practical choice. Build HTML with Thymeleaf and embed fonts correctly, and you’ll cover most reports needed in business systems.
Start with a simple invoice template, then gradually add logos and stamp fields — that’s a smooth way to grow it.