跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 富文本内容生成 PDF 完整落地指南

介绍使用 Java 将富文本内容转换为 PDF 文件的完整方案。技术栈包括 Jsoup 进行 HTML 清洗与 XSS 防护,FreeMarker 渲染模板,Flying-Saucer 结合 iText 生成 PDF。核心流程涵盖富文本标准化、中文字体动态嵌入、图片尺寸适配及分页处理。文中提供了实体类设计、工具类实现代码及常见问题解决方案,如中文乱码、图片断开和样式失效等,适用于 B 端系统报表导出场景。

星星泡饭发布于 2026/3/28更新于 2026/7/2350 浏览
Java 富文本内容生成 PDF 完整落地指南

Java 富文本内容生成 PDF 文件

演示环境:JDK 8 + Spring Boot

1. 前言

B 端系统常见需求:用户在富文本编辑器里撰写月度总结 → 系统一键导出版式固定、中文字体完整、可直接打印的 PDF 文件。 本文给出一个开箱即用的 Java 实现,覆盖:

  1. 富文本清洗 → 防 XSS、图片/表格适配 PDF
  2. FreeMarker 渲染静态 HTML
  3. Flying-Saucer + iText 2.x 生成 PDF
  4. 中文字体(宋体、黑体、楷体、仿宋、小标宋)动态加载 & 嵌入
  5. 本地 | MinIO 双存储策略
  6. 常见坑(图片断开、字体缺失、分页错乱)定位与修复

2. 技术选型

功能组件版本备注
富文本过滤Jsoup1.17.2白名单模式,保留基本样式
模板引擎FreeMarker2.3.32纯 Java,不依赖 Web
HTML→PDFFlying-Saucer9.1.22基于 iText 2.x,完美支持 CSS 2.1
字体加载iText2.1.7支持 TTC/TTF 嵌入
文件存储MinIO / 本地最新通过配置切换

3. 项目结构

src
├── main
│   ├── java/org/example
│   │   ├── entity/
│   │   ├── service/
│   │   └── utils/
│   └── resources/
│       ├── fonts/
│       └── templates/

4. 核心流程

BrowserReportService → RichTextProcessor → HtmlEscapeUtil → PdfGenerateUtil → Storage 提交富文本报告 JSON → processRichTextContent() 清洗后的 HTML → createEscapedReportForRichText() 仅转义纯文本字段 → generatePdf("monthly_report.ftl", model) byte[] → 上传 / 本地落盘返回 PDF 下载地址

5. 代码详解

5.1 实体类(字段节选)
@Data
@Table(name = "monthly_work_report")
public class MonthlyWorkReport implements Serializable {
    private Long id;
    private String month; // 2024-12
    private String creator; 
     String departmentName;
     String reportTitle; 

    
     String monthlyKeyWorkCompletion;
     String monthlyWorkOverview;
     String nextMonthWorkPlan;
     String reviewIssueCoordination;

    
     String fileName;
     String fileUrl;
}
// 填报人
private
private
// 自定义标题
// ======== 富文本字段 ========
private
private
private
private
// ======== 文件相关 ========
private
private
5.2 富文本清洗 & 标准化
@Component
@Slf4j
public class RichTextProcessor {
    public void processRichTextContent(MonthlyWorkReport report) {
        report.setMonthlyKeyWorkCompletion(processFieldContent(report.getMonthlyKeyWorkCompletion()));
        // ... 其余字段同理
    }

    private String processFieldContent(String content) {
        if (content == null || content.trim().isEmpty()) {
            return "<p></p>";
        }
        // 1. 基本 XHTML 修复
        String fixed = fixXhtmlCompatibility(content);
        // 2. 白名单过滤
        Safelist safelist = Safelist.relaxed()
                .addTags("div","span","hr","thead","tbody")
                .addAttributes(":all","style","class","colspan","rowspan")
                .addAttributes("img","src","alt")
                .addProtocols("img","src","data","http","https");
        String cleaned = Jsoup.clean(fixed, safelist);
        // 3. 图片尺寸限制
        cleaned = processImages(cleaned);
        // 4. 表格样式
        cleaned = processTables(cleaned);
        // 5. 样式标准化
        return normalizeStyles(cleaned);
    }

    private String processImages(String html) {
        org.jsoup.nodes.Document doc = Jsoup.parse(html);
        doc.select("img").forEach(img -> {
            img.removeAttr("width").removeAttr("height");
            img.attr("style","max-width:400pt !important;max-height:300pt !important;");
            if(!img.hasAttr("alt")) img.attr("alt","图片");
        });
        return doc.body().html();
    }

    private String fixXhtmlCompatibility(String html) {
        return html.replaceAll("<br>","<br/>")
                   .replaceAll("<hr>","<hr/>")
                   .replaceAll("<img([^>]+)>","<img$1/>");
    }
    // ... 其余方法见源码
}
5.3 模板示例(monthly_report.ftl)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>${rep.reportTitle!"月度工作报告"}</title>
<style>
@page { size: A4; margin: 2.5cm 2cm 2cm 2cm; @bottom-center { content:"第 "counter(page)" 页"; font-family:"SimSun", serif; font-size: 10.5pt; } }
body { font-family:"SimSun","宋体", serif; font-size: 16pt; line-height: 28pt; color: #000; text-align: justify; }
.main-title { font-family:"方正小标宋简", serif; font-size: 22pt; text-align: center; margin: 0 0 40px; }
.section-title { font-family:"SimHei", serif; font-size: 16pt; margin: 35px 0 20px; page-break-after: avoid; }
/* 其余样式略 */
</style>
</head>
<body>
<div class="main-title">${rep.reportTitle}</div>
<div class="basic-info">
    <div><strong>报告人:</strong>${rep.creator!"-"}</div>
    <div><strong>所在部门:</strong>${rep.departmentName!"-"}</div>
    <div><strong>报告月份:</strong>${rep.month!"-"}</div>
</div>
<!-- 一、重点工作清单事项完成情况 -->
<div class="section">
    <div class="section-title">一、重点工作清单事项完成情况</div>
    <div class="rich-content">
        <#attempt> ${rep.monthlyKeyWorkCompletion!} 
        <#recover>
            <div class="plain-text-fallback">暂无内容</div>
        </#attempt>
    </div>
</div>
<!-- 其余章节同理 -->
</body>
</html>
5.4 PDF 生成工具类(核心)
@Slf4j
@Component
public class PdfGenerateUtil {
    @Resource
    private Configuration freemarkerConfig;

    public byte[] generatePdf(String templateName, Map<String,Object> model) throws Exception {
        // 1. 渲染 HTML
        Template template = freemarkerConfig.getTemplate(templateName);
        String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
        // 2. XHTML 兼容修复
        html = validateAndFixHtml(html);
        // 3. 图片尺寸强制转换(pt 单位)
        html = processImageDimensionsForPdf(html);
        // 4. 生成 PDF
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ITextRenderer renderer = new ITextRenderer();
            configureChineseFonts(renderer);
            renderer.setDocumentFromString(html);
            renderer.layout();
            renderer.createPDF(out);
            renderer.finishPDF();
            return out.toByteArray();
        }
    }

    private void configureChineseFonts(ITextRenderer renderer) throws Exception {
        String[][] fontConfigs = {
            {"宋体","fonts/simsun.ttc"},
            {"黑体","fonts/simhei.ttf"},
            {"楷体_GB2312","fonts/楷体_GB2312.TTF"},
            {"仿宋_GB2312","fonts/仿宋_GB2312.TTF"},
            {"方正小标宋简体","fonts/方正小标宋简.TTF"}
        };
        for (String[] fc : fontConfigs) {
            try (InputStream is = getClass().getClassLoader().getResourceAsStream(fc[1])) {
                if (is == null) { log.warn("字体文件不存在:{}", fc[1]); continue; }
                File tmp = createTempFontFile(is, fc[1]);
                renderer.getFontResolver().addFont(tmp.getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
                log.info("成功加载字体:{}", fc[0]);
            }
        }
    }

    private File createTempFontFile(InputStream in, String path) throws IOException {
        String name = path.substring(path.lastIndexOf('/')+1);
        File tmp = File.createTempFile("font_", "_" + name);
        tmp.deleteOnExit();
        Files.copy(in, tmp.toPath(), StandardCopyOption.REPLACE_EXISTING);
        return tmp;
    }

    private String validateAndFixHtml(String html) {
        if (html == null) return "";
        org.jsoup.nodes.Document doc = Jsoup.parse(html);
        doc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
        doc.select("img").forEach(img -> { if (!img.tag().isSelfClosing()) img.tagName("img"); });
        return doc.html();
    }

    private String processImageDimensionsForPdf(String html) {
        return html.replaceAll("(max-width:\s*)\d+px","$1400pt")
                   .replaceAll("(max-height:\s*)\d+px","$1300pt");
    }
}
5.5 防 XSS 工具类
@Component
public class HtmlEscapeUtil {
    /**
     * 富文本字段不做 HTML 转义,仅对纯文本字段转义
     */
    public MonthlyWorkReport createEscapedReportForRichText(MonthlyWorkReport original) {
        MonthlyWorkReport escaped = new MonthlyWorkReport();
        escaped.setMonth(escapeHtmlContent(original.getMonth()));
        escaped.setCreator(escapeHtmlContent(original.getCreator()));
        escaped.setDepartmentName(escapeHtmlContent(original.getDepartmentName()));
        escaped.setReportTitle(escapeHtmlContent(original.getReportTitle()));
        // 以下字段已清洗,不再转义
        escaped.setMonthlyKeyWorkCompletion(original.getMonthlyKeyWorkCompletion());
        escaped.setMonthlyWorkOverview(original.getMonthyWorkOverview());
        escaped.setNextMonthWorkPlan(original.getNextMonthWorkPlan());
        escaped.setReviewIssueCoordination(original.getReviewIssueCoordination());
        return escaped;
    }

    private String escapeHtmlContent(String content) {
        if (content == null) return "";
        return content.replace("&","&amp;")
                      .replace("<","&lt;")
                      .replace(">","&gt;")
                      .replace("\"","&quot;")
                      .replace("'","&#39;");
    }
}

6. 常见坑汇总

问题现象解决方案
中文乱码方框/问号嵌入字体 + 使用 BaseFont.IDENTITY_H
图片断开空白或报错1. 限宽 400pt 2. base64 图片需 data:image/xxx;base64, 前缀
分页错乱表格/标题截断page-break-inside: avoid;
样式失效字体大小异常px→pt、CSS 2.1 子集
图片过大撑破版面横向溢出max-width:400pt !important; display:block; margin:auto

7. 效果展示

7.1 接收到富文本内容示例
MonthlyWorkReport report = new MonthlyWorkReport();
// 基本信息
report.setMonth("2024-12");
report.setCreator("张三");
report.setCreatorId("user_001");
report.setDepartmentId("dept_001");
report.setDepartmentName("技术管理中心");
report.setReportTitle("2024 年 12 月月度工作报告");
report.setReportType(1); // 1 表示在线编辑
// 模拟前端富文本数据(包含各种样式)
report.setMonthlyKeyWorkCompletion("<p><strong>(一)公司截至 9 月 30 日生产情况</strong></p>"
    +"<p class=\"ql-align-justify ql-indent-1\">1.公司收入(验工计价)</p>"
    +"<p class=\"ql-align-justify ql-indent-1\">2.分包结算</p>"
    +"<p><strong>(二)三季度重点工作清单事项完成情况</strong></p>"
    +"<p class=\"ql-indent-1\">技术管理中心待办事项共 5 项,已完成任务 2 项,持续推进 3 项。</p>"
    +"<p class=\"ql-indent-1\">1<strong>.</strong>已完成任务总结如下:</p>"
    +"<p class=\"ql-indent-1\">\t<strong>序号 6,第二批\"揭榜挂帅\"工作。</strong></p>"
    +"<p class=\"ql-indent-1\">\t<strong>完成情况:</strong>组织开展榜挂帅项目揭榜申报,<strong>一是</strong>组织开展报名和申报答辩等各项工作通知书;<strong>二是</strong>开展工作复盘,总结成效和问题,制定下一步优化方案。</p>"
    +"<p class=\"ql-indent-1\">\t<strong>序号 37,智项目启动。</strong></p>"
    +"<p class=\"ql-indent-1\">\t<strong>完成情况:</strong>\"智\"日召开项目策划会,启动项目建设。</p>"
    +"<p class=\"ql-indent-1\">2<strong>.</strong>持续推进中任务进展如下:</p>"
    +"<p class=\"ql-indent-1\">\t<strong>序号 8,构建包专家库。</strong></p>"
    +"<p class=\"ql-indent-1\">\t<strong>完成情况:</strong>优化完善专家库建设方案,梳理专家入库标准,细化各部门职责分工方案。</p>"
    +"<p class=\"ql-indent-1\">\t<strong>序号 9,优化水平。</strong></p>"
    +"<p class=\"ql-indent-1\">\t<strong>完成情况:</strong>开发在线工具,统一各部门生产经营数据出口,实现大交班会材料线上呈现。</p>"
    +"<p><br></p>"
    +"<p><strong>(三)三季度重点工作完成情况</strong></p>"
    +"<p class=\"ql-indent-1\"><strong>1.落实科数部工作</strong></p>"
    +"<p class=\"ql-indent-1\">\t一是完善中国中铁\"十。\"</p>"
    +"<p class=\"ql-indent-1\">\t二是修订信息\"</p>"
    +"<p class=\"ql-indent-1\">\t三是支持科数\"</p>"
    +"<p class=\"ql-indent-1\"><strong>2.项目管理</strong></p>"
    +"<p class=\"ql-indent-1\">\t一是基于股份亿元。\"</p>"
    +"<p class=\"ql-indent-1\">\t二是完成项目策划、变更、验收、收入及成本等流程调整。\"</p>"
    +"<p class=\"ql-indent-1\">\t三是按照股份公同落实。\"</p>"
    +"<p class=\"ql-indent-1\">\t四是组织存在项目材料中编制日期与合同周期不匹配问题。\"</p>"
    +"<p class=\"ql-indent-1\"><strong>3.科研管理</strong></p>"
    +"<p class=\"ql-indent-1\"><strong>4.信息化管理</strong></p>"
    +"<p class=\"ql-indent-1\">\t<strong>(1)信息系统维护</strong></p>"
    +"<p class=\"ql-indent-2\">① 完成 OA 系统升级</p>"
    +"<p class=\"ql-indent-2\">② 优化数据库性能</p>"
    +"<p class=\"ql-indent-1\">\t<strong>(2)网络安全保障</strong></p>"
    +"<p class=\"ql-indent-2\">① 开展安全演练</p>"
    +"<p class=\"ql-indent-2\">② 漏洞修复与防护</p>");
7.2 生成的 PDF 内容

此处为生成的 PDF 预览图,展示排版效果、字体嵌入情况及分页处理结果。

8. 结语

至此,我们完成了:

  • 富文本 → 安全 HTML
  • HTML → 标准 XHTML
  • XHTML + CSS → 嵌入字体 PDF
  • 本地 / MinIO 双存储

整套代码已在生产环境跑通,直接拷贝即可启动。

目录

  1. Java 富文本内容生成 PDF 文件
  2. 1. 前言
  3. 2. 技术选型
  4. 3. 项目结构
  5. 4. 核心流程
  6. 5. 代码详解
  7. 5.1 实体类(字段节选)
  8. 5.2 富文本清洗 & 标准化
  9. 5.3 模板示例(monthly_report.ftl)
  10. 5.4 PDF 生成工具类(核心)
  11. 5.5 防 XSS 工具类
  12. 6. 常见坑汇总
  13. 7. 效果展示
  14. 7.1 接收到富文本内容示例
  15. 7.2 生成的 PDF 内容
  16. 8. 结语
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 验证回文串:双指针解法
  • 基于 Prometheus 与 cpolar 的远程监控告警配置教程
  • 前端大屏展示技术指南
  • 基于 OpenClaw 与飞书开放平台实现 AI 新闻推送机器人
  • LLM 模板注册机制与 QwenTemplate 源码解析
  • Spring Web 项目核心配置:web.xml 关键组件解析
  • 实战 Pi0 机器人控制中心:实现机器人智能操控
  • Windsurf AI IDE 深度使用指南
  • Stable Diffusion 2.0 技术革新:基础模型架构的混合进化与行业变革
  • VSCode Copilot 登录失败常见原因与解决方案
  • Kotlin 变量初始化策略:立即、延迟与惰性
  • 开源 RAG 引擎 RAGFlow 部署与实战指南
  • React 结合 Microi 吾码实现低代码开发教程
  • Trae 高峰期模型排队问题及无问芯穹接入方案
  • 代码中介翻译:从语言模型蒸馏视觉推理数据的探索
  • C++ map 与 set 容器使用详解
  • Web 自动化测试入门指南:从概念到 Selenium 实战
  • 大模型算法二次开发:核心流程与关键技术详解
  • 基于 ROS 与 MAVROS 的无人机定点飞行控制脚本
  • C++ explicit 关键字:防止隐式类型转换

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online