This in-depth guide covers multiple approaches to generating PDFs in Spring Boot applications, exploring various libraries and their advanced features.
Table of Contents
- Introduction to PDF Generation
- iText PDF Library
- Apache PDFBox
- Thymeleaf + Flying Saucer
- JasperReports
- OpenPDF
- Performance Considerations
- Advanced Features
- Comparison Table
- Complete Examples
1. Introduction to PDF Generation
PDF generation requirements typically fall into these categories:
- Simple text documents
- Complex reports with tables/charts
- PDF forms
- PDF manipulation (merge, split, watermark)
Key considerations:
- Library maturity and license
- HTML/CSS support
- Table handling capabilities
- Internationalization (i18n)
- Performance with large documents
2. iText PDF Library
Dependencies
<!-- AGPL licensed version -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.3</version>
<type>pom</type>
</dependency>
<!-- Commercial license available -->
Basic Example
@RestController
@RequestMapping("/api/pdf")
public class PdfExportController {
@GetMapping("/itext/simple")
public ResponseEntity<byte[]> generateSimplePdf() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfDocument pdf = new PdfDocument(new PdfWriter(baos));
Document document = new Document(pdf);
// Add content
document.add(new Paragraph("Spring Boot PDF Export Example")
.setFontSize(20)
.setBold()
.setTextAlignment(TextAlignment.CENTER));
document.add(new Paragraph("\n"));
// Create a table
Table table = new Table(UnitValue.createPercentArray(3)).useAllAvailableWidth();
table.addHeaderCell("ID");
table.addHeaderCell("Name");
table.addHeaderCell("Date");
for (int i = 1; i <= 10; i++) {
table.addCell(String.valueOf(i));
table.addCell("Item " + i);
table.addCell(LocalDate.now().toString());
}
document.add(table);
document.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "simple-document.pdf");
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
}
Advanced Features
- PDF Forms: Create fillable forms with fields
- Digital Signatures: Add digital signatures to documents
- PDF/A Compliance: Generate archival-quality PDFs
- Barcode Generation: QR codes, UPC, etc.
- Watermarking: Add dynamic watermarks
// Advanced example with watermark and encryption
PdfWriter writer = new PdfWriter(baos,
new WriterProperties().setStandardEncryption(
"userpass".getBytes(),
"ownerpass".getBytes(),
EncryptionConstants.ALLOW_PRINTING,
EncryptionConstants.ENCRYPTION_AES_256
));
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
// Watermark
PdfCanvas canvas = new PdfCanvas(pdf.getFirstPage());
canvas.saveState()
.setFillColor(ColorConstants.LIGHT_GRAY)
.rectangle(0, 0, pdf.getDefaultPageSize().getWidth(), pdf.getDefaultPageSize().getHeight())
.fill()
.restoreState();
Paragraph watermark = new Paragraph("CONFIDENTIAL")
.setFontColor(ColorConstants.RED, 0.2f)
.setFontSize(60)
.setRotationAngle(Math.PI / 4)
.setFixedPosition(
pdf.getDefaultPageSize().getWidth() / 2 - 100,
pdf.getDefaultPageSize().getHeight() / 2 - 100,
200
);
document.add(watermark);
3. Apache PDFBox
Dependencies
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version>
</dependency>
Basic Example
@GetMapping("/pdfbox/simple")
public ResponseEntity<byte[]> generatePdfBoxPdf() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument document = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
// Begin content
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 20);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("Spring Boot PDFBox Example");
contentStream.endText();
// Draw a table
float margin = 50;
float yStart = page.getMediaBox().getHeight() - margin;
float tableWidth = page.getMediaBox().getWidth() - 2 * margin;
float yPosition = yStart;
float bottomMargin = 70;
float rowHeight = 20f;
// Draw header
drawTableRow(contentStream, margin, yPosition, tableWidth,
new String[]{"ID", "Name", "Value"}, true);
yPosition -= rowHeight;
// Draw rows
for (int i = 1; i <= 15; i++) {
if (yPosition <= bottomMargin) {
contentStream.close();
page = new PDPage(PDRectangle.A4);
document.addPage(page);
contentStream = new PDPageContentStream(document, page);
yPosition = yStart;
}
drawTableRow(contentStream, margin, yPosition, tableWidth,
new String[]{
String.valueOf(i),
"Product " + i,
"$" + (i * 10.5)
}, false);
yPosition -= rowHeight;
}
}
document.save(baos);
}
// Set response headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "pdfbox-example.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
private void drawTableRow(PDPageContentStream contentStream,
float x, float y, float width, String[] columns, boolean isHeader) throws IOException {
float colWidth = width / columns.length;
float nextX = x;
for (String column : columns) {
contentStream.setStrokingColor(isHeader ? 100 : 220);
contentStream.addRect(nextX, y - 15, colWidth, 20);
contentStream.stroke();
contentStream.beginText();
contentStream.setFont(isHeader ? PDType1Font.HELVETICA_BOLD : PDType1Font.HELVETICA, 10);
contentStream.newLineAtOffset(nextX + 5, y - 10);
contentStream.showText(column);
contentStream.endText();
nextX += colWidth;
}
}
Advanced Features
- PDF Parsing: Extract text/images from existing PDFs
- PDF Merging: Combine multiple PDFs
- OCR Integration: With Tesseract
- Digital Signatures: Sign PDF documents
- PDF Forms: Create and fill forms
// PDF Manipulation Example - Merge PDFs
public byte[] mergePdfs(List<byte[]> pdfs) throws IOException {
PDFMergerUtility merger = new PDFMergerUtility();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument destDoc = new PDDocument()) {
for (byte[] pdf : pdfs) {
try (PDDocument srcDoc = PDDocument.load(pdf)) {
merger.appendDocument(destDoc, srcDoc);
}
}
destDoc.save(baos);
}
return baos.toByteArray();
}
// PDF Text Extraction
public String extractText(byte[] pdfBytes) throws IOException {
try (PDDocument document = PDDocument.load(pdfBytes)) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document);
}
}
4. Thymeleaf + Flying Saucer (HTML to PDF)
Best for converting HTML templates to PDF.
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
Implementation
@Controller
public class PdfHtmlController {
@Autowired
private TemplateEngine templateEngine;
@GetMapping("/pdf/html-template")
public ResponseEntity<byte[]> generatePdfFromHtml() throws Exception {
// Prepare data
Map<String, Object> data = new HashMap<>();
data.put("title", "Invoice #12345");
data.put("date", LocalDate.now().format(DateTimeFormatter.ISO_DATE));
List<InvoiceItem> items = Arrays.asList(
new InvoiceItem("Product A", 2, 49.99),
new InvoiceItem("Product B", 1, 129.99),
new InvoiceItem("Service Fee", 1, 25.00)
);
data.put("items", items);
data.put("total", items.stream().mapToDouble(i -> i.getQuantity() * i.getPrice()).sum());
// Process HTML template
Context context = new Context();
context.setVariables(data);
String html = templateEngine.process("invoice-template", context);
// Convert to PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ITextRenderer renderer = new ITextRenderer();
// For base URL to resolve CSS/images
String baseUrl = FileSystems.getDefault()
.getPath("src", "main", "resources", "templates")
.toUri()
.toURL()
.toString();
renderer.setDocumentFromString(html, baseUrl);
renderer.layout();
renderer.createPDF(baos);
renderer.finishPDF();
// Response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "invoice.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
}
HTML Template (invoice-template.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title th:text="${title}">Invoice</title>
<style>
body { font-family: Arial, sans-serif; margin: 2cm; }
.header { text-align: center; margin-bottom: 30px; }
.title { font-size: 24px; font-weight: bold; }
.date { font-size: 14px; color: #555; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th { background-color: #f2f2f2; text-align: left; padding: 8px; }
td { padding: 8px; border-bottom: 1px solid #ddd; }
.total { font-weight: bold; text-align: right; margin-top: 20px; }
.footer { margin-top: 50px; font-size: 12px; color: #777; }
</style>
</head>
<body>
<div class="header">
<div class="title" th:text="${title}">Invoice</div>
<div class="date" th:text="${date}">Date</div>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.description}">Product</td>
<td th:text="${item.quantity}">1</td>
<td th:text="'$' + ${#numbers.formatDecimal(item.price, 1, 2)}">0.00</td>
<td th:text="'$' + ${#numbers.formatDecimal(item.quantity * item.price, 1, 2)}">0.00</td>
</tr>
</tbody>
</table>
<div class="total">
Total: <span th:text="'$' + ${#numbers.formatDecimal(total, 1, 2)}">0.00</span>
</div>
<div class="footer">
Thank you for your business!
</div>
</body>
</html>
Advanced Features
- CSS Paged Media: For print-specific styling
- Page Breaks: Control content flow across pages
- Headers/Footers: Consistent across pages
- SVG Support: Vector graphics in PDF
// Advanced Flying Saucer Configuration
ITextRenderer renderer = new ITextRenderer();
// Enable PDF/UA (accessibility)
renderer.getSharedContext().setPdfUAConformance(true);
// Set DPI for higher quality
renderer.setDPI(300);
// Custom font provider
IFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont("fonts/arial.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
// PDF metadata
renderer.getWriter().setPdfVersion(PdfWriter.VERSION_1_7);
renderer.getWriter().setViewerPreferences(PdfWriter.DisplayDocTitle);
renderer.getWriter().setTitle("Document Title");
5. JasperReports
Best for complex business reports with charts, subreports, etc.
Dependencies
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.19.1</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>6.0.0</version>
</dependency>
Implementation
@GetMapping("/jasper/simple-report")
public ResponseEntity<byte[]> generateJasperReport() throws Exception {
// Compile JRXML template
JasperReport jasperReport = JasperCompileManager.compileReport(
getClass().getResourceAsStream("/reports/simple-report.jrxml"));
// Create data source
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(
Arrays.asList(
new ReportItem(1, "Laptop", 1200.00),
new ReportItem(2, "Monitor", 350.00),
new ReportItem(3, "Keyboard", 45.00)
)
);
// Fill report
Map<String, Object> parameters = new HashMap<>();
parameters.put("title", "Product Price List");
parameters.put("generatedDate", new Date());
JasperPrint jasperPrint = JasperFillManager.fillReport(
jasperReport, parameters, dataSource);
// Export to PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JasperExportManager.exportReportToPdfStream(jasperPrint, baos);
// Response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "jasper-report.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
JRXML Report Template
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports
http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
name="simple-report" pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20"
topMargin="20" bottomMargin="20">
<parameter name="title" class="java.lang.String"/>
<parameter name="generatedDate" class="java.util.Date"/>
<title>
<band height="70">
<staticText>
<reportElement x="0" y="0" width="555" height="30"/>
<textElement textAlignment="Center">
<font size="18" isBold="true"/>
</textElement>
<text><![CDATA[Product Catalog]]></text>
</staticText>
<textField>
<reportElement x="0" y="30" width="555" height="20"/>
<textElement textAlignment="Center"/>
<textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
</textField>
<textField pattern="MMMM dd, yyyy">
<reportElement x="0" y="50" width="555" height="20"/>
<textElement textAlignment="Center"/>
<textFieldExpression><![CDATA[$P{generatedDate}]]></textFieldExpression>
</textField>
</band>
</title>
<columnHeader>
<band height="30">
<staticText>
<reportElement x="0" y="0" width="100" height="30"/>
<textElement verticalAlignment="Middle">
<font isBold="true"/>
</textElement>
<text><![CDATA[ID]]></text>
</staticText>
<staticText>
<reportElement x="100" y="0" width="200" height="30"/>
<textElement verticalAlignment="Middle">
<font isBold="true"/>
</textElement>
<text><![CDATA[Product Name]]></text>
</staticText>
<staticText>
<reportElement x="300" y="0" width="100" height="30"/>
<textElement verticalAlignment="Middle">
<font isBold="true"/>
</textElement>
<text><![CDATA[Price]]></text>
</staticText>
</band>
</columnHeader>
<detail>
<band height="25">
<textField>
<reportElement x="0" y="0" width="100" height="25"/>
<textElement verticalAlignment="Middle"/>
<textFieldExpression><![CDATA[$F{id}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="100" y="0" width="200" height="25"/>
<textElement verticalAlignment="Middle"/>
<textFieldExpression><![CDATA[$F{name}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="300" y="0" width="100" height="25"/>
<textElement verticalAlignment="Middle"/>
<textFieldExpression><![CDATA["$" + $F{price}]]></textFieldExpression>
</textField>
</band>
</detail>
<pageFooter>
<band height="20">
<textField evaluationTime="Report">
<reportElement x="455" y="0" width="100" height="20"/>
<textElement textAlignment="Right"/>
<textFieldExpression><![CDATA["Page " + $V{PAGE_NUMBER} + " of"]]></textFieldExpression>
</textField>
<textField evaluationTime="Report">
<reportElement x="535" y="0" width="20" height="20"/>
<textFieldExpression><![CDATA[" " + $V{PAGE_NUMBER}]]></textFieldExpression>
</textField>
</band>
</pageFooter>
</jasperReport>
Advanced Features
- Subreports: Nest reports within reports
- Charts: Bar, pie, line charts
- Crosstabs: Pivot table functionality
- Multiple Data Sources: Different sources in one report
- Conditional Formatting: Dynamic styling
// Advanced JasperReports Example with Subreport and Chart
public ResponseEntity<byte[]> generateAdvancedReport() throws Exception {
// Main report
JasperReport jasperReport = JasperCompileManager.compileReport(
getClass().getResourceAsStream("/reports/advanced-report.jrxml"));
// Subreport
JasperReport subReport = JasperCompileManager.compileReport(
getClass().getResourceAsStream("/reports/summary-subreport.jrxml"));
// Chart dataset
JRBeanCollectionDataSource chartData = new JRBeanCollectionDataSource(
Arrays.asList(
new ChartData("Q1", 1200),
new ChartData("Q2", 1800),
new ChartData("Q3", 2100),
new ChartData("Q4", 2500)
)
);
// Parameters
Map<String, Object> parameters = new HashMap<>();
parameters.put("subreport", subReport);
parameters.put("chartDataset", chartData);
// Fill and export
JasperPrint jasperPrint = JasperFillManager.fillReport(
jasperReport, parameters, new JREmptyDataSource());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JasperExportManager.exportReportToPdfStream(jasperPrint, baos);
// Response headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "advanced-report.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
6. OpenPDF (iText fork)
MIT-licensed alternative to iText.
Dependencies
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.30</version>
</dependency>
Implementation
@GetMapping("/openpdf/simple")
public ResponseEntity<byte[]> generateOpenPdf() throws DocumentException, IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Document document = new Document();
PdfWriter.getInstance(document, baos);
document.open();
// Title
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18);
Paragraph title = new Paragraph("OpenPDF Example", titleFont);
title.setAlignment(Element.ALIGN_CENTER);
title.setSpacingAfter(20f);
document.add(title);
// Table
PdfPTable table = new PdfPTable(3);
table.setWidthPercentage(100);
table.setSpacingBefore(10f);
table.setSpacingAfter(10f);
// Table headers
Font headerFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12);
Stream.of("ID", "Name", "Value")
.forEach(header -> {
PdfPCell cell = new PdfPCell(new Phrase(header, headerFont));
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setBackgroundColor(new BaseColor(220, 220, 220));
table.addCell(cell);
});
// Table data
for (int i = 1; i <= 10; i++) {
table.addCell(String.valueOf(i));
table.addCell("Product " + i);
table.addCell("$" + (i * 10.5));
}
document.add(table);
// Barcode
Barcode128 barcode = new Barcode128();
barcode.setCode("PDF-123456789");
barcode.setCodeType(Barcode128.CODE128);
Image barcodeImage = barcode.createImageWithBarcode(
document.getPageSize().getWidth() - 100,
document.getPageSize().getHeight() - 50);
document.add(barcodeImage);
document.close();
// Response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "openpdf-example.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
Advanced Features
- PDF/A Support: Archival PDF generation
- Barcodes: Multiple barcode types
- Watermarking: Similar to iText
- Digital Signatures: Document signing
// Advanced OpenPDF Example with Watermark and Encryption
public ResponseEntity<byte[]> generateSecurePdf() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Document with encryption
Document document = new Document();
PdfWriter writer = PdfWriter.getInstance(document, baos);
writer.setEncryption(
"userpass".getBytes(),
"ownerpass".getBytes(),
PdfWriter.ALLOW_PRINTING,
PdfWriter.STANDARD_ENCRYPTION_128
);
document.open();
// Watermark
PdfContentByte canvas = writer.getDirectContentUnder();
canvas.saveState();
canvas.setColorFill(BaseColor.LIGHT_GRAY);
canvas.rectangle(0, 0, document.getPageSize().getWidth(), document.getPageSize().getHeight());
canvas.fill();
canvas.restoreState();
// Content
Paragraph p = new Paragraph("Confidential Document");
p.setAlignment(Element.ALIGN_CENTER);
document.add(p);
// Add metadata
document.addTitle("Secure Document");
document.addSubject("Example of encrypted PDF");
document.addKeywords("PDF, OpenPDF, Encryption");
document.addCreator("Spring Boot Application");
document.addAuthor("Your Company");
document.close();
// Response
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("filename", "secure-document.pdf");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
7. Performance Considerations
General Optimization Tips
- Reuse Resources:
- Keep font definitions as static fields
- Reuse template objects (JasperReports, Thymeleaf)
- Stream Processing:
// Process large datasets in chunks
try (PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream(in)) {
new Thread(() -> {
try {
PdfWriter writer = PdfWriter.getInstance(document, out);
document.open();
// Write data in chunks
document.close();
} catch (Exception e) {
// Handle error
}
}).start();
// Stream the PDF as it's being generated
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.body(new InputStreamResource(in));
}
- Memory Management:
- Use
ByteArrayOutputStream
with reasonable initial size - For very large PDFs, consider file-based temporary storage
- Use
- Connection Pooling:
- When fetching data from databases, use proper connection pooling
Library-Specific Optimizations
iText/OpenPDF:
- Use
PdfWriter
withPRIMARY_WRITE
mode for large documents - Set
fullCompression
to true for smaller file sizes
PDFBox:
- Enable memory mapping for large documents:
MemoryUsageSetting.setupMainMemoryOnly()
Flying Saucer:
- Cache parsed CSS
- Pre-compile templates
JasperReports:
- Use virtualizers for large reports:
JRFileVirtualizer virtualizer = new JRFileVirtualizer(100);
parameters.put(JRParameter.REPORT_VIRTUALIZER, virtualizer);
8. Advanced Features
1. PDF Generation with Charts
Using JasperReports:
// In JRXML:
<barChart>
<chart evaluationTime="Report">
<reportElement x="50" y="50" width="400" height="250"/>
<chartTitle>
<titleExpression><![CDATA["Sales by Quarter"]]></titleExpression>
</chartTitle>
<categoryDataset>
<dataset>
<datasetRun subDataset="chartDataset"/>
</dataset>
<categorySeries>
<seriesExpression><![CDATA[$F{label}]]></seriesExpression>
<categoryExpression><![CDATA[$F{label}]]></categoryExpression>
<valueExpression><![CDATA[$F{value}]]></valueExpression>
</categorySeries>
</categoryDataset>
<barPlot>
<itemLabel/>
<categoryAxisFormat>
<axisFormat labelFont="SansSerif-12"/>
</categoryAxisFormat>
<valueAxisFormat>
<axisFormat labelFont="SansSerif-12"/>
</valueAxisFormat>
</barPlot>
</chart>
</barChart>
2. Dynamic PDF Forms
Using iText/OpenPDF:
PdfReader reader = new PdfReader("form-template.pdf");
PdfStamper stamper = new PdfStamper(reader, baos);
AcroFields form = stamper.getAcroFields();
// Fill form fields
form.setField("name", "John Doe");
form.setField("address", "123 Main St");
form.setField("signature", "Verified");
// Flatten to make non-editable
stamper.setFormFlattening(true);
stamper.close();
reader.close();
3. PDF Watermarking
Using PDFBox:
PDDocument document = PDDocument.load(inputStream);
for (PDPage page : document.getPages()) {
PDPageContentStream contentStream = new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 48);
contentStream.setNonStrokingColor(200, 200, 200);
// Rotate watermark text
contentStream.beginText();
contentStream.setTextMatrix(
AffineTransform.getRotateInstance(Math.toRadians(45), 100, 100));
contentStream.newLineAtOffset(100, 100);
contentStream.showText("CONFIDENTIAL");
contentStream.endText();
contentStream.close();
}
4. PDF/A Compliance
Using iText:
PdfWriter writer = new PdfWriter(baos,
new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
PdfADocument pdf = new PdfADocument(
writer,
PdfAConformanceLevel.PDF_A_3B,
new PdfOutputIntent("Custom", "", "http://www.color.org",
"sRGB IEC61966-2.1",
new FileInputStream("sRGB_CS_profile.icm")));
Document document = new Document(pdf);
// Add content with proper metadata and tagging
9. Comparison Table
Feature | iText | OpenPDF | PDFBox | Flying Saucer | JasperReports |
---|---|---|---|---|---|
License | AGPL/commercial | MIT | Apache | LGPL | LGPL/commercial |
HTML/CSS Support | Limited | Limited | No | Excellent | Limited |
Table Support | Excellent | Excellent | Manual | HTML tables | Excellent |
Charts/Graphs | Basic | Basic | No | Via HTML | Excellent |
PDF Forms | Excellent | Excellent | Good | No | Limited |
Digital Signatures | Yes | Yes | Yes | No | Yes |
PDF Manipulation | Excellent | Excellent | Excellent | No | Limited |
Performance | High | High | Medium | Medium | Medium-High |
Learning Curve | Steep | Steep | Medium | Easy | Steep |
Best For | Complex PDF generation | iText alternative with MIT license | PDF manipulation | HTML to PDF | Business reports |
10. Complete Examples
Complete Spring Boot Service Example
@Service
public class PdfExportService {
@Autowired
private TemplateEngine templateEngine;
@Value("classpath:reports/")
private Resource templatesLocation;
// iText PDF Generation
public byte[] generateItextPdf(List<DataItem> items) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfDocument pdf = new PdfDocument(new PdfWriter(baos));
Document document = new Document(pdf);
// Title
document.add(new Paragraph("Data Export")
.setFontSize(18)
.setBold()
.setTextAlignment(TextAlignment.CENTER));
// Table
Table table = new Table(UnitValue.createPercentArray(4)).useAllAvailableWidth();
// Headers
Stream.of("ID", "Name", "Value", "Date")
.forEach(header -> table.addHeaderCell(new Cell().add(new Paragraph(header).setBold())));
// Data
items.forEach(item -> {
table.addCell(String.valueOf(item.getId()));
table.addCell(item.getName());
table.addCell(String.format("%.2f", item.getValue()));
table.addCell(item.getDate().format(DateTimeFormatter.ISO_DATE));
});
document.add(table);
document.close();
return baos.toByteArray();
}
// HTML to PDF with Thymeleaf
public byte[] generateHtmlPdf(Map<String, Object> variables, String templateName) throws Exception {
Context context = new Context();
context.setVariables(variables);
String html = templateEngine.process(templateName, context);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ITextRenderer renderer = new ITextRenderer();
// Base URL for resources
String baseUrl = templatesLocation.getURL().toString();
renderer.setDocumentFromString(html, baseUrl);
renderer.layout();
renderer.createPDF(baos);
renderer.finishPDF();
return baos.toByteArray();
}
// JasperReports PDF
public byte[] generateJasperReport(String reportName, Map<String, Object> parameters,
JRDataSource dataSource) throws Exception {
Resource resource = templatesLocation.createRelative(reportName + ".jasper");
JasperReport jasperReport = (JasperReport) JRLoader.loadObject(resource.getInputStream());
JasperPrint jasperPrint = JasperFillManager.fillReport(
jasperReport, parameters, dataSource);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JasperExportManager.exportReportToPdfStream(jasperPrint, baos);
return baos.toByteArray();
}
}
@RestController
@RequestMapping("/api/reports")
public class ReportController {
@Autowired
private PdfExportService pdfExportService;
@GetMapping("/data")
public ResponseEntity<byte[]> generateDataReport() throws Exception {
List<DataItem> data = fetchDataFromService();
byte[] pdf = pdfExportService.generateItextPdf(data);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PDF_VALUE)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=data-report.pdf")
.body(pdf);
}
@GetMapping("/invoice/{id}")
public ResponseEntity<byte[]> generateInvoice(@PathVariable Long id) throws Exception {
Invoice invoice = invoiceService.getInvoice(id);
Map<String, Object> variables = new HashMap<>();
variables.put("invoice", invoice);
variables.put("company", companyService.getCompanyDetails());
byte[] pdf = pdfExportService.generateHtmlPdf(variables, "invoice-template");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PDF_VALUE)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=invoice-" + id + ".pdf")
.body(pdf);
}
}
Error Handling Advice
@ControllerAdvice
public class PdfExportExceptionHandler {
@ExceptionHandler(PdfGenerationException.class)
public ResponseEntity<ErrorResponse> handlePdfGenerationException(
PdfGenerationException ex, WebRequest request) {
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"PDF Generation Failed",
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler({JRException.class, DocumentException.class, IOException.class})
public ResponseEntity<ErrorResponse> handlePdfLibraryExceptions(
Exception ex, WebRequest request) {
ErrorResponse response = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"PDF Processing Error",
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
PDF Generation Monitoring
@Aspect
@Component
@Slf4j
public class PdfGenerationMonitor {
@Around("execution(* com.example.service.PdfExportService.*(..))")
public Object monitorPdfGeneration(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("PDF generation {} completed in {} ms", methodName, duration);
Metrics.counter("pdf.generation", "method", methodName).increment();
Metrics.timer("pdf.generation.time", "method", methodName)
.record(duration, TimeUnit.MILLISECONDS);
return result;
} catch (Exception ex) {
log.error("PDF generation {} failed", methodName, ex);
Metrics.counter("pdf.generation.errors", "method", methodName).increment();
throw ex;
}
}
}
This comprehensive guide covers all major approaches to PDF generation in Spring Boot applications, from simple text documents to complex business reports with charts and subreports. Each library has its strengths, and the choice depends on your specific requirements, licensing constraints, and complexity needs.