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

PDFBox移除二维码后文件体积增大及大PDF处理缓慢问题求助

PDFBox移除二维码后体积暴涨与性能优化问题

问题现象

使用PDFBox 2.0.27处理10000页PDF时出现两个核心问题:

  • 原文件大小811MB,移除二维码后体积增至2.44GB,不符合体积减小或不变的预期
  • 遍历所有页面耗时过长,处理效率低下

体积暴涨原因分析

  1. 内容流未压缩
    代码中创建PDStream时默认未启用压缩,原始PDF的内容流是压缩状态,重新写入后变为未压缩,直接导致体积剧增。
  2. 资源冗余与未优化
    采用创建新文档并导入页面的方式,会完整复制原文档的所有资源;即使移除了二维码XObject,未优化的资源引用和冗余对象仍会被保留。
  3. 资源操作未刷新缓存
    直接操作COS字典移除XObject后,未刷新资源对象的缓存,导致资源变更未正确生效,部分冗余数据仍被保留。

体积优化方案

1. 启用内容流压缩

创建PDStream时指定压缩算法,或在保存文档时全局启用压缩:

// 单个PDStream启用压缩
PDStream newContents = new PDStream(document, COSName.FLATE_DECODE);

// 保存文档时启用全压缩
SaveOptions saveOptions = SaveOptions.builder()
    .setCompressContentStreams(true)
    .setCompressXmpMetadata(true)
    .setOptimizeResources(true) // 自动移除未使用的资源
    .build();
document.save(destPath, saveOptions);

2. 修正资源移除逻辑

移除XObject后刷新资源缓存,确保变更生效:

// 移除XObject后执行
pdResources.getCOSObject().setNeedToBeUpdated(true);
pdResources.clearCache();

3. 避免创建新文档

直接在原文档上修改页面,减少资源复制的开销,避免冗余数据引入。

性能优化建议

1. 直接修改原文档替代新建文档

原代码创建新文档并导入所有页面,会复制整个文档资源,耗时巨大。改为直接操作原文档:

PDDocument pdDocument = PDDocument.load(new File(aBarcodeVO.getSourceFilePath()));
boolean hasModified = false;
int pageIndex = 0;
for (PDPage pdPage : pdDocument.getPages()) {
    String imgUniqueId = aBarcodeVO.getImgUniqueId().concat(String.valueOf(pageIndex++));
    if (removeQRCodeImage(pdDocument, pdPage, imgUniqueId)) {
        hasModified = true;
    }
}
if (hasModified) {
    SaveOptions saveOptions = SaveOptions.builder()
        .setCompressContentStreams(true)
        .setOptimizeResources(true)
        .build();
    pdDocument.save(aBarcodeVO.getDestinationFilePath(true), saveOptions);
}
pdDocument.close();

2. 复用XMP解析器

不要每次解析图片都创建新的DomXmpParser,全局复用一个实例减少对象创建开销:

// 类级别定义复用实例
private static final DomXmpParser XMP_PARSER = new DomXmpParser();

// 方法内使用
XMPMetadata xmpMetadata = XMP_PARSER.parse(pdImageXObject.getMetadata().toByteArray());

3. 减少调试日志输出

10000页循环中频繁输出调试日志会产生大量IO开销,建议关闭调试日志或改为批量统计输出。

4. 并行处理页面(谨慎使用)

若PDF页面资源相互独立,可使用线程池并行处理,但需注意PDFBox的线程安全限制:

ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<Boolean>> futures = new ArrayList<>();
int[] pageIndex = {0};
for (PDPage pdPage : pdDocument.getPages()) {
    int idx = pageIndex[0]++;
    futures.add(executor.submit(() -> {
        String imgUniqueId = aBarcodeVO.getImgUniqueId().concat(String.valueOf(idx));
        return removeQRCodeImage(pdDocument, pdPage, imgUniqueId);
    }));
}
boolean hasModified = false;
for (Future<Boolean> future : futures) {
    if (future.get()) {
        hasModified = true;
    }
}
executor.shutdown();

注意:PDFBox的PDPage和PDDocument并非完全线程安全,并行处理前需充分测试。

修改后的removeQRCodeImage方法示例

private static final DomXmpParser XMP_PARSER = new DomXmpParser();

public static boolean removeQRCodeImage(PDDocument document, PDPage page, String imgUniqueId) throws Exception {
    String qrCodeCosName = null;
    PDResources pdResources = page.getResources();
    boolean hasQRCodeOnPage = false;

    Iterator<COSName> xObjectIterator = pdResources.getXObjectNames().iterator();
    while (xObjectIterator.hasNext()) {
        COSName propertyName = xObjectIterator.next();
        if (!pdResources.isImageXObject(propertyName)) {
            continue;
        }
        try {
            PDXObject o = pdResources.getXObject(propertyName);
            if (o instanceof PDImageXObject) {
                PDImageXObject pdImageXObject = (PDImageXObject) o;
                if (pdImageXObject.getMetadata() != null) {
                    XMPMetadata xmpMetadata = XMP_PARSER.parse(pdImageXObject.getMetadata().toByteArray());
                    if (xmpMetadata.getDublinCoreSchema() != null 
                        && StringUtils.isNoneBlank(xmpMetadata.getDublinCoreSchema().getTitle())
                        && xmpMetadata.getDublinCoreSchema().getTitle().contains("_barcodeimg_")) {
                        xObjectIterator.remove();
                        qrCodeCosName = propertyName.getName();
                        hasQRCodeOnPage = true;
                        pdResources.getCOSObject().setNeedToBeUpdated(true);
                        pdResources.clearCache();
                        break; // 假设每页仅一个二维码,找到后直接退出循环
                    }
                }
            }
        } catch (IOException e) {
            log.error("Exception in removeQRCodeImage() while extracting QR image:" + e, e);
        }
    }

    if (qrCodeCosName == null) {
        return false;
    }

    PDFStreamParser parser = new PDFStreamParser(page);
    parser.parse();
    List<Object> tokens = parser.getTokens();
    List<Object> newTokens = new ArrayList<>();

    for (int j = 0; j < tokens.size(); j++) {
        Object token = tokens.get(j);
        if (token instanceof Operator) {
            Operator op = (Operator) token;
            if (op.getName().equals("Do")) {
                COSName cosName = (COSName) tokens.get(j - 1);
                if (cosName.getName().equals(qrCodeCosName)) {
                    newTokens.remove(newTokens.size() - 1);
                    continue;
                }
            }
        }
        newTokens.add(token);
    }

    PDStream newContents = new PDStream(document, COSName.FLATE_DECODE);
    try (OutputStream out = newContents.createOutputStream()) {
        ContentStreamWriter writer = new ContentStreamWriter(out);
        writer.writeTokens(newTokens);
    }
    page.setContents(newContents);
    return hasQRCodeOnPage;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 10:32:18