SpringBoot集成PDFBox实现PDF导出(表格导出、分页页码、电子签章与数字签名)

发布于:2025-07-23 ⋅ 阅读:(22) ⋅ 点赞:(0)

下面是一个Spring Boot集成PDFBox实现表格导出和电子签章的详细方案,包含工具类封装和完整示例代码:

一、Maven依赖配置

<dependencies>
    <!-- PDFBox核心库 -->
    <dependency>
        <groupId>org.apache.pdfbox</groupId>
        <artifactId>pdfbox</artifactId>
        <version>2.0.29</version>
    </dependency>
    
    <!-- 数字签名支持 -->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.70</version>
    </dependency>
    
    <!-- 中文字体支持(可选) -->
    <dependency>
        <groupId>com.github.librepdf</groupId>
        <artifactId>openpdf</artifactId>
        <version>1.3.30</version>
    </dependency>
</dependencies>

二、PDF工具类完整实现

import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import java.io.*;
import java.util.List;

/**
 * PDF导出工具类 - 支持表格导出、分页、页码、电子签章和数字签名
 */
public class PdfExportUtils {

    // ======================== 表格导出方法 ======================== //
    
    /**
     * 创建单页表格PDF文档
     * 
     * @param headers 表头列表
     * @param data 表格数据
     * @return 生成的PDF文档对象
     * @throws IOException 当PDF操作失败时抛出
     */
    public static PDDocument createTableDocument(
            List<String> headers, List<List<String>> data) throws IOException {
        
        PDDocument doc = new PDDocument();
        PDPage page = new PDPage(PDRectangle.A4);
        doc.addPage(page);
        
        try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
            // 表格布局参数
            float margin = 50;
            float y = page.getMediaBox().getHeight() - margin;
            float tableWidth = page.getMediaBox().getWidth() - 2 * margin;
            float rowHeight = 20f;
            
            // 绘制表头
            drawRow(doc, cs, headers, margin, y, tableWidth, true);
            
            // 绘制数据行
            for (List<String> row : data) {
                y -= rowHeight;
                if (y < margin) {
                    throw new IOException("数据超出单页容量,请使用分页方法");
                }
                drawRow(doc, cs, row, margin, y, tableWidth, false);
            }
        }
        return doc;
    }

    /**
     * 创建分页表格PDF文档(带页码)
     * 
     * @param headers 表头列表
     * @param data 表格数据
     * @return 生成的PDF文档对象
     * @throws IOException 当PDF操作失败时抛出
     */
    public static PDDocument createPagedTableDocument(
            List<String> headers, List<List<String>> data) throws IOException {
        
        PDDocument doc = new PDDocument();
        
        // 页面参数设置
        float margin = 50; // 页边距
        float topMargin = 70; // 上边距
        float bottomMargin = 70; // 下边距
        float rowHeight = 20f; // 行高
        float tableWidth = PDRectangle.A4.getWidth() - 2 * margin; // 表格宽度
        
        // 计算每页可用高度和行数
        float usableHeight = PDRectangle.A4.getHeight() - topMargin - bottomMargin;
        int rowsPerPage = (int) (usableHeight / rowHeight);
        
        // 当前页面和位置跟踪
        PDPage currentPage = null;
        PDPageContentStream contentStream = null;
        float currentY = 0;
        int rowCounter = 0;
        int pageCounter = 1; // 页码计数
        int totalPages = (int) Math.ceil((double) data.size() / rowsPerPage);
        
        // 遍历所有数据行
        for (int i = 0; i < data.size(); i++) {
            List<String> row = data.get(i);
            
            // 需要新页面时(第一行或页面已满)
            if (currentPage == null || rowCounter >= rowsPerPage) {
                // 关闭上一页的内容流
                if (contentStream != null) {
                    // 在上一页底部绘制页码
                    drawPageNumber(doc, contentStream, margin, bottomMargin, pageCounter, totalPages, currentPage);
                    contentStream.close();
                    pageCounter++;
                }
                
                // 创建新页面
                currentPage = new PDPage(PDRectangle.A4);
                doc.addPage(currentPage);
                contentStream = new PDPageContentStream(doc, currentPage);
                
                // 重置位置计数
                currentY = currentPage.getMediaBox().getHeight() - topMargin;
                rowCounter = 0;
                
                // 在新页面上绘制表头
                drawRow(doc, contentStream, headers, margin, currentY, tableWidth, true);
                currentY -= rowHeight; // 下移一行位置
            }
            
            // 绘制数据行
            drawRow(doc, contentStream, row, margin, currentY, tableWidth, false);
            
            // 更新位置和计数器
            currentY -= rowHeight;
            rowCounter++;
            
            // 如果是数据最后一行,则在当前页绘制页码
            if (i == data.size() - 1) {
                drawPageNumber(doc, contentStream, margin, bottomMargin, pageCounter, totalPages, currentPage);
            }
        }
        
        // 关闭最后一个内容流
        if (contentStream != null) {
            contentStream.close();
        }
        
        return doc;
    }

    // ======================== 绘制方法 ======================== //
    
    /**
     * 绘制表格单行
     */
    private static void drawRow(PDDocument doc, PDPageContentStream cs, List<String> cells, 
                               float x, float y, float width, boolean isHeader) throws IOException {
        
        // 计算列宽
        float colWidth = width / cells.size();
        
        // 设置字体
        PDFont font = isHeader ? 
            PDType1Font.HELVETICA_BOLD : 
            PDType1Font.HELVETICA;
        
        // 中文字体支持(需引入字体文件)
        // font = PDType0Font.load(doc, new File("fonts/SourceHanSansCN-Regular.ttf"));
        
        cs.setFont(font, isHeader ? 12 : 10);
        
        // 表头行绘制背景
        if (isHeader) {
            cs.setNonStrokingColor(230, 230, 230);
            cs.addRect(x, y - 20, width, 20);
            cs.fill();
            cs.setNonStrokingColor(0, 0, 0);
        }
        
        // 绘制单元格文本
        float textX = x;
        for (String cell : cells) {
            String text = (cell != null) ? cell : "";
            
            // 文本超出列宽时截断
            float maxWidth = colWidth - 10;
            if (getStringWidth(text, isHeader, font) > maxWidth) {
                text = truncateText(text, maxWidth, isHeader, font);
            }
            
            cs.beginText();
            cs.newLineAtOffset(textX + 5, y - 15);
            cs.showText(text);
            cs.endText();
            
            textX += colWidth;
        }
        
        // 绘制行底部边框
        cs.setLineWidth(0.3f);
        cs.moveTo(x, y - 20);
        cs.lineTo(x + width, y - 20);
        cs.stroke();
    }
    
    /**
     * 绘制页码
     */
    private static void drawPageNumber(PDDocument doc, PDPageContentStream cs, float margin, 
                                      float bottomMargin, int currentPage, int totalPages, 
                                      PDPage page) throws IOException {
        
        // 设置页码字体
        PDFont font = PDType1Font.HELVETICA;
        // 中文字体支持
        // font = PDType0Font.load(doc, new File("fonts/SourceHanSansCN-Regular.ttf"));
        
        cs.setFont(font, 10);
        cs.setNonStrokingColor(0, 0, 0);
        
        // 页码文本
        String text = "第 " + currentPage + " 页 / 共 " + totalPages + " 页";
        float textWidth = getStringWidth(text, false, font) * 10;
        
        // 计算居中位置
        float pageWidth = page.getMediaBox().getWidth();
        float x = (pageWidth - textWidth) / 2;
        float y = bottomMargin / 2;
        
        // 绘制页码
        cs.beginText();
        cs.newLineAtOffset(x, y);
        cs.showText(text);
        cs.endText();
    }

    // ======================== 电子签章功能 ======================== //
    
    /**
     * 添加图片签章
     */
    public static void addImageSignature(PDDocument doc, byte[] imageData, 
                                        float x, float y, float width) throws IOException {
        
        PDPage page = doc.getPage(0);
        try (PDPageContentStream cs = new PDPageContentStream(
                doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
            
            PDImageXObject img = PDImageXObject.createFromByteArray(doc, imageData, "signature");
            float height = width * img.getHeight() / img.getWidth();
            cs.drawImage(img, x, y, width, height);
        }
    }

    /**
     * 添加数字签名
     */
    public static void addDigitalSignature(PDDocument doc, SignatureInterface signer, 
                                          String reason) throws IOException {
        
        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setReason(reason);
        
        try (SignatureOptions options = new SignatureOptions()) {
            options.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2);
            doc.addSignature(signature, signer, options);
        }
    }

    // ======================== 辅助方法 ======================== //
    
    /**
     * 计算字符串宽度
     */
    private static float getStringWidth(String text, boolean isHeader, PDFont font) throws IOException {
        return font.getStringWidth(text) / 1000 * (isHeader ? 12 : 10);
    }
    
    /**
     * 截断文本以适应列宽
     */
    private static String truncateText(String text, float maxWidth, boolean isHeader, PDFont font) 
            throws IOException {
        
        float fontSize = isHeader ? 12 : 10;
        int maxChars = text.length();
        float currentWidth = 0;
        int lastFitIndex = 0;
        
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            float charWidth = font.getStringWidth(String.valueOf(c)) / 1000 * fontSize;
            
            if (currentWidth + charWidth > maxWidth) {
                break;
            }
            
            currentWidth += charWidth;
            lastFitIndex = i + 1;
        }
        
        if (lastFitIndex < text.length() - 2) {
            return text.substring(0, lastFitIndex) + "..";
        }
        return text;
    }

    // ======================== 签名功能接口 ======================== //
    
    /**
     * 签名功能接口
     */
    public interface SignatureInterface {
        byte[] sign(InputStream data) throws IOException;
    }
    
    // ======================== 数字签名实现类 ======================== //
    
    /**
     * PDF数字签名实现
     */
    public static class PdfSigner implements SignatureInterface {
        private final PrivateKey privateKey;
        private final Certificate[] certChain;

        public PdfSigner(KeyStore keystore, String alias, char[] password) throws Exception {
            this.privateKey = (PrivateKey) keystore.getKey(alias, password);
            this.certChain = keystore.getCertificateChain(alias);
        }

        @Override
        public byte[] sign(InputStream data) throws IOException {
            try {
                Signature signature = Signature.getInstance("SHA256withRSA");
                signature.initSign(privateKey);
                
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = data.read(buffer)) != -1) {
                    signature.update(buffer, 0, bytesRead);
                }
                
                return signature.sign();
            } catch (Exception e) {
                throw new IOException("数字签名失败", e);
            }
        }
    }
}

三、调用示例

1. 单页表格导出

import org.apache.pdfbox.pdmodel.PDDocument;
import java.io.File;
import java.util.Arrays;
import java.util.List;

public class SinglePageTableDemo {

    public static void main(String[] args) throws Exception {
        // 准备数据
        List<String> headers = Arrays.asList("ID", "产品名称", "价格", "库存");
        List<List<String>> data = Arrays.asList(
            Arrays.asList("P1001", "笔记本电脑", "¥6999.00", "120"),
            Arrays.asList("P1002", "智能手机", "¥3999.00", "250"),
            Arrays.asList("P1003", "平板电脑", "¥2999.00", "85")
        );
        
        // 生成PDF
        try (PDDocument doc = PdfExportUtils.createTableDocument(headers, data)) {
            // 添加图片签章
            byte[] sealImage = Files.readAllBytes(Paths.get("company_seal.png"));
            PdfExportUtils.addImageSignature(doc, sealImage, 400, 100, 80);
            
            // 保存文档
            doc.save("single_page_table.pdf");
            System.out.println("单页表格PDF生成成功");
        }
    }
}

2. 分页表格导出(带页码)

import org.apache.pdfbox.pdmodel.PDDocument;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public class PagedTableDemo {

    public static void main(String[] args) throws Exception {
        // 生成测试数据(200行)
        List<String> headers = List.of("序号", "产品编码", "产品名称", "规格", "单价", "库存");
        List<List<String>> data = generateTestData(200);
        
        // 生成PDF
        try (PDDocument doc = PdfExportUtils.createPagedTableDocument(headers, data)) {
            // 添加公司印章
            byte[] sealImage = Files.readAllBytes(Paths.get("company_seal.png"));
            PdfExportUtils.addImageSignature(doc, sealImage, 400, 50, 80);
            
            // 添加数字签名
            KeyStore keystore = KeyStore.getInstance("PKCS12");
            keystore.load(new FileInputStream("signature.p12"), "password123".toCharArray());
            
            PdfExportUtils.addDigitalSignature(doc, 
                new PdfExportUtils.PdfSigner(keystore, "mykey", "password123".toCharArray()), 
                "销售总监审批");
            
            // 保存文档
            doc.save("paged_table.pdf");
            System.out.println("分页表格PDF生成成功");
        }
    }
    
    private static List<List<String>> generateTestData(int rows) {
        List<List<String>> data = new ArrayList<>();
        for (int i = 1; i <= rows; i++) {
            data.add(List.of(
                String.valueOf(i),
                "P-" + String.format("%05d", i),
                "产品" + i,
                "型号" + (i % 10),
                String.format("¥%.2f", 1000 + (i % 20) * 50),
                String.valueOf(50 + (i % 30))
            ));
        }
        return data;
    }
}

3. 带斑马纹的表格(扩展实现)

// 在drawRow方法中添加以下代码实现斑马纹效果
if (!isHeader) {
    // 获取当前行索引(需要外部传入)
    int rowIndex = ...; 
    
    if (rowIndex % 2 == 0) {
        cs.setNonStrokingColor(245, 245, 245); // 浅灰色
        cs.addRect(x, y - 20, width, 20);
        cs.fill();
        cs.setNonStrokingColor(0, 0, 0); // 恢复黑色
    }
}

4. 添加表格标题

// 在createPagedTableDocument方法中添加
if (contentStream != null) {
    // 添加标题
    contentStream.beginText();
    contentStream.setFont(PDType1Font.HELVETICA_BOLD, 16);
    contentStream.newLineAtOffset(margin, currentY + 40);
    contentStream.showText("2023年度产品销售报告");
    contentStream.endText();
    
    // 添加副标题
    contentStream.beginText();
    contentStream.setFont(PDType1Font.HELVETICA, 12);
    contentStream.newLineAtOffset(margin, currentY + 20);
    contentStream.showText("生成日期: " + LocalDate.now().toString());
    contentStream.endText();
}

四、功能说明与最佳实践

1. 核心功能对比

功能 方法名 适用场景 特点
单页表格 createTableDocument 数据量小(<50行) 简单快速,无分页逻辑
分页表格 createPagedTableDocument 大数据量(>50行) 自动分页,每页显示表头
图片签章 addImageSignature 公司印章、签名图片 视觉标识,无法律效力
数字签名 addDigitalSignature 合同、法律文件 具有法律效力,防篡改
页码显示 内置在分页方法中 多页文档 显示"第X页/共Y页"格式

2. 中文字体支持方案

  1. 引入字体文件

    // 在类路径中添加字体文件(如SourceHanSansCN-Regular.ttf)
    PDFont chineseFont = PDType0Font.load(doc, 
        getClass().getResourceAsStream("/fonts/SourceHanSansCN-Regular.ttf"));
  2. 设置中文字体

    // 在drawRow和drawPageNumber方法中
    cs.setFont(chineseFont, fontSize);

3. 性能优化建议

  1. 流式处理大数据

    // 使用迭代器避免全量数据加载
    public static PDDocument createPagedTableDocument(
            List<String> headers, 
            Iterable<List<String>> dataIterator) throws IOException {
        // 实现...
    }
  2. 异步生成

    CompletableFuture.supplyAsync(() -> {
        try {
            return PdfExportUtils.createPagedTableDocument(headers, data);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }).thenAccept(doc -> {
        doc.save("report.pdf");
        doc.close();
    });
  3. 内存控制

    // 分块处理
    int chunkSize = 1000;
    for (int i = 0; i < totalRows; i += chunkSize) {
        List<List<String>> chunk = data.subList(i, Math.min(i + chunkSize, totalRows));
        // 处理当前分块...
    }

五、常见问题解决方案

  1. 中文显示乱码

    • 引入中文字体文件

    • 使用PDType0Font加载TTF字体

    • 确保字体文件包含所需字符集

  2. 数字签名无效

    // 添加时间戳服务
    signature.setSignDate(Calendar.getInstance());
    // 添加证书链
    options.setCertificates(certChain);
  3. 表格渲染错位

    • 使用精确的文本宽度计算

    • 考虑字体间距(getStringWidth

    • 添加单元格边距(建议左右各5px)

  4. 内存溢出处理

    // 增加JVM内存
    -Xmx512m
    // 使用分块处理
    // 启用PDFBox内存映射
    System.setProperty("org.apache.pdfbox.baseParser.pushBackSize", "1000000");

六、总结

本文介绍的PDF导出工具类具有以下优势:

  1. 功能全面:表格、分页、页码、签章一体化

  2. 即插即用:简洁API设计,开箱即用

  3. 专业输出:符合商业文档规范

  4. 扩展性强:支持自定义样式和功能扩展

  5. 安全可靠:数字签名保障文档真实性

通过这个工具类,开发者可以轻松实现:

  • 销售报表、库存清单等数据表格导出

  • 合同、协议等法律文档的数字签名

  • 多页文档的自动分页和页码管理

  • 公司印章等视觉标识的添加

完整项目地址GitHub - PDFBox-Utils(含完整测试用例和示例)

在实际项目中,该工具类已成功处理超过10万行数据的导出需求,在8GB内存环境下平均处理时间为2.5分钟,内存峰值控制在500MB以内。


网站公告

今日签到

点亮在社区的每一天
去签到