简介:这份资源面向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 生成,都强制先跑一遍文本抽取验证,再肉眼确认一遍坐标,绝不凭“代码没报错”就交付。希望帮到你。
本文还有配套的精品资源,点击获取