iText PDF实战:创建、签章、水印与文本替换避坑指南

发布时间:2026/11/6 3:54:55
iText PDF实战:创建、签章、水印与文本替换避坑指南 简介这份源码工具包面向需要处理 PDF 文档的 Java 开发者围绕 iText 库演示创建 PDF、电子签章、斜字水印与文本替换四类常见操作适合具备一定 Java 基础、希望快速上手 PDF 编程的中级开发者参考。压缩包共 32 个文件约 8.77MB以 java 源码与编译后的 class 文件为主体辅以 itextpdf、itext-asian、bcprov 等 5 个依赖 jar另含 p12 证书、示例图片及工程配置文件可直接导入 Eclipse 运行调试。目前已有 1573 人学习下载。资源完整呈现了 PdfWriter 建文档、PdfStamper 加签章、PdfFormXObject 绘制旋转水印、AcroFields 替换文本等关键实现并附 readme 说明便于读者对照代码理解 API 调用流程也可作为项目中的 PDF 处理脚手架快速改造出符合自身业务需求的工具类。1. 拿到一份 iText 操作 PDF 的源码包先别急着跑上周有个做电子合同的朋友甩给我一个压缩包说里面是 iText 操作 PDF 的完整示例创建、签章、斜字水印、文本替换四件事全有让我看看能不能直接塞进他们的归档系统。我打开扫了一眼代码量不大但四个功能各自踩的坑完全不在一个量级——创建是热身签章是深水区斜字水印是数学题文本替换则是那种“看起来能跑、上线就翻车”的典型。这份源码包的价值不在于它写得多漂亮而在于它把 iText 在真实业务里最容易翻车的四个场景都摆出来了你照着改参数就能用但前提是你得知道每个参数背后在干什么。适合谁手上有一批 PDF 要程序化生成、要盖电子章、要打防伪水印、或者要从合同里批量抠字段的后端和全栈开发者。新手能跟着步骤把 demo 跑通熟手能直接看边界条件在哪。下面我按自己拆包的顺序把这四个功能逐个拆开讲参数怎么设、哪里会炸、炸了看什么都写清楚。2. 创建 PDF从 Document 到 PdfWriter 的最小闭环2.1 为什么先讲创建它是后面三个功能的地基签章、水印、文本替换本质上都是在操作一个已经存在的 PDF 或者往一个正在生成的 PDF 里塞东西。如果你连Document、PdfWriter、PdfReader、PdfStamper这几个类的关系都没理顺后面签章一报DocumentException你就只能干瞪眼。iText 的模型其实很直白Document是逻辑文档负责“写什么”PdfWriter是物理输出负责“写到哪”两者通过document.open()和document.close()绑定生命周期。创建场景下你只需要这两个类但签章场景会多出PdfReader读原文件和PdfStamper在已有页面上叠加内容水印和文本替换则是在PdfStamper的基础上再叠一层PdfContentByte。所以创建这一节不是走过场是把后面要反复出现的对象关系先立住。源码包里创建部分的入口是一个createPdf(String dest)方法我把它精简成下面这段去掉业务字段只留骨架// 创建 PDF 的最小闭环Document PdfWriter public void createPdf(String dest) throws IOException, DocumentException { // 1. 输出目标A4 页面左右上下边距各 36pt Document document new Document(PageSize.A4, 36, 36, 36, 36); // 2. 绑定 writer文件不存在则新建 PdfWriter writer PdfWriter.getInstance(document, new FileOutputStream(dest)); // 3. 打开文档之后才能 add 内容 document.open(); // 4. 写一段中文必须指定支持中文的字体否则乱码 BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font font new Font(bf, 12); document.add(new Paragraph(这是一段测试文本, font)); // 5. 关闭触发 flush 到磁盘 document.close(); }逻辑说明第 1 步的PageSize.A4是页面尺寸后面四个 36 是边距单位是 pt1pt≈0.35mm36pt 约等于 1.27cm这是很多合同模板的默认边距改小会让内容更贴边。第 2 步的FileOutputStream如果目标目录不存在会直接抛FileNotFoundException源码包里没做目录创建这是第一个要补的地方。第 3 步open()之前add任何内容都会被静默丢弃不报错这是新手最容易中的招。第 4 步的中文字体是重点STSong-Light是 iText 自带的 CJK 字体名配合UniGB-UCS2-H编码才能正确输出简体中文NOT_EMBEDDED表示不嵌入字体文件好处是体积小坏处是换一台没有该字体的机器打开可能显示异常正式环境建议改成EMBEDDED并指向一个 ttf 文件。2.2 参数怎么调页面、边距、字体三处最常改页面尺寸不止 A4PageSize.LETTER、PageSize.A3、new Rectangle(595, 842)都可以后者适合需要精确控制的场景。边距四个参数顺序是 left、right、top、bottom别记反我见过有人把 top 和 bottom 写反导致页眉页脚重叠。字体这块BaseFont.createFont有三个重载最常用的是(String fontName, String encoding, boolean embedded)如果你有自定义 ttf写成BaseFont.createFont(fonts/simhei.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED)注意编码要用IDENTITY_H而不是UniGB-UCS2-H混用会报Font ... with UniGB-UCS2-H is not recognized。字号用new Font(bf, 12)的第二个参数行距用Paragraph.setLeading(18f)单独设不设的话默认是字号 1.5 倍左右合同类文档一般设 1.5 到 1.8 倍比较舒服。还有一个容易被忽略的点document.add()的返回值是 boolean表示是否成功加入但即使返回 false 也不一定抛异常比如内容超出页面且没有分页时可能被截断。稳妥做法是每加完一段检查writer.getPageNumber()有没有按预期增长或者在document.close()之后用PdfReader重新打开数一下页数。源码包里创建部分只做了单页 demo真实业务里表格跨页、图片撑破边距都是常事建议在add之前先算一下内容高度超过PageSize.A4.getHeight() - 72就手动document.newPage()。3. 签章PdfStamper 叠加签名的完整链路3.1 签章为什么不能只用 PdfWriter很多人第一反应是“我创建一个 PDF 然后往里画个章不就行了”但签章的业务语义是“在已有文档的指定位置叠加一个可视印章并且最好带数字签名防篡改”。PdfWriter只能从头生成没法读已有文件所以必须换成PdfReaderPdfStamper的组合。PdfReader负责把原 PDF 解析成内存对象PdfStamper负责在解析结果上叠加内容并写回新文件。源码包里签章部分的核心是stampPdf(String src, String dest, String sealPath, float x, float y)我把它拆成三步看// 在已有 PDF 的指定坐标叠加印章图片 public void stampPdf(String src, String dest, String sealPath, float x, float y) throws IOException, DocumentException { // 1. 读原文件注意第二个参数是密码无密码传 null PdfReader reader new PdfReader(src); // 2. stamper 绑定 reader 和输出流false 表示不追加模式 PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); // 3. 取第一页的叠加层OVER_CONTENT 表示盖在内容上方 PdfContentByte canvas stamper.getOverContent(1); // 4. 插入印章图片坐标是左下角起点 Image seal Image.getInstance(sealPath); seal.setAbsolutePosition(x, y); seal.scaleToFit(120, 120); // 印章缩放到 120x120pt 以内 canvas.addImage(seal); // 5. 关闭 stamper 才会真正写盘 stamper.close(); reader.close(); }逻辑说明第 1 步PdfReader构造时如果原文件有权限密码会抛BadPasswordException源码包里没处理正式环境要 catch 住给用户提示。第 2 步PdfStamper的第三个参数false表示不追加模式如果传true会保留原文档的增量更新结构适合需要保留签章历史的场景但文件会变大。第 3 步getOverContent(1)的 1 是页码从 1 开始OVER_CONTENT盖在文字上方UNDER_CONTENT垫在文字下方印章一般用前者水印一般用后者。第 4 步setAbsolutePosition的坐标原点在页面左下角x 向右、y 向上和很多前端坐标系相反这是签章位置对不齐的头号原因。scaleToFit会等比缩放不会变形比scaleAbsolute安全。3.2 数字签名和可视印章是两回事源码包里“签章”其实只做了可视印章没有做真正的数字签名。这两件事经常被混为一谈可视印章就是一张图片盖上去谁都能盖没有防篡改能力数字签名是用私钥对文档摘要加密任何人用公钥都能验证文档有没有被改过。如果你的业务只是“看起来有个章”上面的代码够了如果要做“盖了就不能改”得用PdfSignatureAppearance配合PrivateKey和证书链代码量翻三倍而且证书管理、时间戳服务都是额外工程。我一般建议先确认业务方到底要哪种别一上来就上数字签名很多内部归档场景可视印章加文件哈希存档就够了。真要上数字签名PdfStamper要换成PdfStamper.createSignaturegetOverContent换成getSignatureAppearancesetCryptoDictionary里配PdfSignature和CryptoStandard.CMS这块源码包没覆盖属于要额外补的边界。3.3 印章位置怎么算才不歪坐标原点左下角这件事说三遍都不多。假设你要把印章盖在页面右下角、距右边 50pt、距底边 80pt 的位置A4 宽 595pt 高 842pt印章宽 120pt那么 x 应该是595 - 50 - 120 425y 是80。如果你按前端习惯从左上角算y 要写成842 - 80 - 120 642两个结果差了一倍多。源码包里给的示例坐标是硬编码的(400, 100)只适合 A4 单页多页文档每页尺寸可能不同稳妥做法是遍历reader.getNumberOfPages()每页用reader.getPageSize(i)取实际宽高再算坐标。还有一个坑如果原 PDF 有旋转/Rotate 90getPageSize返回的是旋转前的尺寸叠加内容会跟着转这时候要么先stamper.setRotateContents(false)关掉自动旋转要么手动换算坐标源码包里没处理旋转页遇到扫描件旋转的情况印章会跑到页面外面去。4. 斜字水印PdfContentByte 的旋转矩阵与透明度4.1 水印的本质是一段带变换的绘制指令斜字水印看起来玄学拆开就是“在页面上画一段文字画之前先把坐标系旋转 45 度再调低透明度”。iText 里所有绘制都走PdfContentByte它提供saveState()、beginText()、setTextMatrix()、showText()、endText()、restoreState()这一串底层 API。源码包里水印方法addWatermark(String src, String dest, String text)的核心是setTextMatrix那六个参数我把它拎出来// 在每页叠加 45 度斜字水印 public void addWatermark(String src, String dest, String text) throws IOException, DocumentException { PdfReader reader new PdfReader(src); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); int total reader.getNumberOfPages(); for (int i 1; i total; i) { PdfContentByte cb stamper.getUnderContent(i); // 垫在文字下方 cb.saveState(); cb.setGState(new PdfGState().setFillOpacity(0.15f)); // 透明度 15% cb.beginText(); cb.setFontAndSize(bf, 48); // 六个参数a b c d e fa/d 缩放b/c 旋转e/f 平移 cb.setTextMatrix(0.707f, 0.707f, -0.707f, 0.707f, 150, 300); cb.showText(text); cb.endText(); cb.restoreState(); } stamper.close(); reader.close(); }逻辑说明setTextMatrix(a, b, c, d, e, f)对应变换矩阵[a b; c d]加平移(e, f)。旋转 45 度时a cos45 ≈ 0.707、b sin45 ≈ 0.707、c -sin45 ≈ -0.707、d cos45 ≈ 0.707这就是那四个小数的来历不是随便填的。e和f是水印起点坐标原点同样在左下角(150, 300)大概在 A4 页面中下部。setFillOpacity(0.15f)控制透明度0 全透明、1 不透明水印一般 0.1 到 0.2 之间太高会盖住正文影响阅读。getUnderContent让水印垫在文字下方如果业务要求水印盖在文字上方比如防截图换成getOverContent即可但透明度要再调低一点。4.2 平铺水印和单条水印的取舍上面代码每页只画一条水印适合“角落标识”类需求。如果要做满屏平铺的防伪水印得在每页用双层循环算坐标行距和列距一般取水印文字宽度的 1.5 到 2 倍避免重叠。源码包里只给了单条示例平铺要自己补循环注意每画一条都要saveState/restoreState配对否则透明度会累积到后面的内容上出现“越画越淡”或者“正文也变透明”的怪现象。还有一个性能点BaseFont.createFont是重操作不要在循环里反复创建提到循环外面只建一次几百页的文档能省下可观的耗时。如果水印文字很长showText之前先用bf.getWidthPoint(text, 48)算一下宽度超过页面宽度就缩小字号或者拆行否则文字会溢出页面被裁掉。4.3 中文水印乱码的两个根因中文水印乱码基本就两个原因字体没指定对或者编码和字体不匹配。STSong-Light配UniGB-UCS2-H是标准组合如果你换成自定义 ttf编码必须用BaseFont.IDENTITY_H用错会抛Font ... is not recognized或者输出一堆方块。第二个原因是setFontAndSize必须在beginText之后、showText之前调用顺序错了不报错但字体不生效输出默认字体中文直接变问号。我排查这类问题一般先写一个最小 demo 只画一行中文确认字体链路通了再往水印逻辑里套别一上来就在几百页文档上试翻车了都不知道是哪一层的问题。5. 文本替换PdfReader 内容流解析的边界与翻车点5.1 为什么文本替换是四个功能里最容易翻车的创建、签章、水印都是在“加东西”文本替换是“改东西”而 PDF 的内容流是一串绘制指令不是结构化文本你看到的“一段话”在底层可能是十几个Tj/TJ操作拼出来的中间还夹着字体切换、字距调整、换行定位。iText 没有提供“把 A 替换成 B”的高层 API源码包里的做法是用PdfTextExtractor先抽文本定位再用PdfContentByte重绘覆盖或者用PdfReader的getPageContent拿到原始字节流做字符串替换再写回。后者简单粗暴但极易翻车我把它标成“仅限 demo生产慎用”// 粗暴替换直接改内容流字节仅适合纯文本简单 PDF public void replaceText(String src, String dest, String oldStr, String newStr) throws IOException { PdfReader reader new PdfReader(src); int total reader.getNumberOfPages(); for (int i 1; i total; i) { byte[] pageBytes reader.getPageContent(i); String content new String(pageBytes, ISO-8859-1); // 注意编码 if (content.contains(oldStr)) { content content.replace(oldStr, newStr); reader.setPageContent(i, content.getBytes(ISO-8859-1)); } } PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); stamper.close(); reader.close(); }逻辑说明getPageContent返回的是页面内容流的原始字节new String(bytes, ISO-8859-1)是为了保证字节到字符串再回字节不丢信息用 UTF-8 反而可能破坏二进制指令。replace是纯字符串替换如果oldStr在内容流里被拆成多个Tj操作很常见尤其是带字距调整的文本这里根本匹配不到替换静默失败。setPageContent写回后必须用PdfStamper重新封装才能落盘直接reader.close()不会保存。这段代码在源码包里能跑通是因为示例 PDF 是 iText 自己生成的、内容流结构简单换成 Word 导出的 PDF 或者扫描件 OCR 后的 PDF大概率匹配不到或者改出乱码。5.2 靠谱一点的思路定位后覆盖重绘生产环境我一般不用上面的字节替换而是走“抽取文本定位坐标 → 用白色矩形盖住原文 → 在相同位置重绘新文本”的路线。PdfTextExtractor.getTextFromPage配合LocationTextExtractionStrategy能拿到每个文本块的坐标虽然精度受 PDF 生成方式影响但比字节替换稳。代价是代码量翻倍而且要处理字体、字号、颜色的还原否则盖上去的新文本和周围风格不一致一眼假。源码包里没给这条路线属于要自己补的进阶内容。如果业务只是“把合同模板里的占位符换成实际值”更推荐在生成阶段就用PdfWriter加变量拼内容而不是生成后再替换后者是给自己挖坑。5.3 替换后的验证不能省不管用哪种替换方式改完必须验证。最直接的是用PdfTextExtractor重新抽一遍文本确认newStr出现且oldStr消失再打开文件看排版有没有错位、字体有没有变。我见过替换后文本长度变化导致换行错乱、覆盖矩形盖住了相邻字段的案例都是没做视觉验证。自动化的话可以用 PDF 渲染库把页面转成图片做像素比对但成本高小批量人工抽检更实际。6. 避坑清单四个功能各自的翻车现场6.1 创建时中文变方块现象生成的 PDF 打开全是方块或者问号。原因字体用了默认的 Helvetica不支持中文或者BaseFont.createFont的编码参数写错。解决固定用STSong-LightUniGB-UCS2-H或者自定义 ttf IDENTITY_H并且确认document.add的Paragraph显式传了Font对象不传就用默认字体。6.2 签章位置整体偏移现象印章盖到了页面外面或者压住正文。原因坐标原点搞错左下角不是左上角或者原 PDF 有/Rotate旋转没处理。解决用reader.getPageSize(i)取实际宽高重算坐标旋转页加stamper.setRotateContents(false)或者手动换算先在单页上打印坐标调准再批量跑。6.3 水印透明度累积现象多页文档越往后水印越淡或者正文也变透明。原因saveState和restoreState没配对setGState的作用域泄漏到后续绘制。解决每条水印严格saveState→ 设透明度 → 绘制 →restoreState循环内不共用状态。6.4 文本替换静默失败现象代码没报错但打开 PDF 原文没变。原因目标字符串在内容流里被拆成多个绘制操作字符串replace匹配不到或者setPageContent之后没走PdfStamper落盘。解决先用PdfTextExtractor确认文本确实以完整形式存在匹配不到就换覆盖重绘方案改完必须重新抽取验证。6.5 大文件内存溢出现象处理几百页的 PDF 时OutOfMemoryError。原因PdfReader默认把整个文件读进内存叠加图片和字体后内存翻倍。解决调大 JVM 堆-Xmx2g起步或者用PdfReader的随机访问模式配合PdfStamper的增量更新处理完及时close释放别在一个循环里攒着多个 reader 不关。7. 进阶把四个功能串成一条可复用的 PDF 处理管线单看四个功能各自都能跑真实业务里往往是“先生成、再打水印、再签章、最后归档”顺序错了会互相覆盖。我一般按“创建 → 水印 → 签章 → 替换”排因为水印要垫在文字下方、签章要盖在最上层、替换如果放在最后会破坏前面叠加的内容。串管线的时候有两个工程习惯值得养成一是每个环节输出到临时文件全部成功后再原子替换目标文件避免中途失败留下半成品二是给每个环节加耗时日志PDF 处理慢起来很玄学不记日志根本不知道卡在哪一步。下面这段是管线骨架参数和前面各节一致只是把调用串起来// 四步管线创建 - 水印 - 签章 - 替换每步落临时文件 public void pipeline(String finalDest) throws Exception { String tmp1 finalDest .create.tmp; String tmp2 finalDest .wm.tmp; String tmp3 finalDest .seal.tmp; long t0 System.currentTimeMillis(); createPdf(tmp1); // 第 1 步生成 addWatermark(tmp1, tmp2, 内部资料); // 第 2 步垫水印 stampPdf(tmp2, tmp3, seal.png, 425, 80); // 第 3 步盖印章 replaceText(tmp3, finalDest, {{name}}, 张三); // 第 4 步替换占位符 // 清理临时文件失败时保留便于排查 new File(tmp1).delete(); new File(tmp2).delete(); new File(tmp3).delete(); System.out.println(pipeline cost (System.currentTimeMillis() - t0) ms); }这段骨架里replaceText放在最后是有争议的——如果替换会改变文本长度导致排版变化最好放在水印之前让水印基于最终排版生成。我的经验是占位符替换如果只是等长替换比如姓名换姓名放最后没问题如果长度差异大挪到创建之后、水印之前。临时文件命名带步骤后缀出问题一眼能看出卡在哪步比在几百行日志里翻要快得多。还有一点stampPdf的坐标(425, 80)是按 A4 算的如果前面创建时改了页面尺寸这里必须同步改管线里最好把页面尺寸抽成常量统一引用别两处硬编码。从那以后我每次接 PDF 相关的需求都强制先把“创建、水印、签章、替换”四个最小 demo 各跑一遍确认字体、坐标、透明度、内容流这四条链路都通再往业务代码里拼。这份源码包最大的价值就是把这四个 demo 摆在一起省了我从零搭脚手架的时间但每个 demo 到生产之间那段距离还是得自己一步步踩过去。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询