
iText是Java生态里最老牌的PDF处理工具库之一我做后端开发这几年凡是遇到生成PDF、解析PDF、合并PDF这些需求基本都是靠它撑场子。合同打印、电子账单、发票存档、报告导出……这些常见的PDF场景用iText都能搞定。这篇入门教程我不打算罗列API文档而是从实际干活的角度把搭建环境、创建PDF、处理中文字体、解析PDF、合并拆分加密这些最常见场景一步一步过一遍。适合刚接触iText或者被PDF乱码、版本差异折磨过的同学参考。1. 为什么是iText以及版本与许可证那些事1.1 iText到底能干什么很多朋友刚接触PDF编程时最大的困惑不是怎么写代码而是不知道该选哪个库。Java生态里处理PDF的库其实不少有PDFBox、OpenPDF、Apache PDFBox等但iText应该说是功能最全面、社区资料最丰富的一个。它能做四类事情第一是生成也就是从零创建一个带文字、表格、图片、条码的PDF第二是解析把一个已有的PDF读进来提取里面的文本、元数据甚至按坐标定位取数据第三是编辑在现有PDF上合并页面、拆分文档、加页眉页脚、套个水印第四是表单处理填写PDF表单控件里的内容或者读取已填写的字段。平时我们说的电子签章、合同套打等功能很多底层实现就是iText。这个库的应用范围远比你想象的大。举个常见的例子电商平台的下单流程里用户确认订单后往往会生成一份电子合同或发票PDF这种需求如果用iText来做只需要在后端把订单数据拼接成一个模板再调用iText输出PDF整个流程几分钟就能跑通。再比如很多企业内部的对账系统需要从大量PDF发票中提取金额和发票号这也是iText的典型使用场景。所以你可以把iText理解成Java世界里操作PDF的瑞士军刀绝大部分PDF需求它都有对应的API。1.2 版本选择iText 5还是iText 7别弄混了这是很多新手踩坑的重灾区。你在网上搜教程搜出来一堆代码复制到自己的项目里一编译发现类名一个都对不上第一反应是怀疑自己依赖没导对其实很可能是版本错位了。iText 5的包名是com.lowagie.text.创建文档用的是Document加PdfWriter.getInstance()这种写法iText 7的包名换成了com.itextpdf.kernel.、com.itextpdf.layout.*创建文档变成了PdfDocument加Document的组合。两者的API设计差距很大不是简单改改import就能互通的。我的建议是新项目直接用iText 7虽然学习曲线稍微陡一点但是架构更清晰流式布局的概念也更接近现代UI框架老项目如果用的是5.x能不升就不升升级成本很高除非你有充足的时间和测试资源。网上很多老教程都是基于iText 5写的你在看的时候一定要先确认教程里的包名和类名再动手否则很容易陷入代码抄了但跑不通的窘境。1.3 许可证AGPL与商业授权的现实问题iText用的是AGPL许可证这个必须提前讲清楚。AGPL是开源协议里比较特殊的一种如果你的程序是开源项目那你可以免费使用iText但如果你的程序是内部系统或商业软件没有把源码开源出来那么基于AGPL协议的库会要求你开放通过网络提供服务的相关源代码。很多公司就是在这里踩了坑项目都快上线了才发现合规问题。iText也有商业授权的版本可以按年购买官方叫iText Software的商业授权。实际项目中我的建议是如果是公司项目先跟法务确认清楚授权情况如果预算有限也可以考虑OpenPDFiText 5的AGPL分支或者PDFBox这类宽松许可证的库。我见过不少团队因为图省事直接在产品里用了iText后来被审计出许可证问题不得不花时间换库那才是最头疼的。所以这一节虽然看起来跟技术无关但它恰恰是实际项目中最容易踩出大坑的地方。2. 从零开始第一个iText程序跑起来2.1 引入依赖Maven坐标与版本号新建一个Java工程如果你用MaveniText 7的坐标是这样dependency groupIdcom.itextpdf/groupId artifactIdkernel/artifactId version7.2.5/version /dependency dependency groupIdcom.itextpdf/groupId artifactIdlayout/artifactId version7.2.5/version /dependency dependency groupIdcom.itextpdf/groupId artifactIdio/artifactId version7.2.5/version /dependency注意只引layout是不够的iText 7把库拆成了kernel、io、layout等多个模块layout依赖kernelkernel依赖ioMaven本身会传递依赖但为了清晰最好还是把常用的三个都显式声明出来。如果你用Gradle也差不多换成implementation com.itextpdf:kernel:7.2.5之类就行。版本号建议用最新的稳定版不要用带alpha或beta的。如果你需要用iText 5坐标则是com.itextpdf:itextpdf:5.5.13.3再加一个com.itextpdf:itext-asian用于中文字体。2.2 创建一个最基本的PDF文件依赖引好了先做最朴素的事生成一个写着Hello iText的PDF。iText 7的代码是这样的import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.Paragraph; public class HelloPdf { public static void main(String[] args) throws Exception { PdfDocument pdfDoc new PdfDocument(new PdfWriter(hello.pdf)); Document doc new Document(pdfDoc); doc.add(new Paragraph(Hello iText)); doc.close(); } }跑起来之后工作目录下应该会出现一个hello.pdf打开里面只有一行文字。这里有几个小细节一是关闭顺序必须调用doc.close()关闭Document它会帮我们处理掉PdfDocument再去手动关PdfDoc就是重复关闭二是PdfWriter接收一个OutputStream或者文件路径如果你要输出到HTTP响应直接传response.getOutputStream()就行。iText 5的对应写法是Document document new Document(); PdfWriter.getInstance(document, new FileOutputStream(hello5.pdf)); document.open(); document.add(new Paragraph(Hello iText)); document.close();对比一下就能看到API风格差异很大。如果你去网上搜教程看到一半代码是com.lowagie开头的就要马上意识到这是iText 5的写法别直接复制到iText 7项目里。2.3 对齐、颜色、段落样式的基础用法实际项目中不可能只输出一行干巴巴的文本。iText的Paragraph对象支持设置对齐方式、行距、字号、颜色、加粗斜体等。iText 7里设置这些非常接近CSS的思路Paragraph p new Paragraph(这是一段加粗红色居中文字); p.setBold(); p.setFontColor(ColorConstants.RED); p.setTextAlignment(TextAlignment.CENTER); p.setFontSize(16f); doc.add(p);注意iText 7的字体颜色要用com.itextpdf.kernel.colors.ColorConstants类这里面的常量定义了很多常用颜色。如果你需要自定义颜色可以用new DeviceRgb(255, 128, 0)这种方式。还要特别注意setBold()这个方法它实际是调用FontProvider在内部加载了加粗字形如果你没有配置中文字体很可能在这里就会出现乱码或者异常所以真正稳定做法还是先把字体配置好下面专门讲字体。除了段落iText 7里还支持建立表格用法上跟HTML的table很接近。下面是一段创建表格的示例Table table new Table(3); table.addCell(编号); table.addCell(名称); table.addCell(价格); table.addCell(001); table.addCell(苹果); table.addCell(5.5); doc.add(table);Table构造参数是列数添加单元格是按顺序从左到右填入的。表格在生成合同明细、订单列表、对账单时非常常用而且支持设置列宽、边框、背景色等样式但最基本的用法就是这种。图片的添加也不复杂用Image对象加载文件路径后doc.add即可。3. 中文与生僻字的坑绕不开的字体问题3.1 为什么默认字体不支持中文新手使用iText最容易碰到的就是中文乱码生成出来的PDF里中文全是方块或者空白。原因其实不复杂PDF文件不像Word那样可以动态引用系统字体它需要在文档里明确指出用哪个字体文件来绘制字符。iText内置的14种标准Type1字体比如Helvetica、Times只支持拉丁字符根本没有中文字形。PDF阅读器打开文件时遇到字体里没有的字符就只能拿一个缺省字体来替代结果就是乱码。所以任何需要输出中文的场景你必须手动指定一个中文字体。这里有个非常容易混淆的点很多人以为设置段落字体就可以解决中文但实际上如果你给iText传一个不支持中文的字体对象它照样乱码。真正要做的是注册或加载一个支持中文的字体文件再把这个字体应用到段落上去。字体选择这件事比你想得更关键。3.2 iText两种字体配置方式iText处理中文字体常见的有两种方案。第一种是用亚洲字体包也就是itext-asian这个jar包配合iText内置的STSong-Light和UniGB-UCS2-H编码来用。这种字体不是嵌入的它依赖PDF阅读器本地字库因此生成的PDF体积小但换一台没有中文字体的设备可能会有显示问题尤其是生僻字很容易出问题。代码写法在iText 7里PdfFont font PdfFontFactory.createFont( STSong-Light, UniGB-UCS2-H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED );第二种是直接加载系统的TrueType字体文件。这种方式更稳定字体是嵌入进PDF的不管在什么设备上打开都一样。Windows环境可以加载simsun.ttc宋体Linux环境可以加载你服务器上装的思源黑体或文泉驿字体。代码写法PdfFont font PdfFontFactory.createFont( C:/Windows/Fonts/simsun.ttc,0, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED );注意simsun.ttc是一个字体集合后面加个,0表示取第一个子字体。用这种方式配置完字体以后再创建Paragraph的时候传入这个font就可以了。iText 5的BaseFont.createFont写法与此不同网上老教程很多用的是BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED)这个写法在iText 7里已经不存在了。3.3 生僻字场景如何处理搜索热词里有itext flying saucer 生僻字这说明很多朋友在做HTML转PDF时被生僻字坑过。生僻字问题本质上和中文问题一样都是字体里不含对应字形。STSong-Light内置字体覆盖的字符有限遇到像犇燚这类生僻字就会显示成空心方块。解决办法也很直接一个是换成覆盖范围更大的字体比如Noto Sans CJK SC、思源宋体这些开源字体它基本覆盖了常用汉字和大部分生僻字另一个是针对特别生僻的字符可能需要你专门准备一个包含该字形的TTF文件比如在系统字体里搜一下这个字是否正常显示然后把这个字体文件路径传给iText。我在一个项目里遇到过需要打印古文生僻字的场景最终就是下载了一个GB18030全字符字体然后动态注册到iText里才解决。另外还要提醒一点就算你用了统一的思源黑体不同子版本对生僻字的覆盖范围也会略有差异涉及到特别罕见的字最好在开发阶段把要测试的生僻字列一个清单逐字跑一遍渲染结果不要等到上线了才发现客户的名字打不出来。4. 实战解析PDF文件中的数据4.1 文本提取其实很简单PDF解析的需求在公司内部很常见比如解析发票PDF、读取上游发过来的电子账单、对账系统里抓取指定字段。iText做文本提取用PdfTextExtractor就可以了。iText 7的API如下PdfDocument pdfDoc new PdfDocument(new PdfReader(invoice.pdf)); int pageNum pdfDoc.getNumberOfPages(); StringBuilder sb new StringBuilder(); for (int i 1; i pageNum; i) { sb.append(PdfTextExtractor.getTextFromPage(pdfDoc.getPage(i))); } System.out.println(sb.toString());这段代码会把每一页的文本拼起来。一个很容易踩的坑是有些PDF看起来有文字但实际上是扫描图片或者文字被转成了曲线转曲。这种PDF用iText提取出来是空的或者只有少量内容因为页面上根本没有文本对象。遇到这种文件必须走OCR路线先把页面渲染成图片再用Tesseract等OCR引擎识别。iText本身不做OCR这点要清楚。还有个小技巧如果你只需要提取某一页可以直接传页码不用循环。如果你提取的内容里有大量空格或换行异常那大概率是PDF本身的文本流顺序写得比较乱这种情况可以试试调整TextExtractionStrategy的策略或者先渲染成图片再看效果不要死磕解析代码。4.2 页面信息、元数据与坐标定位除了纯文本我们经常还需要拿页面尺寸、读取创建时间、标题作者等元数据。iText 7里也可以获取页面宽高和元数据PdfPage page pdfDoc.getPage(1); Rectangle rect page.getPageSize(); System.out.println(width rect.getWidth() , height rect.getHeight());元数据则可以通过PdfDocument.getDocumentInfo()拿到比如getTitle()、getAuthor()、getCreationDate()这些方法。对于很多自动化流程来说先解析出PDF的标题和创建日期再决定后续怎么归类存储这种处理方式非常实用。文本提取默认得到的是整页拼出来的内容但某些场景需要精确到坐标比如固定格式的调拨单发票号码总是出现在右上角某个区域。这种情况就不能再用简单提取了而要用自定义的文本解析策略来做定位提取。比较常用的做法是继承TextExtractionStrategy或实现IEventListener在渲染文字时拿到坐标和文本内容再判断目标是否落在指定区域内。这个属于进阶内容入门阶段知道有这种玩法就行等你真正遇到发票解析一类的需求时再去细抠。5. 实战合并、拆分、加密与HTML转PDF5.1 合并多个PDF公司里经常遇到这种需求一个合同分好几章每章是单独的PDF最后要合并成一个完整文件。iText 7合并代码其实非常简单PdfDocument destDoc new PdfDocument(new PdfWriter(merged.pdf)); for (String filePath : filePaths) { PdfDocument srcDoc new PdfDocument(new PdfReader(filePath)); srcDoc.copyPagesTo(1, srcDoc.getNumberOfPages(), destDoc); srcDoc.close(); } destDoc.close();copyPagesTo的第一个参数是起始页第二个参数是结束页第三个是目标文档。注意srcDoc用完一定要关不然文件句柄泄漏Windows上合并几十个文件就报文件被占用了。iText 5的写法用PdfCopy类也是getImportedPage然后addPage逻辑上差不多但类名完全不同。合并PDF还有一个小问题如果不同PDF的页面尺寸不一样合并后打开可能会觉得版面乱。解决办法是在copyPagesTo之前先遍历每个源文档判断页面尺寸必要时用PdfPageFormXObject做缩放处理。但对于大多数入门项目来说直接把同尺寸的页面合并就行了这个进阶优化可以在有需求时再去研究。5.2 拆分PDF拆分和合并是对称操作。如果要把一个PDF的前10页拆出来单独成文件可以这样PdfDocument srcDoc new PdfDocument(new PdfReader(all.pdf)); PdfDocument destDoc new PdfDocument(new PdfWriter(part.pdf)); srcDoc.copyPagesTo(1, 10, destDoc); destDoc.close(); srcDoc.close();用一个循环就可以把每一页拆成单独文件。拆分PDF在项目里也很常见比如要挑出合同里有签名的那几页单独留档或者把一个多页报告按章节拆成多个PDF文件分发给不同的人。这里还是要提醒一句源文档和目的文档的关闭顺序也很重要一般先关闭目的文档再关闭源文档避免写入还没完成时源文件被锁住。5.3 给PDF设置密码与权限生成合同或者发票之后给PDF加一个打开密码是刚需。iText 7通过PdfWriter的setEncryption配置。PdfWriter writer new PdfWriter(encrypted.pdf); writer.setEncryption( user-pass.getBytes(), owner-pass.getBytes(), EncryptionConstants.ALLOW_PRINTING, EncryptionConstants.ENCRYPTION_AES_128 ); PdfDocument pdfDoc new PdfDocument(writer); Document doc new Document(pdfDoc); doc.add(new Paragraph(机密文件)); doc.close();这里的userPassword是用户打开文档要输入的密码ownerPassword是权限密码设置后用于修改权限或者解除限制。注意password不能直接传null如果只想设置ownerPassword可以把userPassword传一个空数组绕过。权限位可以组合比如ALLOW_PRINTING、ALLOW_COPY、ALLOW_MODIFY_ANNOTATIONS等用按位或运算符组合。iText 5的setEncryption写法在PdfWriter.getInstance后面调用参数里要分别传两个int类型不要弄反了。这个功能在做企业内部系统时非常实用比如财务系统导出的对账单默认加上owner密码限制普通用户复制和编辑只有财务人员才能修改。不过要注意PDF的加密保护层次其实很浅破解工具一大堆它更多是防止误操作而不是防黑客真正高安全要求的场景建议考虑数字签名和证书体系。5.4 flying saucer做HTML转PDF含生僻字配套方案HTML转PDF是一个独立但非常实用的方向。flying saucer就是之前热词里提到的itext flying saucer家族中的一员它可以解析HTML和CSS2.1渲染成PDF。它的底层渲染引擎可以用iText来输出PDF所以如果你已经会iText上手flying saucer会非常快。基本用法是ITextRenderer renderer new ITextRenderer(); renderer.setDocumentFromString(html, baseUrl); renderer.layout(); renderer.createPDF(new FileOutputStream(web.pdf));这里baseUrl用来解析HTML里引用的相对路径图片。flying saucer对CSS3的支持很不完整比如flex布局肯定不行table布局基本能支持但坑也不少。生僻字在flying saucer里同样跟字体相关需要在渲染前注册可用字体比如renderer.getFontResolver().addFont( /path/to/NotoSansCJKsc-Regular.otf, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED );注册之后CSS里写font-family:Arial如果字体文件里没有对应字形生僻字一样会变方块。所以要把registerFont之后的字体family名配到CSS中比较稳妥。另外如果你做的是Web端的PDF预览后端用iText生成PDF后返回文件流前端可以直接用iframe或elementui的弹窗组件来内嵌预览这个配合方式是目前比较主流的做法。6. 常见问题与排查技巧实录6.1 问题速查表把经常遇到的问题整理成表方便对照排查现象常见原因处理办法生成的PDF中文全是方块没有配置中文字体显式注册中文字体并传给Paragraph提取文本为空页面是扫描图片或文字已转曲改用OCR方案PDF打开提示文件已损坏输出流没有正确关闭确保doc.close()被调用Windows下合并文件时报文件被占用源PDF的PdfReader没有close循环里及时关闭srcDoc加密码后文档无法打印权限参数设置不对检查EncryptionConstants.ALLOW_*权限组合生僻字显示为空心方块字体不含该字形更换覆盖更广的字体如思源黑体iText 7运行时报NoClassDefFoundError缺少kernel或io模块显式添加kernel、io、layout依赖HTML转PDF布局错乱flying saucer不支持CSS3全特性改用table布局或调整样式6.2 两个印象深刻的实战案例第一个是发票解析。公司之前的对账系统需要从PDF发票里取金额和发票号最初用简单文本提取偶尔会有金额错位的问题。后来排查发现部分发票页面右上角有一个小的说明文字混在了文本流的输出顺序里。解决办法是把文本按坐标切分成区块只取发票号码附近规定范围内的文字之后准确率就到99%以上了。这个案例也给后续做类似需求提了个醒文本提取不是万能的真正要稳定地从固定版式PDF里取数坐标定位才是可靠方案。另一个是歪斜扫描件的处理。有次接到一个需求要把一批扫描版的PDF做纠偏和漂白加深让打印效果更清楚。这个需求一开始也想用iText解决查了半天发现iText对图像处理几乎不提供支持它不管图像增强。最后方案是先把PDF页面渲染成图片交给OpenCV去做透视变换和对比度调整处理完再重新合成为PDF。iText在这个流程里只负责最终合成那一步。想清楚工具边界能省很多时间遇到需求先判断一下是PDF结构问题还是图像质量问题不要一股脑全往iText上堆。还有一次是在接入第三方系统时对方发来的PDF里有大量按行排列的表格数据用iText直接提取出来的文本顺序完全乱掉了。后来仔细看了页面结构才发现这个PDF是用一个很老的生成器打出来的每个单元格的文本流是分块写入的不同块的写入顺序和视觉顺序不一致。这时候简单的getTextFromPage就不够用了需要自己在内容流里按坐标排序重组。想要在PDF解析这条路上走得远一定要对PDF的页面模型有基本理解至少要知道文本对象是可以被赋予任意位置的。我个人在实际工作中最大的感受是iText虽然名字叫库但里面的坑一点不少尤其是版本选错和字体配置这两件事几乎每个新人都要踩一遍。所以这篇入门教程一开始就把版本和许可证问题放在最前面然后从最基础的创建开始逐步过渡到解析、编辑和HTML转PDF。如果你现在接手的项目还没有接触过PDF处理建议先按第2节和第3节的步骤把基础流程跑通再根据实际需求去扩展。最后再分享一个小技巧生成PDF的代码最好单独封装成一个Service把字体注册、文档关闭、异常处理都写在固定位置。我以前图省事直接在Controller里写PDF生成逻辑后来需求多了改字体的地方散落得满项目都是一出问题就到处找。后来统一抽到一个PdfService里所有PDF输出都用同一个入口排查问题就顺手多了。这个习惯看起来不起眼但越往后做越能体会到它的价值。