ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

iText PDF操作进阶:创建、签章、斜体水印与文本替换实战

iText PDF操作进阶:创建、签章、斜体水印与文本替换实战 简介面向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/PdfWriterPdfStamper变成了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 groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13/version /dependency dependency groupIdcom.itextpdf/groupId artifactIditext-asian/artifactId version5.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 CNTest Signer, OUDev, OCompany, CCN这个命令生成的文件就是后面 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 上做文本替换请把预期降到「只处理自家生成的文件」。我早年间在这些边界上吃过不少教训后来凡是接外部文件一律先做格式探测不做探测就直接跑替换流程迟早要返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表