资讯详情

资讯详情

iText实战:Java生成PDF、斜水印、文本替换与签章全流程

简介使用iText操作PDF文档的完整示例工程面向有一定Java基础、需要在项目中处理PDF的开发者重点解决PDF创建、电子签章、斜字水印、文本替换四类高频需求。整个工程按Eclipse项目组织共包含32个文件整个压缩包大小为8.77MB其中7个Java源文件与9个class文件构成核心示例代码5个jar包提供itextpdf、bcprov等依赖库另有p12证书用于数字签名png/jpg作为图片素材prefs/project/classpath便于直接导入IDEreadme.txt给出使用指引。当前已有1571人学习下载。源码覆盖PdfWriter生成文档、PdfStamper签名与替换文本、PdfFormXObject绘制旋转水印等关键操作并给出证书设置、中文字体、文件体积控制等实用建议可帮助开发者快速迁移到真实业务减少踩坑成本。示例中还针对大型文档分批处理、避免内存溢出等实践做了说明。无论是生成报表、合同签署还是批量加水印都能直接参考这套实现。1. 一次跑通 iText 的四个硬需求建 PDF、盖印章、斜水印、替换文本我在上家做结算系统时每周要批量生成带水印的对账单 PDF还要把合同模板里的旧乙方名称替换成新主体最后财务又要求盖签章。那阵子把 iText 相关的帖子翻了个遍发现资料全是碎片讲创建的不讲替换讲水印的不讲签章真正动手时连版本 API 都对不上。这篇按生产环境的落地顺序把四件事串成一条流程用 iText 创建 PDF、文本替换、斜字水印、数字签章每步给出可以直接粘进项目跑的 Java 代码参数设成什么样、什么地方容易翻车都写在旁边。适合用 Java 写后端服务的读者做报表、合同、票务类文档生成基本都能落到这四个动作上。希望你看完能直接在业务里复现而不是再对着搜索引擎熬一宿。2. 用 iText 创建 PDF版本选型、依赖与第一个中文文档2.1 Maven 依赖里 iText 5 和 iText 7 怎么选包名为什么总对不上先用 Maven 把依赖请进来。iText 从 7.0 开始把模块拆成了 kernel、layout、io、pdfa 等一堆日常做创建和编辑直接用聚合包itext7-core最省事dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.2.x/version typepom/type /dependency版本号里的x建议用你本地 Maven 仓库能拉到的最新稳定版不要长期锁一个过于老的版本。很多人照着老博客写代码报错说找不到com.itextpdf.text.Document就是因为那是 iText 5 的包名iText 7 里内核对象几乎全搬到了com.itextpdf.kernel.*和com.itextpdf.layout.*。我一般建议新项目直接上 7理由有三点一是 7 对 PDF/A、PDF/UA 这类标准化格式支持更完整二是它的底层对象模型比 5 清晰调试时能直接看到页面和资源对象三是 7 还在持续维护再往后做签章、替换这类操作时资料也更接近当前版本。如果你们项目是存量代码那先确认下现在用的包名再决定迁移节奏避免顺手把依赖升上去然后一夜之间编译错几百处。2.2 第一个中文创档程序PdfWriter、PdfDocument 与 Document 的最小闭环下面这段就是生成demo.pdf的最小闭环我特意把中文字体加载也放进来了因为不设置字体的 PDF 一秒变“方块文”。import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.PdfEncodings; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.Paragraph; import java.io.FileOutputStream; public class CreatePdfDemo { public static void main(String[] args) throws Exception { String outputPath ./demo.pdf; PdfWriter writer new PdfWriter(new FileOutputStream(outputPath)); PdfDocument pdfDoc new PdfDocument(writer); Document document new Document(pdfDoc); // 中文字体优先从系统字体目录加载避免 iText 默认字体缺 CJK 字形 PdfFont chineseFont PdfFontFactory.createFont( /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED); document.setFont(chineseFont); document.add(new Paragraph(第一个 iText 生成的 PDF 文件)); document.add(new Paragraph(这一行是中文内容用来确认字体生效。)); document.close(); } }逻辑上这条链子是PdfWriter负责真正向文件流里写字节PdfDocument是内存里的 PDF 对象树Document是高层布局入口你往里add的Paragraph会被排版成页面内容。document.close()必须放在最后它会触发页面销毁、对象写入和资源释放。参数上有几个值得说PdfEncodings.IDENTITY_H表示按 Unicode 字面值写入不乱转码EmbeddingStrategy.PREFER_NOT_EMBEDDED是“能不全量嵌入就不嵌入”生成的 PDF 体积小但换一台没有这个字体的机器打开就会缺字形。生产环境我更推荐把 Noto、思源黑体这类开源字体跟服务一起打包改成PREFER_EMBEDDED强制嵌入虽然包大几百 KB但换机器不乱码这个代价值得。2.3 用 Table 把内容排成合同骨架为后面替换和签章留出位置报表、合同、结算单这类文档光靠段落是不够的用 Table 把字段和值排成两列才是真实业务形态import com.itextpdf.layout.element.Table; import com.itextpdf.layout.element.Cell; import com.itextpdf.layout.properties.UnitValue; Table table new Table(2); table.setWidth(UnitValue.createPercentValue(100)); table.addCell(new Cell().add(new Paragraph(客户名称)).setBold()); table.addCell(new Cell().add(new Paragraph(北京某某科技有限公司))); table.addCell(new Cell().add(new Paragraph(结算金额)).setBold()); table.addCell(new Cell().add(new Paragraph(¥12,800.00))); table.setHeaderRows(1); // 如果表格跨页表头会重复出现 document.add(table);new Table(2)的 2 是列数列宽默认均分setWidth按百分比撑满页面宽度后续调整只需改一个比例比写死 pt 值好用得多。setHeaderRows(1)特别适合跨页报表它让第一行在分页后自动重现在下一页顶部省去手工补表头的蠢操作。这里先留心一件事Table 的行数超过当前页剩余高度时iText 会自动把内容流到下一页。这个“分页”行为也是后面水印必须按页面事件处理的原因——你不能在 add 完所有内容后再画水印因为那时你已经不知道每一页真正的边界在哪。3. 斜字水印用页面结束回调自动盖到每一页3.1 为什么“加个水印”实现上要挂在页面结束事件上而不是画完内容再补水印最常见的做法是在每页内容渲染完成之后、页面关闭之前追加一段透明文字。听起来简单但如果你在Document.add(...)全部执行完后再拿 PdfCanvas 去画水印只会画在最后一个“当前页”上前面几页一个都没有。正确姿势是给PdfDocument注册一个事件处理器监听END_PAGE事件每当一页渲染完回调马上执行画完水印再翻篇。斜字水印为什么是“斜”的也在这个环节体现出意义。旋转 30 到 45 度之后水印与正文形成交叉肉眼很容易辨识复印扫描后也不容易被裁剪掉一块就看不出来。这是内容版权保护场景里最常见的视觉策略不是审美玄学。3.2 用 PdfPageEvent 思路实现水印处理器旋转角、透明度、字体一次配齐iText 7 里事件处理器实现IEventHandler接口核心代码放在handleEventimport com.itextpdf.kernel.events.IEventHandler; import com.itextpdf.kernel.events.Event; import com.itextpdf.kernel.events.PdfDocumentEvent; import com.itextpdf.kernel.pdf.PdfPage; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.kernel.pdf.extgstate.PdfExtGState; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.font.PdfFont; public class WatermarkEventHandler implements IEventHandler { private final String text; private final float angleDegrees; private final float alpha; private final PdfFont font; public WatermarkEventHandler(String text, float angleDegrees, float alpha, PdfFont font) { this.text text; this.angleDegrees angleDegrees; this.alpha alpha; this.font font; } Override public void handleEvent(Event event) { PdfDocumentEvent docEvent (PdfDocumentEvent) event; PdfPage page docEvent.getPage(); PdfCanvas canvas new PdfCanvas(page); canvas.saveState(); float radians (float) Math.toRadians(angleDegrees); canvas.setExtGState(new PdfExtGState().setFillOpacity(alpha)); canvas.setFillColor(ColorConstants.BLACK); canvas.beginText(); canvas.setTextMatrix( (float) Math.cos(radians), (float) Math.sin(radians), (float) -Math.sin(radians), (float) Math.cos(radians), page.getPageSize().getWidth() / 2 - 120, page.getPageSize().getHeight() / 2); canvas.setFontAndSize(font, 48); canvas.showText(text); canvas.endText(); canvas.restoreState(); } }在生成新文档时把它挂上事件链就行pdfDoc.addEventHandler(PdfDocumentEvent.END_PAGE, new WatermarkEventHandler(内部资料, 45, 0.25f, chineseFont));逻辑上setTextMatrix的前四个参数就是旋转矩阵的cos/sin/-sin/cos后两个是平移坐标alpha控制透明度0.25f表示 25% 不透明度既能看到又不会盖住正文。坐标中心取getPageSize()的一半但水印文字本身也有宽度后面参数部分再讲怎么防截断。另一个高频场景是给存量 PDF 加水印。思路不是挂事件因为文档已经生成了而是自己遍历每一页try (PdfDocument pdfDoc new PdfDocument(new PdfReader(src), new PdfWriter(dest))) { for (int i 1; i pdfDoc.getNumberOfPages(); i) { PdfPage page pdfDoc.getPage(i); PdfCanvas canvas new PdfCanvas(page); // 把上面 beginText 到 endText 那段抽成一个方法循环调用 drawWatermark(canvas, page, 内部资料, 45, 0.25f, chineseFont); } }注意这个场景里new PdfWriter(dest)会生成一个新文件不要让它覆盖原文件否则中途报错时原始文件就没了到时候连后悔药都没得吃。3.3 斜字水印的四组参数角度、透明度、字号和密度到底调到多少角度我一般用 45这是最经典的斜水印角度计算也方便。如果内容页有横向表格45 度可能压住关键数据那就改成 30视觉效果依然明显但遮字少。透明度里 0.2 到 0.3 是个安全区间。低于 0.1打印出来几乎看不见高于 0.5正文阅读会明显受阻。如果你水印文字和正文同色还会产生灰阶融合所以颜色上建议统一用黑或深灰。字号要看页面大小。A4 页面单行水印用 40 到 60 比较常见如果公司要求“多行多列文字水印”比如每页要铺 3 行 3 列那字号降到 20 到 24 更合适不然九个大字糊满全页。铺多行多列时多做一层循环for (int row 0; row 3; row) { for (int col 0; col 3; col) { float x page.getPageSize().getWidth() / 3 * col 20; float y page.getPageSize().getHeight() / 3 * row 20; canvas.beginText() .setFontAndSize(font, 24) .setTextMatrix(cos, sin, -sin, cos, x, y) .showText(text) .endText(); } }每一格的起点坐标别从边界 0 开始留出 20 到 30 的点间距避免水印紧贴裁切边。算坐标时还要考虑文字旋转后的包围盒长度文本越长中心点就越要向页面中心回缩否则右上角那一两个水印常被切掉一半。4. 文本替换定位旧文本、重绘新文本的两种落地法4.1 为什么 PDF 文本替换容易翻车这跟业务系统里替换字符串完全是两码事很多从 Word 开发转过来的人第一次做 PDF 文本替换就懵明明 PDF 里能看到“乙方法人”代码提取出来一段乱码或者替换后位置错乱。原因在 PDF 的结构模型上。PDF 页面内容不是“文字块”组成的而是一连串文本绘制指令什么字体、什么字号、在哪个坐标画什么字符彼此之间没有段落概念。尤其当原始 PDF 由某些工具生成时字符序列还被压缩过甚至字体子集把部分字形单独抽走。你说“替换字符串”对 PDF 来说只是“在图像内容流里找一段绘制指令”这天然不稳定。所以做文本替换前先定策略。如果你的模板是自己用 iText 或 Word 生成的我强烈建议不要走“搜索旧文本再替换”的路而是用占位符方案生成模板时就把要变的字段用固定位置矩形记下来替换时直接往矩形区域写新内容。这绕过了 80% 的文本提取问题也是生产里最稳的做法。4.2 基于占位符区域的替换一个矩形搞定乙方名称、金额、日期下面这段代码处理“模板里预留好的空白位置”——比如一个月结算单模板乙方名称那一栏是固定区域你只需替换这个矩形里的内容import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.pdf.PdfEncodings; import com.itextpdf.kernel.geom.Rectangle; public void replaceSlot(String srcPath, String destPath, int pageNo, Rectangle slotRect, String fontPath, String newText) throws Exception { try (PdfDocument pdf new PdfDocument(new PdfReader(srcPath), new PdfWriter(destPath))) { PdfCanvas canvas new PdfCanvas(pdf.getPage(pageNo)); // 第一步用底色盖住旧文本旧文本如果是白底黑字就用白色 canvas.saveState(); canvas.setFillColor(ColorConstants.WHITE); canvas.rectangle(slotRect.getX(), slotRect.getY(), slotRect.getWidth(), slotRect.getHeight()); canvas.fill(); canvas.restoreState(); // 第二步在区域内重绘新文本 PdfFont font PdfFontFactory.createFont(fontPath, PdfEncodings.IDENTITY_H); float fontSize 12f; canvas.saveState(); canvas.setFillColor(ColorConstants.BLACK); canvas.beginText() .setFontAndSize(font, fontSize) .setTextMatrix(slotRect.getX() 4, slotRect.getY() 2) .showText(newText) .endText(); canvas.restoreState(); } catch (Exception e) { // 建议把 srcPath 和 pageNo 打到日志里替换失败时方便回溯 throw new RuntimeException(替换PDF文本失败: page pageNo, e); } }逻辑上这段代码先画了一个白色矩形遮住旧内容再在同一个位置用新字体写字。白色矩形的坐标你从哪拿我一般先输出一版带坐标的 PDF用面积标注工具或者直接拿阅读器量一下文字边缘位置填进Rectangle就行。fontSize不是硬编码就完事。如果newText比旧文本长固定字号会把文字挤出矩形右边界。稳妥做法是按矩形宽度估算字号float estimatedSize Math.min(12f, slotRect.getWidth() / newText.length() * 1.2f);这个估算公式不严谨但够用1.2f是中文汉字宽度的经验系数。英文数字可以降到0.7f。4.3 如果非要全文搜索替换先拿坐标块再精准重绘占位符方案的前提是你会改模板。第三方给的 PDF 没法预留矩形时就得做全文定位。iText 7 里可以用LocationTextExtractionStrategy拿到文本块坐标再结合重绘逻辑实现“搜索式替换”import com.itextpdf.kernel.pdf.canvas.parser.listener.LocationTextExtractionStrategy; import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor; LocationTextExtractionStrategy strategy new LocationTextExtractionStrategy(); PdfTextExtractor.getTextFromPage(page, strategy); for (TextChunk chunk : strategy.getTextChunks()) { if (chunk.getText().contains(旧公司名)) { // chunk.getStartLocation() 拿到 Vector(x, y, z)z 忽略 float x chunk.getStartLocation().get(Vector.I1); float y chunk.getStartLocation().get(Vector.I2); // 在这里调用上面 replaceSlot 的第二步逻辑 } }文本块TextChunk可能不是一个完整的词或句子PDF 内容流经常把一个词拆成多个块。所以你大概率要做邻近块聚合把坐标接近、同一行的块拼起来再比对。这一步没有银弹属于“拿到原始坐标后自己拼业务逻辑”的活。对比三条路的取舍占位符替换最稳适合自己生成的模板成本低全文搜索替换适合一次性处理第三方文档但要处理分块、间距、字体偏差人力成本高还有一种“看到哪里改哪里”的重绘法只适合少量页面的临时微调不适合大批量业务。我通常第一选择永远是占位符全文搜索替换只当作救火方案。5. iText 避坑手记资源回收、中文乱码、坐标偏移与签名失效5.1 数据量大的时候回收资源报错服务直接 OutOfMemory现象高并发批量生成 PDF比如每天上万份对账单跑一段时间后 JVM 报内存溢出或者日志里反复出现PdfDocument has not been closed之类的警告。原因最常见的是Document或PdfDocument没有在 finally 里关闭。另一个隐蔽原因是有人把 PDF 直接写进ByteArrayOutputStream再整体转 byte[]批量场景下几百份大 PDF 同时挤在堆内存里GC 根本来不及回收。解决所有 PDF 创建和编辑操作都放 try-with-resources 里先关Document再关PdfDocument顺序颠倒在某些版本里会提示文档未正确结束。输出目标用临时文件而不是内存流落盘完成后再读文件内容。如果 JVM 内存实在紧张控制并发线程数比如用一个固定线程池把生成任务限流。// 推荐写法三个资源按依赖顺序一次性声明 try (PdfWriter writer new PdfWriter(tempFile); PdfDocument pdf new PdfDocument(writer); Document doc new Document(pdf)) { // 业务逻辑 }这里的窍门是tempFile用File.createTempFile生成处理完再删避免一堆临时垃圾堆在磁盘上。5.2 文本替换后中文变成豆腐块新内容全是方框现象替换出来的中文字符在阅读器里显示成一个个空心方块英文数字正常。原因重绘时用的字体不是中文字体。iText 的默认字体基线通常是 Helvetica 或内置标准字体它们只有拉丁字符集没有 CJK 字形。解决替换逻辑里必须显式加载中文字体并且最好用嵌入方式。用系统字体目录的 Noto Sans CJK 时同时设置PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED保证换一台机器打开也不丢字形。这里有个细节替换用字体最好和原 PDF 所用字体接近否则新旧文字视觉风格明显不一致财务一眼就挑出毛病。5.3 签章或水印坐标对不上笔刷位置和阅读器显示完全两回事现象代码里写印章坐标x100, y100打开 PDF 发现印章跑到了页面左边或偏下跟肉眼预期差很多。原因PDF 坐标系原点在页面左下角x 向右、y 向上而很多人在页面预览工具里看到的坐标是“左上角为原点”。更麻烦的是部分 PDF 页面带旋转属性页面宽高在视觉上和getPageSize()返回的宽高不一致。解决写一个统一坐标转换工具先读page.getRotation()如果旋转了用page.getPageSizeWithRotation()拿旋转后的实际页面尺寸再去算坐标。凡是涉及“左上角量距离”的场景先把目标点转成左下角坐标系。这个坑我踩过不止一次后来强制要求所有签章相关代码先打印一遍页面旋转角再动手。5.4 水印文字被页面边缘裁掉最后一列水印总是一半现象多行多列水印铺完后页面右上角的水印文字明显不完整。原因文字中心点直接按页面宽高的一半算但水印本身有宽度和高度旋转 45 度后包围盒更大边缘部分超出页面裁切区。解决铺水印前先估算单行文本在目标字号下的宽度用font.getWidth(text, fontSize)拿到长度再根据旋转角算出横向和纵向投影把中心点往页面中心方向回缩。经验公式是横向偏移至少减去width * cos(θ) height * sin(θ)的一半这样边缘就不会切字。5.5 签名弄完后再被改一版打开提示“文档已被更改签名无效”现象业务部门先盖了电子章后边又想改一个金额开发直接拿 iText 再打开 PDF 做了文本替换结果阅读器里提示签名失效。原因盖章后的 PDF 一旦被后续编辑增量更新区里保存的签名摘要与修改后的内容不匹配服务端校验直接判无效。解决把签章放在整条处理链的最后一步。所有文本替换、加水印、内容调整必须发生在盖章之前程序上强制规定已签名的文件绝不作为输入再次执行写入操作。如果业务非要改就让流程重新走一遍生成、替换、水印、章。6. 签章与验真盖章图片的坐标技巧和验证手段先说清楚一个概念贴图章和数字签名不是一回事。直接把 PNG 印章图片画到 PDF 上解决的是“视觉上有章”并不具备防篡改能力。真要让签章具备法律效力得用证书走PdfSigner做增量签名。但绝大多数企业内部流程第一步需要的是“盖章效果”下面这段就能用import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.kernel.pdf.xobject.PdfImageXObject; import com.itextpdf.io.image.ImageData; import com.itextpdf.io.image.ImageDataFactory; try (PdfDocument pdf new PdfDocument(new PdfReader(src), new PdfWriter(dest))) { PdfCanvas canvas new PdfCanvas(pdf.getPage(1)); ImageData stampImage ImageDataFactory.create(stamp.png); float width 100f; float height 100f; canvas.addImageAt(stampImage, x, y, width, height, false); }这里的x, y是印章左下角坐标不是图片中心落章前先按 5.3 的旋转规则换算。印章图片我建议用透明背景 PNG红色圆章四周的留白如果是不透明白底盖上去像贴了一块创可贴。图片宽高要等比例传别硬压否则椭圆章变畸变。验证环节分两层。第一层是人工验证用阅读器打开 PDF点一下印章区域查看注释属性如果只是图片章阅读器显示的是图片对象信息。第二层是代码验证对于真正的数字签名可以用SignatureUtil枚举全文档签名再逐个检查摘要匹配。我一般会做一次双重验证先程序输出“签名个数、是否有签名”再让业务同事在 Adobe Reader 打开确认不报“文档已被更改”。如果代码校验过了但阅读器仍然报错通常就是字体嵌入或增量更新顺序出了问题。做签章最怕的是流程顺序错了先盖了章再改内容最后所有验证一起崩。我的习惯是签章永远排在文档产线的最末尾水印、替换、排版全部结束才允许调用签章逻辑。顺序守住了这套 iText 流程才算真正能交给业务长期跑。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →