CODE WITH SIBIN

Solving Real Problems with Real Code


Spring Boot Export Data to PDF(5 different ways)

This in-depth guide covers multiple approaches to generating PDFs in Spring Boot applications, exploring various libraries and their advanced features.

Table of Contents

  1. Introduction to PDF Generation
  2. iText PDF Library
  3. Apache PDFBox
  4. Thymeleaf + Flying Saucer
  5. JasperReports
  6. OpenPDF
  7. Performance Considerations
  8. Advanced Features
  9. Comparison Table
  10. 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
  • Connection Pooling:
    • When fetching data from databases, use proper connection pooling

Library-Specific Optimizations

iText/OpenPDF:

  • Use PdfWriter with PRIMARY_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 

FeatureiTextOpenPDFPDFBoxFlying SaucerJasperReports
LicenseAGPL/commercialMITApacheLGPLLGPL/commercial
HTML/CSS SupportLimitedLimitedNoExcellentLimited
Table SupportExcellentExcellentManualHTML tablesExcellent
Charts/GraphsBasicBasicNoVia HTMLExcellent
PDF FormsExcellentExcellentGoodNoLimited
Digital SignaturesYesYesYesNoYes
PDF ManipulationExcellentExcellentExcellentNoLimited
PerformanceHighHighMediumMediumMedium-High
Learning CurveSteepSteepMediumEasySteep
Best ForComplex PDF generationiText alternative with MIT licensePDF manipulationHTML to PDFBusiness 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.

Leave a Reply

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