尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java后端必看:itext操作PDF全攻略,水印签章文本替换一次讲透

Java后端必看:itext操作PDF全攻略,水印签章文本替换一次讲透 简介这是一份面向Java开发者的PDF处理工具源码包基于iText库实现文档创建、电子签章、斜字水印、文本替换等高频操作能解决实际项目中的PDF生成与编辑难题。包内共32个文件含7个Java源文件与9个class文件便于阅读与调试5个jar包覆盖iText核心库及BouncyCastle安全扩展另有p12数字证书用于签章验签、png/jpg图片作水印素材并附项目配置与readme说明整体约8.77MB目录结构清晰便于查找。已有1571人学习下载适合具备Java基础、想快速上手PDF处理的开发者参考。借助示例工程可掌握PdfWriter创建文档、PdfStamper设置签名、PdfFormXObject绘制斜字水印及AcroFields替换文本等关键API同时理解大文件处理与版本兼容性的注意事项为正式项目提供可复用的代码模板。1. itext操作PDF文档从创建到签章水印这份资源到底能解决什么itext操作PDF文档第一次碰的人往往会被坐标系和内容流整懵。我当初接手合同批处理项目要在几百个PDF上加斜字水印、替换甲方名称、再盖电子公章试了好几个库最后还是落到itext上。这份资源把创建PDF、斜字水印、文本替换、电子签章四个场景拆成可以直接跑的Java示例每段带注释和参数说明适合刚接触itext的Java后端也适合要把PDF批处理功能塞进现有系统的同学。看完你能分清哪些操作是itext的本职哪些是边缘试探省掉翻文档和反复试错的时间。这里说的签章是图片印章不是数字证书签名的复杂玩法覆盖最常见的企业盖章需求。2. itext创建PDF文档搭建最小项目与表格布局这一章解决“从零生成PDF并排布内容”的问题。很多人上来就写Document却不知道页面大小、边距、字体选型会影响后续所有操作。先讲选型理由再给最小可运行代码最后用表格说明参数怎么调。2.1 为什么选itext三个理由和一个限制在Java生态里做PDF生成itext几乎是默认选项。第一它直接暴露PDF底层对象PdfContentByte、PdfStamper都能拿到手做起定制操作来比PDFBox直接拼内容流要顺手第二它内置了中文字体方案通过itext-asian包配合BaseFont能输出中文不依赖服务器系统字体第三它适合无界面服务端环境很多报表导出、合同生成都是它在撑。限制也明显itext分AGPL协议和商业授权内部工具用AGPL没问题但要分清楚是itext5还是itext7两者包名完全不同代码不能混用。我优先用itext5的com.lowagie.text包因为老项目沉淀多网上示例九成都是这个遇到问题容易搜到。itext7改成了com.itextpdf.kernel等新包API更现代但迁移成本高除非全新项目且有人接力否则我不主动选。这份资源里的代码全部基于itext5pom里引入依赖就能跑。2.2 创建PDF的最小可运行代码先看pom依赖dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency dependency groupIdcom.itextpdf/groupId artifactIditext-asian/artifactId version5.2.0/version /dependencyitextpdf是主体itext-asian提供中文字体。我一般还会带上org.slf4j:slf4j-simple因为itext5的日志走自己的logger没有绑定会在控制台输出一堆警告不影响运行但看着烦。然后是创建文档的完整代码import com.lowagie.text.Document; import com.lowagie.text.PageSize; import com.lowagie.text.Paragraph; import com.lowagie.text.Font; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.PdfWriter; import java.io.FileOutputStream; public class CreateDemo { public static void main(String[] args) throws Exception { // 1. 创建Document对象A4页面左右边距54磅约1.9cm上下36磅约1.27cm Document document new Document(PageSize.A4, 54, 54, 36, 36); // 2. 绑定PdfWriter输出到文件 PdfWriter.getInstance(document, new FileOutputStream(create_demo.pdf)); // 3. 打开文档 document.open(); // 4. 注册中文字体STSong-Light UniGB-UCS2-H BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font font new Font(bf, 12, Font.NORMAL); // 5. 添加段落 document.add(new Paragraph(你好itext创建PDF成功。, font)); document.add(new Paragraph(第二行验证分段效果。, font)); // 6. 关闭 document.close(); } }逻辑说明Document是画布PdfWriter负责把画布写入文件。构造参数顺序是页面宽、高、左、右、上、下边距单位是磅point1磅1/72英寸。BaseFont.createFont的第三入参NOT_EMBEDDED表示不嵌入字体生成的文件体积小但换机器打开可能显示异常要可移植就改成BaseFont.EMBEDDED。参数说明A4宽度595磅高度842磅。如果你用毫米录入记得换算公式磅 毫米 * 72 / 25.4。这个换算在签章定位时会反复用。STSong-Light是Adobe的中文字体名配合UniGB-UCS2-H编码能处理简体中文繁体场景把编码换成UniCNS-UCS2-H不然会有缺字问题。如果你直接跑这段代码可能会遇到NoClassDefFoundError: org/slf4j/LoggerFactory那就是缺slf4j。在pom里加slf4j-simple依赖即可。这个依赖问题新手几乎必遇属于itext5的经典玄学。2.3 表格与段落让内容不再是白纸黑字实际业务里创建PDF基本都要带表格。PdfPTable是itext5里最常用的表格类给它添加单元格、设置列宽比例比用绝对坐标排版省心得多。下面是一个带表头、合并单元格的示例import com.lowagie.text.pdf.PdfPTable; import com.lowagie.text.pdf.PdfPCell; import com.lowagie.text.Element; // 在document.open()之后创建表格5列 PdfPTable table new PdfPTable(5); table.setWidthPercentage(100); // 表格占页面宽度 table.setSpacingBefore(10); // 表格前间距 table.setSpacingAfter(10); // 表格后间距 // 设置列宽比例 table.setWidths(new float[]{1, 2, 2, 2, 1}); // 表头单元格 PdfPCell header new PdfPCell(new Paragraph(序号, font)); header.setBackgroundColor(BaseColor.LIGHT_GRAY); header.setHorizontalAlignment(Element.ALIGN_CENTER); table.addCell(header); table.addCell(new PdfPCell(new Paragraph(名称, font))); // 合并单元格 PdfPCell merged new PdfPCell(new Paragraph(跨列合并, font)); merged.setColspan(3); merged.setHorizontalAlignment(Element.ALIGN_CENTER); table.addCell(merged); // 普通数据行 table.addCell(1); table.addCell(合同编号); table.addCell(2025-001); table.addCell(甲方); table.addCell(已盖章); document.add(table);逻辑说明PdfPTable列数在构造时确定之后addCell按行填充。setWidths接收各列宽度比例数组长度必须等于列数否则抛异常。setColspan合并跨列的单元格。背景色BaseColor是itext自己的RGB类别混用java.awt.Color虽然有些情况能跑但那是兼容性玄学。参数说明setWidthPercentage(100)让表格自适应页宽。如果同时调用setTotalWidth指定固定宽度百分比会失效二者只能选一个。单元格默认垂直居中水平靠左要居中必须显式setHorizontalAlignment。很多人创建PDF后会遇到“文字没显示全”的问题因为Paragraph默认行距是字号的1.5倍段落很长时自动换行不会自动分页需要手动document.newPage()。itext的分页策略是当前Y坐标小于下边距时自动新页但表格不会自动拆分表头。如果表格要跨页且每页重复表头就给表头行调用setHeaderRows(1)合同、发票这类多页文档尤其常用。3. 斜字水印实现旋转、透明度与多行多列铺满页面水印是PDF操作里最常见的需求斜字水印尤其多用于合同和内部资料。这一章先讲原理再给多行多列的实现代码最后说参数怎么调。3.1 斜字水印的原理PdfContentByte与图形状态PDF的内容不是图层而是一串绘制指令。itext通过PdfContentByte往页面内容流里追加指令。水印可以放在页面内容之上over或之下under对应PdfWriter的getOverContent(pageNum)和getUnderContent(pageNum)。斜字效果的关键是修改文本矩阵通过setTextMatrix的旋转矩阵实现。先用最原始的方式写一行文字不旋转PdfContentByte cb writer.getDirectContentUnder(); // 底层内容 cb.saveState(); cb.beginText(); cb.setFontAndSize(bf, 40); cb.setTextMatrix(30, 400); // 平移到坐标(30,400) cb.showText(内部资料); cb.endText(); cb.restoreState();这里bf是已创建的BaseFont。运行后能看到一行水平文字。注意beginText和endText之间的所有操作构成一个文本对象saveState/restoreState保护上下文避免影响后续绘制。旋转是setTextMatrix的六个参数(a, b, c, d, e, f)。其中a,b,c,d决定旋转和缩放e,f是平移坐标。要让文字逆时针旋转角度alpha四个值分别是cos(alpha), sin(alpha), -sin(alpha), cos(alpha)。旋转中心不是文字的左上角而是(e,f)这个基点所以多行水印循环时每个基点的位置都要算好。3.2 多行多列斜字水印的代码实现常见做法是把页面切成网格在每个格子里画同一行斜字。页面尺寸和边距要拿准否则水印会贴边或被截断。下面是完整代码适合对已有PDF批量加斜字水印import com.lowagie.text.pdf.PdfContentByte; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.PdfStamper; import com.lowagie.text.pdf.PdfReader; import com.lowagie.text.pdf.PdfGState; public static void addWatermark(PdfReader reader, String outPath, String text, int fontSize, float angleDeg, float opacity) throws Exception { int pageCount reader.getNumberOfPages(); // 取第一页尺寸假设所有页大小一致不一致时需要逐页取 com.lowagie.text.Rectangle pageSize reader.getPageSize(1); float pageW pageSize.getWidth(); float pageH pageSize.getHeight(); PdfStamper stamper new PdfStamper(reader, new java.io.FileOutputStream(outPath)); BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); float alpha (float) Math.toRadians(angleDeg); float cosA (float) Math.cos(alpha); float sinA (float) Math.sin(alpha); // 网格间距按字号比例给可调 float stepX fontSize * 6f; float stepY fontSize * 4f; for (int page 1; page pageCount; page) { PdfContentByte over stamper.getOverContent(page); over.saveState(); if (opacity 1f) { PdfGState gs new PdfGState(); gs.setFillOpacity(opacity); gs.setStrokeOpacity(opacity); over.setGState(gs); } over.beginText(); over.setFontAndSize(bf, fontSize); // 网格循环 for (float y stepY; y pageH; y stepY) { for (float x stepX; x pageW; x stepX) { over.setTextMatrix(cosA, sinA, -sinA, cosA, x, y); over.showText(text); } } over.endText(); over.restoreState(); } stamper.close(); reader.close(); }逻辑说明PdfStamper用于修改已有PDFgetOverContent取顶层内容水印会压在正文上。循环从stepY和stepX开始是为了给页面边缘留出半个间距避免文字写出页外。setTextMatrix前四个参数决定旋转最后两个是基点坐标。每页画完restoreState避免页面间状态污染。参数说明stepX和stepY控制水印密度数值越小越密集。fontSize * 6f和fontSize * 4f是我测试下来视觉比较均匀的默认值。如果水印要防扫描复印把stepX降到fontSize * 3f即可但页面会显得很满。透明度用PdfGState设置注意setFillOpacity和setStrokeOpacity两个都要设不然打印时描边不透明水印会显得生硬。3.3 水印参数调优字号、旋转角度、透明度实际项目中这几个参数是最常改的参数常见值说明angleDeg30~45太直像牢房太斜阅读困难合同类一般30opacity0.3~0.5太淡扫描不见太浓影响阅读fontSize40~60大字用于警告小字用于编号stepX字号*6横向间距要密就*4stepY字号*4纵向间距要疏就*5角度不要低于15度容易和正文方向混淆超过60度时旋转包围盒变大文字容易跑到页面外。透明度低于0.2时复印基本扫不出来等于没加。另一个要注意的点如果原PDF是扫描件内容都是图片over水印会浮在图片上打印没问题但如果是已经电子签章的PDFover水印可能压住签章区域这时改用getUnderContent或者避开签章坐标。我的实践是文本替换、水印、签章的操作顺序固定为“先替换文本再加水印最后签章”这样每一步都不干扰前一步的内容层。4. 文本替换实操模板填充与内容修正的两种路径文本替换是itext里最容易被高估的功能。PDF没有Word那种“查找替换”底层支持这一章给出两条实际可用的路径表单域填充和内容区覆盖重写并说清边界。4.1 文本替换的三种思路及其边界思路一用表单域。如果PDF是用Acrobat做的带表单的情况PdfStamper可以直接设字段值正规且稳定。思路二用PdfCleanUp把目标区域内容擦掉再在同一位置写入新文本模拟替换。思路三直接解析内容流里的Tj和TJ操作符重写字符串对字体嵌入和编码要求极高非必要不用。思路二是通用性最强的也是这份资源推荐的做法。前提是你知道要替换的文字在页面上大概的位置和区域比如合同首页的“甲方名称张三”这一段。通常先用LocationTextExtractionStrategy提取文本定位但准确率受字体影响不是100%可靠。更稳的方法是在生成PDF时就在已知坐标的模板里留好占位符比如“甲方名称[甲方]”后面替换时直接擦掉占位区域写入真实值。如果用表单域模板填充代码最稳PdfReader reader new PdfReader(template.pdf); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(filled.pdf)); AcroFields fields stamper.getAcroFields(); fields.setField(contractName, 采购合同); fields.setField(partyA, 某科技有限公司); stamper.setFormFlattening(true); // 扁平化输出后没有可编辑域 stamper.close(); reader.close();这个方式的前提是你有带表单域的模板。没有的话用下面的区域覆盖方案。4.2 基于PdfCleanUp的字符替换代码PdfCleanUpProcessor从itext5.5.5开始可用它能把指定矩形区域的内容擦除。然后通过PdfStamper写入新文本。下面是代码import com.itextpdf.text.pdf.PdfReader; import com.itextpdf.text.pdf.PdfStamper; import com.itextpdf.text.pdf.parser.PdfCleanUpProcessor; import com.itextpdf.text.pdf.parser.PdfCleanUpLocation; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.BaseFont; import com.itextpdf.text.pdf.PdfContentByte; import java.util.ArrayList; import java.util.List; public static void replaceText(PdfReader reader, String outPath, String newText, float x, float y, float width, float height, BaseFont bf, int fontSize) throws Exception { // 先擦除指定区域 ListPdfCleanUpLocation cleanUpLocations new ArrayList(); cleanUpLocations.add(new PdfCleanUpLocation(1, new Rectangle(x, y, x width, y height), BaseColor.WHITE)); PdfCleanUpProcessor cleaner new PdfCleanUpProcessor(cleanUpLocations, reader); cleaner.cleanUp(); // 再写入新内容 PdfStamper stamper new PdfStamper(reader, new FileOutputStream(outPath)); PdfContentByte over stamper.getOverContent(1); over.saveState(); over.setColorFill(BaseColor.BLACK); over.beginText(); over.setFontAndSize(bf, fontSize); over.setTextMatrix(x, y); over.showText(newText); over.endText(); over.restoreState(); stamper.close(); }逻辑说明PdfCleanUpLocation接收页码、矩形和填充颜色默认用白色矩形把区域内所有可视内容遮住。然后PdfStamper把新文本写到同一位置。这里的Rectangle左下角是(x,y)宽高必须足够覆盖旧文字否则旧文字的边缘残留视觉上像翻车。如果页面本身有底色把BaseColor.WHITE改成和底色一致避免白斑。参数说明cleanUpLocations可以传多个区域批量替换不同位置时很方便。新文本颜色用setColorFill设置。中文字体必须用支持中文的BaseFont不要用默认英文否则新文字变方块。4.3 替换无效时怎么办字体嵌入与内容流解析如果用了上面的代码后新文字错位或者旧文字没擦干净查三件事第一擦除区域坐标是否基于PDF页面原点有些PDF有/Rotate旋转属性坐标要相应变换否则区域会偏移。第二旧文字如果是嵌入字体且颜色是CMYK模式PdfCleanUp对CMYK支持不好可能擦不掉。第三检查单位Rectangle用的是磅如果从UI拿的是毫米记得乘72/25.4。如果只是简单合同模板我建议放弃“原位替换”改为“整体重新生成”。维护一个纯文本模板用坐标填充用itext直接生成最终PDF比在旧PDF上修修补补稳定得多。文本替换只适合少量定制场景不适合大批量处理不同格式的源PDF。要定位旧文字位置可以用PdfReaderContentParser配合LocationTextExtractionStrategy但提取出的字符串有时乱序字间距被拆开我基本只用来做粗略定位然后手动微调几磅。5. itext操作PDF避坑与排查五个高频问题这一章专门记录踩坑经验每条按现象、原因、解决三个部分展开都是我在真实项目中遇过的问题。5.1 问题一生成的PDF中文乱码或显示成空白现象用itext创建文档中文部分要么乱码要么整段消失英文正常。原因没有注册支持中文的BaseFont或者用了系统字体路径但服务器上没有该字体。itext默认字体是Helvetica不支持中文字符所以中文全部丢。解决统一使用BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED)并确保pom里有itext-asian依赖。如果是自定义字体文件比如simhei.ttf把ttf放到classpath用createFont(/font/simhei.ttf, BaseFont.IDENTITY_H, BaseFont.EMBEDDED)。这里IDENTITY_H编码能覆盖所有Unicode字符。注意不要用Helvetica去画中文否则怎么写都是方块。5.2 问题二签章位置偏移盖到别的地方去了现象用图片签章明明坐标是预期值打印出来印章却跑到页面上方或右侧。原因PDF坐标系原点在左下角而大多数图像处理工具原点在左上角此外如果页面有CropBox偏移或旋转属性坐标基准也会变。解决先读取页面实际大小用reader.getPageSizeWithRotation(pageNum)获取页面矩形以getLeft()和getBottom()作为原点。如果从左上角换算公式是pdfX left xpdfY top - y - height其中height是印章图片高度磅。盖章代码里建议用PdfImage和absolutePosition避免受裁剪框影响。签章坐标涉及毫米换算我在封装工具类时已经把毫米转磅的逻辑写进方法了业务层直接传毫米就行。5.3 问题三水印把正文盖得看不清现象水印加完后正文被半透明色块影响文字阅读困难。原因使用getOverContent且透明度设置不当或者水印颜色太深比如用了纯黑。解决换成getUnderContent画水印让水印在正文下方或者把透明度降到0.3以下。另外水印颜色用浅灰如BaseColor(200,200,200)不要用纯黑。如果是扫描件getUnderContent会把水印压在扫描图片下面等于看不见这时只能降低getOverContent的透明度。5.4 问题四替换文本后新内容变成方块现象执行文本替换后新写入的中文在阅读器里显示为一串方块。原因新写入用的BaseFont不是中文字体或者用了setFontAndSize时传入了Helvetica。还有可能是PdfStamper.close()之前被GC回收了字体对象。解决检查bf是否来自STSong-Light或自定义中文字体不要复用创建PDF时那个默认英文font。在PdfStamper中写入时确保BaseFont变量作用域大于stamper.close()的调用点不要在局部方法里创建后立刻被回收。如果还不放心用BaseFont.EMBEDDED嵌入字体能规避大部分显示问题。5.5 问题五itext版本差异导致API找不到现象代码在别人电脑上跑不起来报NoSuchMethodError或ClassNotFoundException。原因项目里同时存在itext5和itext7的依赖或者被传递依赖覆盖了版本。比如用了com.itextpdf.text.Document但另一处引入了com.itextpdf.kernel.pdf.PdfDocument包名完全不兼容。解决在pom的dependencyManagement中锁定itext5版本为5.5.13.3并排除传递依赖里可能带来的其他itext版本。如果必须用itext7就别参考itext5的写法API对象模型完全不同。我的习惯是新建项目时确认一下pom树mvn dependency:tree | grep itext看一眼有没有重复有就排除。6. 进阶玩法把PDF批处理封装成工具顺便聊聊签章防篡改最后一章落到效率提升上。当你把创建、水印、替换、签章都跑通后会发现重复代码很多。我习惯封装一个PdfTool类提供链式调用批量处理几十个文件就是循环。6.1 封装一个PDF工具类批量加水印/替换/签章下面片段演示怎么把水印和签章串起来public class PdfTool { private String srcPath; private String destPath; private String watermarkText; private ListSignatureInfo signatures new ArrayList(); public PdfTool(String srcPath) { this.srcPath srcPath; } public PdfTool withWatermark(String text) { this.watermarkText text; return this; } public PdfTool addSignature(byte[] imgBytes, float x, float y, float width, float height) { signatures.add(new SignatureInfo(imgBytes, x, y, width, height)); return this; } public void execute() throws Exception { PdfReader reader new PdfReader(srcPath); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(destPath)); BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); for (int page 1; page reader.getNumberOfPages(); page) { if (watermarkText ! null) { PdfContentByte over stamper.getOverContent(page); over.beginText(); over.setFontAndSize(bf, 42); over.setTextMatrix(0.866f, 0.5f, -0.5f, 0.866f, 100, 100); over.showText(watermarkText); over.endText(); } for (SignatureInfo sig : signatures) { addImageToPage(stamper, page, sig, bf); } } stamper.close(); reader.close(); } }逻辑说明PdfTool把水印和签章叠加到每一页。addImageToPage内部调getOverContent(page).addImage图片以矩形区域放置。withWatermark只画一行水印足够演示要铺满页面可以复用第3章的网格循环。参数说明addSignature传入图片字节数组和位置尺寸。位置单位是磅如果界面上收的是毫米在传入时换算。setTextMatrix里的0.866f和0.5f是30度旋转的cos和sin值写死了30度。6.2 验证结果用itext读取刚生成的PDF批处理跑完不要只看文件大小要读回来验证。我用下面的代码检查页数和首页文本PdfReader reader new PdfReader(output.pdf); int pages reader.getNumberOfPages(); String content PdfTextExtractor.getTextFromPage(reader, 1); System.out.println(页数 pages , 首页文本 content); reader.close();如果content里能搜到水印文本说明水印画上去了搜不到检查是不是写在扫描件里。文本提取只能拿到文本对象扫描件是图像搜不到很正常。6.3 一个教训签章之后的PDF不能再直接改内容最后必须分享一个血泪教训。有些流程是先盖章再改字段这在技术上非常危险。数字签章哪怕是图片签章在PDF阅读器里会被视为内容完整性的一部分一旦签章后被修改阅读器会提示文档被篡改。我用图片签章时发现只要改了页面任何内容Adobe Reader就会显示“签章有效性未知”或红叉。从那以后我每次处理带签章的PDF都强制把“签章”放在所有修改流程的最后一步。如果后续确实要改内容就用原文件重新生成PDF再重新签章不要直接在已盖章的PDF上动手。这条经验替我省了很多返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表