news 2026/10/16 4:10:06

Java iText 7实战:PDF文本替换与图片插入全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java iText 7实战:PDF文本替换与图片插入全解析

1. 为什么PDF内容编辑总让人头疼

做过文档系统的人多半都有类似的经历:业务方丢过来一份几十页的PDF合同模板,说要把里面的甲方名称、金额、日期全部替换成动态数据,还要在指定位置盖上电子章或者插入签名图片。第一反应可能是找个PDF编辑器手动改,但一旦数量上去、字段变多,手动操作根本扛不住。这时候就得靠代码来解决,而Java生态里做PDF处理,iText几乎是绕不开的一个库。

iText能做什么?简单说,它把PDF的底层结构抽象成了对象模型,你可以像操作DOM一样去读取、修改、生成PDF。修改文本内容、在指定坐标插入图片、合并拆分文档、填充表单,这些都能干。它适合谁?后端开发、报表系统开发者、需要做合同自动化或者票据生成的工程师。哪怕你之前没接触过PDF底层格式,只要理解坐标系和内容流的概念,上手并不难。

但这里有个坑要先说清楚:iText修改已有PDF的文本,并不像改Word那样直接找到文字替换就行。PDF本质上是一堆绘制指令的集合,文字在里面的存储方式取决于生成工具。有些PDF的文字是可提取的,有些是曲线化的,还有些字体子集化之后连字符映射都对不上。所以“修改文本内容”这件事,得先看你的PDF是哪种类型,再决定用什么策略。这篇文章就把我踩过的坑和验证过的方案完整梳理一遍,从环境搭建到文本替换再到图片插入,每一步都给出可复现的操作。

2. 环境准备与版本选择的那些事

2.1 iText 5 还是 iText 7

这是第一个要做的决策。iText 5和iText 7的API差异非常大,几乎是两个不同的库。iText 5的包名是com.itextpdf.text,iText 7改成了com.itextpdf.kernel和com.itextpdf.layout。网上大量教程还是iText 5的,但新项目我建议直接上iText 7,原因有几个:模块化更清晰,对PDF 2.0支持更好,字体处理更完善,而且社区维护重心已经转移。

不过要注意License问题。iText 7是AGPL双许可,商业闭源项目需要购买商业授权。如果项目不能接受AGPL,可以考虑Apache 2.0协议的PDFBox作为替代,但PDFBox在布局和高级操作上没iText顺手。这个取舍得根据你的项目性质来定。

Maven依赖这样引入:

<dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>7.2.5</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>layout</artifactId> <version>7.2.5</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>io</artifactId> <version>7.2.5</version> </dependency>

kernel是核心,layout提供高层布局API,io负责读写。如果你要处理表单,还需要forms模块。

2.2 字体准备:中文处理的命门

iText默认只带了一套基础字体,处理中文必须自己引入字体文件。我一般用STSong-Light配合UniGB-UCS2-H编码,或者直接加载系统的TTF文件。用系统字体更稳妥,因为不同环境的字体可用性不一样。

PdfFont font = PdfFontFactory.createFont("fonts/simsun.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);

这里IDENTITY_H是关键,它支持Unicode,能正确显示中文。PREFER_EMBEDDED表示优先嵌入字体子集,这样生成的PDF在任何设备上都能正常显示,不会因为对方没装字体而乱码。代价是文件会变大一些,但为了兼容性这个代价值得。

注意:字体文件路径不要用中文,某些操作系统下iText读取中文路径会出问题。把字体放在项目resources目录下,用类加载器读取最稳。

3. 修改PDF文本内容的核心思路

3.1 先搞清楚你的PDF是哪一类

动手写代码之前,先做一件事:用iText把PDF的文字内容提取出来看看。如果提取出来是乱码或者空,说明这个PDF的文字不可直接操作,得换思路。

PdfDocument pdfDoc = new PdfDocument(new PdfReader("input.pdf")); for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { String text = PdfTextExtractor.getTextFromPage(pdfDoc.getPage(i)); System.out.println("第" + i + "页: " + text); } pdfDoc.close();

提取正常,说明文字是以文本对象存储的,可以用替换策略。提取为空,可能是扫描件或者曲线化文字,那就只能走OCR或者重新生成的路子。

3.2 文本替换的两种策略

策略一:基于内容流的精确替换。这种方式适合文字结构规整、字体单一的PDF。原理是遍历页面内容流中的文本绘制指令,找到目标字符串,替换成新字符串。iText 7没有直接提供“替换文本”的API,需要自己解析内容流。

策略二:覆盖式替换。在目标文字上方画一个白色矩形盖住原文,再在相同位置写入新文字。这种方式简单粗暴,但要求背景是纯色,且新文字宽度和原文字接近,否则会露出破绽。适合字段位置固定、背景简单的场景,比如票据、标签。

我实际项目中用得最多的是策略二,因为大部分业务PDF的字段位置是固定的,背景也是白色,覆盖式替换稳定可靠,不容易因为字体子集化问题翻车。

3.3 定位目标文字的坐标

不管用哪种策略,都得先知道目标文字在页面上的位置。iText提供了LocationTextExtractionStrategy,可以拿到每个字符的坐标。

public class PositionStrategy extends LocationTextExtractionStrategy { private String searchText; private List<Rectangle> matches = new ArrayList<>(); public PositionStrategy(String searchText) { this.searchText = searchText; } @Override public void eventOccurred(IEventData data, EventType type) { super.eventOccurred(data, type); } @Override public String getResultantText() { return super.getResultantText(); } }

更实用的做法是继承SimpleTextExtractionStrategy,重写renderText方法,在渲染每个文本块时判断是否包含目标文字,如果包含就记录下当前的变换矩阵和坐标。

public class TextPositionListener implements IEventListener { private String target; private List<float[]> positions = new ArrayList<>(); public TextPositionListener(String target) { this.target = target; } @Override public void eventOccurred(IEventData data, EventType type) { if (type == EventType.RENDER_TEXT) { TextRenderInfo info = (TextRenderInfo) data; String text = info.getText(); if (text != null && text.contains(target)) { Vector start = info.getBaseline().getStartPoint(); positions.add(new float[]{start.get(0), start.get(1)}); } } } public List<float[]> getPositions() { return positions; } }

拿到坐标后,注意PDF的坐标系原点在左下角,而很多设计工具的原点在左上角,换算的时候要小心。页面高度减去Y坐标才是从上往下的距离。

4. 指定位置插入图片的完整实现

4.1 图片插入的基本API

iText 7插入图片用Image对象配合Canvas或者Document。如果是往已有PDF的指定位置插图片,用PdfCanvas更直接。

PdfDocument pdfDoc = new PdfDocument(new PdfReader("input.pdf"), new PdfWriter("output.pdf")); PdfPage page = pdfDoc.getPage(1); ImageData imageData = ImageDataFactory.create("seal.png"); Image image = new Image(imageData); // 设置位置和大小 image.setFixedPosition(1, x, y); image.scaleToFit(width, height); // 通过Document添加 Document doc = new Document(pdfDoc); doc.add(image); doc.close();

setFixedPosition的第一个参数是页码,后面是X和Y坐标。scaleToFit会保持宽高比缩放到指定范围内,避免图片变形。

4.2 坐标计算与对齐

插入图片最麻烦的是坐标计算。假设你要在“盖章处”三个字右边插入一个印章图片,你得先拿到“盖章处”的坐标,然后根据文字宽度算出右边的位置。

float[] pos = positions.get(0); float textWidth = ...; // 通过字体和字号计算 float imageX = pos[0] + textWidth + 5; // 右边留5个单位的间距 float imageY = pos[1] - 10; // 稍微下移,让图片和文字基线对齐

文字宽度的计算可以用font.getWidth(text, fontSize)。如果拿不到精确宽度,可以估算:中文字符宽度约等于字号,英文字符约等于字号的一半。

实操心得:插入图片前先用一个透明矩形框标出位置,生成PDF后打开看看位置对不对,确认无误再换成真实图片。这个调试方法能省很多来回改坐标的时间。

4.3 图片透明度和旋转

印章图片经常需要半透明效果,iText支持设置透明度。

PdfCanvas canvas = new PdfCanvas(page); PdfExtGState gs = new PdfExtGState(); gs.setFillOpacity(0.8f); canvas.setExtGState(gs); canvas.addImageFittedIntoRectangle(imageData, rect, false);

旋转用image.setRotationAngle(Math.toRadians(45)),注意参数是弧度不是角度。旋转的中心点默认是图片左下角,如果需要绕中心旋转,得自己调整坐标偏移。

5. 完整实操流程与代码拆解

5.1 项目结构

我一般这样组织代码:

src/main/java/ com/example/pdf/ PdfTextReplacer.java // 文本替换 PdfImageInserter.java // 图片插入 PdfPositionFinder.java // 坐标定位 PdfMain.java // 入口 src/main/resources/ fonts/simsun.ttf templates/contract.pdf images/seal.png

5.2 文本替换的完整实现

以覆盖式替换为例,完整流程是这样的:

public class PdfTextReplacer { public void replaceText(String src, String dest, Map<String, String> replacements) throws IOException { PdfDocument pdfDoc = new PdfDocument( new PdfReader(src), new PdfWriter(dest)); PdfFont font = PdfFontFactory.createFont( "fonts/simsun.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { PdfPage page = pdfDoc.getPage(i); PdfCanvas canvas = new PdfCanvas(page); for (Map.Entry<String, String> entry : replacements.entrySet()) { String oldText = entry.getKey(); String newText = entry.getValue(); List<float[]> positions = findTextPosition(page, oldText); for (float[] pos : positions) { // 用白色矩形覆盖原文 canvas.setFillColor(Color.WHITE); canvas.rectangle(pos[0] - 2, pos[1] - 3, oldText.length() * 12 + 4, 18); canvas.fill(); // 写入新文字 canvas.beginText(); canvas.setFontAndSize(font, 12); canvas.moveText(pos[0], pos[1]); canvas.showText(newText); canvas.endText(); } } } pdfDoc.close(); } private List<float[]> findTextPosition(PdfPage page, String target) { TextPositionListener listener = new TextPositionListener(target); PdfCanvasProcessor processor = new PdfCanvasProcessor(listener); processor.processPageContent(page); return listener.getPositions(); } }

这段代码有几个细节要注意。覆盖矩形的宽度用oldText.length() * 12是粗略估算,中英文混排时不准。更精确的做法是用原字体计算宽度,但覆盖式替换本来就不追求像素级精确,留点余量反而更安全。新文字的字号要和原文一致,否则视觉上会突兀。

5.3 图片插入的完整实现

public class PdfImageInserter { public void insertImage(String src, String dest, String imagePath, int pageNum, float x, float y, float width, float height) throws IOException { PdfDocument pdfDoc = new PdfDocument( new PdfReader(src), new PdfWriter(dest)); PdfPage page = pdfDoc.getPage(pageNum); ImageData imageData = ImageDataFactory.create(imagePath); PdfCanvas canvas = new PdfCanvas(page); Rectangle rect = new Rectangle(x, y, width, height); canvas.addImageFittedIntoRectangle(imageData, rect, false); pdfDoc.close(); } }

addImageFittedIntoRectangle的最后一个参数false表示不保持宽高比,如果设为true则会等比缩放并居中。印章图片一般用false,因为印章通常是正方形,指定正方形区域不会变形。

5.4 参数计算实例

假设A4纸(595 x 842点),要在右下角盖一个直径80点的圆形印章,距离右边距50点,距离下边距80点。

印章左下角坐标:

  • X = 595 - 50 - 80 = 465
  • Y = 80

如果印章要盖在“负责人签字:”这行字的右边,先找到这行字的坐标,假设是(100, 200),文字宽度约72点(6个中文字符 x 12点),那么印章位置:

  • X = 100 + 72 + 10 = 182
  • Y = 200 - 20 = 180(下移让印章中心和文字对齐)

这些数值不是拍脑袋来的,是根据字体度量和视觉习惯算出来的。实际调试时可以用PDF阅读器的测量工具验证。

6. 常见问题与排查技巧实录

6.1 中文显示为方框或乱码

这是最高频的问题。原因通常是字体没有正确嵌入,或者编码方式不对。排查步骤:

  1. 确认字体文件路径正确,文件本身没有损坏
  2. 确认使用IDENTITY_H编码
  3. 确认EmbeddingStrategy设为PREFER_EMBEDDED或FORCE_EMBEDDED
  4. 用提取工具检查生成PDF的文字是否可提取

如果还是乱码,试试换一个字体文件。有些精简版的TTF字体缺少必要的字符映射表,换用完整的宋体或黑体通常能解决。

6.2 替换后的文字位置偏移

覆盖式替换时,新文字的基线位置如果和原文不一致,看起来就会错位。解决办法是记录原文的基线坐标,新文字用相同的基线。如果原文有旋转或缩放变换,还需要把变换矩阵应用到新文字上。

Matrix matrix = info.getTextMatrix(); canvas.setTextMatrix(matrix);

6.3 图片插入后不显示

检查这几点:图片格式是否支持(PNG、JPG、GIF都支持,WebP不支持)、坐标是否在页面范围内、图片数据是否成功加载。有时候图片路径对了但文件是空的,也会导致不显示。

6.4 处理大文件时内存溢出

iText默认会把整个PDF加载到内存。处理几百页的大文件时,可以用PdfReader的随机访问模式,配合PdfDocument的flush机制。

PdfReader reader = new PdfReader(src); reader.setUnethicalReading(true); // 处理加密PDF PdfDocument pdfDoc = new PdfDocument(reader, new PdfWriter(dest)); // 处理完一页后 pdfDoc.flush();

flush()会把已处理的内容写入磁盘并释放内存,但注意flush之后不能再修改之前的页面。

6.5 常见问题速查表

问题现象可能原因解决方法
中文乱码字体未嵌入或编码错误用IDENTITY_H编码,强制嵌入字体
文字位置偏移基线坐标不一致记录原文基线,应用相同变换矩阵
图片不显示格式不支持或坐标越界换PNG/JPG,检查坐标范围
内存溢出大文件全量加载分页处理,及时flush
替换后原文还在覆盖矩形太小增大覆盖范围,或改用内容流替换
生成的PDF打不开文档未正确关闭确保close()被调用,用try-with-resources

避坑技巧:处理前先备份原始PDF。iText的修改是破坏性的,一旦写坏很难恢复。我习惯在代码里加一步自动备份,文件名加时间戳。

7. 进阶技巧与性能优化

7.1 批量处理的并行化

如果要对几百个PDF做相同处理,单线程太慢。可以用线程池并行,但要注意每个线程用独立的PdfDocument实例,iText的对象不是线程安全的。

ExecutorService executor = Executors.newFixedThreadPool(4); List<Future<?>> futures = new ArrayList<>(); for (String file : files) { futures.add(executor.submit(() -> processOne(file))); } for (Future<?> f : futures) { f.get(); } executor.shutdown();

线程数不要超过CPU核心数,PDF处理是IO和CPU混合型任务,线程太多反而会因为磁盘竞争变慢。

7.2 模板化生成 vs 修改已有PDF

如果业务允许,我更推荐模板化生成而不是修改已有PDF。用iText的Document和Paragraph从头构建,布局可控,不用担心字体子集化和坐标偏移。修改已有PDF只适合那些格式固定、无法重新生成的场景,比如对方提供的标准合同。

7.3 字体子集化的取舍

嵌入完整字体文件会让PDF变大,但兼容性最好。子集化只嵌入用到的字符,文件小但偶尔会出现字符缺失。我的经验是:如果PDF要长期存档或者跨平台分发,用完整嵌入;如果只是内部流转,子集化够用。

// 完整嵌入 PdfFontFactory.EmbeddingStrategy.FORCE_EMBEDDED // 子集化 PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED

7.4 处理加密PDF

有些PDF有权限密码,iText读取时会抛异常。如果只是限制编辑不限制阅读,可以用setUnethicalReading(true)绕过。但要注意,这个操作可能涉及合规问题,确保你有权处理这些文档。

PdfReader reader = new PdfReader(src); reader.setUnethicalReading(true);

我在实际项目里遇到最多的情况是:PDF是扫描件,文字提取为空,客户却要求替换里面的文字。这种时候只能老老实实告诉对方,要么提供可编辑的原始文件,要么走OCR识别后重新生成。技术不是万能的,提前和需求方对齐PDF的类型,能省掉大量返工。

最后分享一个小技巧:调试坐标时,可以临时把插入的图片换成一个红色半透明矩形,生成后直接看位置对不对,比反复猜坐标高效得多。等位置确认了,再换成正式图片。这个习惯帮我省下了至少一半的调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 4:09:00

车云协同十年演进:从电话式连接到端云智能共同体

车云协同这个词&#xff0c;十年前我拿着方案去讲的时候&#xff0c;说得再热闹&#xff0c;对方核心诉求也就是"能远程开个空调、查个位置就挺好"。那时候车是车&#xff0c;云是云&#xff0c;中间靠一张SIM卡打电话。十年后再看&#xff0c;云端已经成了智能汽车的…

作者头像 李华
网站建设 2026/10/16 4:07:23

多模融合时序数据库实战:从数据孤岛到统一SQL引擎的国产化之路

进入2026年回头看国产时序数据库这几年&#xff0c;市场已经不是前几年“遍地开花、谁都能讲物联网故事”的局面了。头部玩家的路线差异越来越明显&#xff0c;有人死磕单机性能&#xff0c;有人拼命堆集群规模&#xff0c;而金仓&#xff08;KingbaseES&#xff09;走的多模融…

作者头像 李华
网站建设 2026/10/16 4:06:32

Python实现t-SNE高维数据可视化:原理、参数调优与实践避坑

要说高维数据可视化&#xff0c;t-SNE 是我实际项目里用得最多的方法之一。做用户分群、看特征分布、排查聚类是否合理&#xff0c;甚至写 PPT 之前我都会先用 t-SNE 跑一张二维散点图&#xff0c;快速判断手里的高维数据到底“长得什么样”。这篇文章就用 Python 从头到尾把 t…

作者头像 李华
网站建设 2026/10/16 4:06:31

Python数据可视化:从单变量到多变量的完整实践指南

先问一个问题&#xff1a;你拿到一份数据&#xff0c;打开之后的第一反应是什么&#xff1f;如果你脑子里只想到“画个图看看”&#xff0c;那我建议你花两分钟把这篇文章读完。Python数据可视化并不是把数据丢给matplotlib然后祈祷结果能看&#xff0c;它是一个从单变量到多变…

作者头像 李华
网站建设 2026/10/16 4:06:28

STM32F407 ADC+DMA+FFT信号采集链路实战

简介&#xff1a;这份资源面向学习STM32信号采集与数字信号处理的开发者&#xff0c;尤其是希望把FFT从理论落到实际工程中的嵌入式初学者与进阶者。它基于STM32F407内置ADC&#xff0c;通过定时器触发配合DMA连续搬运采样数据&#xff0c;可灵活设置512kHz、256kHz、128kHz等采…

作者头像 李华
网站建设 2026/10/16 4:05:09

VS Code 快捷键高效开发指南:从多光标到命令面板

1. 快捷键值得专门花时间学吗先说我的结论&#xff1a;值得&#xff0c;而且越早系统梳理一遍&#xff0c;后面省下来的时间越可观。我见过不少开发者&#xff0c;VS Code 用了好几年&#xff0c;日常操作还停留在“鼠标点哪、右键点哪、菜单翻哪”的阶段。不是说这样不行&…

作者头像 李华