
简介这是一套基于 iText 库处理 PDF 文档的完整 Java 工程资源面向需要集成创建、电子签章、斜字水印与文本替换功能的开发者。压缩包共包含 32 个文件内含七个 Java 源码文件及对应的编译类文件五个依赖库文件覆盖 PDF 解析、亚洲字体支持及加密所需组件另有两个数字证书文件用于模拟签名场景还附带若干说明图片、文本说明和开发环境配置项。整体压缩包约 8.77MB目前已有 1571 人学习下载。工程结构清晰源码分别演示了利用写入器类新建文档使用签名器添加电子签章通过表单对象与画布实现倾斜水印借助表单域执行文本替换等核心流程。对于希望快速上手 PDF 自动化处理的开发者这套带依赖与证书的示例可直接导入开发环境运行便于对照源码理解 API 用法并进行二次改造。1. iText操作PDF先把这四个功能的坑一次说透PDF在很多人眼里是“能打开就能改”的文档真上手才发现它比想象中脆。改一个词可能让整份文件乱码签章和水印顺序弄反阅读器会直接提示文档无效。iText是Java生态里最常见的PDF处理库创建、签章、斜字水印、文本替换这四件事每一件都有固定套路和暗坑。这篇按实际项目顺序写先创建再签章再水印最后替换顺带把高频故障列清楚。适合做合同、报告、证书、表单类产品的后端开发者跟着走能直接落进自己的工程里不用从零趟坑。2. 创建PDF的起步姿势版本选型、最小代码与页面常识2.1 版本差异一句话讲清5.x 和 7.x 该怎么选iText目前有两条分支选错分支会浪费大量时间在编译报错上。5.x系列的API集中在一个依赖里PdfWriter、Document、PdfStamper这些类老教程里到处都是网上能搜到的案例也基本以它为主。7.x被拆成kernel、layout、io、sign等多个模块包名和类名全换旧代码直接拿来编译基本过不去。我的习惯是新项目如果不是必须上7.x先选5.x系列。资料多、踩坑记录全团队接手容易。7.x适合愿意读官方文档、对模块划分有明确要求的从零项目但它对新手并不友好。还有一点要提前确认开源许可见官方声明商用前先确认自己用的分支和授权方式满足要求别等项目上线了再补功课。怎么快速分辨代码是哪个分支看import开头com.itextpdf.text是5.xcom.itextpdf.kernel是7.x。同一套代码里混着两个分支的写法会报一堆“方法不存在”这是最常见的翻车原因。2.2 最小可运行的PDF创建代码与参数解读import com.itextpdf.text.*; import com.itextpdf.text.pdf.*; // 最小PDF建立一个A4页面并写入一行中文 Document document new Document(PageSize.A4, 36, 36, 54, 54); PdfWriter.getInstance(document, new FileOutputStream(hello.pdf)); document.open(); // 中文必须显式指定中文字体否则输出乱码或直接抛异常 BaseFont baseFont BaseFont.createFont( STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font font new Font(baseFont, 12, Font.NORMAL, BaseColor.BLACK); Paragraph p new Paragraph(你好iText 创建 PDF, font); p.setAlignment(Element.ALIGN_LEFT); document.add(p); document.close();Document构造参数依次是页面大小、左右边距、上下边距单位是用户坐标点。PdfWriter负责把Document的内容串到输出流document.close()会统一处理flush和关闭漏掉这一步文件可能不完整。最关键的坑在中文iText内置的Helvetica、Times等标准字体不支持中文不指定BaseFontadd段落时要么乱码要么直接抛异常。STSong-Light加UniGB-UCS2-H是常用组合NOT_EMBEDDED表示字体不嵌入PDF文件体积小但换到没装该字体的机器上渲染外观依赖阅读器要求外观一致时改成BaseFont.EMBEDDED代价是文件变大。2.3 页面尺寸、边距与坐标系影响后续水印和签章的基础参数PDF页面坐标系原点在左下角x轴向右y轴向上单位是点。所有水印坐标、签名域Rectangle都从右下角原点开始算这条在后面做斜字水印定位时特别容易踩。PageSize常用尺寸A4是595x842点A3是842x1191点LETTER是612x792点。页面宽x高点典型用途A4595x842合同/报告A3842x1191图纸/附页LETTER612x792涉外表单边距36点等于0.5英寸是常规文档的默认值。水印平铺密度、签名框高度这些参数在创建阶段就要想好否则后面改一版要重复输出整份文件。工程化建议封装一个PdfDocumentBuilder把字体、边距、默认样式收进去后续签章水印复用同一个BaseFont对象避免重复创建字体带来的内存开销。创建这一步看着简单但字体和坐标系理清了后面三个环节会顺手很多。3. 数字签章自签证书、可见签名域与验证闭环电子签章是这四个功能里最容易被“看起来成功”骗到的。签名能写进PDF不代表校验能过。顺序错、证书链不全、签名域坐标不对都会让接收方的阅读器报错。做签章的正确顺序是先备证书再签名最后验证。3.1 用 keytool 生成自签证书并转成 PKCS12# 生成自签证书直接输出为PKCS12格式 keytool -genkeypair \ -alias mykey \ -keyalg RSA \ -keysize 2048 \ -dname CNInternal Test Signer, OUDev, OInternal \ -validity 3650 \ -storetype PKCS12 \ -keystore signer.p12 \ -storepass changeit \ -keypass changeitkeytool是 JDK 自带工具主流系统都可用。参数说明-alias是之后读取KeyStore的入口名-keyalg用RSA-keysize至少2048-validity是天数3650约等于10年-dname里的CN写测试标识即可不要写真实身份信息生产环境应用正规CA签发的证书自签证书只适合内部联调和测试。Java侧读取PKCS12的方法KeyStore ks KeyStore.getInstance(PKCS12); try (FileInputStream in new FileInputStream(signer.p12)) { ks.load(in, changeit.toCharArray()); } PrivateKey key (PrivateKey) ks.getKey(mykey, changeit.toCharArray()); Certificate[] chain ks.getCertificateChain(mykey);PKCS12同时保存私钥和证书链Java侧通过alias取私钥和chain。chain要在签名时整体传给iText很多人只传了叶子证书导致验证时证书链不完整这是自签证书在线上验证失败的主要原因之一。3.2 签名代码参数含义与两种模式的差别PdfReader reader new PdfReader(unsigned.pdf); FileOutputStream out new FileOutputStream(signed.pdf); // tempFile 传临时文件路径大文件建议给路径避免全部进内存 PdfStamper stamper PdfStamper.createSignature( reader, out, sig_temp.tmp, true); PdfSignatureAppearance appearance stamper.getSignatureAppearance(); // WINCER_SIGNED普通签名CERTIFIED_NO_CHANGES_ALLOWED禁止后续改动 appearance.setCrypto(key, chain, null, PdfSignatureAppearance.WINCER_SIGNED); // 可见签名域Rectangle(x, y, width, height)第1页 appearance.setVisibleSignature( new Rectangle(36, 36, 200, 100), 1, signField); stamper.close();createSignature四个参数依次是Reader、输出流、临时文件路径、append。tempFile传null时内部用内存缓存整个文件大文件容易OOM给一个真实路径更稳。appendtrue表示增量更新保留PDF原有对象对已经签过名的文件再次修改append必须为true否则原签名校验会失败。setCrypto的四个参数是私钥、证书链、加密Providernull用默认、签名类型。WINCER_SIGNED是普通签名CERTIFIED_NO_CHANGES_ALLOWED会限制后续任何改动加了这种签名之后连水印都加不了选它要谨慎。setVisibleSignature里Rectangle从左下角起算单位是点第三个参数是签名域字段名阅读器里能看到这个域重名会导致覆盖。3.3 签名自校验签完别急着交付先跑一遍验证签名不是签完就结束。交付前用同一个库做一次验证能避免把“无效签名”发给对方。PdfReader reader new PdfReader(signed.pdf); SignatureUtil sigUtil new SignatureUtil(reader); for (String name : sigUtil.getSignatureNames()) { PdfPKCS7 pkcs7 sigUtil.verifySignature(name); System.out.println(签名有效: pkcs7.verify()); }verifySignature按签名域名逐个取PdfPKCS7verify()检查摘要和签名是否匹配返回true只代表文件没被篡改。证书链是否受信任取决于接收方有没有把自签根证书导入信任库这是两件事新手常混在一起排查。如果verify()返回false优先回查证书是不是同一对、chain是否完整。建议再做一次“改动测试”对signed.pdf多写一个空白页看阅读器是否提示“文档已被更改”用这个现象确认增量保护是否生效。实际交付中签章永远放在所有操作的最后一步水印、文本替换全做完再签顺序几乎不用讨论。4. 斜字水印画布旋转、平铺密度与半透明控制水印的“斜”不是字体斜体而是整行文字旋转一定角度常规用30度或45度。iText里画水印就是在内容层写文字但层级、字体、透明度任何一个没调好效果都会差很远。4.1 内容层覆盖与底层覆盖选错层级最容易翻车PdfStamper有两种内容层getUnderContent画在正文下方getOverContent画在正文上方。under适合文字版合同、报告水印不会遮挡正文但碰上深色背景或扫描件会看不清。over浮在最上层可见性最好但透明度和位置没调好会盖住正文。层级适用场景风险under文字版合同/报告深色背景不清晰over扫描件、图片类PDF不透明时遮挡正文常见做法电子文档默认用under扫描件用over且把透明度调到0.2到0.3之间。4.2 斜字水印绘制代码与参数拆解PdfReader reader new PdfReader(input.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(watermark.pdf)); // 透明度0.2~0.3 是常规范围 PdfGState gs new PdfGState(); gs.setFillOpacity(0.25f); BaseFont bf BaseFont.createFont( STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); for (int i 1; i reader.getNumberOfPages(); i) { PdfContentByte under stamper.getUnderContent(i); under.saveState(); under.setGState(gs); under.beginText(); under.setFontAndSize(bf, 36); under.setColorFill(BaseColor.GRAY); // ALIGN_CENTER 以 (x,y) 为中心rotation 单位是角度30 就是斜字 under.showTextAligned(PdfContentByte.ALIGN_CENTER, 内部资料, 297f, 421f, 30); under.endText(); under.restoreState(); } stamper.close();saveState和restoreState是图形状态栈保证透明度和颜色只作用于这段水印不影响正文渲染。beginText和endText包住文本绘制两者必须成对出现。showTextAligned里297和421是A4宽度和高度的一半即页面中心rotation传30就是斜着穿过页面“斜字水印”的关键就在这个参数传0就是正排水印。字号36是让单条水印在页面上足够醒目。4.3 半透明与平铺商用水印的常见配置单条大字号水印适合“内部资料”这类声明但业务里更常见的是“平铺半透明斜字”的组合防止有人裁掉局部。平铺做法是循环算网格交叉点把每个交叉点作为水印中心float pageW PageSize.A4.getWidth(); float pageH PageSize.A4.getHeight(); float stepX pageW / 3; float stepY pageH / 4; for (int col 0; col 3; col) { for (int row 0; row 4; row) { float x stepX * (col 0.5f); float y stepY * (row 0.5f); under.showTextAligned(PdfContentByte.ALIGN_CENTER, 内部资料, x, y, 45); } }参数怎么调页面越大字号和网格间隔同步放大需要增强警示感时密度提到每页20个以上透明度降到0.15。两个边界要注意密度过高会增大文件体积、拖慢渲染字号过大导致相邻水印重叠反而降低可读性。我一般会先导出一页预览调好参数再跑全量不直接上生产数据。另外一个认知要建立PDF里的水印可以被后续处理抹除iText水印解决的是“可见声明”不是“防篡改”。5. 避坑清单签章和水印环节的5个常见故障以下问题我都在项目里实际遇到过每条按现象、原因、解决三步记录排查时可以对照着看。5.1 已签名文件再改导致校验失败现象对已签章的PDF追加水印或改文本阅读器打开就提示“文档已被更改签名无效”。现象背后是原签名覆盖了文档摘要任何改动都会和摘要不一致校验自然失败。原因PdfStamper默认保存方式会重建整个文件原签名和文档内容绑定改动后无法匹配。遇到这种情况先别怀疑证书先看操作顺序。解决所有修改必须在签名前完成。必须对已签名文件追加修改时用PdfStamper.createSignature的appendtrue做增量保存保留原签名。排错时先调SignatureUtil.getSignatureNames()看文件里已有几个签名域避免重复签名重复签名会让阅读器只认最后一个域前面的全部提示异常。5.2 中文水印变方块或直接抛异常现象页面水印显示成一排方框或者调add时直接抛字体相关异常。原因BaseFont没指定中文字体。iText内置标准字体不支持中文用Helvetica写“内部资料”必然出问题。解决统一用BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.EMBEDDED)。如果部署环境对字体有特殊要求可以加载ttf或ttc文件路径替代STSong-Light。三个参数含义分别是字体名、编码、嵌入标志。EMBEDDED更保险但文件体积变大NOT_EMBEDDED依赖阅读器自带字体适合内部系统可控的场景。字体对象建议全局复用每个页面都createFont一次会增加不少内存开销。5.3 水印看不见或反过来盖住正文现象底纹水印在页面完全看不见或者改成over之后正文被大块色块遮挡。原因层级选错或透明度没调。under在深色扫描件上会被覆盖over默认完全不透明等于直接盖了一块色块在上面。解决文字版PDF用under扫描件用over且gs.setFillOpacity(0.2f到0.3f)。颜色别用纯黑BaseColor.GRAY更常规。验证方法导出两页一页浅色、一页深色分别看效果。先处理首页确认没问题再全量跑。5.4 自签证书在对方机器验证不通过现象自己验证是好的发给对方后提示“无有效证书路径”或“信任站点不存在”。原因自签证书的根没有导入对方信任库或签名时chain只传了叶子证书没有传完整证书链。解决setCrypto时把KeyStore.getCertificateChain取到的完整chain传进去交付时把根证书单独导出并提供导入信任库说明。生产环境建议直接换受信任CA签发的证书不要拿自签证书上生产。有一点要记住本机验证通过不代表交付没问题信任链这件事一定要在接收方环境验一次。5.5 大文件签名内存暴涨或直接OOM现象几百MB的PDF签名时JVM内存飙升偶发OutOfMemoryError。原因createSignature里的临时文件参数传了null这让内部用字节数组缓存整个文件。PDF页数多、资源多时内存占用会成倍放大。解决给createSignature传入真实临时文件路径例如sig_temp.tmp让iText落盘处理同时确认JVM堆配置有余量。关键就是tempFile这个参数宁可写一个临时文件路径也别传null。6. 文本替换能落地的两种思路与验证方法iText官方没有“查找替换”方法能搜到的类似方案要么走表单字段要么走坐标重绘。原因在于PDF正文是一堆绘制指令字符串被拆成多个片段存放直接改内容流轻则乱码重则整页报废。能落地的就两个方向。6.1 表单PDF用setField替换最省事的路径表单型PDF也就是带可填写文本框的文件是唯一能真正“替换内容”的场景PdfReader reader new PdfReader(form.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(filled.pdf)); AcroFields fields stamper.getAcroFields(); // 字段名以PDF表单设计时定义的为准常见如 name/tel/remark fields.setField(name, 张三); fields.setFieldProperty(name, textsize, 12f, null); // 展平把表单域转成普通文本防止被二次编辑 stamper.setFormFlattening(true); stamper.close();getAcroFields拿到表单域setField替换值setFieldProperty调整字号。setFormFlattening(true)会把可变表单域固化成普通文本这一步在交付场景里通常是必须的。前提是PDF本身带AcroForm域不是任何PDF都能走这条路判断方法是用阅读器打开看有没有可输入框。6.2 普通PDF用坐标重绘不碰内容流的替代思路非表单PDF的正确姿势是“贴膏药”先盖一块白底再在原位置画新文本。PdfContentByte cb stamper.getOverContent(1); cb.saveState(); // 1. 白底覆盖原文字区域 cb.setColorFill(BaseColor.WHITE); cb.rectangle(100f, 700f, 180f, 20f); cb.fill(); // 2. 在原位置重绘新文本 cb.beginText(); cb.setFontAndSize(baseFont, 12); cb.showTextAligned(PdfContentByte.ALIGN_LEFT, 替换后的内容, 100f, 702f, 0); cb.endText(); cb.restoreState(); stamper.close();坐标和签章一样原点在左下角。白底矩形高度要大于字号的1.2倍重绘文本基线要比矩形下边高2到3点避免字被裁掉。替换文本的字体、字号必须与原文本一致否则位置对了但字体风格会很突兀。坐标拿不准时先出一页预览反复对。6.3 替换效果怎么验证替换完别只肉眼看。用PDFBox的PDFTextStripper抽一遍文本确认旧文本不再出现、新文本能提取到再检查页数和文件大小没有明显异常。坐标重绘如果抽文本时发现新旧文本混在一起说明白底矩形没完全遮住原文字把矩形加宽再补一层白底即可。文本替换没有银弹表单字段优先其次坐标重绘直接改内容流的方案不要在生产环境用。我自己的习惯是把替换放在签章之前替换完先抽文本验证内容再做水印和签章顺序固定下来就很少返工。希望帮到你。本文还有配套的精品资源点击获取