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

发布时间:2026/11/6 3:54:55
Java iText实战:PDF创建、签章、水印与文本替换全解析 简介这份源码工具包面向需要处理 PDF 文档的 Java 开发者聚焦 iText 库的典型应用场景涵盖新建 PDF 文档、添加电子签章、插入斜字水印以及替换文本内容等常见需求适合具备一定 Java 基础、希望快速上手 PDF 编程的中级开发者参考实践。资源包共 32 个文件包含 7 个 java 源文件、9 个编译后的 class 文件、5 个 jar 依赖包以及 png、jpg 示例图片和 p12 证书文件等压缩包整体约 8.77MB源码与依赖齐全可直接导入工程运行调试。目前已有 1573 人学习下载。借助其中的示例代码与证书素材读者能够对照理解 PdfWriter、PdfStamper、PdfSignatureAppearance 等核心类的调用方式掌握签章位置设置、水印透明度与旋转角度控制、表单文本替换等关键实现思路并留意文件体积增长与内存占用等实际问题为项目中的 PDF 处理任务提供可复用的参考。1. 从一份“改不动”的合同 PDF 说起上周帮一个做供应链系统的朋友看代码他遇到个很典型的需求客户发来一份几十页的采购合同 PDF要求在指定位置盖上公司电子章整页铺一层斜着的“内部资料”水印还要把正文里几处旧的收款账户号替换成新的。他一开始想用前端库在浏览器里改结果字体一嵌就乱、印章位置一缩放就飘最后绕回服务端用 Java 处理。这类“PDF 创建 签章 斜字水印 文本替换”的组合需求在合同、发票、报表、证明文件场景里几乎躲不掉而 iText 正好是 Java 生态里干这活最顺手的工具之一。这份源码把四个高频动作拆成了可运行的示例适合手上正被 PDF 折腾、想直接抄一段能跑代码的后端开发者也适合想搞清楚 PDF 坐标体系和字体嵌入到底怎么回事的人。2. 技术选型与工程结构为什么是 iText四个能力怎么摆2.1 选 iText 而不是别的库理由很实际Java 里能操作 PDF 的库不算少PDFBox 偏底层、拼装页面元素时写得比较啰嗦而 iText 的 API 层次更贴近“我要往页面上放一段文字、一张图、一个印章”这种直觉。这份源码用的就是 iText 的核心能力PdfWriter负责从零生成文档PdfReaderPdfStamper负责在已有文档上叠加内容PdfContentStreamEditor或早期版本的PdfContentByte重写思路负责碰正文里的文本。四个示例分别对应创建、签章、水印、替换正好覆盖了“从无到有”和“在已有上改”两条主线。需要提前说清楚一个版本差异这是很多人第一次跑就翻车的地方iText 5 和 iText 7 的包名、类名完全不同。iText 5 是com.itextpdf.text.*iText 7 拆成了com.itextpdf.kernel.*、com.itextpdf.layout.*等模块。这份源码的写法更接近 iText 5 的风格Document、PdfStamper这套如果你本地拉的是 iText 7 的依赖编译会直接报找不到类。常见做法是看pom.xml里锁的版本跟着源码走别自己升级。2.2 目录结构长什么样工程是标准的 Maven 结构按能力分包读起来不费劲pdf-itext-demo/ ├── pom.xml ├── src/main/java/ │ ├── create/ │ │ └── PdfCreateDemo.java # 从零创建 PDF │ ├── sign/ │ │ └── PdfSignDemo.java # 图片印章叠加 │ ├── watermark/ │ │ └── PdfWatermarkDemo.java # 斜字水印 │ └── replace/ │ └── PdfReplaceDemo.java # 文本替换 ├── src/main/resources/ │ ├── fonts/simhei.ttf # 中文字体 │ ├── images/seal.png # 印章图片 │ └── template/contract.pdf # 待处理模板 └── output/ # 生成结果落盘目录四个包各自独立互不依赖好处是你想只抄水印那段直接把watermark包拎走就行不用被其他示例的依赖拖住。resources下放字体、印章图、模板 PDF这是 PDF 处理项目的通用做法——中文字体必须显式嵌入否则生成的 PDF 在别人机器上打开就是一堆方块。2.3 依赖与运行入口pom.xml里核心就一个 iText 依赖加上日志和测试。跑之前确认 JDK 版本源码按 JDK 8 写的JDK 11/17 也能跑但要注意模块化对反射的影响。每个 Demo 类都有main方法改一下输入输出路径就能单独执行# 编译 mvn clean compile # 单独跑创建示例 mvn exec:java -Dexec.mainClasscreate.PdfCreateDemo如果不想用 exec 插件直接在 IDE 里右键main方法运行也一样。输出统一落到output/目录跑完去那里看结果比在控制台猜有没有成功靠谱得多。3. 创建与签章从空白文档到盖章落位3.1 创建 PDF字体、页面、段落三件事创建示例的核心是把“中文字体嵌入”这件事做对。iText 默认的Font不带中文直接写中文会抛异常或输出乱码所以必须先注册字体// 注册中文字体IDENTITY_H 保证中文按 Unicode 正确编码 BaseFont baseFont BaseFont.createFont( src/main/resources/fonts/simhei.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); Font cnFont new Font(baseFont, 12); Document document new Document(PageSize.A4, 50, 50, 60, 60); PdfWriter.getInstance(document, new FileOutputStream(output/created.pdf)); document.open(); // 标题用大字号正文用 12 号 document.add(new Paragraph(采购合同, new Font(baseFont, 18, Font.BOLD))); document.add(new Paragraph(本合同由甲乙双方于约定日期签订……, cnFont)); document.close();BaseFont.IDENTITY_H是关键参数它告诉 iText 用横向书写、按 Unicode 映射字形中文、日文、韩文都靠它。BaseFont.EMBEDDED表示把字体子集嵌进 PDF文件会大一点但换台机器打开不会掉字。Document构造里的四个数字是左、右、上、下边距单位是磅1 磅约等于 1/72 英寸A4 纸宽 595 磅减掉左右各 50 就是正文可用宽度。段落会自动换行但表格和图片不会后面签章定位就吃这个亏。3.2 签章坐标原点在左下角不是左上角签章示例用PdfStamper在已有 PDF 上盖图片章。最容易错的是坐标——PDF 的坐标原点在页面左下角Y 轴向上而前端和图片处理习惯是左上角原点、Y 轴向下。直接拿设计稿上的像素坐标往里填章会跑到页面外或者上下颠倒。PdfReader reader new PdfReader(src/main/resources/template/contract.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(output/signed.pdf)); int pageNum 1; // 盖章页码从 1 开始 PdfContentByte canvas stamper.getOverContent(pageNum); // 盖在内容之上 Image seal Image.getInstance(src/main/resources/images/seal.png); // 目标章的中心落在页面右下区域 float pageWidth reader.getPageSize(pageNum).getWidth(); float pageHeight reader.getPageSize(pageNum).getHeight(); float sealSize 120f; float centerX pageWidth - 150; float centerY 120; // setAbsolutePosition 传的是左下角坐标所以要减半个尺寸 seal.scaleAbsolute(sealSize, sealSize); seal.setAbsolutePosition(centerX - sealSize / 2, centerY - sealSize / 2); canvas.addImage(seal); stamper.close(); reader.close();getOverContent和getUnderContent的区别值得记一下前者盖在原有文字上面适合印章、签名后者垫在下面适合背景水印。setAbsolutePosition接收的是图片左下角坐标所以想让章的中心对准某个点得把宽高各减一半。scaleAbsolute按目标尺寸缩放别用scaleToFit去猜印章这种有固定视觉尺寸的元素直接给死值最稳。如果章是透明背景的 PNGiText 会保留 alpha 通道盖上去不会出现白底方块。3.3 多页盖章与骑缝章的思路单页盖章上面那段就够了但合同往往是多页常见需求是每页右下角盖一个或者做骑缝章。多页就是循环for (int i 1; i reader.getNumberOfPages(); i)每页取一次getOverContent(i)再addImage。骑缝章稍微麻烦点它要求章被“切”到多页边缘做法是把一张章图按页数等分每页只画对应的一条并且位置要贴着页面右边缘。这个示例没做骑缝但思路是通的canvas.clipToStroke配合saveState/restoreState控制绘制区域属于进阶用法二次开发时按需加。提示盖章前先确认模板 PDF 没有被加密。PdfReader遇到带权限密码的文件会抛BadPasswordException需要先用new PdfReader(path, passwordBytes)打开再判断是否允许修改。4. 斜字水印与文本替换两个最容易踩坑的模块4.1 斜字水印旋转、透明度、平铺三要素水印示例要的是整页斜着铺满的“内部资料”字样。核心是PdfContentByte的变换矩阵先平移到目标点再旋转再写文字。很多人只写一次showTextAligned就以为完事结果只有一行字不是铺满。PdfReader reader new PdfReader(output/created.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(output/watermarked.pdf)); BaseFont wmFont BaseFont.createFont(src/main/resources/fonts/simhei.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); for (int i 1; i reader.getNumberOfPages(); i) { PdfContentByte canvas stamper.getUnderContent(i); // 水印垫在文字下方 canvas.saveState(); canvas.setGState(new PdfGState().setFillOpacity(0.15f)); // 透明度 15% canvas.beginText(); canvas.setFontAndSize(wmFont, 40); canvas.setColorFill(BaseColor.GRAY); // 按网格平铺每行每列错开 for (float x 60; x reader.getPageSize(i).getWidth(); x 220) { for (float y 60; y reader.getPageSize(i).getHeight(); y 180) { canvas.showTextAligned(Element.ALIGN_CENTER, 内部资料, x, y, 45); // 45 度 } } canvas.endText(); canvas.restoreState(); } stamper.close();setFillOpacity(0.15f)控制透明度值太低看不见、太高盖住正文0.1 到 0.2 之间比较合适。showTextAligned的最后一个参数是旋转角度45 度是斜字水印的常见选择。双层循环的步长决定密度x 220、y 180是经验值页面小就调小页面大就调大。saveState/restoreState成对出现保证透明度和颜色不会污染后续绘制——这个不写后面再画别的东西会继承水印的灰色和透明度属于典型的“玄学 bug”。4.2 文本替换为什么不能简单 replace文本替换是四个示例里最容易被低估的。直觉上读 PDF 文本、String.replace、再写回去就行但 PDF 里的文字不是按段落存的而是按“绘制指令”存的一段话可能被拆成多个Tj/TJ操作中间还夹着字体切换和定位。直接替换字符串轻则替换不到重则把内容流结构搞坏。这份源码的做法是基于内容流编辑用PdfReader拿到页面内容解析出文本操作符匹配目标字符串后替换操作数再写回。iText 5 里常用PdfContentStreamEditor部分版本叫PdfContentStreamProcessor的子类来干这事// 自定义内容流编辑器只处理文本绘制操作 class ReplaceEditor extends PdfContentStreamEditor { private final String target; private final String replacement; ReplaceEditor(String target, String replacement) { this.target target; this.replacement replacement; } Override protected void write(PdfContentStreamProcessor processor, PdfLiteral operator, ListPdfObject operands) { // 只关心 Tj / TJ 这类文本操作符 if (Tj.equals(operator.toString()) !operands.isEmpty()) { PdfString text (PdfString) operands.get(0); String value text.toUnicodeString(); if (value.contains(target)) { operands.set(0, new PdfString(value.replace(target, replacement))); } } super.write(processor, operator, operands); } }调用侧把编辑器挂到每一页上PdfReader reader new PdfReader(src/main/resources/template/contract.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(output/replaced.pdf)); ReplaceEditor editor new ReplaceEditor(旧账户号, 新账户号); for (int i 1; i reader.getNumberOfPages(); i) { editor.processContent(reader.getPageContent(i), reader.getPageResources(i)); stamper.getOverContent(i).setLiteral(editor.getResultantContent()); // 写回 } stamper.close();这里有几个现实约束必须说清楚。第一替换后的字符串长度如果和原文差太多可能撑破原有排版因为 PDF 里每个字的位置是算好的替换不会自动重排。第二如果目标文字跨了多个Tj操作比如被字体切换打断单操作符匹配会漏掉需要做跨操作符的拼接匹配复杂度上一个台阶。第三中文文本在内容流里可能是十六进制编码toUnicodeString()能还原但前提是字体带了 ToUnicode 映射扫描件或某些生成工具产出的 PDF 没有这个映射替换会失效。所以这个示例更适合“自己生成的、结构规整的 PDF”拿它去改别人扫描的合同大概率要失望。4.3 四个模块的输入输出对照把四个示例的输入输出理一遍跑的时候心里有数模块输入输出关键类创建无代码内构造内容output/created.pdfDocument、PdfWriter签章template/contract.pdf seal.pngoutput/signed.pdfPdfStamper、Image水印created.pdfoutput/watermarked.pdfPdfContentByte、PdfGState替换template/contract.pdfoutput/replaced.pdfPdfContentStreamEditor注意签章和替换都读template/contract.pdf水印读的是创建示例的产物所以跑的顺序建议是先创建再水印签章和替换各自独立跑。顺序乱了不会报错但你会对着一个空文件纳闷半天。5. 避坑与排查五个真实会卡住你的问题5.1 中文全是方块或问号现象生成的 PDF 打开后中文显示为方块、问号或者直接抛Font simhei with Identity-H is not recognized。原因基本是字体没嵌入或者用了BaseFont.HELVETICA这类内置字体去写中文。解决确认BaseFont.createFont的第三个参数是BaseFont.EMBEDDED第二个参数是BaseFont.IDENTITY_H并且字体文件路径真实存在。用 Maven 打包后路径会变src/main/resources下的文件在 jar 里是 classpath 资源得用getResourceAsStream读不能再用文件路径。5.2 印章位置偏移或上下颠倒现象章盖到了页面外或者位置和预期差一个页面高度。原因把左上角原点的坐标直接填进了 PDF 的左下角坐标系。解决所有 Y 坐标用pageHeight - 设计稿Y换算并且记住setAbsolutePosition传的是图片左下角。调试时先在目标点画一个红色小方块确认坐标再放章图比反复改数字快。5.3 水印盖住了正文文字现象水印颜色太深正文看不清。原因用了getOverContent且透明度没设或设得太高。解决水印一律用getUnderContent透明度控制在 0.1 到 0.2颜色用浅灰。如果模板本身有背景色getUnderContent可能被背景盖住这时才考虑getOverContent配更低透明度。5.4 文本替换没生效也不报错现象代码跑完输出文件里旧文字还在。原因有三种目标文字跨了多个文本操作符PDF 没有 ToUnicode 映射替换的是TJ数组里的元素而代码只处理了Tj。解决先用PdfTextExtractor把页面文本抽出来确认目标字符串在抽取结果里是连续的再打印内容流看实际操作符类型跨操作符的情况需要把相邻操作数拼起来匹配工作量不小评估清楚再动手。5.5 处理大文件时内存溢出现象几十 MB 的 PDF 处理到一半抛OutOfMemoryError。原因PdfReader默认把整个文件读进内存加上PdfStamper的中间缓冲内存翻倍。解决启动参数加-Xmx调大堆或者用PdfReader的随机访问模式new PdfReader(RandomAccessFileOrArray)减少常驻内存批量处理时一页一页 flush别把所有 stamper 攒着一起关。注意iText 5 是 AGPL 协议商用闭源项目需要购买商业授权。二次开发前先确认授权方式这是比任何技术坑都更该先搞清楚的事。6. 二次开发把四个示例拼成一条可复用的处理链四个独立示例真正有价值的地方是它们能拼成一条流水线。我一般会抽一个PdfProcessor接口把“创建、盖章、水印、替换”做成可组合的步骤每个步骤接收InputStream返回InputStream这样既能单独调用也能串起来public interface PdfStep { byte[] apply(byte[] input) throws Exception; } // 组合执行前一步的输出是后一步的输入 public byte[] pipeline(byte[] source, ListPdfStep steps) throws Exception { byte[] current source; for (PdfStep step : steps) { current step.apply(current); } return current; }这么改的好处是业务侧只需要声明“先替换账户号再盖骑缝章最后铺水印”顺序可调、步骤可插拔测试时也能单独验证某一步。验证方法很朴素每一步产出后立刻用PdfTextExtractor抽文本比对关键字段再人工打开看一眼版面别等整条链跑完才发现中间某步把字体搞丢了。还有一个具体技巧值得说印章和水印的坐标、透明度、字体大小这些参数别硬编码在类里抽到application.yml或常量类里。我见过一个项目把章的位置写死在代码里换了个模板尺寸章全飘了改一次要重新发版。参数外置之后运维改配置就能适配新模板省下的返工时间远超当初抽参数的那半小时。最后留一句血泪经验PDF 处理这类活最怕“看起来跑通了”。文件能生成、能打开不代表字体嵌对了、坐标算准了、替换真的生效了。从那以后我每次改完这类代码都强制走一遍“抽取文本比对 换台机器打开 打印页面尺寸”三件套宁可多花五分钟也别让一份盖错位置的合同流到客户手里。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询