You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 17:52:29