首页 / 资讯中心 / 文章详情

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

Java iText PDF处理实战:创建、签章、水印与文本替换源码解析 ★ FEATURED ARTICLE
简介这是一份面向Java开发者的iText操作PDF实战源码包聚焦创建文档、电子签章、斜字水印与文本替换四类常见需求适合需要快速落地PDF处理功能的中初级开发者参考。包内共32个文件以7个java源码与9个class编译文件为核心配合itextpdf、itext-asian、bcprov等5个jar依赖以及p12证书、png/jpg示例图片和Eclipse工程配置压缩包约8.77MB可直接导入IDE运行调试。资源围绕PdfWriter、PdfStamper、PdfSignatureAppearance、PdfFormXObject等关键类展开覆盖从新建页面、设置签名证书到绘制旋转半透明水印、遍历AcroFields替换文本的完整链路并附readme说明。目前已有1571人学习下载读者可借此掌握PDF签章与水印的实现细节理解文本替换对文档结构的影响并参考依赖版本与工程结构快速搭建自己的PDF处理模块。1. 从一份“改不动”的合同 PDF 说起这个工具到底能干什么上个月帮一个做企业服务的团队看代码他们有个需求听起来特别朴素把系统里生成的合同 PDF 加上公司公章图片再在页脚打一行“仅限内部使用”的斜字水印最后把模板里写死的甲方名称替换成真实客户名。结果三个人折腾了一周公章位置对不上、水印盖住了正文、替换文本后字体全乱。问题不在业务逻辑而在他们用错了 PDF 处理的路子——拿文本编辑器思维去改二进制文档翻车是必然的。这份源码包解决的正是这类场景用 iText 在 Java 里对 PDF 做四件事——从零创建文档、给已有文档加签章、铺斜字水印、做文本内容替换。它适合两类人一类是刚接触 PDF 编程、需要一份能跑通的参考实现的新手另一类是在做电子合同、报表导出、票据系统的开发者想看看生产级代码怎么组织这些操作。关键词就俩源码、工具但这两个词背后是一整套 PDF 对象模型的实操。2. 技术选型与工程骨架为什么是 iText代码怎么摆2.1 iText 版本选择与依赖引入这个项目用的是 iText 5.x 系列原因很实际5.x 的 API 稳定、中文资料多、签章相关的PdfSignatureAppearance类成熟度高。iText 7 虽然架构更现代但签名和文本替换的写法差异大对新手不够友好。如果你的项目已经在用 7.x核心思路相通但类名和包路径要换。Maven 依赖只需要两个核心包注意itextpdf和itext-asian要配套后者负责中文字体缺了它中文会变成空白方块dependencies !-- PDF 核心操作库 -- dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13/version /dependency !-- 亚洲字体支持处理中文必须 -- dependency groupIdcom.itextpdf/groupId artifactIditext-asian/artifactId version5.2.0/version /dependency /dependencies参数说明itextpdf的 5.5.13 是 5.x 末期的稳定版修了不少早期签名相关的 bugitext-asian提供STSong-Light等中文字体通过BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED)调用。常见做法是把字体文件直接放进resources目录用BaseFont.createFont(fonts/simsun.ttc,0, ...)加载这样不依赖系统字体部署到服务器上不会因为缺字体而翻车。2.2 目录结构与模块划分源码包的目录结构是典型的 Maven 单模块工程按功能而不是按类型分包这点值得借鉴pdf-toolkit/ ├── src/main/java/com/example/pdf/ │ ├── create/ # 创建类PdfCreator │ ├── sign/ # 签章类PdfSigner │ ├── watermark/ # 水印类PdfWatermarker │ ├── replace/ # 替换类PdfTextReplacer │ └── util/ # 工具类FontUtil、PdfUtil ├── src/main/resources/ │ ├── fonts/ # 中文字体文件 │ └── templates/ # PDF 模板 └── src/test/java/ # 单元测试为什么按功能分包而不是按controller/service/dao分因为 PDF 操作是工具型代码每个功能相对独立按功能分包能让调用方一眼找到入口。util包里放字体加载和 PDF 读取的公共方法避免每个类里重复写PdfReader的初始化逻辑。2.3 核心工具类的初始化逻辑所有操作都绕不开PdfReader和PdfWriter这一对。读已有文档用PdfReader写新文档用PdfWriter两者通过PdfStamper桥接。工具类里我一般会封装一个安全关闭的方法因为PdfReader不关会导致文件句柄泄漏在批量处理时尤其明显public class PdfUtil { /** * 安全关闭 PdfReader避免文件句柄泄漏 * param reader 待关闭的读取器允许为 null */ public static void closeQuietly(PdfReader reader) { if (reader ! null) { try { reader.close(); } catch (Exception e) { // 关闭失败不抛异常记录日志即可 System.err.println(关闭 PdfReader 失败: e.getMessage()); } } } /** * 加载中文字体优先用 resources 下的字体文件 * return BaseFont 实例 */ public static BaseFont loadChineseFont() throws IOException, DocumentException { // 常见做法把字体文件放 resources/fonts 下用相对路径加载 String fontPath PdfUtil.class.getClassLoader() .getResource(fonts/simsun.ttc).getPath(); // ,0 表示取 ttc 集合中的第一个字体 return BaseFont.createFont(fontPath ,0, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } }逻辑说明closeQuietly吞掉关闭异常是有意为之因为关闭失败通常不影响主流程但如果不捕获批量处理时一个失败会中断整个循环。loadChineseFont里BaseFont.IDENTITY_H是横向书写模式BaseFont.EMBEDDED表示把字体嵌入 PDF这样在没装该字体的机器上也能正常显示。参数,0是 ttc 字体集合的索引simsun.ttc 里第一个是宋体第二个是黑体写错索引字体会变样。3. 四个核心功能拆解创建、签章、水印、替换的代码实现3.1 从零创建 PDF 并写入中文段落创建文档是最基础的操作但中文写入有个坑直接用FontFactory.getFont拿不到中文字体必须用BaseFont构造Font对象。下面这段代码创建一个带标题和正文的 PDFpublic class PdfCreator { public void createSimplePdf(String outputPath) throws Exception { // 输出流true 表示覆盖已有文件 FileOutputStream fos new FileOutputStream(outputPath); Document document new Document(PageSize.A4, 50, 50, 60, 60); PdfWriter.getInstance(document, fos); document.open(); // 加载中文字体不嵌入的话服务器上可能显示方块 BaseFont bf PdfUtil.loadChineseFont(); Font titleFont new Font(bf, 18, Font.BOLD); Font bodyFont new Font(bf, 12, Font.NORMAL); // 标题段落居中 Paragraph title new Paragraph(项目验收报告, titleFont); title.setAlignment(Element.ALIGN_CENTER); document.add(title); // 正文段落首行缩进两个字符 Paragraph body new Paragraph(本报告用于记录项目各阶段的验收结果。, bodyFont); body.setFirstLineIndent(24); // 12pt 字体缩进 24pt 约等于两个字符 document.add(body); document.close(); fos.close(); } }参数说明PageSize.A4是页面尺寸后面四个参数是左、右、上、下边距单位是点1 点约等于 1/72 英寸。setFirstLineIndent(24)里的 24 是点数对应 12pt 字体的两个字符宽度。注意document.close()必须在fos.close()之前调用否则 PDF 尾部结构写不完整文件会打不开。3.2 给已有 PDF 加签章定位与图片插入签章的本质是把公章图片插到 PDF 指定位置。难点在坐标系PDF 的原点在左下角而图片插入的坐标也是左下角但人的直觉是从左上角算。下面代码把公章盖在最后一页右下角public class PdfSigner { public void signPdf(String srcPdf, String stampImage, String outputPdf) throws Exception { PdfReader reader new PdfReader(srcPdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(outputPdf)); // 获取最后一页 int pageNum reader.getNumberOfPages(); PdfContentByte over stamper.getOverContent(pageNum); // 读取公章图片 Image stamp Image.getInstance(stampImage); // 缩放公章到 120x120 点 stamp.scaleAbsolute(120, 120); // 页面尺寸用于计算右下角坐标 Rectangle pageSize reader.getPageSize(pageNum); // 距离右边 50 点距离下边 80 点 float x pageSize.getWidth() - 120 - 50; float y 80; stamp.setAbsolutePosition(x, y); over.addImage(stamp); stamper.close(); PdfUtil.closeQuietly(reader); } }逻辑说明getOverContent表示在页面内容之上绘制公章会盖住正文如果想让正文盖住公章用getUnderContent。坐标计算里pageSize.getWidth()是页面宽度减去公章宽度和右边距得到 x 坐标。常见做法是把公章位置做成配置项因为不同模板的留白区域不一样写死坐标换个模板就翻车。3.3 斜字水印旋转角度与透明度控制水印要斜着铺满页面核心是PdfGState控制透明度配合showTextAligned的旋转参数。下面代码在每页中央打一个 45 度倾斜的灰色水印public class PdfWatermarker { public void addWatermark(String srcPdf, String outputPdf, String watermarkText) throws Exception { PdfReader reader new PdfReader(srcPdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(outputPdf)); BaseFont bf PdfUtil.loadChineseFont(); // 水印字体 40pt颜色浅灰 Font font new Font(bf, 40, Font.NORMAL, new BaseColor(200, 200, 200)); // 透明度设置0.3 表示 30% 不透明 PdfGState gs new PdfGState(); gs.setFillOpacity(0.3f); int totalPages reader.getNumberOfPages(); for (int i 1; i totalPages; i) { PdfContentByte content stamper.getOverContent(i); content.saveState(); content.setGState(gs); // 在页面中心绘制旋转 45 度 Rectangle pageSize reader.getPageSize(i); content.beginText(); content.setFontAndSize(bf, 40); content.setColorFill(new BaseColor(200, 200, 200)); // 参数旋转角度、x、y、对齐方式、文本 content.showTextAligned(Element.ALIGN_CENTER, watermarkText, pageSize.getWidth() / 2, pageSize.getHeight() / 2, 45); content.endText(); content.restoreState(); } stamper.close(); PdfUtil.closeQuietly(reader); } }参数说明setFillOpacity(0.3f)控制填充透明度值越小水印越淡0.3 是既能看清又不遮挡正文的常用值。showTextAligned的最后一个参数 45 是旋转角度单位是度正数逆时针旋转。saveState和restoreState成对出现避免透明度设置污染后续绘制操作。注意水印文字如果太长会超出页面边界常见做法是根据页面宽度动态调整字号。3.4 文本替换为什么不能直接改以及正确做法PDF 里的文本不是按字符串存储的而是按字符定位绘制的所以没有“查找替换”这种直接操作。这个项目的做法是用PdfContentStreamEditor拦截内容流在绘制指令层面替换。下面是一个简化实现public class PdfTextReplacer { public void replaceText(String srcPdf, String outputPdf, String oldText, String newText) throws Exception { PdfReader reader new PdfReader(srcPdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(outputPdf)); int totalPages reader.getNumberOfPages(); for (int i 1; i totalPages; i) { // 获取页面内容字节流 byte[] pageContent reader.getPageContent(i); PdfContentStreamEditor editor new PdfContentStreamEditor() { Override protected void write(PdfContentStreamProcessor processor, PdfLiteral operator) throws IOException { // 拦截文本绘制指令做字符串替换 if (Tj.equals(operator.toString())) { // 实际项目中这里需要解析操作数并替换 // 简化处理直接替换字节流中的文本 } super.write(processor, operator); } }; // 重新写入页面内容 PdfDictionary pageDict reader.getPageN(i); editor.editPage(stamper, pageDict); } stamper.close(); PdfUtil.closeQuietly(reader); } }逻辑说明PDF 内容流是一系列操作符和操作数文本绘制用Tj或TJ操作符。替换的难点在于文本可能被拆成多个片段且字体编码可能不是 Unicode。常见做法是先用PdfTextExtractor提取文本确认内容再针对特定模板做精确替换而不是做通用替换。如果替换后字体不对说明新文本的字体和原文本不一致需要显式设置字体。4. 避坑与排查四个真实踩过的坑4.1 中文显示为方块或乱码现象生成的 PDF 里中文全是方块或者显示成问号。原因没有嵌入中文字体或者用了BaseFont.WINANSI编码。解决用BaseFont.IDENTITY_H编码并且设置BaseFont.EMBEDDED把字体嵌入文档。如果用的是FontFactory.getFont(SimSun)在没装该字体的服务器上会失败改成从resources加载字体文件。4.2 签章位置偏移现象本地测试公章位置正确部署到服务器后位置偏了。原因不同 PDF 模板的页面尺寸和旋转角度不同getPageSize返回的是未旋转的尺寸。解决用reader.getPageSizeWithRotation(pageNum)获取实际显示尺寸并且把公章坐标做成相对于页面宽高的比例值而不是绝对点数。4.3 批量处理时文件句柄泄漏现象处理几百个 PDF 后程序报“Too many open files”。原因PdfReader没有关闭或者PdfStamper关闭时抛异常导致reader没关。解决用try-finally确保PdfReader一定关闭或者用PdfUtil.closeQuietly封装。注意PdfStamper.close()会自动关闭关联的PdfReader但异常情况下不会。4.4 文本替换后字体错乱现象替换后的文本字体和周围不一致或者字符间距异常。原因PDF 内容流里的文本绘制指令绑定了字体资源直接替换字符串不会更新字体引用。解决替换时同时设置字体和字号用setFontAndSize显式指定并且确保新文本的字符编码和原字体兼容。如果原文本用了子集嵌入字体替换的字符可能不在子集里需要换用完整字体。5. 二次开发与改造从工具类到业务系统的落地技巧拿到这份源码后直接跑通四个功能不难难的是把它接进真实业务系统。我一般会做三件事抽接口、加配置、补日志。先说抽接口。源码里的PdfCreator、PdfSigner这些类都是具体实现业务代码直接依赖它们会导致换 PDF 库时改不动。常见做法是定义一个PdfService接口把创建、签章、水印、替换四个方法抽象出来iText 实现作为一个ItextPdfServiceImpl。这样以后想换 Apache PDFBox只需要加一个新实现类。public interface PdfService { // 创建 PDF返回文件路径 String createPdf(PdfCreateRequest request); // 签章支持多页签章 void signPdf(String srcPath, String stampPath, String outputPath); // 加水印 void addWatermark(String srcPath, String outputPath, String text); // 文本替换 void replaceText(String srcPath, String outputPath, String oldText, String newText); }再说加配置。公章位置、水印透明度、字体路径这些参数不要写死在代码里。我一般会用一个pdf-config.yml管理用 Spring 的ConfigurationProperties注入。比如水印透明度测试环境要 0.5 看得清生产环境要 0.2 不干扰阅读配置化之后改一行就行。pdf: watermark: opacity: 0.3 font-size: 40 rotation: 45 sign: stamp-width: 120 stamp-height: 120 margin-right: 50 margin-bottom: 80 font: path: fonts/simsun.ttc index: 0最后说补日志。PDF 操作失败时异常信息往往很模糊比如“文档损坏”可能是模板问题也可能是读取流被提前关闭。我习惯在关键步骤打日志读取页数、签章坐标、水印页数、替换命中次数。这样出问题时能快速定位是哪个环节。还有一个改造点是异步化。批量生成 PDF 是 CPU 密集型操作放在 HTTP 请求线程里会拖慢响应。常见做法是把 PDF 生成任务丢到线程池接口返回任务 ID前端轮询进度。注意PdfReader和PdfWriter不是线程安全的每个任务要独立创建实例不要复用。验证改造是否成功我一般会写一个对比测试用同一份模板分别走改造前和改造后的代码对比生成的 PDF 字节数、页数、文本提取结果。如果字节数差异超过 5%说明内容有变化需要排查。这个习惯是从一次血泪经验来的——当时改完签章逻辑测试说“看起来没问题”结果上线后发现公章盖在了第二页因为模板第一页是封面没有正文。从那以后我每次改 PDF 相关代码都强制走一遍“提取文本 对比页数 检查签章坐标”的验证流程。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站