news 2026/10/21 21:02:12

iText实战:PDF创建、签章、斜字水印与文本替换全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iText实战:PDF创建、签章、斜字水印与文本替换全解析

简介:这份资源面向Java开发者与需要处理PDF文档的技术人员,围绕iText库展开,覆盖创建PDF、电子签章、斜字水印与文本替换四类常见需求,适合已有一定Java基础、希望快速上手PDF编程的读者参考实践。压缩包共32个文件,约8.77MB,以java源码与class编译文件为核心,配合itextpdf、itext-asian、bcprov等5个jar依赖包,另有p12证书、png与jpg示例图片、prefs配置及readme说明,构成可直接导入Eclipse运行的工程结构。目前已有1571人学习下载,说明该示例在PDF处理场景中具有一定参考价值。读者可从中获取完整的工程目录与依赖配置,理解PdfWriter、PdfStamper、PdfSignatureAppearance等关键类的实际用法,并借助证书文件与示例图片完成签章、水印的本地验证,为项目中的PDF生成、版权保护与内容修改提供可复用的代码基础。

1. 从一份“看着简单”的 PDF 需求说起

前阵子帮某公司做合同管理模块,需求听起来很朴素:把业务数据填进 PDF 模板、给文件盖个电子章、加一层斜字水印防截图外传,最后还要能批量替换掉模板里的占位文字。我一开始想用现成的 PDF 库糊过去,结果在签章坐标偏移和水印旋转角度上连续翻车两次,才老老实实回到 iText 这条路上来。这份源码包围绕的就是这四个动作:创建、签章、斜字水印、文本替换,全部基于 iText 实现,属于那种“拿过去改改就能塞进项目”的工具型代码。它适合正在做电子合同、报表导出、票据打印的 Java 后端,也适合需要理解 PDF 底层对象模型、不想只会调封装 API 的开发者。下面我按自己拆包的顺序,把每个动作的原理、参数和坑位讲清楚。

2. 创建与文本替换:先搞懂 PDF 的内容流

2.1 为什么“替换文字”在 PDF 里是个伪命题

很多人第一次做 PDF 文本替换,会下意识去找类似replaceText("old", "new")的方法,然后发现 iText 根本没有。这不是库的问题,是 PDF 格式本身决定的。PDF 里没有“段落”和“文字”这种结构化概念,页面内容是一串绘制指令,文字被拆成一个个Tj/TJ操作符,每个操作符带着自己的字体、字号、坐标和编码。你看到的“一句话”,在文件里可能是三段不同字体的指令拼出来的。

所以真正可行的替换思路只有两条:一是基于模板占位符做“填坑”,也就是提前在模板里放好{{name}}这类标记,生成时用PdfStamper定位并覆盖;二是解析内容流,找到目标文本的绘制指令,改掉操作数再写回。前者稳定可控,后者灵活但容易把排版搞崩。源码包里两种都给了,我建议生产环境优先用占位符方案。

2.2 用 PdfStamper 做占位符替换的完整流程

先看核心代码,这段是把模板里的{{contractNo}}替换成真实合同号:

// 读取模板,输出到新文件,注意不要直接覆盖源文件 PdfReader reader = new PdfReader("template.pdf"); PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("output.pdf")); // AcroFields 负责处理表单域,这里演示的是普通文本定位替换 PdfContentByte canvas = stamper.getOverContent(1); // 第 1 页 BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); canvas.beginText(); canvas.setFontAndSize(font, 12); canvas.setTextMatrix(120, 680); // 坐标原点在左下角 canvas.showText("HT-2024-0912"); canvas.endText(); stamper.close(); reader.close();

逻辑上分三步:PdfReader加载模板,PdfStamper建立可写副本,getOverContent拿到页面绘制层。参数里最容易错的是坐标setTextMatrix(x, y),PDF 原点在页面左下角,和前端习惯的左上角原点相反,y 值要拿页面高度去减。字体那行如果模板里没有嵌入中文字体,必须显式指定STSong-Light这类 CJK 字体,否则中文会变成一堆方块。

提示:PdfStamper构造时如果输出流指向源文件本身,会直接把模板写坏,这个后悔药没有,务必输出到新文件。

2.3 内容流级替换:什么时候才该动它

当模板不可控、占位符是别人给的,就得走内容流解析。思路是用PdfTextExtractor先拿到每段文本和它的坐标,匹配到目标后,用PdfContentStreamEditor拦截绘制指令,把对应的Tj操作数换掉。源码包里这段封装成了一个TextReplaceEditor类,核心是重写write(PdfContentStreamProcessor, PdfLiteral operator),判断操作符是不是Tj或TJ,是就替换字节数组。

这条路有三个硬约束:替换后的文字长度不能超过原文字太多,否则会溢出到相邻元素;字体必须和原文一致,否则字宽对不上;跨行的文本匹配不到,因为每行是独立指令。我一般只在“客户给的模板改不了”时才用它,能改模板就绝不碰内容流。

3. 签章与斜字水印:坐标、旋转和透明度的三角关系

3.1 电子签章的本质是图片叠加

签章在 iText 里没有魔法,就是把一张 PNG 印章图片按指定坐标和尺寸画到页面上。关键参数是位置、缩放和旋转。下面这段把印章盖到落款处:

PdfReader reader = new PdfReader("contract.pdf"); PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("signed.pdf")); PdfContentByte canvas = stamper.getOverContent(reader.getNumberOfPages()); // 读取印章图片,透明背景的 PNG 效果最好 Image seal = Image.getInstance("seal.png"); seal.scaleToFit(120, 120); // 等比缩放到 120x120 以内 seal.setAbsolutePosition(380, 150); // 落款坐标,同样左下角原点 seal.setRotationDegrees(-15); // 轻微倾斜更像手盖的 canvas.addImage(seal); stamper.close(); reader.close();

scaleToFit比scaleAbsolute安全,后者会强行拉伸导致印章变形。setRotationDegrees传负值是顺时针倾斜,视觉上更自然。如果印章 PNG 背景不透明,盖上去会糊住底下的文字,所以素材一定要用带 alpha 通道的图。

3.2 斜字水印:旋转角度和透明度的配合

水印和签章是同一套绘制逻辑,区别在于要铺满整页、半透明、带旋转。源码包里用的是PdfGState控制透明度,配合showTextAligned的旋转参数:

PdfContentByte under = stamper.getUnderContent(1); // 水印放底层,不挡正文 PdfGState gs = new PdfGState(); gs.setFillOpacity(0.15f); // 透明度,0.1~0.2 之间比较合适 gs.setStrokeOpacity(0.15f); under.setGState(gs); BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); under.beginText(); under.setFontAndSize(font, 40); under.setColorFill(BaseColor.GRAY); // 参数依次:文字、x、y、旋转角度 under.showTextAligned(Element.ALIGN_CENTER, "内部资料 禁止外传", 300, 400, 45); under.endText();

getUnderContent和getOverContent的区别很关键:水印要放底层,否则会盖住正文影响阅读;签章要放顶层,否则会被后来的内容压住。旋转角度 45 度是斜字水印的常见值,透明度超过 0.3 就会明显干扰阅读,低于 0.08 打印出来几乎看不见,0.15 是实测比较稳的中间值。

3.3 多页水印的循环写法与性能

单页水印好办,整份合同几十页就得循环。常见做法是遍历页数,每页都取一次getUnderContent:

int total = reader.getNumberOfPages(); for (int i = 1; i <= total; i++) { PdfContentByte under = stamper.getUnderContent(i); under.setGState(gs); under.beginText(); under.setFontAndSize(font, 40); under.showTextAligned(Element.ALIGN_CENTER, "内部资料 禁止外传", 300, 400, 45); under.endText(); }

这里有个性能细节:BaseFont和PdfGState要在循环外创建一次,循环内复用。我见过有人在循环里反复createFont,几十页的文件生成时间从 1 秒涨到 8 秒,纯属自己给自己挖坑。字体对象是重量级的,能复用就复用。

4. 避坑与排查:那些让我返工的细节

4.1 中文变方块或乱码

现象:生成的 PDF 里中文全是方块,或者显示成问号。原因:iText 默认字体不含 CJK 字形,BaseFont.createFont没指定编码或者用了HELVETICA这类西文字体。解决:显式用STSong-Light配UniGB-UCS2-H编码,或者引入itext-asian依赖包。如果用了自定义 TTF,记得把字体文件路径写对,并且嵌入模式选EMBEDDED。

4.2 签章位置在不同页面上偏移

现象:第 1 页印章位置对,第 2 页就跑到页面外面去了。原因:不同页面的尺寸(MediaBox)可能不一样,硬编码的坐标只对某一页有效。解决:先reader.getPageSize(i)拿到每页的实际宽高,再按比例算坐标,别写死数字。

4.3 替换文字后排版错位

现象:内容流替换后,新文字挤到旁边元素上,或者行距变了。原因:新文字的字宽和原文字不一致,PDF 按原字宽预留了空间。解决:替换文本长度尽量和原文接近;如果必须变长,得同步调整TJ数组里的字间距参数,这个操作很细,建议能改模板就改模板。

4.4 文件被占用导致写入失败

现象:FileOutputStream报“另一个程序正在使用此文件”。原因:源 PDF 被 PDF 阅读器打开着,Windows 下文件锁没释放。解决:输出文件名和源文件区分开,别原地覆盖;服务端场景加个临时目录,生成完再原子性地移动过去。

4.5 水印在打印时消失

现象:屏幕上看得到水印,打印出来没有。原因:水印用了getOverContent且透明度设得太低,部分打印机驱动会忽略极低透明度的图层。解决:水印放getUnderContent,透明度提到 0.15 以上,颜色用深灰而不是浅灰。

5. 进阶技巧:把四个动作串成一条流水线

单独会创建、会签章、会水印、会替换,不等于能交付。真实项目里这四个动作往往要在一次文件读写里完成,反复开关PdfStamper既慢又容易出错。我的习惯是把它们合并到一个 stamper 生命周期里,顺序是:先替换文本,再加水印,最后盖签章。原因是水印和签章都是叠加层,替换文本动的是内容流,先做替换能避免叠加层干扰文本定位。

PdfReader reader = new PdfReader("template.pdf"); PdfStamper stamper = new PdfStamper(reader, new FileOutputStream("final.pdf")); // 第一步:文本替换(占位符方案) PdfContentByte canvas = stamper.getOverContent(1); BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); canvas.beginText(); canvas.setFontAndSize(font, 12); canvas.setTextMatrix(120, 680); canvas.showText("HT-2024-0912"); canvas.endText(); // 第二步:整份文件加水印 PdfGState gs = new PdfGState(); gs.setFillOpacity(0.15f); for (int i = 1; i <= reader.getNumberOfPages(); i++) { PdfContentByte under = stamper.getUnderContent(i); under.setGState(gs); under.beginText(); under.setFontAndSize(font, 40); under.setColorFill(BaseColor.GRAY); under.showTextAligned(Element.ALIGN_CENTER, "内部资料 禁止外传", 300, 400, 45); under.endText(); } // 第三步:末页盖签章 Image seal = Image.getInstance("seal.png"); seal.scaleToFit(120, 120); seal.setAbsolutePosition(380, 150); seal.setRotationDegrees(-15); stamper.getOverContent(reader.getNumberOfPages()).addImage(seal); stamper.close(); reader.close();

这段代码把三个动作压进一次读写,文件 IO 从三次降到一次,几十页的合同生成时间能省一半以上。参数上唯一要动态算的是签章坐标,实际项目里我会根据落款文字的PdfTextExtractor结果反推位置,而不是写死 380、150。

验证生成结果我一般走两步:先用PdfTextExtractor把文本抽出来,确认替换内容进去了、水印文字也在;再用PdfReader读页面尺寸和图片数量,确认签章图片确实被加到了末页。这两步能覆盖 90% 的“看起来生成了但内容不对”的问题。

从那以后我每次做 PDF 生成,都强制先跑一遍文本抽取验证,再肉眼确认一遍坐标,绝不凭“代码没报错”就交付。希望帮到你。

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

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

Photoroom 入门:给商品图抠图、换背景并导出

适合人群&#xff1a;开网店、做二手转卖、写产品测评&#xff0c;需要把手机随手拍的商品照片处理成干净主图的同学。需要说明&#xff1a;Photoroom 的免费计划仅限个人非商业用途&#xff0c;AI 功能消耗 AI 积分&#xff0c;导出次数和分辨率也有上限&#xff1b;下文以写作…

作者头像 李华
网站建设 2026/10/21 20:52:39

SAP S/4HANA 时代依然危险的 10 个 ABAP 性能反模式

把一个运行了十几年的 SAP ECC 系统迁移到 SAP S/4HANA 之后,项目团队很容易产生一种错觉,数据库已经换成 SAP HANA,很多过去需要特别关注的 ABAP 性能问题应该自然消失了。 实际情况恰恰没有这么简单。 SAP HANA 确实改变了 ABAP 应用处理数据的方式。列式存储、内存计算…

作者头像 李华
网站建设 2026/10/21 20:51:24

AM12%/-+@#_-(

TestGLM123321

作者头像 李华
网站建设 2026/10/21 20:47:21

AI 什么都会,为什么还“看不了“ B 站、抖音视频?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 20:40:31

Obsidian 插件开发入门:从三件套到上架社区商店的完整路径

先说结论&#xff1a;插件本质上就是一个"带约定入口的 JS 包"Obsidian 本身用 Electron 构建&#xff0c;插件系统并不神秘&#xff1a;一个插件就是一个符合约定目录结构的文件夹&#xff0c;核心是 manifest.json、main.js、styles.css 三个文件。Obsidian 启动时…

作者头像 李华
网站建设 2026/10/21 20:35:53

内蒙古科技大学《Applied Catalysis A:General》:60s,850°C焦耳热构筑钴钼碳化物异质界面,助力高效析氧

研究背景电催化分解水是通往“绿氢”经济的核心桥梁&#xff0c;然而阳极析氧反应因其迟缓的四电子转移动力学&#xff0c;成为制约系统能效的“卡脖子”环节。商业贵金属催化剂虽活性卓越&#xff0c;但高昂成本与稀缺储量倒逼学界寻找替代方案。过渡金属碳化物因其类铂电子结…

作者头像 李华