☰
iText 7 实战:PDF 创建、数字签章、水印与文本替换
2026/10/29 4:51:50 网站建设 项目流程

简介:面向需要处理PDF文档的Java开发者,这份源码工具包围绕新建文档、电子签章、斜字水印与文本替换四类常见任务,提供可直接导入Eclipse的示例工程和实现参考,适合课程设计、企业文档处理及二次开发场景中的初中级读者。压缩包共32个文件,约8.77MB,包含Java源码、编译后的class、iText与BouncyCastle等jar依赖、p12证书、图片素材以及工程配置和说明文件,目录按源码、库文件与输出结果组织,便于运行调试和拆分复用。已有1572人学习下载,说明其在PDF处理入门与工具选型中具有一定参考价值。读者可从完整工程中对照理解文档写入、盖章签名、签名外观、水印绘制与文本替换等核心接口的调用方式,掌握签章位置与证书配置、水印透明度与旋转角度、文本替换的边界与风险,并依据依赖清单搭建本地环境,减少从零摸索成本,也可作为接口速查与功能验证的辅助材料。

1. 从一份被退回的 PDF 说起:iText 能接住哪些活

一份 PDF 在交付前被退回,理由不是内容写错,而是签章域压住了骑缝位置;另一份是水印角度太正,打印出来像贴了一张便签。iText 操作 PDF 文档这件事,听起来像库函数调用,真落到创建、签章、斜字水印和文本替换四件事上,边界比想象多。这套源码包把四块拆成独立工具类:创建负责页面与字体嵌入,签章负责数字签名和可见印章,水印负责旋转文本与透明度,替换负责在既有页面上定位并重写。适合手里有批量 PDF 交付、合同附件、内部资料加水印需求的一线开发,也适合想找一份能直接改参数就跑的源码工具的人。下面按能复现的顺序拆。

2. 依赖与创建:把 iText 7 拉进工程并生成第一份中文 PDF

2.1 依赖拆分:kernel、layout、sign 各管什么

iText 7 的模块划分比老版本细,刚上手容易把依赖一股脑塞进去,最后编译能过、运行时报ClassNotFoundException。常见做法是按能力拆三块:kernel管 PDF 对象、页面、内容流;layout管Document、Paragraph、表格这类高层排版;sign管数字签名、证书链和签名域。只创建 PDF 时kernel + layout就够,要签章再加sign,水印和替换基本落在kernel的PdfCanvas上。

下面这份 Maven 依赖把版本抽成属性,REPLACE_ME换成团队仓库实际锁定的版本即可,不要在不同模块里各写一个版本号,签章和内容流对版本差异很敏感。

<properties> <itext.version>REPLACE_ME</itext.version> </properties> <dependencies> <dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>${itext.version}</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>layout</artifactId> <version>${itext.version}</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>sign</artifactId> <version>${itext.version}</version> </dependency> </dependencies>

kernel是底座,layout依赖它,sign又依赖前两者,Maven 会做传递依赖,但显式写出来便于排查冲突。源码包里通常会把字体加载、页面尺寸、输出目录封成一个PdfCreateTool,不要在每个业务类里重复PdfFontFactory.createFont,字体对象复用能减少嵌入体积,也能避免同一文档里出现多份字体子集。

我一般把输出目录固定成out/,输入放in/,证书和印章图片放resources/,这样批量脚本不用改路径。工具类只暴露三个参数:目标路径、标题、正文列表,业务侧不直接碰PdfDocument。这样后面加水印、加签章时,创建链路不会被业务代码绑死。

2.2 创建第一份 PDF:页面、字体和段落

创建 PDF 的代码不长,但中文场景下字体是第一个卡点。iText 7 默认字体不含中文字形,直接写中文会得到空白或方框。下面这段代码用PdfEncodings.IDENTITY_H以 Unicode 方式嵌入 TTF,FONT_PATH换成有授权的字体文件,路径不要带空格,Windows 和 Linux 批处理都省事。

import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.Paragraph; import com.itextpdf.layout.properties.TextAlignment; import java.io.IOException; public class PdfCreateTool { private static final String FONT_PATH = "fonts/cjk.ttf"; // 换成有授权的字体文件 public static void main(String[] args) throws IOException { String dest = "out/created.pdf"; PdfWriter writer = new PdfWriter(dest); PdfDocument pdfDoc = new PdfDocument(writer); Document document = new Document(pdfDoc); document.setMargins(56, 56, 56, 56); PdfFont font = PdfFontFactory.createFont(FONT_PATH, PdfEncodings.IDENTITY_H); Paragraph title = new Paragraph("项目交付说明") .setFont(font) .setFontSize(20) .setTextAlignment(TextAlignment.CENTER); Paragraph body = new Paragraph("这是一段用于验证字体嵌入的中文正文。") .setFont(font) .setFontSize(12) .setFirstLineIndent(24) .setMultipliedLeading(1.5f); document.add(title); document.add(body); document.close(); } }

PdfWriter负责字节输出,PdfDocument是内存中的 PDF 模型,Document是排版层。setMargins四个参数按上、右、下、左给,单位是 point,56 约等于 2 厘米。setFirstLineIndent(24)是首行缩进,setMultipliedLeading(1.5f)是行距倍数,中文正文用 1.4 到 1.6 比较稳。document.close()会级联关闭PdfDocument和PdfWriter,不要只关PdfDocument又忘了关Document,否则内容可能没 flush 完。

如果标题需要换行,Paragraph里用\n不一定按预期断行,常见做法是拆成多个Paragraph,或者用setWidth(UnitValue.createPercentValue(80))控制宽度让排版层自动折行。正文多段时不要一个大Paragraph塞到底,后面加水印和替换时定位会粗,拆段更利于排查。

2.3 字体嵌入的坑:为什么中文不能只靠默认字体

默认字体是西文标准字体,中文没有对应字形,阅读器可能显示空白,也可能显示替换字体,看起来本地正常、发到别人机器上就变样。PdfFontFactory.createFont(FONT_PATH, PdfEncodings.IDENTITY_H)会把字体嵌入 PDF,但要注意字体授权,不要把系统字体直接打进对外交付的文件里。

另一个坑是字体子集。iText 默认会做子集嵌入,只保留用到的字形,体积小,但后续如果往同一个PdfFont对象里追加新字符,可能出现新字缺字形。血泪经验是:创建阶段一次性把要用的字符集固定下来,或者替换阶段重新加载完整字体,不要拿一个半路复用的PdfFont到处showText。

还有个隐蔽问题是字体文件路径在 jar 包里。开发时用fonts/cjk.ttf能跑,打成 jar 后相对路径变了,IOException直接抛在启动阶段。工具类里我一般用getResourceAsStream读字体字节,再交给PdfFontFactory.createFont(byte[], String),路径只作为兜底。这样源码包换机器运行时,字体不会成为第一个翻车点。

3. 签章链路:数字签名、可见印章与验签失败信号

3.1 签章不是盖图片:哈希、私钥和证书链

很多人把“签章”理解成把印章 PNG 贴到 PDF 上,这只做了外观。真正的数字签名是对 PDF 字节范围做哈希,用私钥签名,再把签名值、证书链和签名属性写进签名域。验签时阅读器重新计算哈希,用证书公钥解签名,比对成功才认为文档没被改。印章图片只是PdfSignatureAppearance的外观层,和完整性校验是两件事。

测试环境可以用keytool生成自签名证书,生产环境换成 CA 签发的证书链。下面命令只用于本地跑通,-dname里的字段是占位,不要直接拿去对外签文件。-storepass和-keypass在脚本里不要写死,放环境变量或密钥管理里。

keytool -genkeypair -alias pdfsign -keyalg RSA -keysize 2048 -validity 3650 \ -dname "CN=PDF Test, OU=Demo, O=Example, L=City, ST=State" \ -keystore pdfsign.p12 -storetype PKCS12 -storepass changeit -keypass changeit

-alias是密钥别名,后面ks.getKey("pdfsign", ...)要对应;-keysize 2048是 RSA 位数,低于 2048 的证书在新阅读器里可能被提示弱算法;-validity 3650是有效期天数,测试可以短一点,避免证书过期后误判代码问题。PKCS12格式方便 Java 读取,别用 JKS 再转来转去。

签章前要确认待签 PDF 没有被加密到无法写入,或者签名域位置落到了CropBox之外。可见签名域用页面坐标,原点在左下,Rectangle(x, y, width, height)的y不是从顶部算。这个坐标和前端画布坐标相反,直接拿前端坐标过来会上下颠倒。

3.2 外签代码:PdfSigner 与可见签名域

下面代码做外签:读入创建好的 PDF,写出签名后的文件,同时设置可见印章图片和签名描述。PdfSigner的setFieldName要唯一,批量签多页时不要每页都用同一个Signature1,否则第二个签名域会冲突。

import com.itextpdf.io.image.ImageDataFactory; import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.StampingProperties; import com.itextpdf.signatures.*; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.io.FileInputStream; import java.security.KeyStore; import java.security.PrivateKey; import java.security.Security; import java.security.cert.Certificate; public class PdfSignTool { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(new FileInputStream("pdfsign.p12"), "changeit".toCharArray()); PrivateKey pk = (PrivateKey) ks.getKey("pdfsign", "changeit".toCharArray()); Certificate[] chain = ks.getCertificateChain("pdfsign"); PdfReader reader = new PdfReader("out/created.pdf"); PdfWriter writer = new PdfWriter("out/signed.pdf"); PdfSigner signer = new PdfSigner(reader, writer, new StampingProperties()); signer.setFieldName("Signature1"); signer.setReason("Approval"); signer.setLocation("Demo"); signer.getSignatureAppearance() .setPageRect(new Rectangle(350, 50, 180, 70)) .setPageNumber(1) .setSignatureGraphic(ImageDataFactory.create("stamp.png")) .setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC_AND_DESCRIPTION); signer.signDetached( new BouncyCastleDigest(), new PrivateKeySignature(pk, "SHA-256", "BC"), chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES ); } }

signDetached的参数依次是摘要实现、私钥签名器、证书链、时间戳客户端、CRL 客户端、OCSP 客户端、预估签名大小、签名标准。0表示让 iText 估算预留空间,批量签大文件时如果报签名域空间不足,可以调到 8192 或更大。CryptoStandard.CADES会带签名属性,适合长期验证;老系统只认 CMS 时再换CMS,不要两边混用。

setRenderingMode用GRAPHIC_AND_DESCRIPTION会同时画印章图和文字描述,只想显示图片就用GRAPHIC。印章图片不要用透明背景过大的 PNG,签名域高度不够时图片会被裁切,看起来像“章盖了一半”。setPageNumber(1)是页码,从 1 开始,不是索引。

3.3 验签与失败信号:返回 false 之后看什么

签完不验等于没签。下面代码遍历签名域,调用verifySignature()拿到PdfPKCS7,再调verify()做完整性校验。输出里要同时看完整性和签名者证书主题,别只看一个布尔值。

import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.signatures.PdfPKCS7; import com.itextpdf.signatures.PdfSignature; import com.itextpdf.signatures.PdfSignatureFormField; public class PdfVerifyTool { public static void main(String[] args) throws Exception { PdfDocument pdfDoc = new PdfDocument(new PdfReader("out/signed.pdf")); for (PdfSignatureFormField field : pdfDoc.getForm().getSignatureFields()) { PdfSignature signature = field.getSignature(); PdfPK <p> <a href="https://download.csdn.net/download/weixin_38669628/11154677" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询