Java 实践:定位 PDF 指定内容的坐标
Java 实践 | 定位 PDF 指定内容的精准坐标
Java 实践:定位 PDF 指定内容的坐标
在日常开发中,我们经常需要对 PDF 文件进行二次处理:自动打码、添加水印、标注关键字、电子签章……这些需求都有一个共同的前提——先拿到目标文字在 PDF 页面中的精确坐标。
本文将从 PDF 坐标系的底层原理出发,一步步推导出一个完整、可用于生产的关键字定位实现。
1. 应用场景
| 场景 | 说明 |
|---|---|
| PDF 内容打码 | 找到手机号、身份证等敏感字段的坐标,在原位绘制白色矩形覆盖 |
| 关键字高亮标注 | 找到目标文字坐标后,叠加透明彩色矩形实现高亮 |
| 电子签章 / 盖章 | 在指定文字(如"甲方签字:")右侧精确插入印章图片 |
| 表单字段自动填充 | 定位空白字段位置,再用 iText 填写文字或勾选框 |
| 多语言翻译替换 | 先定位原文坐标,白色覆盖后再于同坐标写入译文 |
2. 必须先懂的原理:PDF 坐标系
2.1 PDF 使用"左下角原点"坐标系
PDF 的坐标系与屏幕(或 Java Graphics2D)的坐标系完全相反:

- 屏幕坐标:原点在左上角,Y 轴向下递增。
- PDF 坐标:原点在左下角,Y 轴向上递增。
这意味着:当你从 iText 中读取到一个字符的 y = 200,在 A4 页面(高度 841.89pt)上,对应的屏幕 Y 坐标是 841.89 - 200 - charHeight。
2.2 PDF 单位:磅(pt)
PDF 的距离单位是磅(Point,pt),换算关系:
1 inch = 72 pt , 1 mm ≈ 2.835 pt 1 \; \text{inch} = 72 \; \text{pt}, \quad 1 \; \text{mm} \approx 2.835 \; \text{pt} 1inch=72pt,1mm≈2.835pt
A4 纸尺寸:595.28 pt × 841.89 pt(即 210mm × 297mm)。
2.3 页面旋转(Rotation)
PDF 页面可以附带旋转角度(0°、90°、180°、270°)。旋转并不改变页面内容的原始坐标,而是改变渲染方向。若要将坐标映射到视觉上正确的位置,必须对旋转做逆变换。
3. 思路设计:如何精确定位关键字?
3.1 朴素思路:提取全文再 indexOf?
最直接的想法:用 PdfTextExtractor.getTextFromPage() 把一页文字提取成 String,然后 indexOf(keyword) 拿到偏移量——但这个偏移量只是字符串下标,完全无法对应 PDF 中的坐标,因为文字提取时丢弃了所有位置信息。
3.2 正确思路:事件驱动,逐字符捕获
iText 7 提供了一套渲染事件监听机制:
通过监听 RENDER_TEXT 事件,我们可以拿到每一个字符及其在 PDF 坐标系中的精确边界框(Rectangle)。
3.3 匹配算法:带回溯的前缀累积
拿到字符流后,如何匹配关键字?
注意:PDF 中的字符是流式输出的,没有 “单词边界” 概念,且可能存在跨文本块的情况。
设计一个带回溯的前缀匹配算法:
维护:currentWord(已累积字符串) + currentRects(对应的矩形列表)
每来一个字符 ch:
1. 追加到 currentWord,并记录 charRect
2. 若 keyword.startsWith(currentWord):
a. 若 currentWord == keyword → 完全匹配!记录合并矩形,重置
b. 否则 → 继续等待下一个字符
3. 若不是前缀(匹配失败):
从 accumulated[1], accumulated[2], ... 依次尝试
找到最长的仍能作为 keyword 前缀的后缀,作为新起点
(处理 "ABAB" 中查找 "ABAB" 的重叠情况)
这个算法的关键在于回溯步骤,它处理了关键字中存在重叠前缀的场景(类似 KMP 算法的思想)。
4. 完整实现解析
这里用到了 itext 的依赖包,需要导入依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>8.0.5</version>
<type>pom</type>
</dependency>
<!-- 如处理中文 PDF,建议添加以下依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>8.0.5</version>
</dependency>
4.1 结果数据类
public static class PdfKeywordResult {
public int pageNumber; // 页码(从 1 开始)
public String text; // 匹配到的关键字
// PDF 原始坐标(左下角原点,单位 pt)
public float originalX, originalY, originalWidth, originalHeight;
// 屏幕坐标(左上角原点,已处理旋转,单位 pt)
public float x, y, width, height;
@Override
public String toString() {
return String.format(
"Page[%d] Keyword:'%s' -> ScreenCoord: (%.2f, %.2f, %.2f, %.2f) | Original: (%.2f, %.2f, %.2f, %.2f)",
pageNumber, text, x, y, width, height,
originalX, originalY, originalWidth, originalHeight);
}
}
同时保留原始坐标与屏幕坐标,是因为两者在不同场景下都有用:
- 原始坐标:用 iText 向 PDF 写内容时需要(iText 使用 PDF 原生坐标系)
- 屏幕坐标:在 Swing/JavaFX 等 UI 上预览、绘制高亮框时需要
4.2 主流程:逐页处理
public static List<PdfKeywordResult> findKeyword(String pdfPath, String keyword) throws IOException {
List<PdfKeywordResult> results = new ArrayList<>();
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfPath))) {
for (int pageNum = 1; pageNum <= pdfDoc.getNumberOfPages(); pageNum++) {
PdfPage page = pdfDoc.getPage(pageNum);
int rotation = page.getRotation(); // ① 记录页面旋转角度
float pageHeight = page.getPageSize().getHeight(); // ② 记录页面高度(坐标转换用)
List<PdfKeywordResult> pageResults = new ArrayList<>();
int finalPageNum = pageNum;
// ③ 创建事件监听器(见 4.3)
IEventListener listener = buildListener(keyword, finalPageNum, pageResults);
// ④ 执行页面内容解析,触发事件
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(page);
// ⑤ 对每个匹配结果做旋转修正 + 坐标系转换(见 4.4、4.5)
for (PdfKeywordResult raw : pageResults) {
Rectangle rawRect = new Rectangle(
raw.originalX, raw.originalY, raw.originalWidth, raw.originalHeight);
Rectangle rotated = handlePageRotation(rawRect, rotation, page.getPageSize());
Rectangle screen = convertToScreenCoordinates(rotated, pageHeight);
raw.x = screen.getX();
raw.y = screen.getY();
raw.width = screen.getWidth();
raw.height = screen.getHeight();
results.add(raw);
}
}
}
return results;
}
4.3 核心:事件监听器与字符匹配
IEventListener listener = new IEventListener() {
private final StringBuilder currentWord = new StringBuilder();
private final List<Rectangle> currentRects = new ArrayList<>();
@Override
public void eventOccurred(IEventData data, EventType type) {
if (!(data instanceof TextRenderInfo)) return;
TextRenderInfo tri = (TextRenderInfo) data;
// iText 7 最佳实践:拆到单字符级别
for (TextRenderInfo charInfo : tri.getCharacterRenderInfos()) {
String ch = charInfo.getText();
if (ch == null || ch.isEmpty()) continue;
// CharacterRenderInfo 提供精确的单字符边界框
CharacterRenderInfo cri = new CharacterRenderInfo(charInfo);
Rectangle charRect = cri.getBoundingBox();
currentWord.append(ch);
currentRects.add(charRect);
String full = currentWord.toString();
if (keyword.startsWith(full)) {
if (full.equals(keyword)) {
// ✅ 完全匹配
Rectangle merged = mergeRectangles(currentRects);
PdfKeywordResult result = new PdfKeywordResult();
result.pageNumber = finalPageNum;
result.text = keyword;
result.originalX = merged.getX();
result.originalY = merged.getY();
result.originalWidth = merged.getWidth();
result.originalHeight = merged.getHeight();
pageResults.add(result);
currentWord.setLength(0);
currentRects.clear();
}
// else:仍是前缀,继续等待
} else {
// ❌ 匹配失败:回溯找最长可用前缀
String accumulated = currentWord.toString();
List<Rectangle> accRects = new ArrayList<>(currentRects);
currentWord.setLength(0);
currentRects.clear();
for (int start = 1; start < accumulated.length(); start++) {
String suffix = accumulated.substring(start);
if (keyword.startsWith(suffix)) {
currentWord.append(suffix);
currentRects.addAll(accRects.subList(start, accRects.size()));
break;
}
}
}
}
}
@Override
public Set<EventType> getSupportedEvents() {
return new HashSet<>(Collections.singletonList(EventType.RENDER_TEXT));
}
};
关键细节:为什么用 CharacterRenderInfo 而不是 TextRenderInfo.getBaseline()?
TextRenderInfo 提供的 getBaseline() / getAscentLine() 返回的是整个文本片段的基线,无法精确到单字符。CharacterRenderInfo.getBoundingBox() 是 iText 7 专门封装的单字符边界矩形,精度更高,也是官方推荐做法。
4.4 辅助方法:合并矩形
将关键字所有字符的边界框合并为一个包围矩形:
private Rectangle mergeRectangles(List<Rectangle> rects) {
if (rects.isEmpty()) return null;
float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE;
float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE;
for (Rectangle r : rects) {
minX = Math.min(minX, r.getX());
minY = Math.min(minY, r.getY());
maxX = Math.max(maxX, r.getX() + r.getWidth());
maxY = Math.max(maxY, r.getY() + r.getHeight());
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
4.5 辅助方法:页面旋转修正
当 PDF 页面存在旋转时,需要将字符坐标反向旋转到视觉正确位置:
private static Rectangle handlePageRotation(Rectangle rect, int rotation, Rectangle pageSize) {
if (rotation == 0) return new Rectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
float W = pageSize.getWidth(), H = pageSize.getHeight();
float x = rect.getX(), y = rect.getY(), w = rect.getWidth(), h = rect.getHeight();
switch (rotation) {
case 90: return new Rectangle(y, W - x - w, h, w);
case 180: return new Rectangle(W - x - w, H - y - h, w, h);
case 270: return new Rectangle(H - y - h, x, h, w);
default: return rect;
}
}
旋转变换原理(以 90° 为例):
( x ′ y ′ ) = ( 0 1 − 1 0 ) ( x y ) + ( 0 W ) \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ -1 & 0 \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} + \begin{pmatrix} 0 \\ W \end{pmatrix} (x′y′)=(0−110)(xy)+(0W)
即:x' = y,y' = W - x,再考虑矩形宽高互换。
4.6 辅助方法:PDF 坐标转屏幕坐标
private static Rectangle convertToScreenCoordinates(Rectangle rect, float pageHeight) {
// PDF Y 轴向上,屏幕 Y 轴向下;矩形顶边的 PDF Y = rect.getY() + rect.getHeight()
float screenY = pageHeight - (rect.getY() + rect.getHeight());
return new Rectangle(rect.getX(), screenY, rect.getWidth(), rect.getHeight());
}
转换示意:

5. 完整可运行代码
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.CharacterRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
public class PdfKeywordLocator {
// 与你的 main 方法兼容的结果类
public static class PdfKeywordResult {
public int pageNumber;
public String text;
public float originalX, originalY, originalWidth, originalHeight;
public float x, y, width, height;
@Override
public String toString() {
return String.format("Page[%d] Keyword:'%s' -> ScreenCoord: (%.2f, %.2f, %.2f, %.2f) | Original: (%.2f, %.2f, %.2f, %.2f)",
pageNumber, text, x, y, width, height, originalX, originalY, originalWidth, originalHeight);
}
}
/**
* 查找 PDF 中所有关键字的位置
* @param pdfPath PDF 文件路径
* @param keyword 要查找的关键字(区分大小写)
* @return 位置信息列表
*/
public static List<PdfKeywordResult> findKeyword(String pdfPath, String keyword) throws IOException {
List<PdfKeywordResult> results = new ArrayList<>();
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfPath))) {
for (int pageNum = 1; pageNum <= pdfDoc.getNumberOfPages(); pageNum++) {
PdfPage page = pdfDoc.getPage(pageNum);
int rotation = page.getRotation();
float pageHeight = page.getPageSize().getHeight();
// 存储当前页的所有匹配结果
List<PdfKeywordResult> pageResults = new ArrayList<>();
int finalPageNum = pageNum;
IEventListener listener = new IEventListener() {
private final StringBuilder currentWord = new StringBuilder();
private final List<Rectangle> currentRects = new ArrayList<>();
@Override
public void eventOccurred(IEventData data, EventType type) {
if (data instanceof TextRenderInfo) {
TextRenderInfo tri = (TextRenderInfo) data;
// 拆成单个字符处理
for (TextRenderInfo charInfo : tri.getCharacterRenderInfos()) {
String ch = charInfo.getText();
if (ch == null || ch.isEmpty()) continue;
// 获取单个字符的边界框(iText 7 推荐方式)
CharacterRenderInfo cri = new CharacterRenderInfo(charInfo);
Rectangle charRect = cri.getBoundingBox();
// 尝试匹配关键字
currentWord.append(ch);
currentRects.add(charRect);
String full = currentWord.toString();
if (keyword.startsWith(full)) {
// 尚未完全匹配,继续累积
if (full.equals(keyword)) {
// 完全匹配:计算所有字符的合并矩形
Rectangle merged = mergeRectangles(currentRects);
// 保存结果(原始坐标)
PdfKeywordResult result = new PdfKeywordResult();
result.pageNumber = finalPageNum;
result.text = keyword;
assert merged != null;
result.originalX = merged.getX();
result.originalY = merged.getY();
result.originalWidth = merged.getWidth();
result.originalHeight = merged.getHeight();
pageResults.add(result);
// 重置,准备下一个匹配
currentWord.setLength(0);
currentRects.clear();
}
} else {
// 匹配失败:从已积累字符串的第 1 位开始回溯,
// 找到最长的能作为 keyword 前缀的子串,作为新起点
String accumulated = currentWord.toString();
List<Rectangle> accRects = new ArrayList<>(currentRects);
currentWord.setLength(0);
currentRects.clear();
// 从 accumulated 的第 1 位(跳过第 0 位)逐步尝试
for (int start = 1; start < accumulated.length(); start++) {
String suffix = accumulated.substring(start);
if (keyword.startsWith(suffix)) {
currentWord.append(suffix);
currentRects.addAll(accRects.subList(start, accRects.size()));
break;
}
}
}
}
}
}
@Override
public Set<EventType> getSupportedEvents() {
return new HashSet<>(Collections.singletonList(EventType.RENDER_TEXT));
}
private Rectangle mergeRectangles(List<Rectangle> rects) {
if (rects.isEmpty()) return null;
float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE;
float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE;
for (Rectangle r : rects) {
minX = Math.min(minX, r.getX());
minY = Math.min(minY, r.getY());
maxX = Math.max(maxX, r.getX() + r.getWidth());
maxY = Math.max(maxY, r.getY() + r.getHeight());
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
};
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.processPageContent(page);
// 处理该页找到的所有结果:页面旋转 + 坐标转换
for (PdfKeywordResult raw : pageResults) {
Rectangle rawRect = new Rectangle(raw.originalX, raw.originalY,
raw.originalWidth, raw.originalHeight);
// 1. 处理页面旋转
Rectangle rotated = handlePageRotation(rawRect, rotation, page.getPageSize());
// 2. 转换为屏幕坐标(左上角原点)
Rectangle screen = convertToScreenCoordinates(rotated, pageHeight);
raw.originalX = rawRect.getX();
raw.originalY = rawRect.getY();
raw.originalWidth = rawRect.getWidth();
raw.originalHeight = rawRect.getHeight();
raw.x = screen.getX();
raw.y = screen.getY();
raw.width = screen.getWidth();
raw.height = screen.getHeight();
results.add(raw);
}
}
}
return results;
}
private static Rectangle handlePageRotation(Rectangle rect, int rotation, Rectangle pageSize) {
if (rotation == 0) return new Rectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
float width = pageSize.getWidth();
float height = pageSize.getHeight();
float x = rect.getX();
float y = rect.getY();
float w = rect.getWidth();
float h = rect.getHeight();
switch (rotation) {
case 90:
return new Rectangle(y, width - x - w, h, w);
case 180:
return new Rectangle(width - x - w, height - y - h, w, h);
case 270:
return new Rectangle(height - y - h, x, h, w);
default:
return rect;
}
}
private static Rectangle convertToScreenCoordinates(Rectangle rect, float pageHeight) {
float screenY = pageHeight - (rect.getY() + rect.getHeight());
return new Rectangle(rect.getX(), screenY, rect.getWidth(), rect.getHeight());
}
/**
* 按照规则提取 pdf 文档中的内容
* @param pdfPath PDF 文件路径
* @return 订单号列表(如 ["PO-02974"])
*/
public static List<String> extractPdfContent(String pdfPath, Function<String, List<String>> function) throws IOException {
List<String> orderNumbers = new ArrayList<>();
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfPath))) {
for (int pageNum = 1; pageNum <= pdfDoc.getNumberOfPages(); pageNum++) {
String pageText = PdfTextExtractor.getTextFromPage(pdfDoc.getPage(pageNum));
orderNumbers.addAll(function.apply(pageText));
}
}
return orderNumbers;
}
}
6. 调用示例
6.1 查找关键字
public static void main(String[] args) throws IOException {
String pdfPath = "D:/documents/contract.pdf";
String keyword = "甲方签字";
List<PdfKeywordLocator.PdfKeywordResult> results =
PdfKeywordLocator.findKeyword(pdfPath, keyword);
for (PdfKeywordLocator.PdfKeywordResult r : results) {
System.out.println(r);
// 输出示例:
// Page[3] Keyword:'甲方签字' -> ScreenCoord: (72.00, 650.40, 56.80, 14.20) | Original: (72.00, 177.29, 56.80, 14.20)
}
}
6.2 配合 iText 7 在关键字位置绘制白色矩形(打码)
public static void whiteoutKeyword(String srcPath, String destPath, String keyword) throws IOException {
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(srcPath), new PdfWriter(destPath))) {
List<PdfKeywordLocator.PdfKeywordResult> results =
PdfKeywordLocator.findKeyword(srcPath, keyword);
for (PdfKeywordLocator.PdfKeywordResult r : results) {
PdfPage page = pdfDoc.getPage(r.pageNumber);
PdfCanvas canvas = new PdfCanvas(page);
// 注意:向 PDF 写内容时使用原始坐标(originalX/Y),而非屏幕坐标
canvas.setFillColor(ColorConstants.WHITE)
.rectangle(r.originalX - 2, r.originalY - 2,
r.originalWidth + 4, r.originalHeight + 4)
.fill();
}
}
}
注意事项:iText 在向现有 PDF 追加内容时,使用的依然是 PDF 原生坐标系(左下角原点)。因此绘制时应使用
originalX/Y,只有在 UI 展示场景才需要x/y(屏幕坐标)。
7. 方案对比与选型建议
| 方案 | 实现难度 | 精度 | 适用场景 |
|---|---|---|---|
正则 + 文本提取(PdfTextExtractor) |
低 | 无坐标信息 | 仅需要提取文字内容,不关心位置 |
| iText 7 事件监听(本文方案) | 中 | 字符级精度 | 需要精确定位坐标,主流选择 |
Apache PDFBox PDFTextStripper |
中 | 词组级精度 | 开源免费,精度稍低于 iText 7 |
| Tesseract OCR | 高 | 依赖图像质量 | 扫描件 PDF(无文字层)时使用 |
选型建议:
- 有文字层的 PDF(即数字 PDF) → iText 7 事件监听,精度最高
- 扫描件 PDF(图片型) → OCR 方案(如 Tesseract + 坐标映射)
- 只需文字内容不需坐标 →
PdfTextExtractor.getTextFromPage()足矣
8. 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 逐页遍历 | O§ | P 为总页数 |
| 事件监听字符处理 | O© | C 为单页总字符数 |
| 前缀匹配 + 回溯 | O(C × K) | K 为关键字长度,最差情况 |
| 矩形合并 | O(K) | 合并关键字长度内的字符矩形 |
| 整体 | O(P × C × K) | 实际 C×K 极小,接近 O(P×C) |
实测参考:一份 50 页的 A4 商业合同 PDF,查找 10 个关键字,耗时约 200~500ms,满足绝大多数业务需求。
9. 实际应用
之前接到一个需求就是对固定格式的PDF采购单翻译成中文,原本是打算用模板+OCR/AI去实现,但是OCR和AI识别表格数据总是有偏差,考虑到采购单的格式固定,待翻译的字段也基本固定,于是就采用先定位需要翻译的字符串,然后用白色覆盖掉原内容,接着替换成翻译之后的内容。
例如一下采购单,我只需要翻译框选的内容即可,我只需要先将对应的字符串给定位出来:

测试代码:
public static void main(String[] args) throws IOException {
String pdfPath = "src/test/java/com/mike/test/pdf/1.pdf";
List<String> keywords = Arrays.asList("PURCHASE ORDER",
"Purchase Order No: PO-02974","Date","Ex. Factory","Terms","Supplier Reference","SupplierCurrency",
"Part Number","Part Description","Supplier Code","HS Code","HST Code","Quantity","Price","Amount",
"Total Order","Before Tax","Tax","Total",
"NOTES");
Map<String, List<PdfKeywordLocator.PdfKeywordResult>> map = new HashMap<>();
for (String keyword : keywords) {
map.put(keyword, handlerSingleKeyword(pdfPath, keyword));
}
File input = new File(pdfPath);
File output = new File("output/PO-replaced3.pdf");
// 1. 加载 PDF 文档
try (PDDocument document = PDDocument.load(input)) {
// 2. 获取要操作的第1页(PDFBox中页码从0开始)
PDPage page = document.getPage(0);
// 3. 准备向该页面添加新的内容流,使用追加模式 APPEND
try (PDPageContentStream cs = new PDPageContentStream(document, page,
PDPageContentStream.AppendMode.APPEND, true, true)) {
// 4. 设置画笔颜色为白色并填充
cs.setNonStrokingColor(Color.RED);
for (Map.Entry<String, List<PdfKeywordLocator.PdfKeywordResult>> entry : map.entrySet()) {
String key = entry.getKey();
List<PdfKeywordLocator.PdfKeywordResult> value = entry.getValue();
System.out.println("当前处理关键字: " + key);
for (PdfKeywordLocator.PdfKeywordResult pdfKeywordResult : value) {
float[] rect = {pdfKeywordResult.originalX, pdfKeywordResult.originalY, pdfKeywordResult.originalWidth, pdfKeywordResult.originalHeight};
float x = rect[0]; // 矩形左下角X坐标
float y = rect[1]; // 矩形左下角Y坐标(原点在左下角)
float width = rect[2];
float height = rect[3];
cs.addRect(x, y, width, height);
cs.fill(); // 执行填充,完成遮盖
}
}
} // 内容流自动关闭并保存更改
// 6. 保存修改后的PDF
document.save(output);
}
handler();
}
效果:

10. 常见问题与注意事项
Q1:为什么有些关键字找不到?
PDF 中的文字存储顺序不一定是视觉阅读顺序,同一行文字可能被拆成多个文本块,中间插入其他文本块。遇到此问题可以先用 PdfTextExtractor 提取文本,确认关键字确实存在,再排查文本块分割问题。
Q2:中文关键字能正常匹配吗?
可以。iText 7 的字符渲染事件对 Unicode 字符(包括中文)完全支持,CharacterRenderInfo 能正确返回每个汉字的边界框。
Q3:加密 PDF 怎么处理?
在创建 PdfReader 时传入密码:
new PdfReader(pdfPath, new ReaderProperties().setPassword("your_password".getBytes()))
Q4:坐标精度够用吗?float 精度为约 7 位有效数字,对于 PDF 坐标(通常在 0~1000 pt 范围内)完全足够。如有极高精度需求可改用 double。
11. 总结
本文从 PDF 坐标系的底层原理出发,完整推导并实现了PDF字符串定位问题:
- 理解 PDF 坐标系:左下角原点,Y 轴向上,单位为磅(pt)
- 事件驱动字符捕获:利用 iText 7 的
IEventListener+PdfCanvasProcessor,逐字符获取精确坐标 - 带回溯的前缀匹配:处理跨文本块、重叠前缀等边界情况
- 旋转修正 + 坐标系转换:让结果同时具备 PDF 原生坐标(写入用)和屏幕坐标(显示用)
这个方案已在生产环境的采购单翻译、PDF 内容打码等场景中稳定运行,欢迎结合实际需求扩展使用。
更多推荐

所有评论(0)