HTML to PDF with Flying Saucer in Java: Complete Guide

Flying Saucer is a Java library that renders well-formed XML/XHTML (and even some HTML5) to PDF. It uses CSS for styling and supports most CSS 2.1 properties.


Setup and Dependencies

Maven Dependencies
<properties>
<flying-saucer.version>9.1.22</flying-saucer.version>
<itext.version>2.1.7</itext.version>
<jsoup.version>1.16.1</jsoup.version>
</properties>
<dependencies>
<!-- Flying Saucer Core -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>${flying-saucer.version}</version>
</dependency>
<!-- iText for PDF generation -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>${itext.version}</version>
</dependency>
<!-- JSoup for HTML parsing and cleaning -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
<!-- Spring Boot (optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>

Core PDF Service Implementation

1. Basic PDF Generator
import org.xhtmlrenderer.pdf.ITextRenderer;
import com.lowagie.text.DocumentException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
@Service
public class PdfGenerationService {
private static final Logger logger = LoggerFactory.getLogger(PdfGenerationService.class);
/**
* Convert XHTML content to PDF byte array
*/
public byte[] generatePdfFromXhtml(String xhtmlContent) throws DocumentException, IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
// Set document content
renderer.setDocumentFromString(xhtmlContent);
// Layout and render
renderer.layout();
renderer.createPDF(outputStream);
return outputStream.toByteArray();
}
}
/**
* Convert XHTML content to PDF file
*/
public void generatePdfFromXhtml(String xhtmlContent, String outputFilePath) 
throws DocumentException, IOException {
try (OutputStream outputStream = new FileOutputStream(outputFilePath)) {
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(xhtmlContent);
renderer.layout();
renderer.createPDF(outputStream);
}
logger.info("PDF generated successfully: {}", outputFilePath);
}
/**
* Convert XHTML file to PDF
*/
public byte[] generatePdfFromXhtmlFile(String xhtmlFilePath) 
throws DocumentException, IOException {
String xhtmlContent = new String(Files.readAllBytes(Paths.get(xhtmlFilePath)));
return generatePdfFromXhtml(xhtmlContent);
}
}
2. Advanced PDF Generator with Configuration
@Component
public class AdvancedPdfGenerator {
private static final Logger logger = LoggerFactory.getLogger(AdvancedPdfGenerator.class);
/**
* Generate PDF with custom configuration
*/
public byte[] generatePdf(PdfGenerationRequest request) 
throws DocumentException, IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ITextRenderer renderer = createConfiguredRenderer(request);
// Set document
if (request.getHtmlContent() != null) {
String xhtml = convertToXhtml(request.getHtmlContent());
renderer.setDocumentFromString(xhtml);
} else if (request.getUrl() != null) {
renderer.setDocument(request.getUrl());
}
// Layout and render
renderer.layout();
renderer.createPDF(outputStream);
logger.debug("PDF generated successfully, size: {} bytes", outputStream.size());
return outputStream.toByteArray();
}
}
private ITextRenderer createConfiguredRenderer(PdfGenerationRequest request) {
ITextRenderer renderer = new ITextRenderer();
// Configure DPI for better image quality
renderer.setDpi(192);
// Set custom font provider if fonts are specified
if (request.getFonts() != null && !request.getFonts().isEmpty()) {
FontResolver fontResolver = renderer.getFontResolver();
registerFonts(fontResolver, request.getFonts());
}
return renderer;
}
private void registerFonts(FontResolver fontResolver, List<FontResource> fonts) {
for (FontResource font : fonts) {
try {
fontResolver.addFont(
font.getFontPath(),
font.getEncoding(),
font.isEmbedded(),
font.getFontFamily()
);
logger.debug("Registered font: {}", font.getFontPath());
} catch (Exception e) {
logger.warn("Failed to register font: {}", font.getFontPath(), e);
}
}
}
/**
* Convert HTML to well-formed XHTML
*/
private String convertToXhtml(String htmlContent) {
// JSoup can be used to clean and convert HTML to XHTML
// This is a basic implementation
return org.jsoup.Jsoup.parse(htmlContent)
.outputSettings(new org.jsoup.nodes.Document.OutputSettings()
.syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml)
.charset("UTF-8"))
.html();
}
}
3. Request and Configuration Models
public class PdfGenerationRequest {
private String htmlContent;
private String url;
private String baseUrl;
private List<FontResource> fonts;
private PdfConfig config;
// constructors, getters, setters
public PdfGenerationRequest() {}
public PdfGenerationRequest(String htmlContent) {
this.htmlContent = htmlContent;
this.config = new PdfConfig();
}
}
public class PdfConfig {
private int dpi = 192;
private boolean compress = true;
private PageSize pageSize = PageSize.A4;
private PageOrientation orientation = PageOrientation.PORTRAIT;
private Margin margin = new Margin(36, 36, 36, 36); // 0.5 inches
// constructors, getters, setters
}
public class Margin {
private float top;
private float right;
private float bottom;
private float left;
public Margin(float top, float right, float bottom, float left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
// getters, setters
}
public class FontResource {
private String fontPath;
private String encoding = "Identity-H";
private boolean embedded = true;
private String fontFamily;
// constructors, getters, setters
}
public enum PageSize {
A0, A1, A2, A3, A4, A5, LETTER, LEGAL
}
public enum PageOrientation {
PORTRAIT, LANDSCAPE
}

HTML Template Management

1. Template Engine Service
@Service
public class TemplateService {
private final ResourceLoader resourceLoader;
public TemplateService(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* Load HTML template from classpath
*/
public String loadTemplate(String templatePath) throws IOException {
Resource resource = resourceLoader.getResource("classpath:templates/" + templatePath);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
/**
* Replace placeholders in template with actual values
*/
public String populateTemplate(String template, Map<String, Object> variables) {
String result = template;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
String placeholder = "{{" + entry.getKey() + "}}";
String value = entry.getValue() != null ? entry.getValue().toString() : "";
result = result.replace(placeholder, value);
}
return result;
}
/**
* Generate HTML content from template with data
*/
public String generateHtml(String templateName, Map<String, Object> data) throws IOException {
String template = loadTemplate(templateName);
return populateTemplate(template, data);
}
}
2. Sample HTML Templates
<!-- templates/invoice-template.html -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Invoice {{invoiceNumber}}</title>
<style type="text/css">
@page {
size: A4;
margin: 2cm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 10px;
}
}
body {
font-family: Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
color: #333;
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #333;
padding-bottom: 20px;
}
.company-info {
float: left;
width: 50%;
}
.invoice-info {
float: right;
width: 40%;
text-align: right;
}
.clear {
clear: both;
}
.section {
margin: 20px 0;
}
.billing-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.billing-table th {
background-color: #f5f5f5;
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.billing-table td {
border: 1px solid #ddd;
padding: 8px;
}
.total-row {
font-weight: bold;
background-color: #f9f9f9;
}
.footer {
margin-top: 50px;
text-align: center;
font-size: 10px;
color: #666;
}
.page-break {
page-break-before: always;
}
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
</div>
<div class="company-info">
<h3>{{companyName}}</h3>
<p>{{companyAddress}}</p>
<p>Phone: {{companyPhone}}</p>
<p>Email: {{companyEmail}}</p>
</div>
<div class="invoice-info">
<p><strong>Invoice #:</strong> {{invoiceNumber}}</p>
<p><strong>Date:</strong> {{invoiceDate}}</p>
<p><strong>Due Date:</strong> {{dueDate}}</p>
</div>
<div class="clear"></div>
<div class="section">
<h3>Bill To:</h3>
<p>{{customerName}}<br/>
{{customerAddress}}<br/>
{{customerEmail}}</p>
</div>
<table class="billing-table">
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{{lineItems}}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3" style="text-align: right;">Subtotal:</td>
<td>{{subtotal}}</td>
</tr>
<tr class="total-row">
<td colspan="3" style="text-align: right;">Tax ({{taxRate}}%):</td>
<td>{{taxAmount}}</td>
</tr>
<tr class="total-row">
<td colspan="3" style="text-align: right;">Total:</td>
<td>{{totalAmount}}</td>
</tr>
</tfoot>
</table>
<div class="footer">
<p>Thank you for your business!</p>
<p>{{companyName}} | {{companyAddress}} | {{companyPhone}}</p>
</div>
</body>
</html>
<!-- templates/report-template.html -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{reportTitle}}</title>
<style type="text/css">
@page {
size: A4 landscape;
margin: 1cm;
}
body {
font-family: "DejaVu Sans", Arial, sans-serif;
font-size: 10px;
}
.report-header {
text-align: center;
margin-bottom: 20px;
}
.table-container {
width: 100%;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 9px;
}
.data-table th {
background-color: #2c3e50;
color: white;
padding: 6px;
text-align: left;
border: 1px solid #34495e;
}
.data-table td {
padding: 5px;
border: 1px solid #bdc3c7;
}
.data-table tr:nth-child(even) {
background-color: #ecf0f1;
}
.summary {
margin-top: 20px;
padding: 10px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
}
</style>
</head>
<body>
<div class="report-header">
<h1>{{reportTitle}}</h1>
<p>Generated on: {{generationDate}}</p>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
{{tableHeaders}}
</tr>
</thead>
<tbody>
{{tableRows}}
</tbody>
</table>
</div>
<div class="summary">
<p><strong>Total Records:</strong> {{totalRecords}}</p>
<p><strong>Generated By:</strong> {{generatedBy}}</p>
</div>
</body>
</html>

Complete PDF Service with Templates

1. Comprehensive PDF Service
@Service
public class InvoicePdfService {
private static final Logger logger = LoggerFactory.getLogger(InvoicePdfService.class);
private final TemplateService templateService;
private final AdvancedPdfGenerator pdfGenerator;
public InvoicePdfService(TemplateService templateService, AdvancedPdfGenerator pdfGenerator) {
this.templateService = templateService;
this.pdfGenerator = pdfGenerator;
}
/**
* Generate invoice PDF
*/
public byte[] generateInvoicePdf(InvoiceData invoiceData) throws Exception {
// Prepare template variables
Map<String, Object> variables = prepareInvoiceVariables(invoiceData);
// Generate HTML content
String htmlContent = templateService.generateHtml("invoice-template.html", variables);
// Generate PDF
PdfGenerationRequest request = new PdfGenerationRequest(htmlContent);
request.setConfig(createInvoicePdfConfig());
return pdfGenerator.generatePdf(request);
}
/**
* Generate report PDF
*/
public byte[] generateReportPdf(ReportData reportData) throws Exception {
Map<String, Object> variables = prepareReportVariables(reportData);
String htmlContent = templateService.generateHtml("report-template.html", variables);
PdfGenerationRequest request = new PdfGenerationRequest(htmlContent);
request.setConfig(createReportPdfConfig());
return pdfGenerator.generatePdf(request);
}
private Map<String, Object> prepareInvoiceVariables(InvoiceData invoiceData) {
Map<String, Object> variables = new HashMap<>();
// Company information
variables.put("companyName", invoiceData.getCompanyName());
variables.put("companyAddress", invoiceData.getCompanyAddress());
variables.put("companyPhone", invoiceData.getCompanyPhone());
variables.put("companyEmail", invoiceData.getCompanyEmail());
// Invoice information
variables.put("invoiceNumber", invoiceData.getInvoiceNumber());
variables.put("invoiceDate", formatDate(invoiceData.getInvoiceDate()));
variables.put("dueDate", formatDate(invoiceData.getDueDate()));
// Customer information
variables.put("customerName", invoiceData.getCustomerName());
variables.put("customerAddress", invoiceData.getCustomerAddress());
variables.put("customerEmail", invoiceData.getCustomerEmail());
// Line items
variables.put("lineItems", generateLineItemsHtml(invoiceData.getLineItems()));
// Totals
variables.put("subtotal", formatCurrency(invoiceData.getSubtotal()));
variables.put("taxRate", invoiceData.getTaxRate());
variables.put("taxAmount", formatCurrency(invoiceData.getTaxAmount()));
variables.put("totalAmount", formatCurrency(invoiceData.getTotalAmount()));
return variables;
}
private String generateLineItemsHtml(List<InvoiceLineItem> lineItems) {
StringBuilder sb = new StringBuilder();
for (InvoiceLineItem item : lineItems) {
sb.append("<tr>")
.append("<td>").append(escapeHtml(item.getDescription())).append("</td>")
.append("<td>").append(item.getQuantity()).append("</td>")
.append("<td>").append(formatCurrency(item.getUnitPrice())).append("</td>")
.append("<td>").append(formatCurrency(item.getAmount())).append("</td>")
.append("</tr>");
}
return sb.toString();
}
private Map<String, Object> prepareReportVariables(ReportData reportData) {
Map<String, Object> variables = new HashMap<>();
variables.put("reportTitle", reportData.getTitle());
variables.put("generationDate", formatDateTime(reportData.getGenerationDate()));
variables.put("generatedBy", reportData.getGeneratedBy());
variables.put("totalRecords", reportData.getTotalRecords());
variables.put("tableHeaders", generateTableHeaders(reportData.getHeaders()));
variables.put("tableRows", generateTableRows(reportData.getRows()));
return variables;
}
private String generateTableHeaders(List<String> headers) {
StringBuilder sb = new StringBuilder();
for (String header : headers) {
sb.append("<th>").append(escapeHtml(header)).append("</th>");
}
return sb.toString();
}
private String generateTableRows(List<List<String>> rows) {
StringBuilder sb = new StringBuilder();
for (List<String> row : rows) {
sb.append("<tr>");
for (String cell : row) {
sb.append("<td>").append(escapeHtml(cell)).append("</td>");
}
sb.append("</tr>");
}
return sb.toString();
}
private PdfConfig createInvoicePdfConfig() {
PdfConfig config = new PdfConfig();
config.setPageSize(PageSize.A4);
config.setOrientation(PageOrientation.PORTRAIT);
config.setMargin(new Margin(36, 36, 36, 36));
return config;
}
private PdfConfig createReportPdfConfig() {
PdfConfig config = new PdfConfig();
config.setPageSize(PageSize.A4);
config.setOrientation(PageOrientation.LANDSCAPE);
config.setMargin(new Margin(20, 20, 20, 20));
return config;
}
private String formatCurrency(BigDecimal amount) {
if (amount == null) return "$0.00";
return String.format("$%.2f", amount);
}
private String formatDate(LocalDate date) {
return date != null ? date.format(DateTimeFormatter.ofPattern("MMM dd, yyyy")) : "";
}
private String formatDateTime(LocalDateTime dateTime) {
return dateTime != null ? 
dateTime.format(DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm")) : "";
}
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
}
2. Data Models
public class InvoiceData {
private String companyName;
private String companyAddress;
private String companyPhone;
private String companyEmail;
private String invoiceNumber;
private LocalDate invoiceDate;
private LocalDate dueDate;
private String customerName;
private String customerAddress;
private String customerEmail;
private List<InvoiceLineItem> lineItems;
private BigDecimal subtotal;
private BigDecimal taxRate;
private BigDecimal taxAmount;
private BigDecimal totalAmount;
// constructors, getters, setters
}
public class InvoiceLineItem {
private String description;
private int quantity;
private BigDecimal unitPrice;
private BigDecimal amount;
// constructors, getters, setters
}
public class ReportData {
private String title;
private LocalDateTime generationDate;
private String generatedBy;
private int totalRecords;
private List<String> headers;
private List<List<String>> rows;
// constructors, getters, setters
}

Spring Boot REST Controllers

1. PDF Generation Controller
@RestController
@RequestMapping("/api/pdf")
@Validated
public class PdfController {
private final InvoicePdfService invoicePdfService;
public PdfController(InvoicePdfService invoicePdfService) {
this.invoicePdfService = invoicePdfService;
}
@PostMapping("/invoice")
public ResponseEntity<byte[]> generateInvoice(@Valid @RequestBody InvoiceData invoiceData) {
try {
byte[] pdfBytes = invoicePdfService.generateInvoicePdf(invoiceData);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, 
"attachment; filename=\"invoice-" + invoiceData.getInvoiceNumber() + ".pdf\"")
.header(HttpHeaders.CONTENT_TYPE, "application/pdf")
.body(pdfBytes);
} catch (Exception e) {
logger.error("Failed to generate invoice PDF", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/report")
public ResponseEntity<byte[]> generateReport(@Valid @RequestBody ReportData reportData) {
try {
byte[] pdfBytes = invoicePdfService.generateReportPdf(reportData);
String filename = "report-" + 
LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + ".pdf";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, 
"attachment; filename=\"" + filename + "\"")
.header(HttpHeaders.CONTENT_TYPE, "application/pdf")
.body(pdfBytes);
} catch (Exception e) {
logger.error("Failed to generate report PDF", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/html")
public ResponseEntity<byte[]> generateFromHtml(@Valid @RequestBody HtmlPdfRequest request) {
try {
PdfGenerationRequest pdfRequest = new PdfGenerationRequest(request.getHtmlContent());
byte[] pdfBytes = pdfGenerator.generatePdf(pdfRequest);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"document.pdf\"")
.header(HttpHeaders.CONTENT_TYPE, "application/pdf")
.body(pdfBytes);
} catch (Exception e) {
logger.error("Failed to generate PDF from HTML", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
2. Request Models
public class HtmlPdfRequest {
@NotBlank
private String htmlContent;
private PdfConfig config;
// constructors, getters, setters
}

Advanced Features

1. Custom Font Registration
@Component
public class FontRegistry {
private static final Logger logger = LoggerFactory.getLogger(FontRegistry.class);
@Value("${pdf.fonts.directory:fonts/}")
private String fontsDirectory;
/**
* Register system and custom fonts
*/
public void registerFonts(ITextRenderer renderer) {
FontResolver fontResolver = renderer.getFontResolver();
try {
// Register common system fonts
registerSystemFonts(fontResolver);
// Register custom fonts from directory
registerCustomFonts(fontResolver);
} catch (Exception e) {
logger.warn("Failed to register some fonts", e);
}
}
private void registerSystemFonts(FontResolver fontResolver) throws Exception {
// Register Arial
fontResolver.addFont("/System/Library/Fonts/Arial.ttf", "Identity-H", true);
// Register Times New Roman
fontResolver.addFont("/System/Library/Fonts/Times New Roman.ttf", "Identity-H", true);
// Register DejaVu Sans for better Unicode support
fontResolver.addFont("classpath:fonts/DejaVuSans.ttf", "Identity-H", true);
}
private void registerCustomFonts(FontResolver fontResolver) throws Exception {
Resource[] fontResources = new PathMatchingResourcePatternResolver()
.getResources("classpath:" + fontsDirectory + "**/*.ttf");
for (Resource fontResource : fontResources) {
try {
String fontPath = fontResource.getURL().getPath();
String fontName = fontResource.getFilename();
fontResolver.addFont(fontPath, "Identity-H", true);
logger.debug("Registered custom font: {}", fontName);
} catch (Exception e) {
logger.warn("Failed to register font: {}", fontResource.getFilename(), e);
}
}
}
}
2. PDF Generation with Headers and Footers
@Service
public class HeaderFooterPdfService {
public byte[] generatePdfWithHeaderFooter(String htmlContent, 
String headerHtml, 
String footerHtml) throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
// Create custom user agent for header/footer
CustomUserAgent userAgent = new CustomUserAgent(renderer.getOutputDevice());
userAgent.setSharedContext(renderer.getSharedContext());
renderer.getSharedContext().setUserAgentCallback(userAgent);
renderer.setDocumentFromString(htmlContent);
renderer.layout();
// Add header/footer logic here
addHeaderFooter(renderer, headerHtml, footerHtml);
renderer.createPDF(outputStream);
return outputStream.toByteArray();
}
}
private void addHeaderFooter(ITextRenderer renderer, String headerHtml, String footerHtml) {
// Implementation for adding headers and footers
// This requires extending ITextRenderer or using page events
}
}
3. Batch PDF Generation
@Service
public class BatchPdfService {
private final AdvancedPdfGenerator pdfGenerator;
private final TemplateService templateService;
public byte[] generateBatchPdf(List<InvoiceData> invoices) throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
com.itextpdf.text.Document document = new com.itextpdf.text.Document();
com.itextpdf.text.pdf.PdfWriter writer = com.itextpdf.text.pdf.PdfWriter.getInstance(
document, outputStream);
document.open();
for (int i = 0; i < invoices.size(); i++) {
if (i > 0) {
document.newPage();
}
byte[] singlePdf = generateSingleInvoice(invoices.get(i));
// Merge PDF logic here
}
document.close();
return outputStream.toByteArray();
}
}
private byte[] generateSingleInvoice(InvoiceData invoice) throws Exception {
// Generate individual invoice PDF
return invoicePdfService.generateInvoicePdf(invoice);
}
}

Error Handling and Validation

@ControllerAdvice
public class PdfExceptionHandler {
@ExceptionHandler(DocumentException.class)
public ResponseEntity<ErrorResponse> handleDocumentException(DocumentException ex) {
logger.error("PDF generation document error", ex);
ErrorResponse error = new ErrorResponse("PDF_GENERATION_ERROR", 
"Failed to generate PDF document: " + ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(IOException.class)
public ResponseEntity<ErrorResponse> handleIOException(IOException ex) {
logger.error("PDF generation IO error", ex);
ErrorResponse error = new ErrorResponse("PDF_IO_ERROR", 
"IO error during PDF generation: " + ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
logger.error("Unexpected error during PDF generation", ex);
ErrorResponse error = new ErrorResponse("PDF_GENERIC_ERROR", 
"Unexpected error during PDF generation");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
this.timestamp = LocalDateTime.now();
}
// getters, setters
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class PdfGenerationServiceTest {
@Mock
private TemplateService templateService;
@InjectMocks
private InvoicePdfService invoicePdfService;
@Test
void shouldGenerateInvoicePdf() throws Exception {
// Given
InvoiceData invoiceData = createTestInvoice();
String expectedHtml = "<html>...</html>";
when(templateService.generateHtml(anyString(), anyMap())).thenReturn(expectedHtml);
// When
byte[] pdfBytes = invoicePdfService.generateInvoicePdf(invoiceData);
// Then
assertThat(pdfBytes).isNotEmpty();
assertThat(pdfBytes.length).isGreaterThan(1000); // Reasonable PDF size
}
private InvoiceData createTestInvoice() {
InvoiceData invoice = new InvoiceData();
invoice.setInvoiceNumber("INV-001");
invoice.setCompanyName("Test Company");
// ... set other properties
return invoice;
}
}
2. Integration Test
@SpringBootTest
class PdfGenerationIntegrationTest {
@Autowired
private InvoicePdfService invoicePdfService;
@Test
void shouldGenerateValidPdf() throws Exception {
// Given
InvoiceData invoiceData = createSampleInvoice();
// When
byte[] pdfBytes = invoicePdfService.generateInvoicePdf(invoiceData);
// Then
assertThat(pdfBytes).isNotNull();
assertThat(pdfBytes[0]).isEqualTo((byte) '%'); // PDF header
assertThat(pdfBytes[1]).isEqualTo((byte) 'P');
assertThat(pdfBytes[2]).isEqualTo((byte) 'D');
assertThat(pdfBytes[3]).isEqualTo((byte) 'F');
}
}

Best Practices

  1. XHTML Compliance: Ensure HTML is well-formed XHTML
  2. CSS Support: Use CSS 2.1 properties (limited CSS3 support)
  3. Font Registration: Register fonts for consistent rendering
  4. Error Handling: Comprehensive error handling for PDF generation failures
  5. Memory Management: Properly close streams and renderers
  6. Testing: Test with various HTML templates and content
@Component
public class PdfHealthCheck {
@Scheduled(fixedRate = 300000) // 5 minutes
public void validatePdfGeneration() {
try {
String testHtml = "<html><body><h1>Test</h1></body></html>";
byte[] pdf = pdfGenerator.generatePdf(new PdfGenerationRequest(testHtml));
if (pdf.length == 0) {
logger.error("PDF health check failed: Empty PDF generated");
}
logger.debug("PDF health check passed");
} catch (Exception e) {
logger.error("PDF health check failed", e);
}
}
}

Conclusion

Flying Saucer provides a robust solution for HTML to PDF conversion in Java:

  • CSS-based styling for professional-looking documents
  • XHTML compliance ensures consistent rendering
  • Flexible template system for dynamic content
  • Advanced features like headers, footers, and custom fonts
  • Spring Boot integration for easy web application deployment

This implementation provides a complete, production-ready PDF generation system that can handle invoices, reports, and other document types with professional formatting and layout.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper