合并后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
相关产品推荐
相关产品推荐

