资讯动态

HTML转PDF工具类实战:ITextRenderer中文字体注册与CSS边界

发布时间:2026/10/18 9:16:30 来源:尧图企业网站定制
简介面向 Java 开发者的 HTML 转 PDF 实用工具包基于 ITextRenderer 封装了一个可直接调用的转换工具类解决报告导出、打印留存、离线阅读等常见需求。资源共 23 个文件压缩包大小 28.74MB内含 10 个依赖 jar、2 个 Java 源码、HTML 样例、XML 配置与中文字体文件并带 Eclipse 工程配置便于直接导入工程查看完整实现。已有 3249 人浏览学习适合 Java 初中级开发者快速落地 PDF 生成功能也适合需要批量生成电子文档的后端项目参考。工具类按照 setDocument → layout → createPDF 的主流程封装且项目中提供了示例 html 与字体文件能直观验证中文排版效果同时依赖清单齐全省去手动匹配 Flying Saucer、Batik 等 jar 版本的工作。若涉及复杂 CSS 或动态模板可结合自带 Freemarker 依赖继续扩展。下载后打开工程即可对照源码、依赖与 PDF 输出进行调试节省从零搭建环境的时间。1. 从 HTML 到 PDF这个 ITextRenderer 工具类到底解决了什么做 Java 后端的基本都遇到过这种需求把订单、报表、合同导出成 PDF。有人直接用 iText 摆坐标写文本改一版模板就得调一上午有人引入 ITextRenderer把 HTML 模块直接渲染成 PDF。ITextRenderer 是某开源渲染引擎项目常被称为 Flying Saucer的上层封装内部用 iText 落地专门负责把 XHTML 加 CSS 解析成 PDF 样式适合工单、报表这类以文本和表格为主的内容。这个工具类的核心价值只有一个给一个合法的 HTML 字符串调用方不关心字体、布局、流管理那些杂事拿到 byte[] 就能继续写文件。适合正在维护报表系统的开发者也适合新手想少走弯路直接复用的场景。2. 工具类怎么搭ITextRenderer 环境准备与字体注册2.1 类库选型为什么是 ITextRenderer 而不是直接写 iText直接拿 iText 写 PDF代码本身不难难的是换布局。一个订单模块可能包含表头、明细、备注、签字栏纯代码布局遇到字段增减就要动坐标等于每次改需求都在改底层代码。ITextRenderer 这种方案把 HTML 当中间层页面结构交给 CSSJava 这边只负责把字符串喂进去、把 PDF 吐出来。Maven 里引入的构件是org.xhtmlrenderer:flying-saucer-pdf它会传递依赖 OpenPDF 的底层 API。实际项目里常见两个包名老版本用com.lowagie.text.pdf.BaseFont新版可能用com.itextpdf.text.pdf.BaseFont这个看你的依赖树。版本号我不写死选你项目里验证过的稳定版即可但要注意它依赖的 iText 系列包会互相冲突如果项目里已经有用到 iText 的地方优先统一版本。dependency groupIdorg.xhtmlrenderer/groupId artifactIdflying-saucer-pdf/artifactId /dependency引入后第一件事不是写业务代码而是验证中文字体。如果你觉得“HTML 能显示中文PDF 当然也能”这会成为你踩的第一个坑。ITextRenderer 本身不感知操作系统的字体库必须在代码里显式注册一个中文字体文件否则中文全部变成黑色豆腐块。2.2 工具类骨架字体注册、HTML 解析、PDF 输出的顺序不能乱我一般把工具类写成静态方法输入是一个 HTML 字符串和一个 baseUrl输出是 byte[]。之所以返回 byte[]而不是直接写文件是为了让调用方决定写到本地还是传回前端。import com.lowagie.text.pdf.BaseFont; import org.xhtmlrenderer.pdf.ITextRenderer; import java.io.ByteArrayOutputStream; public final class HtmlPdfUtils { // 字体文件放在 src/main/resources/fonts/ 下 private static final String FONT_PATH /fonts/simsun.ttf; private HtmlPdfUtils() {} public static byte[] render(String html, String baseUrl) { ByteArrayOutputStream baos new ByteArrayOutputStream(); try { ITextRenderer renderer new ITextRenderer(); renderer.getFontResolver().addFont(FONT_PATH, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); renderer.setDocumentFromString(html, baseUrl null ? : baseUrl); renderer.layout(); renderer.createPDF(baos); } catch (Exception e) { throw new RuntimeException(HTML模块生成PDF失败, e); } return baos.toByteArray(); } }这段代码里的操作顺序值得细说。getFontResolver().addFont要在setDocumentFromString之前调用字体解析器先建立字体映射后续布局阶段才会用这个映射计算中文宽度和行高。BaseFont.IDENTITY_H是统一码的横向编码对中文来说基本是固定写法第三个参数建议用BaseFont.EMBEDDED这样 PDF 会把字体子集嵌进去换到没有宋体的服务器上也不会乱码。我代码里写的是NOT_EMBEDDED适合字体一定存在的内网环境通用一点还是建议嵌入。setDocumentFromString的第一个参数是一个完整的 HTML 文档不是一段 fragment后面会讲为什么。第二个参数 baseUrl 是资源解析的根路径它必须是一个 URL 形式的字符串比如file:///app/templates/不能直接传/app/templates/这种普通路径。layout()比较好理解就是让渲染器计算每个元素的大小和落点createPDF把结果写到 OutputStream。注意这个方法不会帮你关闭流ByteArrayOutputStream 本身不用关如果换成 FileOutputStream关闭时机由调用方控制。2.3 输入输出边界把文件还给调用方别让工具类干太多我第一次写这个工具类时方法签名是void render(String html, String outputPath)直接在工具类里写文件。看起来方便后来发现一个问题调用方想生成 PDF 后直接响应到前端下载或者存进数据库根本没有文件路径。逼着人家先写临时文件再读出来纯属多此一举。改用 byte[] 返回后调用方想怎么写都行byte[] pdfBytes HtmlPdfUtils.render(html, baseUrl); try (FileOutputStream fos new FileOutputStream(order.pdf)) { fos.write(pdfBytes); }这样做的另一个好处是异常处理变得清晰。工具类内部任何一步失败统一包装成 RuntimeException 抛出去调用方不需要关心字体注册失败和 HTML 解析失败有什么区别只需要处理“这个订单模板生成不了 PDF”这一个业务异常。还要提醒一个细节不要把 PDF 文件路径硬编码在工具类里。换一台服务器字体路径、模板路径都可能变。字体路径用 classpath 相对路径最保险模板路径通过 baseUrl 传进来这样工具类本身没有任何环境依赖测试也好写。3. 样式不“翻车”XHTML 合规与 CSS 兼容的边界3.1 你的 HTML 必须能被当作 XML 解析ITextRenderer 的 HTML 解析器不是浏览器它要求输入必须是格式良好的 XHTML。很多前端同事给的模板在浏览器里一切正常放到这个渲染器里直接报错标签没闭合、属性没加引号、没转义。我常用的说法是把它当 XML 写而不是当 HTML 写。举个例子img srclogo.png这种省略自闭合的写法浏览器没问题渲染器会报错。br同理。必须写成/结尾。布尔属性也是浏览器里input checked能用XHTML 里得写input checkedchecked/。!-- 浏览器正常、渲染器报错 -- img srclogo.png br !-- 渲染器正确姿势 -- img srclogo.png altlogo width80 height30/ br/另外正文里出现符号比如“Tom Jerry”必须转成amp;。模板里如果有大量这类字符建议生成 HTML 的阶段统一做转义处理别指望渲染器帮你纠正。这个坑属于出现一次就能记住的但第一次碰上排查起来很费时间因为报错信息往往含糊只告诉你某个位置的标签有问题。3.2 能用的 CSS 和你必须放弃的布局习惯ITextRenderer 内部做 CSS 解析但它支持的 CSS 版本偏老对 CSS3 布局基本无感。flex、grid完全不能指望position: absolute只在有限场景下有效。我亲眼见过同事把一个用 flex 排版的表单页面渲染成 PDF所有 div 全部纵向堆在一起跟散架了一样。这里给出我实测过比较稳的兼容边界CSS 功能兼容情况折中方案字体、字号、颜色稳定正常使用table 布局、border、background稳定主流排版建议全用 tablefloat部分支持少用容易错位flex / grid不支持改用 table 或内联块position: absolute有限简单角标可用复杂布局放弃page-break-before / after支持分页控制靠它page size / margin支持固定纸张大小用这个我建议模板里所有水平排列的内容比如单据的“客户名称”和“订单编号”并列就用一行三列的 table 实现不要用 div 加 float。边框、底色、间距这些用内联样式或者独立 CSS 类都行。还有一个综合技巧由于渲染器按 XML 处理class 和 id 选择器都能用但优先级和浏览器里的 CSS 规范一致写样式时不要依赖浏览器才支持的选择器。页面大小用page规则定义这个比较常用page { size: A4; margin: 1cm 1.5cm; }如果没有这条规则渲染器默认输出 A4但页边距按内置默认值走和业务上要求的“左边距 1.5 厘米”对不上。设置了之后布局阶段的宽度计算也会按这个可视区域走table 的百分比宽度才不会超出页面。3.3 图片与静态资源baseUrl 的正确传法PDF 里要放 logo、二维码、签章图代码上就是给 img 标签配一个合理的 src。真正的问题在于src 用相对路径的话渲染器去哪里找文件答案就在setDocumentFromString的第二个参数 baseUrl 里。我第一次写的时候直接把模板所在目录的绝对路径传进去结果图片全丢。后来查了文档才知道这个参数要的是 URL 形式的地址不是本地路径。正确做法是用Path.toUri().toString()做一次转换Path templateDir Paths.get(src/main/resources/templates).toAbsolutePath(); String baseUrl templateDir.toUri().toString(); byte[] pdfBytes HtmlPdfUtils.render(htmlString, baseUrl);假设模板目录下有images/logo.pngHTML 里写img srcimages/logo.png/渲染器就会基于 baseUrl 拼接出完整路径。baseUrl 末尾的斜杠我也踩过如果toUri()出来没有以/结尾拼接最后一段会出错我一般会在工具类里做防御判断一下不是/结尾就补上。图片格式方面JPG、PNG 基本没问题GIF 动图只会取第一帧这是底层图像解码的限制不用纠结。Base64 内嵌的图片某些版本支持但兼容性不稳定我不建议在生产模板里用。4. 避坑ITextRenderer 实战中五个高频问题与处置4.1 字体注册了还是“豆腐块”现象代码里 addFont 没有报错生成的 PDF 中文全是方块或空白。原因字体文件路径不对或者使用的是 .ttc 字体集合没有指定具体索引。解决优先把字体转成 .ttf 文件放到 classpath 下用getResource方式读取。如果只能用 .ttc在某些底层实现里需要给路径加索引后缀但不同环境表现不一致建议直接换 .ttf 一劳永逸。4.2 baseUrl 没按 URL 传图片全部丢失现象PDF 其他内容正常唯独图片区域一块空白不报错。原因baseUrl 传成了D:/project/templates/渲染器把它当成一个无法识别的协议拼接图片路径自然全失败。解决一律用Paths.get(...).toUri().toString()生成file:///D:/project/templates/这种形式。另外注意图片 src 不要以/开头否则会被当成根路径寻址最终指到磁盘根目录。4.3 flex 布局导致内容全部堆叠现象模板在浏览器里预览完全正常转成 PDF 后原本横向排列的区块全部变成纵向。原因这个渲染器的 CSS 解析器不支持 flex解析时跳过该属性元素回归默认块级排列。解决模板开发前先和前端确认PDF 版本用 table 布局重写横向结构。我见过一个项目因为这个问题改了三天样式最后把整块功能用 table 重写半小时就通了。4.4 表格行被跨页拦腰截断现象一张表格的某一行内容上半部分在第一页下半部分在第二页阅读体验极差。原因默认分页时不会自动保持行完整性。解决给需要保持完整的行加stylepage-break-inside: avoid;这属于渲染器支持得比较好的属性。如果还是压不住检查该行内是否有过高的图片或嵌套表格把行内内容拆细。更极端的情况是整表从中间断开我一般会在 table 上设table-layout: fixed;宽度固定后分页计算更规矩。4.5 createPDF 之后流没关文件损坏现象PDF 生成到一半或者文件打不开报“文件被占用”。原因createPDF不负责关闭输出流调用方的 FileOutputStream 没有 close。解决把流关闭放在 finally 块或者 try-with-resources 里。我习惯让工具类返回 byte[]就是为了把流的生命周期完全交给调用方从根上避免这种问题。如果一定要在工具类里写文件记得方法内部用 try-with-resources别把流对象传进传出。5. 进阶动态模板渲染、页脚页码与自动化验证5.1 动态模板把订单数据灌进 HTML 再渲染htmlToPdf 这个方法接收的是最终 HTML 字符串实际业务里 HTML 不可能写死常见的做法是用 FreeMarker 这类模板引擎。把订单数据丢进模板先生成完整的 XHTML 字符串再交给 ITextRenderer。MapString, Object model new HashMap(); model.put(orderNo, SD20250101); model.put(userName, 张三); String html FreeMarkerTemplateUtils.processTemplateIntoString(orderTemplate, model); byte[] pdfBytes HtmlPdfUtils.render(html, baseUrl);这里有个前提生成出的 HTML 必须是合规的 XHTML。FreeMarker 本身不负责帮你闭合标签模板里写什么就是什么。我在项目里的习惯是公共部分做成宏每个模块单独一个模板文件生成后用一个字符串校验器快速检查是否满足基本 XML 格式提前拦截问题。动态模板带来的另一个好处是页面上的“生成时间”“操作人”这些字段不用写在 Java 代码里模板自己就能处理。5.2 PDF 渲染后的自动化验证三步法生成 PDF 之后不能直接丢出去我每次都会做三层检查第一层看文件大小正常模板生成的 PDF 大小应该在合理区间比如订单模块大概几十 KB如果只有 1KB 说明可能内容为空第二层打开 PDF 检查中文和图片位置第三层是多数据抽查同一个模板换三四组不同长度的数据重点看分页有没有截断、页码是否连续。我常用一个简单脚本连续生成多个 PDF 后统计页数和大小再手动抽检其中分量最足的那份。这个方法帮我发现过两次表格行被截断的问题。从那以后我每次改完 HTML 模板都会强制走一遍浏览器先看再跑工具类生成 3 份不同内容的 PDF最后逐页对着原型核对确认没有乱码和截断再入库。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑