企业级方案:Java 动态模板转 PDF 并输出高清晰度图片

(Freemarker + iText7 + PDFBox 深度实战)

1. 为什么选择这个组合?

在生产环境中,我们需要解决三大痛点:

  1. 样式还原度:iText7 配合 html2pdf 对 CSS3 的支持远超 iText5。

  2. 中文乱码:通过自定义 FontProvider 完美解决。

  3. 动态图片:支持将 Base64 或网络图片直接注入模板。


2. 依赖管理 (Maven)

引入最新的稳定版本,确保功能最全。

code Xml

downloadcontent_copy

expand_less

<properties>
    <itext.version>4.0.5</itext.version>
    <pdfbox.version>2.0.29</pdfbox.version>
</properties>

<dependencies>
    <!-- 模板引擎 -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.31</version>
    </dependency>
    <!-- iText7 HTML转PDF核心 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>html2pdf</artifactId>
        <version>${itext.version}</version>
    </dependency>
    <!-- PDFBox 用于PDF转图片 -->
    <dependency>
        <groupId>org.apache.pdfbox</groupId>
        <artifactId>pdfbox</artifactId>
        <version>${pdfbox.version}</version>
    </dependency>
    <!-- 常用工具类 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
        <optional>true</optional>
    </dependency>
</dependencies>

3. 高级 HTML 模板设计 (report.ftl)

为了体现专业性,我们加入表格、Base64图片、分页符页眉页脚区域

code Html

play_circledownloadcontent_copy

expand_less

<!DOCTYPE html>
<html>
<head>
    <style>
        @page {
            size: A4;
            margin: 50pt;
            /* 定义页眉 */
            @top-center { content: "XX集团业务报告"; font-family: 'SimSun'; font-size: 9pt; color: #999; }
            /* 定义页脚显示页码 */
            @bottom-right { content: "第 " counter(page) " 页,共 " counter(pages) " 页"; font-family: 'SimSun'; font-size: 9pt; }
        }
        body { font-family: 'SimSun'; line-height: 1.6; }
        .header { border-bottom: 2px solid #333; padding-bottom: 10px; text-align: center; }
        .logo { width: 100px; height: auto; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        .page-break { page-break-after: always; } /* 强制分页 */
        .highlight { color: #e74c3c; font-weight: bold; }
    </style>
</head>
<body>
    <div class="header">
        <img src="${logoBase64}" class="logo" />
        <h1>${title}</h1>
    </div>

    <div class="content">
        <p>尊敬的 <span class="highlight">${userName}</span>:</p>
        <p>以下是您的季度消费明细:</p>
        <table>
            <thead>
                <tr>
                    <th>日期</th>
                    <th>项目</th>
                    <th>金额</th>
                </tr>
            </thead>
            <tbody>
                <#list items as item>
                <tr>
                    <td>${item.date}</td>
                    <td>${item.name}</td>
                    <td>¥${item.price}</td>
                </tr>
                </#list>
            </tbody>
        </table>
    </div>

    <!-- 分页示例 -->
    <div class="page-break"></div>
    <div class="content">
        <h2>附件:详细条款</h2>
        <p>此处为第二页内容...</p>
    </div>
</body>
</html>

4. 核心工具类封装

4.1 资源路径与字体处理

在生成PDF时,最麻烦的是字体。建议将 .ttf 字体文件放在 resources/fonts 下。

code Java

downloadcontent_copy

expand_less

@Slf4j
public class PdfGeneratorUtil {

    /**
     * 执行HTML转PDF
     * @param htmlContent 填充后的HTML字符串
     * @param destFile 目标文件
     */
    public static void createPdf(String htmlContent, File destFile) throws IOException {
        ConverterProperties props = new ConverterProperties();
        FontProvider fontProvider = new FontProvider();
        
        // 1. 自动注册系统字体(Windows/Linux下存在的字体)
        fontProvider.addSystemFonts();
        
        // 2. 显式注册自定义字体(解决Linux环境无宋体的问题)
        // String fontPath = "src/main/resources/fonts/simsun.ttf";
        // fontProvider.addFont(fontPath);
        
        props.setFontProvider(fontProvider);
        
        // 3. 设置基础路径,方便读取相对路径的图片
        // props.setBaseUri("src/main/resources/static/");

        try (FileOutputStream os = new FileOutputStream(destFile)) {
            HtmlConverter.convertToPdf(htmlContent, os, props);
        }
    }
}

4.2 PDF 转多张高清图片

如果 PDF 有多页,我们需要循环渲染并拼接或单独保存。

code Java

downloadcontent_copy

expand_less

public class ImageGeneratorUtil {
    /**
     * PDF转图片
     * @param pdfFile 源文件
     * @param outputDir 输出目录
     * @param dpi 渲染精度,建议300
     */
    public static List<String> pdfToImages(File pdfFile, String outputDir, int dpi) throws IOException {
        List<String> imagePaths = new ArrayList<>();
        try (PDDocument document = PDDocument.load(pdfFile)) {
            PDFRenderer renderer = new PDFRenderer(document);
            for (int i = 0; i < document.getNumberOfPages(); i++) {
                BufferedImage image = renderer.renderImageWithDPI(i, dpi);
                String fileName = "page_" + (i + 1) + ".png";
                File outFile = new File(outputDir, fileName);
                ImageIO.write(image, "PNG", outFile);
                imagePaths.add(outFile.getAbsolutePath());
            }
        }
        return imagePaths;
    }
}

5. 业务逻辑整合

code Java

downloadcontent_copy

expand_less

@Service
public class DocumentService {

    public void generateReport(String userId) throws Exception {
        // 1. 模拟数据准备
        Map<String, Object> data = new HashMap<>();
        data.put("title", "2023年度消费报告");
        data.put("userName", "王小明");
        data.put("logoBase64", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."); // 实际使用时替换为真实Base64
        
        List<Map<String, String>> items = new ArrayList<>();
        items.add(Map.of("date", "2023-01-01", "name", "云服务器续费", "price", "1200.00"));
        items.add(Map.of("date", "2023-02-15", "name", "域名注册", "price", "55.00"));
        data.put("items", items);

        // 2. Freemarker 渲染 HTML
        String html = TemplateUtil.processTemplate("report.ftl", data);

        // 3. 生成 PDF
        File pdfFile = new File("D:/temp/report.pdf");
        PdfGeneratorUtil.createPdf(html, pdfFile);

        // 4. PDF 转 高清图片 (第一页)
        ImageGeneratorUtil.pdfToImages(pdfFile, "D:/temp/images/", 300);
        
        log.info("全流程处理完成!");
    }
}

6. 避坑指南(进阶必看)

  1. Linux 环境部署

    • 现象:本地 Windows 正常,发到服务器中文全乱码。

    • 根因:Linux 默认没有 SimSun (宋体)。

    • 对策:必须在 Java 代码中通过 fontProvider.addFont("path/to/simsun.ttf") 手动加载字体文件。

  2. 图片加载失败

    • 现象:<img> 标签找不到图片。

    • 对策:推荐将图片转为 Base64 字符串通过模板注入,这样不需要处理复杂的文件路径问题,也不会受限于内网防火墙。

  3. 内存溢出

    • 现象:生成超大型 PDF(几百页)时 OOM。

    • 对策:iText7 支持 PdfDocument 的流式写入。对于 PDF 转图片,避免一次加载过大的 PDF 文件,或者分批处理页面。

  4. CSS 支持限制

    • iText7 虽然强大,但不支持 flex 和 grid 布局。请回归到 float 布局或 table 布局。


7. 总结

本文展示了从模板引擎渲染PDF高保真转换,再到高清图像输出的完整闭环。这套方案不仅适用于简单的证书生成,也能胜任复杂的金融报表导出。

源码获取/讨论:欢迎在评论区交流,点赞收藏不迷路!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐