PDFBox移除二维码后文件体积增大及大PDF处理缓慢问题求助
PDFBox移除二维码后体积暴涨与性能优化问题
问题现象
使用PDFBox 2.0.27处理10000页PDF时出现两个核心问题:
- 原文件大小811MB,移除二维码后体积增至2.44GB,不符合体积减小或不变的预期
- 遍历所有页面耗时过长,处理效率低下
体积暴涨原因分析
- 内容流未压缩
代码中创建PDStream时默认未启用压缩,原始PDF的内容流是压缩状态,重新写入后变为未压缩,直接导致体积剧增。 - 资源冗余与未优化
采用创建新文档并导入页面的方式,会完整复制原文档的所有资源;即使移除了二维码XObject,未优化的资源引用和冗余对象仍会被保留。 - 资源操作未刷新缓存
直接操作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
相关产品推荐
相关产品推荐

