【问题标题】:Fastest way to count PDF images using PDFBox 2.x使用 PDFBox 2.x 计算 PDF 图像的最快方法
【发布时间】:2016-07-19 17:55:47
【问题描述】:

我们偶尔会遇到一些非常大的 PDF,其中包含整页高分辨率图像(文档扫描的结果)。例如,我有一个包含 3500 多张图像的 1.7GB PDF。加载文档大约需要 50 秒,但计算图像大约需要 15 分钟。

我确定这是因为图像字节是作为 API 调用的一部分读取的。有没有办法在不实际读取图像字节的情况下提取图像计数?

PDFBox 版本:2.0.2

示例代码:

@Test
public void imageCountIsCorrect() throws Exception {
    PDDocument pdf = readPdf();
    try {
        assertEquals(3558, countImages(pdf));
        // assertEquals(3558, countImagesWithExtractor(pdf));
    } finally {
        if (pdf != null) {
            pdf.close();
        }
    }
}

protected PDDocument readPdf() throws IOException {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    FileInputStream stream = new FileInputStream("large.pdf");
    PDDocument pdf;
    try {
        pdf = PDDocument.load(stream, MemoryUsageSetting.setupMixed(1024 * 1024 * 250));
    } finally {
        stream.close();
    }

    stopWatch.stop();
    log.info("PDF loaded: time={}s", stopWatch.getTime() / 1000);
    return pdf;
}


protected int countImages(PDDocument pdf) throws IOException {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    int imageCount = 0;
    for (PDPage pdPage : pdf.getPages()) {
        PDResources pdResources = pdPage.getResources();
        for (COSName cosName : pdResources.getXObjectNames()) {
            PDXObject xobject = pdResources.getXObject(cosName);
            if (xobject instanceof PDImageXObject) {
                imageCount++;
                if (imageCount % 100 == 0) {
                    log.info("Found image: #" + imageCount);
                }
            }
        }
    }

    stopWatch.stop();
    log.info("Images counted: time={}s,imageCount={}", stopWatch.getTime() / 1000, imageCount);
    return imageCount;
}

如果我将 countImages 方法更改为依赖 COSName,计数会在不到 1 秒的时间内完成,但我对依赖名称前缀有点不确定。这似乎是 pdf 编码器的副产品,而不是 PDFBox(我在他们的代码中找不到对它的任何引用):

if (cosName.getName().startsWith("QuickPDFIm")) {
    imageCount++;
}

【问题讨论】:

  • 附带说明,您的代码只计算每页的即时位图图像 resources。它既不包含内联图像,也不包含 xobject 或模式中包含的图像。另一方面,图像资源不需要在页面上使用。因此,有时您还会计算太多图像。对于通用解决方案,您需要考虑内容流。
  • 啊,这可以解释我在使用 PDFGraphicsStreamEngine 的自定义实现来计算图像时发现的图像计数之间的一些不一致。我将深入研究该代码以找出我缺少的内容。谢谢!
  • 我的想法是修改 ExtractImages 示例并删除所有创建图像对象的内容,并使用 DrawObject extends GraphicsOperatorProcessor 处理器调用 addOperator(new DrawObject());,如果它是图像,则不会创建 xobjects 但会遵循表格。参见 org.apache.pdfbox.contentstream.operator.DrawObject 的源码。
  • 我明白了...我最初的失败是使用 NOOP 方法扩展 PDFGraphicsStreamEngine,但 drawImage 增加了计数。看起来我应该使用您所描述的 addOperator 扩展 PDFStreamEngine 。感谢您朝正确的方向轻推!
  • 根据您的反馈添加了答案。谢谢!

标签: java pdf pdfbox


【解决方案1】:

所以以前的方法有一些额外的缺陷(可能会错过内联图像等)。感谢 mkl 和 Tilman Hausherr 的反馈!

TIL - PDF object streams contain useful operator codes!

我的新方法扩展了 PDFStreamEngine 并为在 PDF 内容流中找到的每个“Do”(绘制对象)运算符增加一个 imageCount。使用这种方法,图像计数只需几百毫秒:

public class PdfImageCounter extends PDFStreamEngine {
    protected int documentImageCount = 0;

    public int getDocumentImageCount() {
        return documentImageCount;
    }

    public PdfImageCounter() {
        addOperator(new OperatorProcessor() {
            @Override
            public void process(Operator operator, List<COSBase> arguments) throws IOException {
                if (arguments.size() < 1) {
                    throw new MissingOperandException(operator, arguments);
                }
                if (isImage(arguments.get(0))) {
                    documentImageCount++;
                }
            }

            protected Boolean isImage(COSBase base) {
                return (base instanceof COSName) &&
                        context.getResources().isImageXObject((COSName)base);
            }

            @Override
            public String getName() {
                return "Do";
            }
        });
    }
}

为每个页面调用它:

protected int countImagesWithProcessor(PDDocument pdf) throws IOException {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    PdfImageCounter counter = new PdfImageCounter();
    for (PDPage pdPage : pdf.getPages()) {
        counter.processPage(pdPage);
    }

    stopWatch.stop();
    int imageCount = counter.getDocumentImageCount();
    log.info("Images counted: time={}s,imageCount={}", stopWatch.getTime() / 1000, imageCount);
    return imageCount;
}

【讨论】:

  • 但是你不是在抓取不是图像的操作数,例如PDFormX 对象。看看 org.apache.pdfbox.contentstream.operator.DrawObject。这个有一个有趣的策略来避免创建图像。
  • 谢谢!我更新了答案以跳过不是真正图像的对象。
  • 我的意思不止于此。如果您点击一个表单或透明度组,您也需要处理它,就像 org.apache.pdfbox.contentstream.operator.DrawObject 一样。这些也可以包含图像。接下来要做的是确保图像是唯一的,使用 Set.
猜你喜欢
  • 2021-11-13
  • 2012-08-25
  • 2014-02-21
  • 1970-01-01
  • 1970-01-01
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 2023-02-10
相关资源
最近更新 更多