
做PDF处理这件事我最早是被同事“逼”上路的。财务那边每个月要导出一批带电子章的PDF合同行政那边隔三差五要合并几十份扫描件还有产品非要给在线报告加个动态水印。一开始我也想过手动操作试过Adobe Acrobat也试过各种在线工具但文件一多、格式一复杂手动方案根本撑不住效率低到让人怀疑人生。后来我把目光放到程序化处理上在Java生态里选型绕不开的就是iText。这个库在PDF处理领域算是老牌玩家了API覆盖了从零创建PDF、读取解析已有文件、合并拆分、添加水印、填写表单到HTML转PDF的几乎所有场景。如果你也是个Java开发或者你所在团队有“批量处理PDF文档”这类需求这篇入门教程正是你需要的。我会从选型理由、环境准备、核心API、实操Demo到性能调优一条线讲清楚尽量把容易踩的坑都提前指出来。1. 为什么我最终选了iText而不是PDFBox先聊选型。Java这边处理PDF主要就三个流派iText、Apache PDFBox、OpenPDF。很多人第一次接触时都会纠结该用哪个。我的建议很简单先看你要做什么再选库。能力维度iText 7Apache PDFBoxOpenPDF创建复杂版式报表强Table布局很成熟弱需要画坐标中继承旧iText 4体系读取/提取文本好用逐页取文本好用还支持文本位置获取能用但API偏旧合并/拆分PDFPdfMerger封装完善PDFMergerUtility也可以支持有限动态表单填写表单模块很完善支持但细节处理繁琐表单支持一般HTML转PDF官方pdfHTML模块集成度高没有官方方案需要外部拼凑基本不用考虑资料丰富度官方教程、Stack Overflow最多也不少小众遇到问题难搜许可模式AGPL/商业双许可Apache 2.0完全宽松AGPL/商业双许可我做选型时的核心需求有三条第一要能生成漂亮的表格型报表第二要在现有合同PDF上加印章和水印第三最好能把前端的HTML排版直接转成PDF省掉重复排版的精力。三条需求一条条对照下来PDFBox在第一条和第三条上明显吃亏PDFBox适合做“读PDF、拆PDF、提取内容”这类偏文档解析的活但让它排版复杂报表你得自己在坐标系里画线画框那体验相当酸爽。iText 7则完全是另一套打法。它把“内容”和“坐标”解耦了你只需要像写HTML一样声明文档结构——段落、列表、表格——它自动帮你做流式排版。同时新的模块化架构把功能拆分成kernel、io、layout、forms、pdfHTML等独立包按需引入不像旧版那样一坨全塞进来。加上iText入华早中文社区里遇到字体问题、中文乱码问题一搜就是现成方案。综合下来我选择了iText。OpenPDF其实是个特殊的存在它是iText 4的社区分支API老、迭代慢除非你在维护一个遗留项目不想换库否则新项目真不建议从它起步。网上有些教程教OpenPDF核心思路可以借鉴但遇到新版PDF特性时就会卡壳。2. 版本差异和许可边界这俩坑必须先搞明白2.1 iText 5与iText 7的API差距大得离谱网上搜iText教程大概率会搜到一堆老代码典型特征是import com.lowagie.text.Document、BaseFont.createFont()、PdfWriter.getInstance()。这些都是iText 5或更早版本的写法。如果你照着写再把iText 7的依赖扔进项目里编译直接报错。新版iText 7的包名彻底变了最核心的是com.itextpdf.kernel.pdf.PdfDocument和com.itextpdf.layout.Document。旧版的PdfWriter.getInstance换成了直接new PdfWriter(path)。旧版的BaseFont被PdfFontFactory取代。可以说除了“iText”这个名字几乎没有一行代码能直接复用。所以看教程第一步先认包名com.lowagie开头的是iText 2/5时代的产物com.itextpdf开头的才是iText 7的正统。我见过太多人拿着老代码折腾半天编不过最后发现是版本错位。2.2 iText 7的模块化结构iText 7不再是一个大而全的jar而是拆成多个模块。但官方为了方便提供了itext7-core这个聚合POM把最常用的模块打包在一起。Maven依赖写法是dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.2.5/version typepom/type /dependency如果你喜欢按需引入最常用的几个模块我列一下模块名artifactId功能说明kernelitext-kernel最底层PdfDocument、PdfReader、PdfWriter都在这里ioitext-ioIO工具字体、图片解码相关layoutitext-layout高级布局Document、Table、Paragraph等formsitext-forms表单填写与读取pdfHTMLitext-html2pdfHTML转PDF功能font-asianitext-font-asian亚洲字体包含简体中文字体定义集成时引入aggregate的pom即可不用自己一个个对版本号避免依赖冲突。新手老手我都推荐这么干省事。2.3 AGPL许可不是闹着玩的iText 7本身采用AGPL协议。简单说你用AGPL组件开发了软件只要你通过这个软件向外部用户提供网络服务你的软件源代码就必须以AGPL协议向用户开放。如果是公司内部使用的工具、或者闭源商业软件分发给客户使用就属于高风险场景需要购买商业许可。我见过一些小团队因为没注意到这一点产品上线后收到iText官方的邮件函告。这个坑真得提前规避。如果不想买商业许可又担心AGPL传染那就用PDFBox如果你必须用iText的表格排版和HTML转PDF能力且公司能接受开源条款那就严格遵守要求把源码开源否则趁早找法务评估买license或者切技术栈。许可的事情越早定越省钱不要等代码写完了再纠结。3. 环境准备依赖、字体、还有最折磨人的中文问题3.1 一个跑得通的最小环境长什么样我用的是Maven JDK 8 iText 7.2.x的组合JDK 8到JDK 17都能跑没有特别的版本要求。创建一个空Maven工程把上面那个依赖加进去。第一个能跑通的Hello World长这样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 { String dest hello.pdf; PdfWriter writer new PdfWriter(dest); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); document.add(new Paragraph(Hello iText)); document.close(); } }这里有个关键点Document.close()会同时关闭下层PdfDocument和PdfWriter并且把最终内容完整写入文件。新手容易漏掉close或者关了PdfDocument又去关Document结果抛IllegalStateException。最简单的方式只在finally块里关闭最外层的Document。3.2 中文显示问题runsfine但方块哪都缺不了这道坎如果Hello World输出的是纯英文你会发现一切顺利。可一旦换成一串中文生成出来的PDF上就是一个个方框。原因很简单iText默认的标准字体没有嵌入中文它根本不认识中文字符。解决方案也很直接让iText加载一个TrueType中文字体文件。我的习惯是把字体文件放到src/main/resources/fonts/下然后从classpath加载import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; String fontPath src/main/resources/fonts/NotoSansCJK-Regular.ttc,0; PdfFont font PdfFontFactory.createFont( fontPath, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED ); document.setFont(font);注意几点这里的IDENTITY_H表示按Unicode编码映射字形处理中文、日文、韩文都用它。.ttc字体集合文件比如Windows的simsun.ttc、msyh.ttc后面要加,0表示取第0个字体不加的话iText可能报错或取到繁体版本。如果你用的是.ttf单字体文件不需要加后缀。EmbeddingStrategy.PREFER_EMBEDDED会把字体嵌入PDF文件中别人在别的设备上打开时不会因为缺字体而乱排。代价是生成的文件体积会变大几MB但这对正式文档来说是值得的。字体加载这块有不少坑。我特意做了一张速查表场景推荐做法踩过的坑Windows开发机用系统字体C:/Windows/Fonts/simsun.ttc,0直接加载ttc不加索引会报错Linux服务器上传思源黑体NotoSansCJK-Regular.ttc到项目目录服务器不一定有中文字体必须随包携带Mac开发机用/System/Library/Fonts/PingFang.ttc路径空格和权限问题偶尔出现生僻字/特殊人名用覆盖字库大的字体如IPAmj明朝很多字体缺罕见汉字必须实测3.3 字体版权也要注意嵌入字体前先确认字体许可允许再分发。Windows的simsun.ttc是商业字体把字体文件打包到应用里分发是有版权风险的。开源项目建议直接用思源黑体、思源宋体它们是OFI许可随便分发。说真的我早期图省事直接打包simsun.ttc上生产后来法务提醒才知道这有隐患赶紧全量替换成了思源字体这种事情真得留个心眼。4. 第一个完整Demo从零生成订单确认单环境准备好了下面写一个相对完整的例子。我用“订单确认单”当素材覆盖标题、文字样式、列表、表格、图片、自定义页边距这些常用能力。import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.io.image.ImageData; import com.itextpdf.io.image.ImageDataFactory; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.geom.PageSize; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.borders.Border; import com.itextpdf.layout.element.Cell; import com.itextpdf.layout.element.Image; import com.itextpdf.layout.element.List; import com.itextpdf.layout.element.Paragraph; import com.itextpdf.layout.element.Table; import com.itextpdf.layout.properties.TextAlignment; import com.itextpdf.layout.properties.VerticalAlignment; public class OrderDemo { public static void main(String[] args) throws Exception { String dest order.pdf; PdfWriter writer new PdfWriter(dest); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf, PageSize.A4); // 全局中文字体 PdfFont font PdfFontFactory.createFont( src/main/resources/fonts/NotoSansCJK-Regular.ttc,0, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); // 标题 Paragraph title new Paragraph(订单确认单) .setFont(font) .setFontSize(20) .setBold() .setTextAlignment(TextAlignment.CENTER); document.add(title); // 订单信息列表 List list new List().setFont(font).setFontSize(12); list.add(订单编号SO-2024-001); list.add(客户名称某某科技有限公司); list.add(下单时间2024-06-18 14:30); document.add(list); // 商品明细表格 Table table new Table(new float[]{3, 2, 1, 2}); table.useAllAvailableWidth(); String[] headers {商品名称, 规格, 数量, 单价元}; for (String header : headers) { table.addHeaderCell(new Cell().add(new Paragraph(header).setFont(font)) .setBackgroundColor(ColorConstants.LIGHT_GRAY) .setTextAlignment(TextAlignment.CENTER)); } table.addCell(new Cell().add(new Paragraph(企业版云服务).setFont(font))); table.addCell(new Cell().add(new Paragraph(旗舰版).setFont(font))); table.addCell(new Cell().add(new Paragraph(1).setFont(font)).setTextAlignment(TextAlignment.CENTER)); table.addCell(new Cell().add(new Paragraph(1599).setFont(font)).setTextAlignment(TextAlignment.RIGHT)); // 合并单元格 Cell noteCell new Cell(1, 4).add(new Paragraph(备注本订单确认后预计3个工作日内完成交付。).setFont(font)); table.addCell(noteCell); document.add(table); // 插入图片 ImageData imageData ImageDataFactory.create(logo.png); Image image new Image(imageData); image.scaleToFit(100, 100); document.add(image); document.close(); } }这个Demo里值得事后琢磨的地方有三处。第一new float[]{3, 2, 1, 2}定义的是四列相对宽度而不是绝对宽度。这样表格在页面拉伸时各列会按比例分配空间比设固定宽度更灵活。第二useAllAvailableWidth()让表格撑满页面可用宽度。什么概念如果你忘了调用这行表格宽度是默认值四个列可能挤在左半边丑得很。第三图片scaleToFit(100, 100)会在保持图片宽高比的前提下缩放进100x100的框里。直接调用setWidth(100)也可以但可能拉伸图片变形。建议一律用scaleToFit做等比缩放。运行完这个Demo你就掌握了iText生成PDF的全部核心套路。剩下的事情基本上都是在这个基础上组合出更多内容类型。再补充一个自定义页面大小和页边距的技巧// 自定义页面尺寸比如打印小票的80mm宽度 PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf, new PageSize(80f * 72 / 25.4f, 200f * 72 / 25.4f)); document.setMargins(10, 10, 10, 10);PageSize里填的数字单位是“点”pt1英寸是72点1毫米约等于2.8346点。上面80f * 72 / 25.4f就是把80毫米换算成点数。很多做小票打印、物流面单的开发者一开始都会困惑“我设了宽度为啥不对”核心就是因为单位没换算。5. 读别人的PDF合并拆分、提取文本、统计尺寸有了创建经验再来处理“既有PDF”。这类需求在真实项目里占比反而更高财务合并发票、运营拆分报告、法务提取合同条款都是高频操作。5.1 读取基本信息核心API是PdfReaderPdfDocument。先看读取信息的代码PdfDocument pdf new PdfDocument(new PdfReader(existing.pdf)); int pageCount pdf.getNumberOfPages(); for (int i 1; i pageCount; i) { PdfPage page pdf.getPage(i); Rectangle rect page.getPageSize(); System.out.println(第 i 页 尺寸: rect.getWidth() x rect.getHeight()); } pdf.close();rect.getWidth()和rect.getHeight()返回的单位是pt。要转成毫米除以72再乘以25.4。这个技能点平时真能用上很多做打印适配的团队需要确认PDF文件的尺寸规格。页面边距信息也可以用page.getCropBox()获取。这里稍微区分一下getPageSize()返回裁剪前的页面媒体框大小getCropBox()返回最终显示裁剪框。打印场景一般看后者。5.2 合并多个PDFiText 7提供了PdfMerger类合并多个文件非常简单String[] srcFiles {a.pdf, b.pdf, c.pdf}; String dest merged.pdf; PdfDocument destDoc new PdfDocument(new PdfWriter(dest)); PdfMerger merger new PdfMerger(destDoc); for (String src : srcFiles) { PdfDocument srcDoc new PdfDocument(new PdfReader(src)); merger.merge(srcDoc, 1, srcDoc.getNumberOfPages()); srcDoc.close(); } merger.close(); destDoc.close();这里有个容易忽略的细节merge(srcDoc, 1, srcDoc.getNumberOfPages())的起止页参数都是包含边界的和很多语言的[start, end)习惯不一样我第一次写就传错成了merge(srcDoc, 1, srcDoc.getNumberOfPages() - 1)结果每次少合并最后一页。所以文档里说merge是“inclusive”的含义就是两个边界都算。5.3 按页码范围拆分拆分PDF比合并稍微绕一点但没有想象中复杂。比如把一个大文件的前三页拆成单独文件PdfDocument srcDoc new PdfDocument(new PdfReader(big.pdf)); PdfDocument dstDoc new PdfDocument(new PdfWriter(prefix.pdf)); srcDoc.copyPagesToDocument(1, 3, dstDoc); dstDoc.close(); srcDoc.close();如果要把每一页都拆成独立文件写个循环就行。遇到需要按关键词匹配拆分的情况比如URL签名、订单号就得先提取文本定位页码再执行拆分流程也是上面这套组合。5.4 提取文本内容提取PDF里的文字在iText里叫“解析”而不是“读取”。iText能提取的只是文本层的内容也就是那些能选中、能复制的文字。纯粹扫描件在PDF里是一张图片没有任何文本层iText提取出来是空字符串。那种情况得先走OCR路线比如Tesseract或百度OCR把图片转为文字后再说。文本提取代码PdfDocument pdf new PdfDocument(new PdfReader(report.pdf)); for (int i 1; i pdf.getNumberOfPages(); i) { PdfPage page pdf.getPage(i); String text PdfTextExtractor.getTextFromPage(page); System.out.println(第 i 页内容: text); } pdf.close();这个功能对付“从一堆报价单里批量收集金额”“把合同的关键条款抽出来做审核”这类场景非常顺手。不过要提醒的是PDF的文本流结构千奇百怪某些工具生成的PDF文本顺序会乱提取出来夹着乱序字符。真遇到这种特殊情况别死磕iText可以考虑用PDFBox或pdfplumberPython做二次尝试不同库对不同生成器的兼容性各有千秋。5.5 读加密文档时常见到加密PDFnew PdfReader(path)直接打不开。iText支持传入密码ReaderProperties props new ReaderProperties().setPassword(123456.getBytes()); PdfReader reader new PdfReader(path, props);注意setPassword接收的是byte[]不要直接传字符串。另外还有一种只有打印权限、没有打开密码的受限文档iText默认会直接拒绝要先reader.setUnethicalReadingMode(true)才能强制提取这个操作在合规风险上要自己掂量一般不推荐。6. 在现有PDF上做文章叠加水印、盖章、坐标计算6.1 iText的坐标系想往现有文件上叠加内容必须先理解它的坐标系。iText的坐标原点在页面左下角x轴向右y轴向上单位pt。这和网页的左上原点习惯完全相反在写水印、印章推荐位置时最容易搞错经常会看到东西跑到了页脚下方。点位关系示例对于A4纸595 x 842 pt页面上方中央点坐标大约是 (297.5, 760)页面正中央坐标是 (297.5, 421)参考右下角盖公章如果离右边界留50pt、离下边界留50pt坐标就是(595 - 50 - 印章宽度, 50)。6.2 给PDF批量加半透明水印给合同、报告批量加上“内部资料”或“已审核”水印是使用频率相当高的需求。iText 7里可以用PdfCanvas直接往页面内容流里插入文字PdfDocument pdf new PdfDocument(new PdfReader(input.pdf), new PdfWriter(output.pdf)); PdfFont font PdfFontFactory.createFont(src/main/resources/fonts/NotoSansCJK-Regular.ttc,0, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); for (int i 1; i pdf.getNumberOfPages(); i) { PdfPage page pdf.getPage(i); Rectangle size page.getPageSize(); PdfCanvas canvas new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdf); // 设置半透明 PdfExtGState gs new PdfExtGState().setFillOpacity(0.3f); canvas.saveState(); canvas.setExtGState(gs); // 绘制文字水印 canvas.beginText() .setFontAndSize(font, 48) .setFillColor(ColorConstants.LIGHT_GRAY) .moveText(size.getWidth() / 2 - 100, size.getHeight() / 2) .showText(内部资料) .endText(); canvas.restoreState(); } pdf.close();这个Demo里有三个细节是实操中总结出来的newContentStreamBefore()决定这层内容在原有页面的下面还是上面。水印、背景用Before印章、签字放在上面用After。放错了会出现水印被正文盖住的情况。saveState()和restoreState()必须成对出现中间的透明度设置不会影响页面其他内容。如果漏了restoreState()可能出现整页内容都带着透明度这种bug排查起来特别隐蔽。水印文字位置用moveText()设置的是文字左下角基线起点想要垂直居中需要把Y坐标往下多偏移几个pt才能在视觉上居中。这个偏移量跟字号有关48号的偏移大概在-14pt左右小数字不敏感大号水印肉眼可见地偏上。6.3 叠加图片印章公章、签名图片的叠加逻辑跟水印一样只是绘制内容换成了图片ImageData stampData ImageDataFactory.create(stamp.png); PdfXObject stampXObject new PdfXObject(stampData); PdfCanvas canvas new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdf); canvas.addXObject(stampXObject, 100, 100, 120, 120);addXObject的最后四个参数分别是x、y、宽、高。第四条路径传120代表把图片缩放为120x120pt放在左下角上方100pt处。盖章常见的一个坑是透明背景图片被画成黑块那是因为图片本来带了未处理的透明通道信息导出时先把PNG统一转成不透明的RGBA或直接使用JPG能省很多麻烦。6.4 给每页加页脚页码页码是所有正式文档的刚需尤其是在“生成报告”“导出合同”这种场景。iText 7推荐的做法是自定义IEventHandler通过事件机制在页面渲染完成时往底部写入页码public class PageNumberHandler implements IEventHandler { private PdfFont font; public PageNumberHandler(PdfFont font) { this.font font; } Override public void handleEvent(Event event) { PdfDocumentEvent docEvent (PdfDocumentEvent) event; PdfDocument pdf docEvent.getDocument(); PdfPage page docEvent.getPage(); Rectangle size page.getPageSize(); PdfCanvas canvas new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdf); int totalPages pdf.getNumberOfPages(); int currentPage pdf.getPageNumber(page); String text currentPage / totalPages; canvas.beginText() .setFontAndSize(font, 10) .moveText(size.getWidth() / 2 - 20, 20) .showText(text) .endText(); } }在生成文档前注册事件PdfDocument pdf new PdfDocument(new PdfWriter(dest)); pdf.addEventHandler(PdfDocumentEvent.END_PAGE, new PageNumberHandler(font));事件机制是iText 7一个挺巧的设计它干的事情类似AOP不用每写一页手动调一次方法页面渲染事件发生时自动触发。理解这个模式后页眉、页脚、公司Logo水印都可以用同一套范式做出来不需要重复造轮子。7. HTML转PDFflying saucer与pdfHTML的取舍很多业务系统里报价单、报告、凭证早有一套成熟的HTML模板。如果能直接把HTML渲染成PDF就可以完全复用前端排版不用在Java代码里一个格子一个格子地拼Table。这需求太常见了再说一个高频话题iTextHTML转PDF怎么选方案。7.1 搜索引擎里那个“flying saucer”是什么flying saucer也叫xhtmlrenderer是一个老牌的Java HTML渲染库底层借用iText 2.x/5.x的API把HTML/CSS输出成PDF。很多传统项目到现在还在用它。它的特点是对CSS 2.1支持得比较古典、稳定但HTML5和CSS3的新特性比如flex布局基本支持不好。如果你维护的项目已经用它跑了好几年没有大问题就别动了。但如果你在选新方案我建议优先考虑iText官方的pdfHTML模块毕竟它和iText 7同源同代处理样式的能力和后续维护力度都更强。7.2 pdfHTML的极简用法引入额外依赖dependency groupIdcom.itextpdf/groupId artifactIditext-html2pdf/artifactId version4.0.5/version /dependency一行代码把HTML字符串转成PDFString html htmlheadmeta charset\UTF-8\style body { font-family: Noto Sans SC; } h1 { color: #333; } .price { color: red; font-weight: bold; } /style/headbody h1报价单/h1 p总金额span class\price\¥1,599.00/span/p /body/html; PdfWriter writer new PdfWriter(quote.pdf); HtmlConverter.convertToPdf(html, writer);请注意pdfHTML默认的字体解析策略并不会自动处理所有中文环境。它优先从本地查找匹配font-family的字体找不到就降级结果就是中文全部变成方块。正确做法是先用ConverterProperties把字体注册进去ConverterProperties properties new ConverterProperties(); // 直接通过字体提供者注册 properties.setFontProvider(new DefaultFontProvider(false, false, false)); FontProvider fontProvider properties.getFontProvider(); // 注册项目自带的ttf/ttc字体文件 fontProvider.addFont(src/main/resources/fonts/NotoSansCJK-Regular.ttc,0); HtmlConverter.convertToPdf(html, writer, properties);这里DefaultFontProvider(false, false, false)三个参数分别表示是否注册系统字体、是否注册内置字体、是否注册标准字体14种。我推荐全设成false避免服务器环境差异导致同一个HTML在开发机和Linux上渲染出完全不同的字体效果然后只从项目目录里加载的确定字体文件这样输出才可预期。7.3 生僻字的坑系统里用户姓名带生僻字比如“”“㑇”在PDF里显示成方框这是客服反馈高频问题之一。根因在于你选择的字体文件里没有收录这些字形浏览器都识别不了PDF当然也渲染不出来。解决办法是换一个生僻字覆盖范围足够大的字体。开源字体里建议试试IPAmj明朝它覆盖了大量汉字扩展B、扩展C区块的生僻字形。或者用“思源宋体/黑体 Noto Sans SC IPAmj”做多字体优先级链。选完字体后别嫌麻烦至少要实际渲染一遍包含生僻字的样例页确认输出结果。7.4 分页控制HTML转PDF的另一个痛点是分页。内容长了表格被硬生生从中间切断、标题留在上一页底部这类问题都会冒出来。pdfHTML支持一些基本的CSS分页属性.page-break-before { page-break-before: always; } .page-break-inside { page-break-inside: avoid; }对于复杂的长表格分页我目前的经验是限制每个Table的语义拆分用thead标签让表头在每一页重复显示。pdfHTML对thead的支持还可以能自动在跨页时重复表头这点比手动拼Table要省心得多。8. 性能调优与异常排查实战8.1 处理大PDF时先这样调整如果你要处理的是上百MB、几千页的PDF直接new PdfReader(path)一把梭可能会让内存告急。iText对读取做了延迟加载机制内核层面默认不会把所有页面都加载进内存但在早期版本或者某些复杂文件上还是可能触发OOM。两个最实用的调整一个是给PdfReader开缓存另一个是合并相同对象。PdfReader reader new PdfReader(huge.pdf); reader.setCacheFile(true); // 内存不足时把数据刷到临时文件创建PDF时如果用到了大量重复的资源比如每页都嵌入了同一个Logo图片可以让PdfWriter开启“智能模式”合并重复对象显著缩减输出体积PdfWriter writer new PdfWriter(dest); writer.setSmartMode(true);这个SmartMode在生成大量相同页面的合同或发票时特别有用体积优化肉眼可见。但它也会让处理速度稍微变慢属于用时间换空间的典型tradeoff具体开不开看场景。8.2 别原地覆盖原文件有一个习惯我强烈建议你养成所有读取写入同素材的操作一定要另存为一个新的目标文件别覆盖原文件。// 危险写法 PdfDocument pdf new PdfDocument(new PdfReader(input.pdf), new PdfWriter(input.pdf)); // 标准写法 PdfDocument pdf new PdfDocument(new PdfReader(input.pdf), new PdfWriter(output.pdf));原因是PdfWriter可能在你还在读原文件时就开始截断文件了轻则输出损坏重则原文件彻底残留未关闭结构。这类问题大概率是在生产环境被真金白银坑过才长记性。所以凡是修改操作我都建议先把结果写到临时文件确认没问题再改名替换并且使用完成后显式close释放文件锁。8.3 常见异常速查异常信息可能原因处理思路ClassNotFoundException: com.itextpdf.kernel...依赖没引入完整确认是否引入itext7-core聚合POM或对应模块PdfException: Invalid password文档加密且密码不对用ReaderProperties注入正确密码PdfException: Font provider...HTML转PDF时字体未注册通过ConverterProperties注册字体文件IOException: The document has been closedclose顺序不对保持最外层Document只关一次OutOfMemoryError文件过大/默认内存不足设置reader的cacheFile或增加JVM堆内存PdfException: AcroForm field not found表单填写的字段名不对先用AcroForm.getFormFields()输出所有字段名核对Cannot find font...字体路径错误或字体文件损坏检查classpath路径和ttc索引号上面这些坑我基本都踩过一遍。真要排错时先把问题限定在“是读取、写入、还是字体”再看报错堆栈和代码行号比瞎试快得多。有个冷知识也分享下iText处理不了PDF转曲把文字转成轮廓曲线这类矢量级操作也做不了“把PDF转成Word”这种双向转换更没有图像纠偏、漂白功能。这些属于其他工具链的活。遇到这类需求别在iText里死磕换Adobe Acrobat或Ghostscript专业的事交给专业工具。写在最后从选型到环境从创建到修改从HTML到性能排查iText这条主线路基本都走了一遍。入门这件事其实不难真正值钱的是踩坑经验——版本API差异、中文字体、坐标系统、许可协议、关闭顺序每一个都能卡住新人大半天。我自己当初就被老教程和旧API坑了不少时间后来养成了一个习惯先确认包名再写代码遇到问题先去官方Examples找对应章节。最后给个实用小建议不管项目多简单都建议你从第一行代码开始就封装一层“PdfService”统一管理字体加载、文件路径、输出目录把iText的原始调用收敛在一个类里。这样后续升级版本或切换PDFBox改动面才可控。希望这篇教程能帮你把PDF这块硬骨头啃下来。如果你在实际使用中碰到了什么奇葩问题欢迎在评论区描述你的场景我后续可以再补充对应的实战案例。