如何用PDFBox 3.0.0优化重复合并图片型PDF的文件大小
低内存下PDFBox 3.0.0复用图片资源合并PDF文档
问题背景
使用Java 17搭配PDFBox 3.0.0合并多个经GhostScript转换的单页图片型PDF时,重复添加同一文件会导致输出文档体积过大——默认合并逻辑会重复存储相同图片资源。现有代码已启用OPTIMIZE_RESOURCES_MODE但未解决问题,且需处理最多100个多页文件,要求在不加载所有图片到内存的前提下实现图片资源复用(同一图片仅存储一次,后续通过引用调用)。
原实现代码:
private static void mergeFiles() throws IOException { RandomAccessStreamCache.StreamCacheCreateFunction streamCache = IOUtils.createMemoryOnlyStreamCache(); PDDocument emptyDocument = createBlankValidDocument(); PDFMergerUtility pdfMergerUtility = new PDFMergerUtility(); pdfMergerUtility.setDocumentMergeMode( PDFMergerUtility.DocumentMergeMode.OPTIMIZE_RESOURCES_MODE); pdfMergerUtility.setAcroFormMergeMode(PDFMergerUtility.AcroFormMergeMode.JOIN_FORM_FIELDS_MODE); appendFile("input-gs.pdf", streamCache, emptyDocument, pdfMergerUtility); appendFile("input2-gs.pdf", streamCache, emptyDocument, pdfMergerUtility); appendFile("input-gs.pdf", streamCache, emptyDocument, pdfMergerUtility); emptyDocument.save("output-merged.pdf"); } private static void appendFile(String filename, RandomAccessStreamCache.StreamCacheCreateFunction streamCache, PDDocument emptyDocument, PDFMergerUtility pdfMergerUtility) throws IOException { File file = new File(filename); PDDocument document = Loader.loadPDF(new RandomAccessReadBuffer(new FileInputStream(file)), streamCache); pdfMergerUtility.appendDocument(emptyDocument, document); } public static PDDocument createBlankValidDocument() { try { RandomAccessStreamCache.StreamCacheCreateFunction streamCache = IOUtils.createMemoryOnlyStreamCache(); PDDocument document = new PDDocument(streamCache); // Conformance level + Part XMPMetadata xmp = XMPMetadata.createXMPMetadata(); PDFAIdentificationSchema id = xmp.createAndAddPDFAIdentificationSchema(); id.setConformance("A"); id.setPart(1); // Metadata setMetadata(document, xmp); return document; } catch (Exception e) { e.printStackTrace(); } return null; } private static void setMetadata(PDDocument document, XMPMetadata xmp) throws TransformerException, IOException { PDDocumentCatalog catalogue = document.getDocumentCatalog(); XmpSerializer serializer = new XmpSerializer(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); serializer.serialize(xmp, outputStream, true); PDMetadata metadata = new PDMetadata(document); metadata.importXMPMetadata(outputStream.toByteArray()); catalogue.setMetadata(metadata); }
低内存占用解决方案
核心思路:用磁盘缓存替代内存缓存减少内存压力,基于文件哈希标识图片资源,缓存已导入的图片引用,合并时优先复用缓存资源
1. 替换内存缓存为磁盘缓存
原代码的IOUtils.createMemoryOnlyStreamCache()会将所有PDF数据加载到内存,换成磁盘缓存后,数据会写入临时文件,避免内存溢出:
// 创建基于磁盘的流缓存,使用系统临时目录 RandomAccessStreamCache.StreamCacheCreateFunction streamCache = IOUtils.createTempFileStreamCache(new File(System.getProperty("java.io.tmpdir")));
2. 实现图片资源缓存工具类
通过文件MD5哈希作为唯一标识,缓存已导入目标文档的图片对象,确保同一图片仅存储一次:
import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.graphics.image.PDXObjectImage; import org.apache.pdfbox.io.IOUtils; import java.io.File; import java.io.FileInputStream; import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; public class PdfImageCache { // 缓存:文件哈希 -> 目标文档中的图片引用 private final Map<String, PDXObjectImage> imageCache = new HashMap<>(); private final PDDocument targetDoc; public PdfImageCache(PDDocument targetDoc) { this.targetDoc = targetDoc; } // 计算文件MD5哈希作为唯一标识 public String getFileHash(File file) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[8192]; int readLen; while ((readLen = fis.read(buffer)) != -1) { md.update(buffer, 0, readLen); } } byte[] hashBytes = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : hashBytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } // 获取或导入图片资源到目标文档 public PDXObjectImage getOrImportImage(File pdfFile) throws Exception { String fileHash = getFileHash(pdfFile); if (imageCache.containsKey(fileHash)) { return imageCache.get(fileHash); } // 用磁盘缓存加载源PDF,仅处理第一页(单页图片PDF) try (PDDocument sourceDoc = Loader.loadPDF(pdfFile, IOUtils.createTempFileStreamCache(new File(System.getProperty("java.io.tmpdir"))))) { PDPage sourcePage = sourceDoc.getPage(0); // 提取页面中的图片资源(GhostScript转换的PDF仅含一个图片XObject) PDXObjectImage sourceImage = (PDXObjectImage) Stream.of(sourcePage.getResources().getXObjectNames()) .map(name -> sourcePage.getResources().getXObject(name)) .filter(obj -> obj instanceof PDXObjectImage) .findFirst() .orElseThrow(() -> new IllegalArgumentException("目标PDF中未找到图片资源")); // 将图片导入目标文档,获取可复用的引用 PDPage importedPage = targetDoc.importPage(sourcePage); PDXObjectImage importedImage = (PDXObjectImage) importedPage.getResources().getXObject(sourceImage.getName()); imageCache.put(fileHash, importedImage); return importedImage; } } }
3. 修改合并逻辑,复用缓存资源
不再依赖PDFMergerUtility的默认追加逻辑,手动创建页面并绘制复用的图片:
private static void mergeFiles() throws Exception { // 使用磁盘缓存创建目标文档 RandomAccessStreamCache.StreamCacheCreateFunction streamCache = IOUtils.createTempFileStreamCache(new File(System.getProperty("java.io.tmpdir"))); PDDocument targetDoc = createBlankValidDocument(streamCache); if (targetDoc == null) { throw new RuntimeException("无法创建空白PDF文档"); } PdfImageCache imageCache = new PdfImageCache(targetDoc); // 待合并的文件列表(包含重复项) String[] filesToMerge = {"input-gs.pdf", "input2-gs.pdf", "input-gs.pdf"}; for (String filename : filesToMerge) { File file = new File(filename); PDXObjectImage image = imageCache.getOrImportImage(file); // 创建与图片尺寸一致的新页面 PDPage newPage = new PDPage(image.getMediaBox()); targetDoc.addPage(newPage); // 在新页面绘制复用的图片 try (PDPageContentStream contentStream = new PDPageContentStream(targetDoc, newPage)) { contentStream.drawImage(image, 0, 0, image.getMediaBox().getWidth(), image.getMediaBox().getHeight()); } } targetDoc.save("output-merged.pdf"); targetDoc.close(); } // 修改createBlankValidDocument方法,支持传入自定义流缓存 public static PDDocument createBlankValidDocument(RandomAccessStreamCache.StreamCacheCreateFunction streamCache) { try { PDDocument document = new PDDocument(streamCache); XMPMetadata xmp = XMPMetadata.createXMPMetadata(); PDFAIdentificationSchema id = xmp.createAndAddPDFAIdentificationSchema(); id.setConformance("A"); id.setPart(1); setMetadata(document, xmp); return document; } catch (Exception e) { e.printStackTrace(); return null; } } // 原setMetadata方法保持不变 private static void setMetadata(PDDocument document, XMPMetadata xmp) throws TransformerException, IOException { PDDocumentCatalog catalogue = document.getDocumentCatalog(); XmpSerializer serializer = new XmpSerializer(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); serializer.serialize(xmp, outputStream, true); PDMetadata metadata = new PDMetadata(document); metadata.importXMPMetadata(outputStream.toByteArray()); catalogue.setMetadata(metadata); }
关键优化说明
- 磁盘缓存:所有PDF流数据写入临时文件,避免内存堆积,适合处理大量文件
- 资源复用:同一文件仅导入一次图片资源,后续合并直接复用引用,大幅减少输出体积
- 按需加载:每次仅加载源PDF的第一页,处理完成后立即关闭源文档,及时释放资源
内容的提问来源于stack exchange,提问作者user3441233
相关产品推荐
相关产品推荐

