iText 这个库,在 Java 生态里摸爬滚打的程序员基本都绕不开。它的接口 API 覆盖面极广,从最简单的 PDF 生成,到复杂动态报表、电子签章,再到和 Flying Saucer 配合做 HTML 转 PDF,几乎所有“想把内容变成 PDF”的需求都能找到对应入口。这篇文章就结合我实际项目里踩过的坑,把 iText 接口 API 的关键用法、生僻字处理、暗坑排查一次讲透,适合刚接手 PDF 功能的开发同学,也适合已经在用但被乱码、分页折腾过的老手。
1. iText接口API是什么:先搞明白它能干嘛
1.1 一个PDF库能解决什么问题
iText 是一套用于生成和操作 PDF 文档的 Java 库,对外暴露的是一系列操作文档对象、内容元素、字体、图形、注解的接口 API。简单说,你在 Word 里做的事情——写字、画表格、插图片、设页眉页脚、搞目录——iText 都能用代码实现,而且是精确到坐标级别的控制。它不是“把 HTML 截个图”那种偏门方案,而是真正在 PDF 内部构建内容结构,生成的文档体积小、可检索、适合长期归档。
我最早接触 iText,是做一个电子合同项目。合同有固定模板,但是金额、日期、甲乙双方信息需要动态替换。用 iText 接口 API 可以直接在指定位置绘制文本和线条,比先生成 Word 再转 PDF 轻太多。后面又遇到过要批量导出上万份对账单、用 Flying Saucer 把前端写好的 HTML 报告转成 PDF 的场景,iText 都能稳稳接住。
适合谁来学?只要你需要在 Java 服务端生成 PDF,或者需要把 PDF 拆页、合并、加密、填表单,iText 就是绕不开的基础工具。前端也可以了解它的接口设计思路,很多 PDF 需求最终落到服务端都是这套逻辑。
1.2 iText5和iText7怎么选
这是新手问得最多的一个问题。iText 目前主流是 iText 5 和 iText 7 两个大版本,它俩的包名、API 风格差别很大。iText 5 的代码通常长这样:
java复制Document document = new Document();
PdfWriter.getInstance(document, new FileOutputStream("hello.pdf"));
document.open();
document.add(new Paragraph("Hello World"));
document.close();
iText 7 则是这样的:
java复制PdfWriter writer = new PdfWriter("hello.pdf");
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
document.add(new Paragraph("Hello World"));
document.close();
包名从 com.itextpdf.text.* 变成了 com.itextpdf.kernel.*、com.itextpdf.layout.*。如果是从 iText 5 升级到 iText 7,基本等于重写一遍业务代码。我的建议很简单:新项目直接上 iText 7,生态和后续更新都在往 7 上靠。老项目维护就别强行升了,稳定压倒一切。
iText 7 的设计更贴近“对象组合”思路:PdfDocument 代表底层的 PDF 文档对象,Document 是面向排版的高级容器,Paragraph、Table 是内容元素,PdfFont 负责字体。分层更清晰,扩展时不容易改一处崩一片。
要注意许可问题,iText 是 AGPL 协议,商用闭源项目想要避开开源传染,需要买商业授权。这个属于法务范畴,但作为开发者心里要有数,别等代码写完了才发现授权堵路。
1.3 对比其他PDF生成方案
Java 世界里 PDF 方案不少,除了 iText,还有 Apache PDFBox、OpenPDF(iText 5 的分支)、以及晚近流行的 Html2Pdf、wkhtmltopdf、Chromium 无头浏览器打印等。如果项目里已经有人用了某一种,别急着替换,先看应用场景。
- PDFBox:更偏向 PDF 文档的低层操作,适合做表单填写、文本抽取、文档合并,但排版能力弱,写一段漂亮的段落和表格要写很多代码。
- OpenPDF:API 和 iText 5 很接近,胜在开源许可友好(LGPL/MPL),适合对 AGPL 敏感的项目。缺点是更新节奏一般。
- wkhtmltopdf / Chromium 打印:适合“前端页面转 PDF”,排版能力强,但依赖系统服务和浏览器内核,部署环境比较重,高并发下资源占用也大。
- iText + Flying Saucer:可以做到“HTML/CSS 精美排版 + 服务端精准控制”,比较适合报表、票据、合同这类对版式有要求的场景。
iText 真正的优势是接口 API 完整,从底层 IO 到顶层布局都有官方支持,操作空间大。缺点也明显:API 庞杂,很多方法名相似但行为不同,不踩几次坑记不住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 上手实操:用iText接口API生成第一个PDF
2.1 引入依赖与中文字体准备
我以 Maven 项目为例,iText 7 的核心依赖只需要两个:
xml复制<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.2.5</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.2.5</version>
</dependency>
还需要引入 io 模块,用于处理图片和字体文件:
xml复制<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>7.2.5</version>
</dependency>
紧接着要处理字体。iText 默认字体是不支持中文的,直接写中文会变成乱码。准备一个中文字体文件,比如思源黑体、微软雅黑,放到 resources/fonts 目录下,后面注册字体用。
很多教程让你直接下载 iTextAsian.jar 之类的旧方案,那是 iText 5 时代的思路,iText 7 里直接加载系统字体文件或项目字体文件更干净。而且生僻字能不能显示,完全取决于字体包里有没有这个字形,和 iText 版本没太大关系。
2.2 创建文档的完整流程
一个完整的 PDF 输出流程,分四步:创建写入器、创建文档对象、填充内容、关闭文档。看代码:
java复制// 1. 指定输出路径
PdfWriter writer = new PdfWriter("output/hello.pdf");
// 2. 创建PdfDocument,这是PDF文档的核心对象
PdfDocument pdf = new PdfDocument(writer);
// 3. 创建Document,负责处理布局、分页
Document document = new Document(pdf);
// 4. 添加内容
document.add(new Paragraph("你好,iText!"));
// 5. 关闭文档
document.close();
PdfWriter 也可以接 OutputStream,这样就能把 PDF 输出到内存、HTTP 响应流里,接口下载场景很常用:
java复制ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
document.add(new Paragraph("Hello"));
document.close();
byte[] pdfBytes = baos.toByteArray();
要注意 Document.close() 会同时关闭底层 PdfDocument 和 PdfWriter,所以我一般只关最外层的 Document,不重复关流,避免输出流被提前关闭导致 PDF 损坏。
2.3 常用API接口详解:段落、表格、图片
iText 7 的内容元素都继承自 BlockElement,常用的几个:
Paragraph(段落):可以设置字体、字号、颜色、对齐方式、行距。它还支持用 add() 方法往里追加不同样式的文本片段,比如一个段落里,几个关键词要加粗或变红,可以这样做:
java复制Paragraph p = new Paragraph();
p.add(new Text("订单号:").setFontSize(10));
p.add(new Text("SN20240601").setBold().setFontColor(ColorConstants.RED));
document.add(p);
Table(表格):创建列数之后按顺序填充单元格即可。iText 的表是宽度自适应的,默认按页面可用宽度排列。看一段实际报表里常用的代码:
java复制float[] columnWidths = {100f, 200f, 150f};
Table table = new Table(columnWidths);
table.addCell("项目");
table.addCell("数量");
table.addCell("金额");
Cell cell = new Cell().add(new Paragraph("总金额"));
cell.setBackgroundColor(ColorConstants.LIGHT_GRAY);
table.addCell(cell);
每个 addCell 都会填充到下一个空白格,按行优先排列。如果单元格内容多,行高会自动撑开。表格分页问题后面单讲。
Image(图片):加载图片后指定宽度,避免图片过大撑破页面。常见操作是让它居中对齐:
java复制Image img = new Image(ImageDataFactory.create("logo.png"));
img.setWidth(120);
img.setHorizontalAlignment(HorizontalAlignment.CENTER);
document.add(img);
ImageDataFactory.create() 支持文件路径、URL、字节数组。从数据库读图片二进制时,用 ImageDataFactory.create(bytes) 很方便。
2.4 页面大小、边距与参数计算
Document 默认是 A4 纵向,页边距上下左右各 36 磅(约1.27厘米)。如果你要自定义,可以在创建 Document 时通过 PdfPageSize 和边距参数控制:
java复制Document document = new Document(pdf, PageSize.A4, new Margins(36, 30, 30, 30));
Margins 的参数顺序是上、右、下、左,别记反了。业务里常见需求是“每页固定多少行”,这时就要算一下页面可用高度和行高。
例如 A4 高度是 842 磅,上下边距各 36 磅,可用高度是 842 - 36 - 36 = 770 磅。段落默认字号 12 磅、行距 1.5 倍时,单行高约 18 磅,那一页大约能放 42 行。知道这个计算逻辑,你就能去控制分页位置了。
另一个常见坑是:PdfWriter 默认会把 PDF 版本写成某个较低版本,某些高级特性(比如带透明度、特定字体嵌入)可能导致预览器报错。我一般会显式设置 PDF 版本和压缩级别:
java复制PdfWriter writer = new PdfWriter("output.pdf");
writer.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
PdfDocument pdf = new PdfDocument(writer);
复杂模板配合 Canvas 类可以精确到坐标绘制,适合固定模板盖章、签名等场景,后面遇到再提。
3. 生僻字与复杂排版:iText + Flying Saucer 的进阶玩法
3.1 生僻字为什么会变成方块
不少人第一次遇到“iText 生成的 PDF 里生僻字全是方块”,第一反应是换编码、加 -Dfile.encoding=UTF-8,其实方向不对。
PDF 里的文本显示依赖字体文件里的字形表。你设置的字体如果不包含某个汉字的字形,渲染时就只能显示为占位符方块。生僻字,比如人名里出现的“煊、昶、赟”,常用字体不一定收录,所以就算你设置了“宋体”,它也可能没有这个字形。
解决办法是找到包含该字的字体。思源黑体、思源宋体这类开源字体对 CJK 覆盖比较全,生僻字体验好很多。如果是政府办事场景,用“方正小标宋”这类专有字体时,一定要确认授权和字形覆盖率。
3.2 注册系统字体解决生僻字问题
iText 7 里注册字体有几种方式,最可靠的是直接加载字体文件路径:
java复制PdfFont font = PdfFontFactory.createFont("/path/to/SourceHanSansCN-Regular.otf",
PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
这里有两个关键点:
- 编码要选
PdfEncodings.IDENTITY_H,这是 CID 字体编码,支持 Unicode,能覆盖绝大多数生僻字。别再用"UniGB-UCS2-H"这种老编码,生僻字容易漏。 EmbeddingStrategy.PREFER_EMBEDDED表示优先嵌入字体。嵌入后 PDF 在任何设备上打开都保持一致的显示效果,缺点是文件体积大。如果只是内部预览不想嵌入,可以用PREVIEW_AND_PRINT,看你的业务需求。
也可以批量注册整个字体目录:
java复制FontProgramFactory.registerDirectory("/fonts");
PdfFont font = PdfFontFactory.createRegisteredFont("SourceHanSansCN-Regular",
PdfEncodings.IDENTITY_H);
注册之后再创建 Paragraph 时指定字体:
java复制Paragraph p = new Paragraph("实名认证:王赟");
p.setFont(font);
document.add(p);
如果没指定字体,iText 7 会用默认 Helvetica,中文依然显示不了。所以项目里最好封装一个全局字体工厂类,打印所有中文字符都统一走这个字体实例。
3.3 Flying Saucer 渲染 HTML 转 PDF 的接口对接
Flying Saucer 是一个纯 Java 的 CSS 渲染引擎,它不自己生成 PDF,而是委托给底层的 PDF 库。网上资料很多还停留在 iText 2.x 时代,实际上 Flying Saucer 可以通过 ITextRenderer 这个适配类,和 iText 完成对接。
先引入依赖:
xml复制<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
代码里最核心的是 ITextRenderer:
java复制ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(htmlContent, baseUrl);
renderer.layout();
try (OutputStream os = new FileOutputStream("report.pdf")) {
renderer.createPDF(os);
}
这里 htmlContent 是完整的 HTML 字符串,baseUrl 用来解析相对路径的图片或 CSS 文件。注意 Flying Saucer 支持的是 XHTML,HTML 标签必须闭合、属性必须用引号,否则渲染会出问题。
当然它自身有字体解析逻辑,默认只能识别少数几种字体。要让 HTML 里的 CSS 字体生效,需要注册字体:
java复制ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont("/fonts/SourceHanSansCN-Regular.otf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
NOT_EMBEDDED 表示不嵌入字体,生成速度快,文件小。如果客户要求打开 PDF 时在所有电脑上都一样,就改成 BaseFont.EMBEDDED。
3.4 处理页面边距与CSS分页
Flying Saucer 渲染时,PDF 页面参数可以通过 @page 指令控制。在 HTML 里写:
css复制@page {
size: A4;
margin: 20mm;
}
这和浏览器打印的 @page 规则类似。注意 Flying Saucer 对 CSS3 支持有限,弹性和网格布局基本别碰,老老实实用 display: table、浮动和 position: absolute 实现分栏。
分页控制方面,和 Chrome 打印不同,Flying Saucer 支持 page-break-before: always、page-break-inside: avoid 这些经典属性。比如某个章节要从新的一页开始,给它的容器设:
css复制.chapter-title {
page-break-before: always;
}
表格头跨页重复可以用:
css复制thead {
display: table-header-group;
}
还有一个小技巧:Flying Saucer 对 font-family 的处理比较死板,就算你写 font-family: "SourceHanSansCN-Regular",它也可能匹配不上。最省事的办法是在 addFont 时记录字体注册名,然后 CSS 里用同一个名字。遇到 CSS 里字体生效但生僻字乱码,回去检查注册时用的编码,务必用 BaseFont.IDENTITY_H。
4. iText接口API实战中的问题排查清单
4.1 流关闭与内存溢出
生成 PDF 时最常见的运行时问题就是 PdfWriter 和 OutputStream 的关闭顺序。错误写法如下:
java复制FileOutputStream fos = new FileOutputStream("a.pdf");
PdfWriter writer = new PdfWriter(fos);
Document doc = new Document(new PdfDocument(writer));
doc.add(new Paragraph("test"));
doc.close();
fos.close();
这段看起来没问题,但 doc.close() 已经把 fos 关了,再次 fos.close() 在某些平台上会报 “Stream Closed”。建议只关最外层 Document,不再手动关流。
内存溢出主要发生在批量生成大批量 PDF 的场景。比如循环里每次 new Document(),结果忘了 close(),所有 PDF 对象都滞留在内存。另一个细节是图片处理,一张几 MB 的高清图片会被解码成几十 MB 的像素数据,循环生成时最好先压缩到目标尺寸再交给 iText:
java复制ImageData data = ImageDataFactory.create(bytes);
Image img = new Image(data);
img.scaleToFit(500, 400);
如果单次导出的 PDF 数量极大,建议每生成一个就立刻写出并 close(),不要囤在 List<byte[]> 里最后统一输出。
4.2 加粗斜体无效的处理
有同学在 Paragraph 上用 setBold() 后发现中文没有加粗。原因很简单:中文字体往往只注册了常规字重,没有注册粗体字重,iText 无法自动生成粗体。
解决方式有两个。一是使用独立的粗体字体文件,显式设置:
java复制PdfFont boldFont = PdfFontFactory.createFont("/fonts/SourceHanSansCN-Bold.otf",
PdfEncodings.IDENTITY_H);
paragraph.setFont(boldFont);
二是用 TextRenderMode 模拟加粗,比如给文本多描边一次。但对生僻字来说,用独立粗体文件更稳妥,很多字体文件本身粗体里面会保留部分常规字重没有的细节字形。
斜体同理,中文没有“斜体”传统,一般用 setSkew(12, 0) 做视觉倾斜,不要指望 setItalic() 对每个字体都生效。
4.3 表格跨页断行的处理
报表里表格超过一页时,默认会直接断开,甚至表头不会重复,看起来非常不专业。iText 7 中处理方式:
java复制Table table = new Table(columnWidths);
table.setHeaderRows(1);
table.setSkipFirstHeader(true);
setHeaderRows(1):指定第一行为表头,跨页时自动在每页顶部重复。setSkipFirstHeader(true):第一页不重复表头,因为本来就在开头。
如果希望某一行不能被拆开,可以设置:
java复制Cell cell = new Cell();
cell.setKeepTogether(true);
这个让单元格内容尽量保持在同一页。如果整行内容过长,系统仍会强行拆分,所以逻辑上尽量控制行高。
另一个和表格相关的暗坑是表格宽度。不同列的宽度总和超过了当前页可用宽度,渲染时会自动压缩列宽,造成文字折行、布局错乱。算宽度时把左右边距留足,别用满整页 A4。
4.4 高频问题速查表
我在做 PDF 导出时整理了一个速查表,遇到问题直接查,节省不少时间。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 中文乱码/生僻字方块 | 未注册 CJK 字体,或字体不含字形 | 加载思源黑体等字体,编码用 IDENTITY_H |
| PDF文件打不开或损坏 | 流提前关闭或未关 Document |
只关最外层 Document |
| 图片变形或过大 | 图片比例未设置 | 用 scaleToFit 按比例缩放 |
| 表格跨页表头消失 | 未设置 setHeaderRows |
设置表头行数 |
| 中文加粗无效 | 使用了单一常规字重字体 | 加载独立 Bold 字体 |
| 字体嵌入后文件很大 | 字体被完整嵌入 | 按需使用 NOT_EMBEDDED |
| Flying Saucer CSS 不生效 | 使用了不支持的 CSS3 布局 | 改用 table/float 布局 |
| 生成的PDF文本无法搜索 | 字体编码设置错误 | 用 PdfEncodings.IDENTITY_H |
| 批量导出内存飙升 | 未及时关闭资源 | 循环内及时 close() |
| 页面边距不对劲 | Margins参数顺序错误 |
按上、右、下、左设置 |
排查时优先看字体,再查流和布局,将近一半问题的根源都在这三个方向。PDF 渲染比较特殊,没有浏览器那种“开发者工具”可看,所以调试时不要靠猜,把关键参数打印出来,逐步缩小范围。比如表格宽度是否超出页面可用宽度,可以用页面宽度减去左右边距算出来,比对一下就知道问题在哪。
5. 一些实际操作中的体会
iText 接口 API 的边界很深,但从“能用”到“用得顺”之间,主要就隔了几个认知门槛:字体怎么选、流怎么关、表格分页怎么控。这些坑在一次完整交付后基本都能摸清,但第一次踩的时候确实头疼。
我个人在实际项目里,习惯把字体、页面基础配置封装成公共组件,所有 PDF 生成代码复用同一套字体工厂和页面参数。这样即使后来有新人加入,也不容易出现中文字体不一致、边距不统一的问题。另一个经验是:能用 Flying Saucer 渲染 HTML 转 PDF 的场景,尽量不要手写长表格和复杂布局,HTML/CSS 的调试效率远高于手写 iText 布局代码,但最终生成 PDF 的精度控制还是要靠 iText 的底层能力来兜底。
最后再分享一个小技巧:如果你发现生成出来的 PDF 在某个 PDF 阅读器里文字重叠或表格错位,先别怀疑 iText 的 API 用错了,用 Adobe Acrobat 或 macOS 预览器再打开对比一次。很多阅读器对 PDF 标准的支持不完整,会导致同样的文档显示效果不同,这时候要做的不是改代码,而是和客户确认他们用什么阅读器打开。稳一点的做法是生成 PDF 时设置 PdfADefaults 和标准的 PDF/A 输出,兼容性会好很多,适合对归档有硬性要求的项目。
