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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 13:23:12