news 2026/10/17 12:07:24

iText 7 PDF处理实战:创建、文本替换、水印与数字签章全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iText 7 PDF处理实战:创建、文本替换、水印与数字签章全攻略

简介:面向 Java 开发者的 PDF 处理实用源码包,聚焦 iText 库的四大核心操作:创建文档、电子签章、斜字水印与文本替换。其中创建文档涵盖页面设置、字体与段落排版;签章支持证书导入与签名区域定义;水印可设置透明度及旋转角度;文本替换则依赖表单域完成。资源基于 iText 5.5.11 与亚洲字体包,并附带加密、证书等相关依赖库,适合需要在业务系统中集成 PDF 生成、签署或内容修订的工程师参考学习。

压缩包共三十二个文件,包含七个 Java 源码与九个编译后的 class 文件、五个核心依赖库、两个 p12 证书、若干图片素材以及工程配置文件,整体体积约 8.77MB。目录结构清晰,说明文档对功能模块进行描述,可快速导入开发环境运行演练。

目前该源码包已有 1571 人学习,从中可看到完整的 iText 操作 PDF 工程样例:既提供可直接运行的代码实现,也包含签名证书、水印图形等配套资源,尤其对 PDF 签章与文本替换中的常见问题有直观演示,能帮助使用者快速上手 iText,避免重复踩坑,提升开发效率。

1. 为什么用 iText 处理 PDF:一次说清创建、签章、水印、替换这四件事的关系

后端只要沾到合同、报告、回执这类业务,基本躲不开 PDF,而 iText 是 JVM 生态里绕不过去的那一套库。标题里列的四件事——创建、签章、斜字水印、文本替换,看起来是四个独立功能,实际都建立在同一个 PdfDocument 和 PdfCanvas 模型上:创建负责从无到有生成文档,水印和文本替换是往页面内容流里追加或覆盖绘制指令,签章则是在文件末尾加一段不可篡改的签名容器。很多团队在调研阶段最容易被“文本替换”带偏,因为 iText 官方并没有一个 replaceText 方法,网上能找到的代码又多是 iText 5 老 API,拿 7.x 一跑就报 NoClassDefFoundError。这篇文章我会按一条可落地的链路讲:先有一个能稳定输出中文 PDF 的工程,再依次把替换、水印、签章加进去,最后把最容易翻车的五个坑单独拎出来,每条都给出现象和解决路径。适合正在做电子签章、批量报告生成、敏感词遮盖的 Java 开发者,尤其是项目里已经翻过车、准备换方案的人。

2. 创建 PDF:最小 Java 工程与中文不乱的字体配置

2.1 先分清 Document 和 PdfDocument:iText 7 的分层逻辑

iText 5 时代,一个 PdfWriter 加一个 Document 就能干活,很多老教程到今天还能用,但换到 iText 7 以后,整个模型被拆成了 kernel 和 layout 两层。PdfDocument 属于 kernel 层,负责页面、内容流、字体、注解这些底层对象;Document 属于 layout 层,负责把 Paragraph、Table、Image 这些高层的“块”自动排版到页面上。一句话记:PdfDocument 是画板和画笔,Document 是帮你排版的排版工。

创建 PDF 时最常见的写法是先 new PdfWriter,把它包进 PdfDocument,再用 Document 包一层。两个对象都有 close 方法,但关闭顺序有讲究:必须先关 Document,再关 PdfDocument。如果反过来,Document 的缓存内容还没有刷进内容流,输出文件就会少页或者整页空白。这个顺序问题在 Spring Boot 里尤其隐蔽,因为容器关闭时资源清理顺序不可控,最好在 finally 里显式按顺序关。

PdfWriter writer = new PdfWriter("out.pdf"); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf); // 这里写段落、表格 document.close(); pdf.close();

第二个参数是输出流,也支持 OutputStream,批处理场景建议直接用 ByteArrayOutputStream 或临时文件流,避免直接写磁盘路径带来的权限问题。PdfWriter 还有一个带 StampingProperties 的构造,专门用于读已有 PDF 再改写的场景,第 3 章文本替换会用到。

2.2 最小可运行 Demo:从空白页到“中文不乱码”的段落

很多第一次用 iText 7 的人会在中文字体上卡住。默认的 Helvetica 是标准 Type1 字体,不包含中文字形,直接写中文,出来的 PDF 全是空白或者方框。解决路径有两条:一是加载系统字体文件,比如 Windows 的 simsun.ttc、Linux 的 NotoSansCJK;二是用 iText 官方提供的 font-asian 包,里面内置了 Adobe 的亚洲字体集合,最常用的是 STSongStd-Light,配合 UniGB-UCS2-H 编码,就能输出简体中文。

Maven 依赖里除了 itext7-core,还要额外加 font-asian,版本和 iText 主包保持一致。下面是最小可运行的工程结构:

<dependency> <groupId>com.itextpdf</groupId> <artifactId>itext7-core</artifactId> <version>7.2.5</version> <type>pom</type> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>font-asian</artifactId> <version>7.2.5</version> </dependency>

itext7-core 是一个聚合 POM,会把 kernel、layout、io、signatures 等模块一起拉下来,日常项目直接用这一个依赖就行。font-asian 单独引入,因为它不是核心模块的默认依赖,很多团队漏掉这一行,然后死活找不到 STSongStd-Light。

创建带中文段落的完整代码:

PdfFont font = PdfFontFactory.createFont( "STSongStd-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); PdfWriter writer = new PdfWriter("hello.pdf"); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf, PageSize.A4); Paragraph p = new Paragraph("你好,iText PDF 文档处理") .setFont(font) .setFontSize(14f) .setFontColor(ColorConstants.BLUE); document.add(p); document.close(); pdf.close();

createFont 的第一个参数是 PostScript 字体名,第二个参数是 CMAP 编码名。UniGB-UCS2-H 表示简体中文横向排版,H 是 horizontal,V 是 vertical。EmbeddingStrategy 建议用 PREFER_EMBEDDED,这样即使对方机器没装中文字体,PDF 打开也不会掉字。如果业务要求必须减小文件体积,可以改成 PREFER_NOT_EMBEDDED,但前提是目标环境一定有中文字体,否则打印或预览时会替换字体导致排版错乱。

2.3 页面尺寸、边距和坐标:排版跑偏往往从这里开始

创建 Document 时不传 PageSize,默认是 A4 纵向。A4 的尺寸是 595 x 842 点,1 点约等于 0.3528 毫米,这个换算在画水印、签章矩形时经常用到。Document 的第二个构造参数是左右上下四个边距,比如 new Document(pdf, PageSize.A4, 36, 36, 36, 36) 表示四边各留 36 点,也就是 1.27 厘米左右的页边距。

需要精确控制位置的内容,不要依赖 Document.add,而是用 Paragraph.setFixedPosition(pageNumber, x, y, width) 或者 Image.setFixedPosition。注意这里的 x、y 是页面的左下角坐标系,不是常见的左上角。很多人从 HTML/CSS 思维转过来,习惯性认为 y 越小越靠上,结果元素跑到了页面外面。换算公式很简单:页面内顶部位置 topY = pageHeight - marginTop - elementHeight,如果要从顶部往下 100 点开始画,y 应该写 842 - 100 - elementHeight。

Paragraph stamp = new Paragraph("固定位置文本") .setFont(font) .setFixedPosition(1, 72, 720, 200); document.add(stamp);

setFixedPosition 的第一个参数是页码,从 1 开始。如果这段代码放在循环里给每一页都加内容,页码参数要取当前页索引。另一个常见的误区是带着内容再去 new Document 套已有 PDF:Document 默认就是“新建文件 + 追加内容”,如果你打算打开一个已有 PDF 往里插一段文字,再用 Document.add,会得到一个空白的覆盖文件。正确做法是第 3 章的 PdfReader + PdfDocument 组合,而不是 Document 直接包 PdfWriter,很多老项目在这上面出过生产事故。

3. 文本替换:iText 没有现成 replaceText,我用 Canvas 覆盖旧文本

3.1 为什么官方不提供 replaceText API

PDF 打开后看起来是一个个可编辑的字符,但底层文件里记录的只是“在某个坐标用某个字体绘制这些字形”的指令,并没有保留文档级文本结构。所以替换文本不是一个查找替换操作,而是要把旧的绘制指令“擦掉”,在同一个位置绘制新的指令。这个“擦掉”又分两层:一是视觉上覆盖掉旧内容,二是内容流里旧文本对象仍可能被复制出来,如果对安全性有要求,必须用 redaction 方式真正清除内容流,而不仅仅是画一个白底矩形。

常见的做法有两类。一类是覆盖法:在旧文本位置画一个不透明矩形盖住,然后再写入新文本,适合替换字符串长度接近、对内容流残留不敏感的场景。另一类是 redaction 法:iText 的 pdfSweep 模块可以把指定文本区域从内容流中物理删除,代价是性能开销大、坐标定位复杂。我在日常业务中默认用覆盖法,因为大部分需求是把“张三”替换成“李四”,或者在敏感字段上打码,覆盖法足够。

文本替换需要先回答一个问题:怎么找到旧文本的坐标。iText 7 提供了一个内容解析器,可以拿到页面里所有文本片段的渲染信息,包括基线坐标、字号、宽度。下面是自定义的坐标收集器。

class TextLocationExtractor extends LocationTextExtractionStrategy { List<TextRect> locations = new ArrayList<>(); @Override public void renderText(TextRenderInfo renderInfo) { LineSegment baseline = renderInfo.getBaseline(); float x = baseline.getStartPoint().get(Vector.I1); float y = baseline.getStartPoint().get(Vector.I2); float width = renderInfo.getWidth(); float height = renderInfo.getFontSize(); String text = renderInfo.getText(); locations.add(new TextRect(x, y, width, height, text)); } }

TextRenderInfo.getBaseline() 返回的是一段基线线段,getStartPoint 取线段起点。这里有一个坐标系细节:PDF 的 y 轴是自下而上的,所以基线的 y 值是文字底部的坐标,绘制水印或替换文本时的 setTextRenderingMode 和文本坐标都用这套逻辑。TextRect 是自己定义的 POJO,用来保存位置信息。

拿到所有文本片段后,遍历匹配目标字符串。匹配逻辑要注意两点:一是 PDF 的一行文字可能被拆成多个 TextRenderInfo,尤其是有空格和中英文混排时;二是如果目标文本跨行了,就要按行拆解后逐行处理。我一般做的是单行精确匹配,复杂需求会先按行聚合再匹配。

try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(src))) { for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { PdfPage page = pdfDoc.getPage(i); TextLocationExtractor strategy = new TextLocationExtractor(); PdfTextExtractor.getTextFromPage(page, strategy); for (TextRect rect : strategy.locations) { if (rect.text.contains("张三")) { replaceText(page, rect, "李四", font); } } } }

这段代码里用 try-with-resources 管理 PdfDocument,会自动关闭底层流,比手动关闭省心。PdfReader 不会锁定文件,所以同一个文件可以被重复读取,但写入时要注意输出流不能和输入流指向同一个物理文件,否则会报“流已关闭”或文件锁异常,建议输出到临时文件再覆盖。

3.2 覆盖旧文本:自己画矩形,防止“黑历史”残留

定位到旧文本位置后,下一步是在这个矩形区域上画一个不透明矩形,把旧内容盖住,再写入新文本。矩形颜色要和页面背景一致,通常是白色,但如果原 PDF 背景不是纯白,或者表格单元格带底色,就不能用白色,得先读页面背景色。简单场景下可以直接用白色,覆盖区域不会大到影响阅读。

覆盖的时候,矩形的高度不能只取字号,因为中文字形上下都有留白,行高一般要按字号乘以 1.2 到 1.4 来算。用基线 y 坐标作为矩形底边,实际代码里我会给 y 减去一个偏移量,让矩形覆盖从上一行底部到下一行顶部的范围,否则容易露出残影。

public void replaceText(PdfPage page, TextRect old, String newText, PdfFont font) { PdfCanvas canvas = new PdfCanvas(page); canvas.saveState(); canvas.setFillColor(ColorConstants.WHITE); canvas.rectangle(old.x - 2, old.y - old.height * 0.2f, old.width + 4, old.height * 1.4f); canvas.fill(); canvas.restoreState(); canvas.beginText(); canvas.setFontAndSize(font, old.height); canvas.setTextRenderingMode(PdfCanvas.TEXT_RENDER_MODE_FILL); canvas.setFillColor(ColorConstants.BLACK); float newWidth = font.getWidth(newText, old.height); float startX = old.x + (old.width - newWidth) / 2; canvas.moveText(startX, old.y); canvas.showText(newText); canvas.endText(); }

参数说明:old.x 是旧文本左边界,old.y 是基线,old.height 是旧字号。rectangle 的宽度在原文本宽度上加 4,是为了覆盖轻微的抗锯齿边缘;高度用 1.4 倍字号,避免上下的墨迹残留。newWidth 通过 PdfFont.getWidth 按实际字号计算,新文本在旧区域水平居中,视觉效果最接近原排版。

replaceText 方法里用的是同一个 PdfCanvas 实例,saveState 和 restoreState 成对出现,确保覆盖矩形不会污染后续的内容流状态。这里有一个容易被忽略的坑:覆盖用的 canvas 和写入新文本的 canvas 如果是两个对象,内容流顺序可能会颠倒,导致新文本先画、矩形后画,结果把新文本也盖住了。所以必须用同一个 canvas 实例,顺序是矩形在前,文本在后。

3.3 替换后文本长度不一致怎么办

这是文本替换需求里最考验人的部分。替换“张三”为“李四”,宽度差不多,覆盖后正常;但如果把“北京市朝阳区”替换成“北京市朝阳区某某街道某某号”,新文本宽度可能超出旧区域。解决办法是按新文本实际宽度重新计算覆盖区域,把矩形扩大,同时检查是否超出页面边界或和其他文字重叠。

float newWidth = font.getWidth(newText, old.height); float rectWidth = Math.max(old.width, newWidth) + 4; float rectHeight = old.height * 1.4f; canvas.rectangle(old.x - 2, old.y - old.height * 0.2f, rectWidth, rectHeight);

如果替换后文本明显变长,并且原区域右侧有其他重要内容,扩大覆盖区域就会盖掉不该盖的字符。这种冲突没有万能解法,常见做法是把新文本缩小字号,或者把替换内容拆成两行,二选一要看业务是否允许。还有一种做法是不做“替换”,而是把旧文本打成马赛克,然后把新文本写到旧文本下方,很多电子合同里的修改痕迹就是这么做的,既能保留变化记录,又避免重叠。

多页替换时建议用一个计数器记录每页处理了多少处替换,如果某页目标文本出现多次,循环里要每次重新获取该页的 PdfCanvas,不能复用上一页的 canvas 实例。内容流写入完成后再调用 PdfDocument.close,替换才会真正刷进输出文件。text-replacement 定位这块最容易踩的坑是 y 坐标和字号不匹配,导致矩形盖在了文字上方或下方,后面避坑章会专门谈到。

4. 斜字水印:旋转、透明度、平铺的实现与参数

4.1 “斜字水印”到底指什么

很多需求文档写“加斜字水印”,字面理解是字体倾斜,但实际交付时,需求方要的往往是“斜向排布的大水印”,也就是那种从左下角到右上角、贯穿整个页面的“内部资料”“复印件无效”。真正让水印有斜向感的是旋转角度,而不是字体本身的 italic。iText 里设置斜体字需要字体文件自带 Italic 变体,否则只能靠人工倾斜,效果很差。所以我在实现时按“旋转 45 度”的方式来处理,这也是行业里最普遍的做法。

用 PdfCanvas 画水印,优势是可以精确控制每个页面的绘制顺序。PdfCanvas 构造时可以指定内容流的位置:PdfPage.NEXT_CANVAS 表示画在页面已有内容之后,会盖住正文;PdfPage.PREVIOUS_CANVAS 表示画在页面内容之前,正文会盖住水印。水印一般要浮在文字之上,所以用 NEXT_CANVAS,透明度降到 0.15 到 0.25 之间,既醒目又不影响阅读。

PdfCanvas canvas = new PdfCanvas(page, PdfPage.NEXT_CANVAS); canvas.saveState(); PdfExtGState gs = new PdfExtGState().setFillOpacity(0.2f); canvas.setExtGState(gs); canvas.beginText(); canvas.setFontAndSize(font, 48); canvas.setColor(ColorConstants.LIGHT_GRAY, true); canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), centerX, centerY); canvas.showText("内部资料"); canvas.endText(); canvas.restoreState();

concatMatrix 的六个参数对应 PDF 变换矩阵:a、b、c、d 控制旋转和缩放,e、f 控制平移。这里 e 和 f 传的是页面中心点坐标,效果是先把坐标系平移到中心,再绕这个点旋转 45 度。PdfCanvas.beginText 和 endText 之间只能写文本绘制命令,写完 showText 再 restoreState,画布状态不会影响后续内容。

4.2 水印字体、大小、角度的参数设置

水印字号没有固定标准,我的习惯是取页面短边的 8% 到 12%。A4 短边是 595 点,短边的 10% 就是 59.5 点,取 48 到 60 之间视觉上刚刚好。字号太小像插画,太大盖住正文字体。

旋转角度建议取 30 到 60 度之间。45 度最平衡,但长文本水印在 45 度时横向跨度最大,容易超出页面边界。如果水印文字较长,比如“内部资料禁止外传”八个字,45 度会导致左上角和右下角有部分文字溢出页面,这时要缩小字号,或者改成 30 度。一个比较实用的计算方式是:水印文字长度乘以字号得到文本宽度,再按对角线长度 842 点去反推最大字号。

颜色方面,水印一般用灰色或浅蓝色。setColor(ColorConstants.LIGHT_GRAY, true) 的第二个参数 true 表示填充色,false 表示描边色。如果水印需要更淡的灰,可以直接 new DeviceRgb(0.9f, 0.9f, 0.9f),不要用纯黑加很低透明度,那样打印机出来的效果会不均匀。

水印位置不能简单地从页面中心点开始写。showText 是从当前文本坐标开始绘制的,文本的左下角会落在 centerX, centerY 这个点,如果不做偏移,整行水印会偏在页面右上方。我一般会在旋转前先把原点平移到页面中心,再把文本坐标左移半个文本宽度、下移半个字号,这样水印才真正居中。平移放进 concatMatrix 的 e、f 参数里。

float textWidth = font.getWidth("内部资料", size); float x = (PageSize.A4.getWidth() - textWidth) / 2; float y = (PageSize.A4.getHeight() - size) / 2; canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), x, y);

4.3 全页平铺水印:密度和间隔怎么调

单页一个大水印只适合封面,正文每一页都需要全页平铺的水印,样式通常是每条水印间隔固定距离,横向排成几排。平铺的关键是间隔参数:横向间隔建议取水印宽度的 1.2 倍,纵向间隔取字号加上 120 到 180 点。间隔太小会有密集恐惧,间隔太大又显得稀稀拉拉。实际上 A4 页面 45 度斜排,三行四列是比较舒服的密度。

for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { PdfPage page = pdfDoc.getPage(i); PdfCanvas canvas = new PdfCanvas(page, PdfPage.NEXT_CANVAS); canvas.saveState(); PdfExtGState gs = new PdfExtGState().setFillOpacity(0.15f); canvas.setExtGState(gs); for (int row = -1; row <= 3; row++) { for (int col = -1; col <= 4; col++) { float x = col * 220f; float y = row * 160f - 100f; canvas.beginText(); canvas.setFontAndSize(font, 40); canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), x, y); canvas.showText("内部资料"); canvas.endText(); } } canvas.restoreState(); }

平铺时行和列的起点最好从负值开始,覆盖页面边缘,否则左上角和右下角会出现空白三角区。这里 row、col 的范围 -1 到 3/4,比实际需要的行列数多一圈,目的就是把溢出部分留出裁剪空间。conatMatrix 旋转之后,原本规则的网格会跟着旋转,所以间隔要根据旋转后的视觉间隔去试调,20% 的误差不会有太大影响。

水印性能也是要注意的点。每页画 20 条水印,每个水印都 beginText/endText,性能最差。优化方式是只做一个 beginText,中间用 moveText 调整坐标,只在画不同的旋转角度时才重新 concatMatrix。文档是十页以内时无所谓,到了几百页的批量报告,性能差别能到一倍。用 moveText 调整的是文本光标位置,不会重置变换矩阵,适合在同一旋转角度下画多条水印。

5. 避坑:5 个容易翻车的现象、原因与解决办法

5.1 中文乱码:PDF 打开是方框

现象:生成的 PDF 在浏览器预览正常,但发给同事后用 Adobe Reader 打开,中文全变成方框,或者干脆不显示。原因:用了系统字体但没嵌入,或者用了标准 Type1 字体写中文。第二个原因最常见,很多团队从 iText 5 迁移到 7 时,代码里保留着 BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED),但丢掉了 font-asian 依赖,导致字体加载失败后静默回退到 Helvetica。解决:引入 font-asian,并显式指定 EmbeddingStrategy.PREFER_EMBEDDED。如果用的是 Windows 字体文件,最好复制到项目 resources 目录再加载,不要直接依赖服务器系统路径,因为部署环境经常没有中文字体。

5.2 文本替换后,新文字盖住了旁边的内容

现象:把“张三”换成“李四”没问题,但把“张三”换成“张三四五六”时,新文字溢出,盖住了右侧的正文。原因:覆盖矩形宽度是按旧文本算的,新文本比旧文本宽。解决:替换前用 PdfFont.getWidth(newText, fontSize) 计算新文本宽度,取新旧文本宽度的较大值作为矩形宽度。如果矩形扩大后触碰到右侧其他文字,说明这段文本不适合原地替换,需要改成缩小字号或者移动到下一行。这是文本替换方案里最考验现场判断的部分,核心原则是“宁多盖、勿漏盖”,多出来的空白至少不影响安全性,漏盖就会暴露敏感信息。

5.3 水印旋转中心跑偏

现象:水印文字旋转后出现在页面角落,而不是以页面中心为轴旋转,或者每条水印的位置毫无规律。原因:对 concatMatrix 的变换顺序理解反了。concatMatrix 的参数是把新坐标系变换到当前坐标系,不是“先旋转再平移”的直觉顺序。解决:把页面中心的坐标直接放进 e、f 参数,再把文本绘制坐标调整到页面中心。如果旋转前已经设置了文本坐标,那么旋转后再平移会导致文本在旋转后的坐标系里叠加偏移。建议统一改成这次的水印写法:一组 concatMatrix 完成旋转和平移,后续不再叠加 moveText,除非你刻意要画多条间隔水印。

5.4 签章后文档被提示“自签名无效”

现象:签章后打开 PDF,Adobe 提示签名无效,或者看不到签名字段。原因:最常见的是签名后又被 iText 重新打开保存过一次,PdfSigner 的签名容器会被二次打开后的内容流改动破坏;其次是自签名证书本身没有受信任根,Adobe 会提示“证书不受信任”,这跟签章代码无关。解决:签章必须是处理链的最后一环,签完的 PDF 不要再用任何代码打开保存。如果你需要把其他元数据写进去,必须在签章之前完成。受信任问题需要把自签名证书导入系统证书库,或者使用受信任的 CA 签发证书,这属于部署层面的准备,不是代码能解决的。

5.5 大批量 PDF 内存溢出

现象:一次处理几百份 PDF,每份几 MB,结果 JVM 堆溢出。原因:PdfReader 默认是把整个文件读进内存,PdfWriter 输出到 ByteArrayOutputStream 时又把整个文件再放一份在内存,两个大对象叠加,堆不够很正常。解决:能传文件路径就不要传 byte[],PdfReader 和 PdfWriter 都支持直接读文件流;output 端用临时文件或 FileOutputStream;如果必须用内存流,处理完一个文档立刻置空引用,不要用一个 List<byte[]> 囤住所有结果。还有一个习惯问题:每份 PDF 处理完后要显式 close,不能等 GC,否则文件句柄堆积,Windows 上会直接报“文件被占用”。

6. 进阶:数字签章的完整链路与验证习惯

6.1 证书与 Maven 依赖

签章不是简单地把一张图片“贴”到 PDF 上,它是在 PDF 文件末尾写入一个 PKCS#7/CMS 签名容器,里面包含签名值、证书链、摘要算法等信息。iText 的 signDetached 方法封装了大部分细节,但底层依赖 BouncyCastle,所以签章功能除了 itext7-core,还要引入 bcprov 和 bcpkix 两个依赖。证书可以用 .p12/.pfx 文件,也可以用 HSM 设备,日常测试最方便的是用 keytool 生成自签证书,再用 KeyStore 加载。

<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency>

6.2 最小签章代码与参数

签章流程分四步:加载证书、创建 PdfSigner、配置签章外观、执行 signDetached。下面是完整的最小代码:

KeyStore ks = KeyStore.getInstance("PKCS12"); try (InputStream is = new FileInputStream("keystore.p12")) { ks.load(is, "password".toCharArray()); } String alias = ks.aliases().nextElement(); PrivateKey pk = (PrivateKey) ks.getKey(alias, "password".toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); PdfSigner signer = new PdfSigner( new PdfReader("source.pdf"), new FileOutputStream("signed.pdf"), new StampingProperties()); PdfSignatureAppearance appearance = signer.getSignatureAppearance(); appearance.setPageNumber(1); appearance.setPageRect(new Rectangle(36, 36, 200, 100)); appearance.setSignatureCreator("EnterpriseDoc"); appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION); IExternalSignature signature = new PrivateKeySignature(pk, "SHA-256", "BC"); signer.signDetached(signature, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS);

signDetached 的第三个参数是 CRL 列表,第四个是 OCSP 客户端,第五个是时间戳服务器地址,测试环境都可以传 null,生产环境建议至少配置时间戳,否则证书过期后签名验证会失败。PdfSigner 的 StampingProperties 如果不带参数,默认允许签名后再次打开,但这会导致文档被 final 修改后签名失效,我会在这里加一句:生产环境请用 useAppendMode(),让签名以增量方式写入,保留原始内容,这是电子合同行业的标准做法。

6.3 签名后的验证习惯

验证签名不能只看 Adobe 的绿标,服务器端要做程序化验证。iText 的 SignatureUtil 可以按签名名称取出 PdfPKCS7,然后校验摘要和证书链。

PdfDocument pdfDoc = new PdfDocument(new PdfReader("signed.pdf")); SignatureUtil util = new SignatureUtil(pdfDoc); Map<String, PdfPKCS7> signatures = util.verifyAllSignatures(); for (Map.Entry<String, PdfPKCS7> entry : signatures.entrySet()) { PdfPKCS7 pkcs7 = entry.getValue(); boolean valid = pkcs7.verifySignatureIntegrityAndAuthenticity(); System.out.println("签名 " + entry.getKey() + ":" + valid); }

我在生产环境还加过一道更保险的检查:验证完签名后,再计算一遍文档校验和,记录到数据库。如果用户拿到的文件和当时签的不一致,即使 PDF 签名校验通过,也要人工复核。这个习惯帮我拦住过一次“文档在传输过程中被企业网盘自动加过水印”的事故,那一次 PDF 签名本身是好的,但业务内容已经不是原始版本。

处理 PDF 这几年,最大的教训就是永远不要对着 Adobe 的输出猜代码行为,签章、文本替换这类操作必须用程序化手段做正向校验,别省这五分钟。希望这篇笔记能帮你把 iText 这条处理链一次跑通。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 12:07:23

大模型Linux服务器部署实战:vLLM推理服务与显存优化指南

大模型之Linux服务器部署大模型&#xff0c;坦白说并没有想象中那么玄乎。把开源模型权重放到一台装有GPU的Linux服务器上&#xff0c;通过推理框架把服务跑起来&#xff0c;再提供一个OpenAI风格或兼容HTTP的API接口&#xff0c;这条链路我已经在内部环境里反复验证过多次。无…

作者头像 李华
网站建设 2026/10/17 12:06:54

卷工业、能效、体验:工业数字化设备的实用化选型指南

1. 卷工业、卷能效、卷体验&#xff0c;三个“实用化”信号到底在说什么这段时间跑了几个行业交流&#xff0c;和不同厂家的技术负责人聊下来&#xff0c;发现大家嘴上说的事情出奇一致&#xff1a;以前谈项目&#xff0c;先问“你这方案能跑什么算法、能接多少点位”&#xff…

作者头像 李华
网站建设 2026/10/17 12:04:57

前端省市区三级联动:数据设计、异步加载与回显避坑指南

简介&#xff1a;JavaScript省市区三级联动是前端表单开发中常见的地理信息选择交互&#xff0c;这份PDF示例以原生JS演示完整实现&#xff0c;面向前端初学者或需要快速集成地址选择的开发者&#xff0c;适合用于用户注册、收货地址等场景。示例从基本概念讲起&#xff0c;说明…

作者头像 李华
网站建设 2026/10/17 12:02:56

Ubuntu双系统根分区空间不足?详解扩容原理与实操步骤

你装双系统的时候是不是也这样&#xff1a;Windows 分了两百多G&#xff0c;Ubuntu 这边只给了根目录一百G&#xff0c;结果编译几次代码、装两三个大型开发环境&#xff0c; / 分区就直接撑满。弹窗提示空间不足的时候&#xff0c;第一反应是买块新硬盘&#xff0c;再一查发…

作者头像 李华
网站建设 2026/10/17 12:01:56

OpenClaw离线助理免费联网搜索实战:破解模型一本正经胡说

老有人问我&#xff1a;你那台跑在本地、不带任何云依赖的助理&#xff0c;怎么一碰到“今天天气怎么样”“那个软件现在什么版本”就答非所问&#xff1f;更有一次&#xff0c;它认认真真给我编了一个不存在的发布会日期&#xff0c;还配了一段压根搜不到的项目名。离线模型知…

作者头像 李华
网站建设 2026/10/17 12:00:51

artcraft项目实战:分层渲染与参数化生成打造手工感创作工具

1. 从“artcraft”这个名字说起&#xff1a;一个被低估的创作工具定位 第一次看到“artcraft”这个词&#xff0c;我脑子里蹦出来的不是某个具体软件&#xff0c;而是一种工作方式——把“art”&#xff08;艺术感、审美判断&#xff09;和“craft”&#xff08;手艺、工程化落…

作者头像 李华