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("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } } 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
- XHTML Compliance: Ensure HTML is well-formed XHTML
- CSS Support: Use CSS 2.1 properties (limited CSS3 support)
- Font Registration: Register fonts for consistent rendering
- Error Handling: Comprehensive error handling for PDF generation failures
- Memory Management: Properly close streams and renderers
- 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.