如何让PDFTextStripper提取PDF注释文本并按位置排序?
问题描述
我用PDFTextStripper(3.0.3版本)提取PDF文本做自定义解析,为保证文档逻辑正确,设置了按位置排序文本(比如表头在表格数据前),代码如下:
try (PDDocument document = Loader.loadPDF(file)) { final PDFTextStripper pdfStripper = new PDFTextStripper(); pdfStripper.setSortByPosition(true); text = pdfStripper.getText(document); }
最近遇到一份PDF,需要提取的大部分文本无法被PDFTextStripper获取,但在Adobe里能正常显示和复制。调试后发现这些文本存在于页面注释中(示例为PDAnnotationWidget类型),注释包含V字段存储文本值,还有Rect字段标注位置,外观流里也有SHOW_TEXT操作。现在需要让这些注释文本和普通文本一起按位置排序输出,不能单独提取后拼接。
解决方案
PDFTextStripper默认不会处理注释内容,需要自定义子类扩展它的逻辑,核心思路是:
- 重写页面处理方法,收集注释的文本及位置信息
- 把注释文本条目和常规文本条目合并后按位置排序
- 统一输出合并后的文本
自定义PDFTextStripper实现代码
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class AnnotationIncludingTextStripper extends PDFTextStripper { // 存储所有文本条目(含注释) private final List<PositionedText> allTextEntries = new ArrayList<>(); public AnnotationIncludingTextStripper() throws IOException { super(); // 关闭默认排序,我们会自行处理合并排序逻辑 setSortByPosition(false); } @Override protected void startPage(PDPage page) throws IOException { super.startPage(page); // 提取当前页面的注释文本及位置 extractAnnotationText(page); } @Override protected void writeString(String text, List<TextPosition> textPositions) throws IOException { // 收集常规文本的位置和内容 if (!text.isEmpty()) { TextPosition firstPos = textPositions.get(0); allTextEntries.add(new PositionedText( firstPos.getYDirAdj(), firstPos.getX(), text )); } } @Override public String getText(PDDocument document) throws IOException { allTextEntries.clear(); super.getText(document); // 按PDF排版逻辑排序:从上到下(Y坐标降序)、从左到右(X坐标升序) allTextEntries.sort(Comparator.comparingDouble(PositionedText::getY).reversed() .thenComparingDouble(PositionedText::getX)); // 拼接所有文本,可根据需求调整分隔符模拟原排版 StringBuilder sb = new StringBuilder(); for (PositionedText pt : allTextEntries) { sb.append(pt.getText()); sb.append(" "); } return sb.toString().trim(); } private void extractAnnotationText(PDPage page) { for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationWidget) { PDAnnotationWidget widget = (PDAnnotationWidget) annotation; // 优先从V字段提取文本,这是Adobe可复制文本的主要来源 String annotationText = widget.getCOSObject().getString("V"); if (annotationText == null || annotationText.isEmpty()) { // 若V字段为空,可扩展解析外观流中的SHOW_TEXT操作(需参考PDFTextStripper文本解析逻辑) continue; } // 转换注释的矩形坐标,匹配常规文本的位置基准 PDRectangle rect = widget.getRectangle(); float pageHeight = page.getMediaBox().getHeight(); // PDF坐标原点在左下角,转换为从上到下的Y坐标 float yPos = pageHeight - rect.getUpperRightY(); float xPos = rect.getLowerLeftX(); allTextEntries.add(new PositionedText(yPos, xPos, annotationText)); } } } // 辅助类:存储文本的位置和内容 private static class PositionedText { private final double y; private final double x; private final String text; public PositionedText(double y, double x, String text) { this.y = y; this.x = x; this.text = text; } public double getY() { return y; } public double getX() { return x; } public String getText() { return text; } } }
使用方式
try (PDDocument document = Loader.loadPDF(file)) { AnnotationIncludingTextStripper stripper = new AnnotationIncludingTextStripper(); String fullText = stripper.getText(document); // 处理提取后的文本 }
关键说明
- 优先读取注释的
V字段,比解析外观流更高效,且和Adobe复制的文本内容一致 - 坐标转换需匹配PDF的坐标系规则,确保注释文本和常规文本的位置排序统一
- 排序逻辑和PDFTextStripper默认的
sortByPosition=true行为一致,保证排版逻辑正确 - 若部分注释无
V字段,可扩展代码解析外观流中的文本绘制指令,参考PDFTextStripper处理页面内容流的逻辑
内容的提问来源于stack exchange,提问作者rveach
相关产品推荐
相关产品推荐

