简介:面向Java开发者的iText PDF操作源码包,覆盖创建PDF、电子签章、斜字水印与文本替换四类常见需求,适合需要集成PDF读写、签名及版权保护功能的开发者。压缩包共32个文件,包含7个Java源文件、9个class字节码、5个核心jar库(itextpdf、itext-asian与BouncyCastle安全组件),并附2个p12证书、图片素材及Eclipse工程配置,整体8.77MB,导入IDE即可运行。已有1571人学习。示例工程演示PdfWriter建文档、PdfStamper签章替换、PdfFormXObject旋转水印等关键API,附readme说明,帮助快速上手并迁移到实际业务。
1. 用 iText 直接改 PDF:这四个需求为什么总被绑在一起讨论
用 iText 操作 PDF,意味着不借助 Word 转存、不用浏览器打印,纯代码把「创建、签章、斜字水印、文本替换」四件事一次做掉。它们几乎总在同一批真实任务里出现:合同系统生成空白协议,盖章后加水印归档,最后把模板里的占位符替换成真实客户名。如果你正在选型 Java 后端的 PDF 处理方案,或者已经遇到「签章后整份文件锁定」「中文变方块」「替换文本后文件打不开」这类问题,这篇就是为你整理的落地笔记。读完你能得到一套可运行的 iText 5.x/7.x 代码骨架,以及每种操作背后的取舍。
2. 选型与最小创建:iText 5 还是 7,第一个 PDF 怎么出
2.1 iText 5 与 7 的差异,选了之后很难临时改
iText 在 Java 生态里有两条主流分支:iText 5.x 和 iText 7.x。对绝大多数没有特殊合规要求的项目,我一般推荐直接用 iText 5.x 的稳定 API,原因有三个:一是中文资料和网上可查的排错案例几乎都基于com.itextpdf.text包;二是签章、水印、替换这套传统打法的 API 在 5.x 下非常直接;三是 5.x 的 AGPL 许可证和很多公司现有的部署方式兼容度更高。7.x 不是不能用,它的底层重新设计后,对高并发、大文件更友好,但 API 拆分得更碎,签章和水印要用不同的模块依赖。
这里有个隐藏的坑,网上经常有人搞混:iText 2.x 时代的包名是com.lowagie.text,从 iText 5.0 开始才改成com.itextpdf.text。如果你在 Maven 里引的是旧坐标,或者复制了一段com.lowagie.text.Document的老代码,编译就会直接挂。选型时先定版本,别混用。
7.x 里对应关系大致如下:Document/PdfWriter变成了PdfDocument/PdfWriter,PdfStamper变成了PdfDocument加PdfPage的组合操作,签名相关的PdfSignatureAppearance变成了PdfSigner。如果你从 5.x 迁到 7.x,几乎每个类都要改名。签章、水印这类功能里,5.x 的一行setCrypto在 7.x 里被拆成PrivateKeySignature和PdfSigner两个类的协作,所以我的建议是:项目里如果没有强需求,别在 5/7 之间摇摆,认准一个就锁死版本。
2.2 创建 PDF 的最小代码:中文字体是第一道坎
先把依赖和最小创建代码跑通。Maven 坐标这里写的是 iText 5.5.13,它是 5.x 的最后一个正式版本,也是最容易找到解决案例的版本。由于 iText 对中文支持依赖单独的 asian 包,所以需要同时引入 itext-asian 才能用内置的 STSong-Light 字体,这一点经常被漏掉。
<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency>然后是最小创建代码。这段代码的目标是:生成一个 A4 页面,写入一行中文和一行带样式的英文,并输出到本地文件。如果这段能跑通,后续签章和水印就都有了一个可复用的起点。
String dest = "/tmp/first.pdf"; Document document = new Document(PageSize.A4, 36, 36, 54, 36); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(dest)); document.open(); BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); Font titleFont = new Font(bf, 16, Font.BOLD, BaseColor.BLACK); Font contentFont = new Font(bf, 12, Font.NORMAL); document.add(new Paragraph("合同编号:HT-2024-001", titleFont)); document.add(new Paragraph("This is a test paragraph.", contentFont)); document.close(); writer.close();这段代码里,Document的构造参数分别表示页面大小和左右上下四边距,PageSize.A4是 595 乘 842 磅;PdfWriter负责把文档流写到磁盘;BaseFont.createFont的第一个参数是字体名,第二个参数是编码映射。这里用的UniGB-UCS2-H是「Unicode 到 GB 编码的横向映射」,中文必须靠它才能正常写入,否则出来的就是空文本或乱码。Font的参数依次是 basefont、字号、样式和颜色,其中Font.ITALIC就是后面水印要用到的斜体样式。
需要注意,document.close()之前,所有add的 Element 都还没真正落盘,所以不要习惯性地在 close 之前就去读取输出文件。close 的顺序建议先 document 再 writer,反过来在极端情况下会漏掉尾部 xref 数据。这段代码运行完后,用任意 PDF 阅读器打开first.pdf,如果能正常看到两行文字,环境就算搭好了。
3. 数字签章:证书、可见签名与锁定整份文档
3.1 准备测试证书:先理解签章在验证 PDF 时的作用
数字签章的目标是:让接收方能验证「文件确实由我签的、签名后没有人改过」。iText 的签章不是把一张图片贴上去,而是用私钥对文件摘要做签名,并把签名结果写进 PDF 的签名表。因此你需要一个私钥和对应的证书链。在没有公司 CA 的时候,用 keytool 生成的是自签名证书,开发测试没问题,对外签发则需要第三方 CA 签发的证书。
生成 PKCS12 格式密钥库的命令如下。这里我用 alias 名叫 signer,密码是 keystorePassword,你可以换成自己环境的参数。PKCS12 是跨工具链支持最好的格式,iText 的KeyStore.getInstance("PKCS12")可以直接加载。
keytool -genkeypair -alias signer -keyalg RSA -keysize 2048 \ -storetype PKCS12 -keystore signer.p12 \ -storepass keystorePassword -keypass keystorePassword \ -dname "CN=Test Signer, OU=Dev, O=Company, C=CN"这个命令生成的文件就是后面 Java 代码里的密钥库来源。注意:-storepass和-keypass在 PKCS12 里通常保持一致,如果后面加载时报UnrecoverableKeyException,先怀疑这两个密码不一致。
3.2 签章代码:签到什么位置、锁定哪些修改
iText 5 的签章用PdfStamper.createSignature打开一个签名模式,然后在PdfSignatureAppearance上配置密钥和可见外观。核心代码:
KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(new FileInputStream("signer.p12"), "keystorePassword".toCharArray()); String alias = "signer"; PrivateKey pk = (PrivateKey) ks.getKey(alias, "keystorePassword".toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); PdfReader reader = new PdfReader("first.pdf"); PdfStamper stp = PdfStamper.createSignature(reader, new FileOutputStream("signed.pdf"), '\0', null, true); PdfSignatureAppearance sap = stp.getSignatureAppearance(); sap.setCrypto(pk, chain, null, PdfSignatureAppearance.WINCERIFICATED); sap.setVisibleSignature(new Rectangle(100, 100, 300, 150), 1, "signField"); sap.setReason("合同签章"); sap.setLocation("北京"); stp.close(); reader.close();这里几个关键参数:createSignature的第三个参数'\0'表示 PDF 版本不强制改变,第四个参数是临时文件,大文件签章时建议传一个磁盘路径而不是 null,避免内存峰值;第五个参数 true 表示允许后续追加内容,false 则签完后整份文档进入锁定状态。setCrypto最后那个枚举WINCERIFICATED表示已验证签名但未做认证锁定,如果你需要「签完后人不能改任何内容」,改成CERTIFIED_NO_CHANGES_ALLOWED。setVisibleSignature第一个参数是签名显示区右下角和左上角的坐标,第二个是页号,第三个是签名字段名。同一份文档多次签章时,字段名必须唯一,否则后面的签章会覆盖前面的。
签章完成后用 Adobe Reader 打开signed.pdf,右侧签名面板应能看到签名有效。如果它提示「文档已更改」,说明你在签章后又用普通PdfStamper往文档里加了内容,这会破坏原签名。签章和后期编辑的顺序冲突,是这里最常见的返工原因。
4. 斜体字水印:单条、平铺与角度控制
4.1 用 PdfContentByte 画一条斜体水印:斜体不是旋转
水印的本质是在页面内容流里叠加文字和图形。iText 5 的PdfStamper提供getUnderContent和getOverContent两个入口,前者把内容画在页面原有内容下面,后者画在上面。选择哪个取决于水印用途:防篡改的水印通常画在内容上层,防止截图后直接拷贝文字;防透传的底纹水印画在下层,不遮挡正文。绝大多数需要斜体文字水印的场景,我选getOverContent,这样水印不会被正文盖住。
画水印的最小代码:
PdfReader reader = new PdfReader("first.pdf"); PdfStamper stp = new PdfStamper(reader, new FileOutputStream("watermark.pdf")); PdfGState gs = new PdfGState(); gs.setFillOpacity(0.3f); BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); for (int page = 1; page <= reader.getNumberOfPages(); page++) { PdfContentByte cb = stp.getOverContent(page); cb.saveState(); cb.setGState(gs); cb.beginText(); cb.setFontAndSize(bf, 24); cb.setColorFill(BaseColor.GRAY); cb.showTextAligned(Element.ALIGN_CENTER, "内部资料", 297.5f, 421.5f, 30f); cb.endText(); cb.restoreState(); } stp.close(); reader.close();showTextAligned的最后一个参数是旋转角度,单位是弧度。30f 大约相当于 5.2 度,视觉上文字有轻倾斜。如果你想做真正的斜体(字形本身倾斜),应该在Font层面用Font.ITALIC,或者加载 TTF 字体时指定按斜体路径加载。区别是:旋转改变的是整行文字的方向,斜体改变的是字形姿态,二者叠加效果才是常见的「斜字水印」。
setFillOpacity是透明度,0.3f 表示叠在正文上时既能看到水印又不遮住正文,但这个值在不同 PDF 阅读器上没有统一标准,有些打印驱动会把透明度强行变成 1。如果你遇到「屏幕上看是浅的,打印出来是黑的」,检查一下 target 环境的打印设置,这是领导的常见抱怨点。
4.2 多行多列平铺:循环参数和页面坐标怎么配合
单条水印在大页面上不够醒目,合规的电子发文经常要求整页平铺。做法是按页面宽度和高度循环计算水印的 x、y 坐标。以 A4 为例,页面横坐标范围约 0 到 595,纵坐标约 0 到 842,常用间隔是行距 120 磅、列距 180 磅,每行错开半个间距让水印呈斜向排列。
float pageW = reader.getPageSize(page).getWidth(); float pageH = reader.getPageSize(page).getHeight(); float stepX = 180f; float stepY = 120f; for (float y = stepY; y < pageH; y += stepY) { for (float x = stepX; x < pageW; x += stepX) { float offsetX = ((y / stepY) % 2 == 0) ? 0f : stepX / 2f; cb.showTextAligned(Element.ALIGN_CENTER, "内部资料", x + offsetX, pageH - y, 30f); } }这里的 offsetX 让偶数行列起点不同,形成棋盘式错位;y 从下往上递增,页面坐标原点在左下角,所以pageH - y把 y 反转为视觉上的从上到下。平铺水印的性能问题:一页一般循环到 60 次左右showTextAligned,如果处理几百页的大文件,建议先预估总绘制次数,控制在每页 80 次以内,否则内容流膨胀会导致文件体积增加 30% 以上。在实际项目中,我一般把透明度和字号参数配置化,避免每种文档需求都重新编译部署。
showTextAligned在循环里每一次都会开启一个文本对象,频繁调用时 beginText/endText 的配对不可少。如果少了endText,最后生成的文件会显示水印丢失或内容流报错。这个「玄学问题」多数时候就是 state 没配对。
5. 文本替换:内容流解析、Tj/TJ 操作符与落地方案
5.1 为什么 iText 没有现成的 replaceText API
很多人一上来就搜 replaceText,结果发现 iText 5 和 7 都没有一个把「旧字符串替换成新字符串」的高层方法。原因是 PDF 里文本不是连续的字符串,而是由内容流里的 BT/ET 块、Tj/TJ 操作符,配合字体编码组合出来的。一个词在视觉上连续,在内容流里可能被拆成多个片段,每个片段还有一个位置矩阵。解析时要同时解决两个问题:内容流解码(FlateDecode 压缩)和字符编码映射(WinAnsi、Identity-H 等)。
7.x 有PdfCleanUp模块,它做的是信息遮蔽(redaction),可以把指定区域文字涂黑删除,但替换成别的文本要自己补。5.x 时代更常见的做法是两条路线。路线 A:处理生成型 PDF 时不要等文档下发再替换,在模板里留占位符字符串,用内存里的替换逻辑在输出前改掉。路线 B:对无法改生成端的 PDF,解析 content stream,定位 Tj/TJ 的括号字符串,按需替换后写回。B 路线就是下面要讲的,它只适用于简单 PDF,流程图、复杂排版、字体子集化的文件会翻车。
5.2 定位和替换代码:压缩流交给 iText,编码坑自己扛
用 iText 5 的getPageContent和setPageContent可以避开手动解压 FlateDecode 的麻烦,它们内部会做过滤器的转换。核心思路是:把页面内容流读出来,定位包含目标文本的 Tj 操作,替换匹配到的字符串,然后写回。
PdfReader reader = new PdfReader("template.pdf"); int pageNum = 1; byte[] raw = reader.getPageContent(pageNum); String content = new String(raw, "ISO-8859-1"); // 只处理标准 WinAnsi 编码的简单文本:目标字符串以括号形式出现在 Tj 前 content = content.replace("(PLACEHOLDER_ORDER_NO)", "(HT-2024-001)"); byte[] updated = content.getBytes("ISO-8859-1"); reader.setPageContent(pageNum, updated); PdfStamper stp = new PdfStamper(reader, new FileOutputStream("replaced.pdf")); stp.close(); reader.close();这段代码有两个必须知道的前提。第一,getPageContent返回的是经过解码的原始内容流,若原 PDF 页面内容被 FlateDecode 压缩,setPageContent会帮我们重新编码,所以不用手动处理压缩。但如果原内容流是对象引用或增量更新,直接setPageContent有时会因为 content stream 的 length 对象没更新而打开报错;解决办法是对这种文件先另存一份再重新打开修理。第二,ISO-8859-1 编码下括号、斜杠这些字节不会被破坏,但如果目标文本是中文,在内容流里通常以十六进制字符串<4E2D6587>形式出现在 TJ 里,字符串直接 replace 是不行的,需要先把 hex 文本转成字节,匹配到字符后再转回 hex。遇到这种文件,我一般先打印内容流开头 1000 字节判断编码类型,再决定用哪种 replace。
具体到中文替换,一个更稳的方案是先定位可视文本的矩形区域,再调用覆盖逻辑:把旧内容用白色矩形涂掉,再在同一位置写好新文本。这方案不依赖于内容流的字符串组织方式,缺点也很明显:替换前后字数不同会导致右侧留白或重叠,所以它只适合等宽替代或替换后重新排版。
5.3 替换后的验证手段
文本替换完成后,验证不能只靠眼睛看。用PdfReader的文本抽取策略快速确认是否替换成功,这个检查一定要做,它同时验证了 xref 是否完好。
PdfReader reader = new PdfReader("replaced.pdf"); PdfReaderContentParser parser = new PdfReaderContentParser(reader); for (int page = 1; page <= reader.getNumberOfPages(); page++) { TextExtractionStrategy strategy = parser.processContent(page, new LocationTextExtractionStrategy()); String text = strategy.getResultantText(); if (text.contains("HT-2024-001")) { System.out.println("page " + page + " passed"); } } reader.close();如果抽取结果里查不到目标字符串,说明替换要么没落在内容流,要么编码混了。这一步比拿阅读器打开更可靠,因为在阅读器里看到的可能是字体渲染缓存,不一定真是内容流的文本。
6. 避坑:四条高频翻车记录和最后一道验证习惯
以下四条都来自实际项目里反复出现的「翻车现场」,按现象、原因、解决三行记录,供你把教训留作规避清单。
(1)现象:生成 PDF 中文全部变方块或空白。原因:BaseFont 用了默认 Helvetica,它不支持中文;或没引 itext-asian 包,STSong-Light 创建失败。解决:统一用BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED),并把 itext-asian 加进依赖。
(2)现象:批量处理上千个 PDF 时,程序在回收资源阶段报错,先是 IOException "Too many open files",后来是 OutOfMemoryError。原因:在循环里 newPdfReader/PdfStamper后没有及时 close,文件句柄和 reader 缓存堆积,最终把操作系统的句柄上限打爆。解决:每个文件处理完立即关闭 reader 和 stamper,不要等 GC;如果一次要处理整个目录,就分页批次处理,每批 200 个,处理完一批清理一次。
(3)现象:先签章后加水印,水印加完签名失效。原因:数字签名是对整个文件摘要做的,签名后的任何内容改动都会破坏摘要,包括新增水印。解决:编排流程时把水印、文本替换等全部编辑操作放在签名前完成,签名作为整条流水线的最后一步;需要签多个人时,后一个签名会覆盖前一个可见签名区,签名区域要错开。
(4)现象:文本替换后文件用阅读器打开直接提示修复或空白页。原因:直接替换内容流时没有同步更新 xref 和 Length 对象,或者替换内容把 Tj 操作符的括号配对破坏了。解决:替换前先备份原始内容流;替换后立即用PdfReader重新打开一次,能正常打开再返回;如果打开报错,还原备份并改为覆盖式替换(涂白再写新文本)。
关于验证,我个人的习惯是:每次签章、水印或替换做完,不直接交付,而是先调PdfReader重新打开文件并抽取首页文本,同时把 PDF 文件大小对比一次。文件大小异常变小,通常意味着内容流被截断;异常变大很多,则常见于字体被重复嵌入。这两条检查十秒钟就能做,能在交付前拦掉一大半的问题。
用 iText 操作 PDF 这条技术路线,值不值得投入,我的结论很明确:如果你的业务形态是「程序生成模板 → 批量盖章 → 归档」,iText 5.x 这套 API 足够稳定,完全可以用它搭建生产流水线;但如果你需要在任意第三方 PDF 上做文本替换,请把预期降到「只处理自家生成的文件」。我早年间在这些边界上吃过不少教训,后来凡是接外部文件,一律先做格式探测,不做探测就直接跑替换流程,迟早要返工。希望帮到你。
本文还有配套的精品资源,点击获取