You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Apache PDFBox读取扫描PDF时文本乱序/缺失的技术咨询

我来帮你拆解这个PDF文本提取的常见问题——扫描生成的PDF确实经常踩这些坑,咱们一个个说清楚:

1. 此类问题产生的根本原因

扫描生成的PDF本质是图像转PDF,问题几乎都出在OCR(光学字符识别)生成的文本层上,具体有这几个核心原因:

  • 文本绘制顺序≠视觉阅读顺序:PDF的文本内容是按绘制指令的顺序存储的,很多OCR工具为了识别效率,不会严格按照人眼从上到下、从左到右的顺序生成指令——比如先扫右侧再左侧,或者跨行乱序,导致提取时顺序彻底混乱。
  • 文本片段缺失/未嵌入:OCR识别不准确时,部分区域的字符可能没被识别成可提取的文本;更极端的情况是,有些工具生成的PDF只有图像层,根本没生成文本层(这种情况你在Adobe里也选不到任何文本)。
  • 文本块边界盒(BBox)错误:PDF里的文本元素靠边界盒定位,如果OCR计算的边界盒偏移、重叠,PDFBox的默认提取逻辑(比如按坐标排序)就会出错,导致提取时跳过部分内容。
2. 如何用Java编程检测该问题

你可以基于PDFBox自定义文本提取器,监控文本块的顺序和覆盖范围,来判断是否存在问题:

检测文本顺序混乱

继承PDFTextStripper,记录每个文本片段的坐标,再对比视觉排序后的顺序是否和原提取顺序一致:

public class TextOrderChecker extends PDFTextStripper {
    private List<TextChunk> textChunks = new ArrayList<>();

    public TextOrderChecker() throws IOException {
        super();
    }

    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
        TextPosition firstPos = textPositions.get(0);
        // 转换为视觉坐标系(PDF默认y轴从下往上,YDirAdj转为从上到下)
        float visualY = firstPos.getYDirAdj();
        float visualX = firstPos.getXDirAdj();
        textChunks.add(new TextChunk(text, visualX, visualY));
    }

    // 判断是否存在顺序问题:对比原顺序和视觉排序后的顺序
    public boolean hasOrderIssue() {
        List<TextChunk> sortedByVisual = new ArrayList<>(textChunks);
        // 按从上到下(y降序)、从左到右(x升序)排序
        sortedByVisual.sort((a, b) -> {
            int yCompare = Float.compare(b.getY(), a.getY());
            if (yCompare != 0) return yCompare;
            return Float.compare(a.getX(), b.getX());
        });

        for (int i = 0; i < textChunks.size(); i++) {
            if (!textChunks.get(i).getText().equals(sortedByVisual.get(i).getText())) {
                return true;
            }
        }
        return false;
    }

    // 内部类存储文本片段和坐标
    private static class TextChunk {
        private final String text;
        private final float x;
        private final float y;

        public TextChunk(String text, float x, float y) {
            this.text = text;
            this.x = x;
            this.y = y;
        }

        public String getText() { return text; }
        public float getX() { return x; }
        public float getY() { return y; }
    }
}

检测文本缺失

计算文本覆盖的区域占页面总面积的比例,如果比例过低(比如<5%),大概率存在文本缺失:

// 在TextOrderChecker中添加该方法
public float getTextCoverageRatio(PDDocument document) throws IOException {
    PDPage page = document.getPage(0); // 以第一页为例,可扩展为多页
    Rectangle pageRect = page.getMediaBox();
    float pageArea = pageRect.getWidth() * pageRect.getHeight();
    float totalTextArea = 0;

    for (TextChunk chunk : textChunks) {
        // 简化计算:按字符数估算宽度,固定行高
        float approxWidth = chunk.getText().length() * 8;
        float approxHeight = 12;
        totalTextArea += approxWidth * approxHeight;
    }
    return totalTextArea / pageArea;
}

使用示例

try (PDDocument doc = PDDocument.load(new File("your-scanned.pdf"))) {
    TextOrderChecker checker = new TextOrderChecker();
    checker.getText(doc);
    
    boolean hasOrderProblem = checker.hasOrderIssue();
    float coverageRatio = checker.getTextCoverageRatio(doc);
    
    System.out.println("文本顺序是否异常:" + hasOrderProblem);
    System.out.println("文本覆盖率:" + String.format("%.2f%%", coverageRatio * 100));
} catch (IOException e) {
    e.printStackTrace();
}
3. 潜在解决思路
  • 重新OCR生成规范文本层:这是最彻底的方案,用专业OCR工具(比如Tesseract、Adobe Acrobat的OCR功能)重新识别PDF的图像层,生成符合阅读顺序的文本层。
  • 修复现有PDF的绘制顺序:解析PDF的内容流,重新排序文本绘制指令,使其匹配视觉阅读顺序(难度较高,适合对PDF结构熟悉的场景)。
  • 基于坐标重排提取文本:提取时完全忽略PDF的绘制顺序,按文本块的坐标(x,y)重新排序后再拼接。
  • 换用更智能的提取工具:比如Apache Tika(基于PDFBox做了优化)、iText 7,它们的文本排序逻辑可能更适配扫描PDF的场景。
4. 除了设置readSorted=true外的修复方案

自定义PDFTextStripper的排序逻辑

重写sortTextPositions方法,优化行对齐判断(比如把y坐标接近的文本视为同一行),适配多列、不规则布局:

public class CustomSortedStripper extends PDFTextStripper {
    public CustomSortedStripper() throws IOException {
        super();
        this.setSortByPosition(true); // 开启基础排序
    }

    @Override
    protected List<TextPosition> sortTextPositions(List<TextPosition> textPositions) {
        return textPositions.stream()
                .sorted((a, b) -> {
                    // 同一行判断:y坐标差小于5像素视为同一行
                    if (Math.abs(a.getYDirAdj() - b.getYDirAdj()) < 5) {
                        return Float.compare(a.getXDirAdj(), b.getXDirAdj());
                    }
                    // 不同行按从上到下排序
                    return Float.compare(b.getYDirAdj(), a.getYDirAdj());
                })
                .collect(Collectors.toList());
    }
}

结合Tesseract做二次OCR

如果原文本层损坏严重,先提取PDF的图像,用Tesseract重新识别:

// 提取PDF页面为图像
PDPage page = document.getPage(0);
BufferedImage pageImage = page.convertToImage(BufferedImage.TYPE_INT_RGB, 300); // 300DPI提高识别精度

// Tesseract识别文本
Tesseract tesseract = new Tesseract();
tesseract.setDatapath("tessdata"); // 设置Tesseract语言包路径
String correctedText = tesseract.doOCR(pageImage);

你可以直接用识别结果作为提取内容,也可以把识别后的文本重新嵌入PDF。

使用PDFTextStripperByArea适配固定布局

如果PDF是多列或分块布局,手动划分区域提取后按顺序拼接:

PDFTextStripperByArea areaStripper = new PDFTextStripperByArea();
// 按页面布局定义区域(示例:左右两列)
areaStripper.addRegion("left-column", new Rectangle(0, 0, 300, 842));
areaStripper.addRegion("right-column", new Rectangle(300, 0, 300, 842));
areaStripper.extractRegions(page);

// 按视觉顺序拼接文本
String fullText = areaStripper.getTextForRegion("left-column") + "\n" + areaStripper.getTextForRegion("right-column");

内容的提问来源于stack exchange,提问作者Jay

火山引擎 最新活动