资讯动态

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

发布时间:2026/10/23 6:56:12 来源:尧图企业网站定制
简介这是一份面向Java开发者的iText实战源码包聚焦PDF文档的创建、签章、斜字水印与文本替换四类常见操作适合需要快速上手PDF处理的初中级工程师参考。压缩包共32个文件约8.77MB包含7个java源码、9个class编译文件、5个jar依赖含itextpdf、itext-asian及bc系列加密库、2个p12证书、2个png与2个jpg示例图片以及project、classpath、prefs等工程配置文件和readme说明可直接导入Eclipse运行调试。资源围绕PdfWriter、PdfStamper、PdfSignatureAppearance、PdfFormXObject等核心类展开覆盖新建文档、电子签名、旋转透明水印绘制与AcroFields文本替换等典型场景并涉及文件体积控制与内存优化思路。目前已有1571人学习下载可作为PDF处理功能的代码模板与排错参考。1. 从一份合同 PDF 说起为什么我盯上了 iText上个月帮一个做企业服务的团队看代码他们的业务系统每天要生成几百份带公司抬头的合同 PDF还要在末尾盖电子章、在正文里打上「仅限内部使用」的斜字水印偶尔遇到模板里写死的旧条款得批量替换成新版本。一开始他们用 Word 转 PDF 的土办法结果字体乱码、签章位置漂移、水印一放大就糊运维天天被业务追着跑。后来换成 iText 这套 Java PDF 库才算把创建、签章、水印、文本替换四件事收进一个工程里。这份源码包就是围绕这四个高频操作写的完整示例适合正在做电子合同、报表导出、票据生成的 Java 后端开发者也适合想搞懂 PDF 底层对象结构的人。它不教你 PDF 规范只给你能直接跑、能直接改的代码。2. 技术选型与工程结构为什么是 iText 而不是别的2.1 iText 的版本分叉与选型逻辑iText 有两个主要分支老版本的com.lowagie.text2.xLGPL/MPL和 5.x 之后的com.itextpdf.textAGPL以及 7.x 的com.itextpdf.kernelAGPL/商业。这份源码用的是 5.x 系列原因是它在国内企业项目里存量最大API 稳定网上能搜到的签章、水印示例几乎都基于它。选型时我一般会看三点一是许可证AGPL 对闭源商用有传染性如果项目要闭源卖要么买商业授权要么把 PDF 模块拆成独立服务二是功能覆盖5.x 的PdfStamper、PdfReader、PdfWriter三件套足够应付创建、签章、水印、替换三是依赖体积iText5 加上 BouncyCastle 也就几 MB比引入一整条 PDF 渲染链轻得多。提示如果项目是全新起步且不介意 AGPL可以直接上 iText 7API 更现代但如果是维护老系统5.x 的示例和社区答案更多改造成本更低。2.2 工程目录与依赖清单源码包是一个标准 Maven 工程目录结构如下itext-pdf-demo/ ├── pom.xml ├── src/main/java/com/example/pdf/ │ ├── CreatePdf.java // 创建 PDF │ ├── SignPdf.java // 签章 │ ├── WatermarkPdf.java // 斜字水印 │ ├── ReplaceTextPdf.java // 文本替换 │ └── util/ │ ├── PdfUtil.java // 公共方法 │ └── KeystoreUtil.java // 证书加载 ├── src/main/resources/ │ ├── fonts/simsun.ttf // 中文字体 │ ├── keystore/test.p12 // 测试证书 │ └── template/contract.pdf // 待处理模板 └── src/test/java/... // 单元测试pom.xml里核心依赖只有两个dependencies !-- iText 5.x提供 PDF 创建与操作能力 -- dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency !-- BouncyCastle签章时做证书解析和加密 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version /dependency /dependenciesitextpdf负责文档模型和读写bcprov负责 PKCS#12 证书和数字签名算法。版本号我固定用 5.5.13.3这是 5.x 最后一个稳定维护版再往上就是 7.x 的包名了混用会直接报ClassNotFoundException。2.3 四个模块的职责边界CreatePdf从零生成一份带段落、表格、图片的文档SignPdf在已有 PDF 上追加数字签名域WatermarkPdf用PdfStamper在每页叠加旋转文字ReplaceTextPdf则要解析内容流把指定字符串替换掉。四个类都依赖PdfUtil里的字体加载和异常封装KeystoreUtil只在签章时用。这种拆分的好处是每个类只做一件事二次开发时想改水印角度就只动WatermarkPdf不会牵连签章逻辑。3. 核心模块拆解创建、签章、水印、替换的代码实现3.1 创建 PDF从 Document 到 PdfWriter创建是最基础的一步但中文字体是第一个坑。iText 默认字体不含中文直接写会输出空白或方块。代码里显式加载simsun.ttfpublic static void createPdf(String dest) throws Exception { // 1. 创建 Document指定页面大小和边距 Document document new Document(PageSize.A4, 50, 50, 50, 50); // 2. 绑定 PdfWriter输出到文件 PdfWriter.getInstance(document, new FileOutputStream(dest)); document.open(); // 3. 加载中文字体BaseFont.IDENTITY_H 支持 Unicode BaseFont bf BaseFont.createFont(fonts/simsun.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); Font titleFont new Font(bf, 18, Font.BOLD); Font bodyFont new Font(bf, 12, Font.NORMAL); // 4. 写入标题和正文 document.add(new Paragraph(采购合同, titleFont)); document.add(new Paragraph(甲方某公司, bodyFont)); document.add(new Paragraph(乙方某供应商, bodyFont)); // 5. 插入表格 PdfPTable table new PdfPTable(3); table.setWidthPercentage(100); table.addCell(new PdfPCell(new Phrase(序号, bodyFont))); table.addCell(new PdfPCell(new Phrase(名称, bodyFont))); table.addCell(new PdfPCell(new Phrase(金额, bodyFont))); document.add(table); document.close(); }PageSize.A4和边距参数决定版面BaseFont.IDENTITY_H是水平书写模式EMBEDDED表示把字体子集嵌入 PDF避免对方机器没装宋体时显示异常。PdfPTable的列数在构造时固定setWidthPercentage(100)让表格撑满可用宽度。这段代码跑完dest路径下就是一份可打印的合同。3.2 签章PdfStamper 与 PdfSignatureAppearance签章不是画个图片上去而是往 PDF 里写一个数字签名域包含证书信息和加密摘要。核心是PdfStamper的createSignaturepublic static void signPdf(String src, String dest, String keystore, String password) throws Exception { // 1. 读取源 PDF PdfReader reader new PdfReader(src); // 2. 创建 Stamper追加模式不覆盖原内容 FileOutputStream os new FileOutputStream(dest); PdfStamper stamper PdfStamper.createSignature(reader, os, \0); // 3. 获取签名外观设置签章位置和大小 PdfSignatureAppearance appearance stamper.getSignatureAppearance(); appearance.setReason(合同确认); appearance.setLocation(北京); // 参数页码、左下角 x、左下角 y、右上角 x、右上角 y appearance.setVisibleSignature(new Rectangle(400, 50, 550, 120), 1, sig1); // 4. 加载 PKCS#12 证书 KeyStore ks KeyStore.getInstance(PKCS12); ks.load(new FileInputStream(keystore), password.toCharArray()); String alias ks.aliases().nextElement(); PrivateKey pk (PrivateKey) ks.getKey(alias, password.toCharArray()); Certificate[] chain ks.getCertificateChain(alias); // 5. 创建签名SHA256 摘要BC 提供者 ExternalSignature pks new PrivateKeySignature(pk, SHA256, BC); ExternalDigest digest new BouncyCastleDigest(); MakeSignature.signDetached(appearance, digest, pks, chain, null, null, null, 0, MakeSignature.CryptoStandard.CMS); stamper.close(); reader.close(); }createSignature的第三个参数\0表示不追加 PDF 版本号。setVisibleSignature的Rectangle用四个坐标定义签章框页码从 1 开始。MakeSignature.signDetached是分离签名签名值不嵌入内容流而是放在 PDF 的签名字典里这样原文档内容不会被改动。CryptoStandard.CMS是通用标准兼容大多数阅读器。3.3 斜字水印旋转文字与透明度控制水印的难点在于旋转和透明度。iText 用PdfContentByte的变换矩阵实现旋转public static void addWatermark(String src, String dest, String text) throws Exception { PdfReader reader new PdfReader(src); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); BaseFont bf BaseFont.createFont(fonts/simsun.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); // 设置水印颜色和透明度 PdfGState gs new PdfGState(); gs.setFillOpacity(0.3f); // 30% 不透明度 int pageCount reader.getNumberOfPages(); for (int i 1; i pageCount; i) { // 在内容上方叠加水印 PdfContentByte canvas stamper.getOverContent(i); canvas.saveState(); canvas.setGState(gs); canvas.beginText(); canvas.setFontAndSize(bf, 40); canvas.setColorFill(BaseColor.GRAY); // 旋转 45 度cos45, sin45, -sin45, cos45, x, y canvas.setTextMatrix(0.707f, 0.707f, -0.707f, 0.707f, 200, 400); canvas.showText(text); canvas.endText(); canvas.restoreState(); } stamper.close(); reader.close(); }setFillOpacity(0.3f)让水印不遮挡正文。setTextMatrix的六个参数是仿射变换矩阵前四个控制旋转和缩放后两个是平移量。45 度对应的 cos 和 sin 都是 0.707所以矩阵写成(0.707, 0.707, -0.707, 0.707, x, y)。getOverContent表示水印在正文上方如果想让水印在文字下面换成getUnderContent。3.4 文本替换解析内容流与 Token 处理文本替换是四个操作里最麻烦的因为 PDF 不存「字符串」只存绘制指令。iText 5 没有直接替换 API得用PdfContentStreamEditor或手动解析PdfContentByte。源码里用的是继承PdfContentStreamEditor的方式public static void replaceText(String src, String dest, String oldText, String newText) throws Exception { PdfReader reader new PdfReader(src); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); for (int i 1; i reader.getNumberOfPages(); i) { PdfContentStreamEditor editor new PdfContentStreamEditor(reader) { Override protected void write(PdfContentStreamProcessor processor, PdfLiteral operator) { // 只处理显示文本的 Tj / TJ 操作符 if (Tj.equals(operator.toString()) || TJ.equals(operator.toString())) { // 这里拿到的是操作数实际替换需结合上一轮文本 // 简化处理直接写出新文本 } super.write(processor, operator); } }; editor.editPage(stamper, i); } stamper.close(); reader.close(); }这段代码是骨架实际替换要在write里维护一个文本缓冲区遇到Tj时把缓冲区里的旧字符串换成新字符串再写出。因为 PDF 可能把一句话拆成多个Tj所以不能只匹配单个操作数。源码里PdfUtil封装了一个TextBuffer类来做这件事思路是遇到BT开始文本块累积所有Tj/TJ的字符串遇到ET时统一替换并重新写出。注意文本替换对字体编码敏感。如果原 PDF 用的是子集嵌入字体替换后的字符可能不在子集里会显示为空白。稳妥做法是替换前先确认字体是否包含目标字符或者干脆用重新生成的方式代替替换。4. 跑起来环境准备与四个模块的验证步骤4.1 环境与依赖安装需要 JDK 8 或以上Maven 3.6。把源码包解压后在根目录执行# 编译并下载依赖 mvn clean compile # 运行单元测试会依次生成四个输出文件 mvn test测试类里每个方法对应一个模块输出文件默认落在target/output/下。如果只想跑某一个用-DtestCreatePdfTest指定。第一次跑会下载 iText 和 BouncyCastle国内网络建议配好 Maven 镜像否则可能卡在bcprov的下载上。4.2 创建与签章的验证创建模块跑完后打开target/output/created.pdf检查三点中文是否正常显示、表格边框是否完整、页面尺寸是否为 A4。签章模块需要先生成一份测试证书源码包里已经带了test.p12密码是123456。跑完signed.pdf后用 Adobe Acrobat 打开右侧应该出现签名面板显示「合同确认」和「北京」。如果签名面板是灰色说明证书链不完整或bcprov没注册检查Security.addProvider(new BouncyCastleProvider())是否在签名前调用。4.3 水印与替换的验证水印跑完后watermarked.pdf每页应该有一行 45 度灰色文字透明度约 30%。如果文字是实心黑色说明PdfGState没生效检查setGState是否在beginText之前调用。替换模块跑完replaced.pdf用文本搜索功能找旧字符串应该搜不到新字符串能搜到。如果替换后排版错乱多半是替换后的字符串长度和原字符串差异太大导致文本矩阵错位这时候需要同步调整Td或TJ里的间距参数。4.4 常见失败点速查现象可能原因排查方向中文显示为方块未嵌入字体或用了默认字体检查BaseFont.createFont路径和IDENTITY_H签章后阅读器报错证书链不完整或摘要算法不匹配确认chain包含完整证书链SHA256 与证书算法一致水印不透明PdfGState未设置或设置顺序错误setGState必须在绘制前调用替换后文字消失字体子集不含新字符换用完整字体或重新生成 PDF运行报NoClassDefFoundErroriText 5 和 7 包混用检查pom.xml是否同时引入两个版本5. 避坑与排查四个操作里最容易翻车的地方5.1 字体嵌入的坑为什么本地好好的服务器上就乱码现象是本地 Windows 跑出来中文正常部署到 Linux 服务器后 PDF 里中文全变方块。原因是代码里用了BaseFont.createFont(simsun.ttf, ...)但没把字体文件打进 jar服务器上找不到这个路径。解决方式是把字体放在src/main/resources/fonts/下用getResourceAsStream读取或者用BaseFont.createFont(STSong-Light, UniGB-UCS2-H, false)调用 iText 内置的 CJK 字体。我一般会优先用内置字体省去打包字体的麻烦但内置字体不支持加粗和斜体需要样式时还是得嵌入 TTF。5.2 签章位置漂移坐标系原点到底在哪现象是签章框在本地看着在右下角换一台机器或换一个阅读器就跑到左上角去了。原因是 PDF 坐标系原点在页面左下角而Rectangle的 y 轴是从下往上算的。如果按屏幕坐标左上角原点去填y 值就会反。解决方式是先获取页面高度reader.getPageSize(1).getHeight()然后用height - y换算。另外setVisibleSignature的页码参数是 1-based传 0 会报错。5.3 水印旋转角度算错矩阵参数不是角度值现象是设了 45 度但水印是歪的或者倒的。原因是setTextMatrix接收的是 cos 和 sin 值不是角度。45 度对应cos(45°)0.707sin(45°)0.707矩阵顺序是(cos, sin, -sin, cos, x, y)。如果直接传45矩阵会变成缩放 45 倍水印巨大且位置离谱。解决方式是提前算好弧度float rad (float) Math.toRadians(45); float cos (float) Math.cos(rad); float sin (float) Math.sin(rad);再代入。5.4 文本替换的编码陷阱为什么搜得到却替不掉现象是用阅读器能搜到旧字符串但代码里替换后没变化。原因是 PDF 内容流里的文本可能是十六进制编码或带字符间距的TJ数组直接按明文匹配匹配不到。解决方式是先把操作数转成字符串再匹配TJ要把数组里的每个元素拼起来。另外如果原文本用了Tf设置了非 Unicode 编码替换后的字符串也要用相同编码写出否则阅读器解析不了。5.5 内存与流关闭大文件处理时的 OOM现象是处理几十 MB 的 PDF 时抛OutOfMemoryError。原因是PdfReader默认把整个文件读进内存PdfStamper又会复制一份。解决方式是用PdfReader的随机访问模式new PdfReader(new RandomAccessFileOrArray(...))并在finally块里确保stamper.close()和reader.close()都执行。如果只是追加签章可以用PdfStamper.createSignature的追加模式避免全量重写。6. 二次开发把四个模块串成一条 PDF 处理流水线6.1 从单次操作到链式处理实际业务里很少只做一件事通常是「创建 → 替换 → 水印 → 签章」一条链。最直接的做法是每个模块输出一个临时文件下一个模块读上一个的输出。但这样磁盘 IO 多大文件时慢。更好的方式是用ByteArrayOutputStream在内存里传递public byte[] processPipeline(byte[] input) throws Exception { // 第一步替换文本 ByteArrayOutputStream step1 new ByteArrayOutputStream(); PdfReader reader new PdfReader(input); PdfStamper stamper new PdfStamper(reader, step1); // ... 替换逻辑 stamper.close(); reader.close(); // 第二步加水印 ByteArrayOutputStream step2 new ByteArrayOutputStream(); reader new PdfReader(step1.toByteArray()); stamper new PdfStamper(reader, step2); // ... 水印逻辑 stamper.close(); reader.close(); return step2.toByteArray(); }这样整个流水线不落盘适合接口里实时返回 PDF 的场景。代价是内存占用翻倍所以要在入口限制文件大小比如超过 20MB 就走临时文件模式。6.2 配置化改造把硬编码参数抽出来源码里水印角度、字体大小、签章位置都是写死的。二次开发时我一般会抽一个PdfConfig类用 JSON 或数据库配置驱动配置项类型说明watermark.textString水印文字watermark.anglefloat旋转角度默认 45watermark.opacityfloat透明度0~1sign.reasonString签章原因sign.locationString签章地点sign.pageint签章页码-1 表示最后一页replace.rulesList旧文本到新文本的映射这样业务方改水印文字不用重新编译改配置就行。replace.rules用列表是因为一份合同可能有多处条款要替换按顺序执行。6.3 验证改造效果三个必查点改完代码后我习惯用三个检查点验证一是用pdfbox或qpdf命令行工具 dump 一下 PDF 结构确认签名域和字体对象都在二是用不同阅读器Acrobat、Chrome、Foxit各打开一次确认显示一致三是把处理后的 PDF 再跑一遍替换模块确认幂等性——同一份文件处理两次结果应该一样否则说明有状态残留。这三个点走完基本能挡住 90% 的回归问题。6.4 一个具体技巧用模板占位符代替文本替换文本替换的坑太多后来我干脆换了个思路创建 PDF 时就用占位符比如{{partyA}}、{{amount}}生成时直接替换占位符再输出而不是对已有 PDF 做替换。这样完全绕开内容流解析用Paragraph拼接字符串就行。代价是模板必须由系统生成不能处理外部来的 PDF。如果业务允许这是最稳的方案。从那以后我每次接 PDF 需求都先问一句「模板能不能我们自己控」能控就用占位符不能控才上PdfContentStreamEditor。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑