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

合并后PDF体积过大求助:Kotlin/Java/Spring Boot场景解决方案

PDF合并后体积过大的解决方案(基于Kotlin/Spring Boot + PDFBox)

问题背景

我有一个基于Kotlin/Java/Spring Boot的项目,流程为:接收用户上传的PDF文件,使用PDDocument拆分页面,根据页面内容将同类页面通过PDFMergerUtility合并,最终将合并后的PDF转成字节数组存入数据库。用户可下载该合并后的PDF,必要时重新上传。但合并后的PDF体积比拆分前的原文件大很多,试过Stack Overflow上的Android PdfDocument体积优化、Java PDF压缩方法等方案均无效,现寻求可行解决方案。

原核心代码:

//file: MultipartFile -> file is send from front-end using API

var inpStream: InputStream = file.getInputStream()
inpStream = file.getInputStream()
pdfDocument = PDDocument.load(inpStream)


// splitting the pages of a PDF document
pagesPdf = splitter.split(pdfDocument)
val n = pdfDocument.numberOfPages

val batchSize:Int = 200
val finalBatchSize: Int = n % batchSize
val numOfBatch: Int = (n - finalBatchSize) / batchSize
val batchFinal: Int = if (finalBatchSize == 0) numOfBatch else (numOfBatch + 1)
var batchNo: Int = 1
var startPage: Int
var endPage: Int = 0
while (batchNo <= batchFinal) {
    startPage = endPage + 1
    if (batchNo > numOfBatch) {
        endPage = endPage + finalBatchSize
    } else {
        endPage = endPage + batchSize
    }
    val splitter:Splitter = Splitter()
    splitter.setStartPage(startPage)
    splitter.setEndPage(endPage)

    // splitting the pages of a PDF document
    pagesPdf = splitter.split(pdfDocument)

    batchNo++
    i = startPage
    var groupPage: Int = i
    var pageNo = 0
    
    
    var pdfMerger: PDFMergerUtility = PDFMergerUtility()
        var mergedFileByteArrOut: ByteArrayOutputStream = ByteArrayOutputStream()
        pdfMerger.setDestinationStream(mergedFileByteArrOut)
    var fileObj:ByteArray? = null,
    for (pd in pagesPdf) {
        pageNo++;
        if (!pd.isEncrypted) {
        val stripper = PDFTextStripper()
        //CODE TO GET CONTENT
        
        if(condition1 == true){
          var fileByteArrOut: ByteArrayOutputStream = ByteArrayOutputStream()
              pd.save(fileByteArrOut)
              pd.close()
              var fileByteArrIn: ByteArrayInputStream = ByteArrayInputStream(fileByteArrOut.toByteArray())
              pdfMerger.addSource(fileByteArrIn)
          fileObj = fileByteArrOut.toByteArray(),
        } 
        if(condition2 == true){
        
            //I want to compress fileObj  first before save to DB
            //code to save to DB
            
            fileObj = null
            pdfMerger = PDFMergerUtility()
                      mergedFileByteArrOut= ByteArrayOutputStream()
                      pdfMerger.setDestinationStream(mergedFileByteArrOut)
        }
      }
    }

问题根源分析

合并后体积暴涨的核心原因:

  • 拆分页面后单独保存子文档时,每个小PDDocument会重复嵌入原PDF中的共享资源(如字体、图片),合并后这些资源被多次存储
  • PDFMergerUtility默认不启用压缩优化,会保留冗余的PDF结构数据
  • 频繁的ByteArrayOutputStream/InputStream中间操作会引入不必要的格式冗余

可行优化方案

核心优化点

  • 复用共享资源:拆分页面时直接获取页面对象,避免单独保存子文档导致资源重复嵌入
  • 启用多维度压缩:对合并后的文档统一设置内容流、字体、图片的压缩规则
  • 简化中间流程:减少单个页面的IO写入操作,直接在内存中完成页面合并
  • 禁用增量保存:避免生成冗余的更新日志数据

优化后的代码

// 处理上传的MultipartFile
val pdfDocument = PDDocument.load(file.inputStream)
try {
    val splitter = Splitter()
    // 按单页拆分,保留原文档资源引用
    splitter.setSplitAtPage(1)
    val pageDocs = splitter.split(pdfDocument)

    // 按业务条件分组页面(替换为你的实际分组逻辑)
    val pageGroups = pageDocs.groupBy { pdDoc ->
        if (!pdDoc.isEncrypted) {
            val stripper = PDFTextStripper()
            val content = stripper.getText(pdDoc)
            // 示例:提取内容关键标识作为分组key
            content.takeIf { it.isNotEmpty() }?.substring(0, 100)?.trim() ?: "empty"
        } else {
            "encrypted"
        }
    }

    // 处理每个分组,合并并压缩
    for ((_, groupDocs) in pageGroups) {
        val mergedDoc = PDDocument()
        try {
            // 直接合并页面,避免中间IO操作
            groupDocs.forEach { pageDoc ->
                val page = pageDoc.getPage(0)
                mergedDoc.addPage(page)
                pageDoc.close() // 及时关闭单个页面文档,释放资源
            }

            // 1. 压缩页面内容流
            mergedDoc.documentCatalog.pages.children.forEach { page ->
                page.contents?.let { contentStream ->
                    contentStream.setFilter(COSName.FLATE_DECODE)
                    page.cosObject.setNeedToBeUpdate(true)
                }

                // 2. 子集化嵌入字体(只保留用到的字符)
                page.resources.fonts?.values?.forEach { font ->
                    if (font is PDType0Font) {
                        font.subset()
                    }
                }

                // 3. 压缩图片资源
                page.resources.xObjects?.values?.forEach { xobj ->
                    if (xobj is PDImageXObject) {
                        xobj.stream.setFilter(COSName.FLATE_DECODE)
                    }
                }
            }

            // 4. 禁用增量保存,减少冗余数据
            mergedDoc.saveIncremental = false
            // 移除不必要的安全设置
            mergedDoc.setAllSecurityToBeRemoved(true)

            // 转成字节数组存入数据库
            val byteArrayOut = ByteArrayOutputStream()
            mergedDoc.save(byteArrayOut)
            val finalBytes = byteArrayOut.toByteArray()

            // 执行存入数据库逻辑
            // saveToDatabase(finalBytes)

        } finally {
            mergedDoc.close()
        }
    }
} finally {
    pdfDocument.close()
}

额外优化建议

  • 若图片占比大,可引入第三方图片压缩库(如Thumbnails)对PDF中的图片进行更极致的压缩后再替换
  • 对于大体积PDF,可分批次处理时复用资源池,避免重复加载资源
  • 存储时可对字节数组再做一次GZIP压缩,进一步减小存储体积(下载时需解压)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 05:36:18