現(xiàn)圖片合并PDF:內(nèi)存優(yōu)化與壓縮實(shí)踐)
1. 項(xiàng)目整體思路拆解需求到底是什么先說(shuō)一個(gè)容易被低估的事實(shí)把多張圖片合并成PDF看起來(lái)只是調(diào)個(gè)接口、寫(xiě)個(gè)循環(huán)的事但實(shí)際落地時(shí)你會(huì)發(fā)現(xiàn)真正的問(wèn)題全藏在細(xì)節(jié)里——圖片尺寸不一、內(nèi)存占用、PDF文件體積失控、圖片方向錯(cuò)亂、透明通道變黑底……哪一個(gè)都能讓你本來(lái)半小時(shí)能跑完的活兒變成一整天的返工。這個(gè)項(xiàng)目的核心需求可以拆成三層第一層最基本的輸入輸出。給定一組圖片可能是JPG、PNG也可能混合按指定順序合并成一個(gè)PDF文件每一張圖片占用PDF的一頁(yè)。這是所有方案的共同底線。第二層工程化約束。圖片數(shù)量可能是幾張也可能是幾千張圖片來(lái)源可能是本地文件、Base64字符串、遠(yuǎn)程URL輸出的PDF需要控制體積不能一張高分辨率照片直接變成幾十MB的怪獸文件內(nèi)存占用要可控不能在處理百?gòu)堃陨系膱D片時(shí)直接把堆內(nèi)存吃滿。第三層體驗(yàn)與擴(kuò)展。最終使用者可能希望每頁(yè)圖片自適應(yīng)鋪滿頁(yè)面、希望在圖片之間插入空白頁(yè)或自定義頁(yè)眉頁(yè)腳、希望圖片按指定尺寸擺放比如證件掃描件要保證物理尺寸一致。這些需求會(huì)直接影響技術(shù)選型。從技術(shù)方案上看Java生態(tài)里能實(shí)現(xiàn)圖片轉(zhuǎn)PDF的主流路徑有三個(gè)iText系列iText 5、iText 7、Apache PDFBox、以及OpenPDF。我在實(shí)際項(xiàng)目里三個(gè)都試過(guò)簡(jiǎn)單說(shuō)下取舍邏輯。iText功能最強(qiáng)對(duì)圖片壓縮、頁(yè)面布局、字體嵌入的控制粒度最細(xì)但iText的AGPL授權(quán)對(duì)很多公司來(lái)說(shuō)是個(gè)繞不開(kāi)的坎——除非你愿意把項(xiàng)目開(kāi)源或者購(gòu)買(mǎi)商業(yè)授權(quán)。OpenPDF是iText 4的社區(qū)分支授權(quán)相對(duì)寬松但功能停留在比較老的形態(tài)遇到特殊需求時(shí)文檔少、擴(kuò)展也麻煩。PDFBox是Apache基金會(huì)的項(xiàng)目Apache 2.0許可完全不用操心授權(quán)問(wèn)題對(duì)圖片PDF這種基礎(chǔ)需求支持得很穩(wěn)唯一的缺點(diǎn)是API沒(méi)有iText那么高冷好用但勝在邏輯清晰、調(diào)試容易。這個(gè)項(xiàng)目我用的是PDFBox理由很簡(jiǎn)單圖片轉(zhuǎn)PDF本身就是PDFBox的看家功能之一它把創(chuàng)建文檔—添加圖片頁(yè)—寫(xiě)入文件的過(guò)程封裝得足夠干凈同時(shí)沒(méi)有授權(quán)風(fēng)險(xiǎn)。如果你的項(xiàng)目只是把圖片合并成PDF沒(méi)有太多花哨的排版需求PDFBox是性價(jià)比最高的選擇。也許你會(huì)問(wèn)為什么不用更簡(jiǎn)單的方案比如直接用命令行工具如果你在服務(wù)器上手動(dòng)執(zhí)行一次用工具確實(shí)快但如果這個(gè)能力要被集成到Web應(yīng)用里、被不同的用戶以不同的參數(shù)反復(fù)調(diào)用就必須有一個(gè)程序化的實(shí)現(xiàn)。這才是這個(gè)項(xiàng)目的題中之義——把能力沉淀成代碼而不是依賴人工操作。2. 核心細(xì)節(jié)解析從圖片讀取到PDF生成的每一步2.1 圖片讀取策略按需加載別一口氣全塞進(jìn)內(nèi)存最容易踩的坑就是寫(xiě)代碼的時(shí)候圖省事先把所有圖片全部讀進(jìn)BufferedImage列表再遍歷生成PDF。圖片少比如十張以內(nèi)一點(diǎn)問(wèn)題沒(méi)有但一旦超過(guò)三五十張每張又是三四MB的手機(jī)照片原圖內(nèi)存直接就爆了。我見(jiàn)過(guò)一個(gè)真實(shí)案例某開(kāi)發(fā)者在Windows本地測(cè)得好好的一上Linux服務(wù)器處理兩百?gòu)垐D片就OOM就是吃了這個(gè)虧。正確策略是邊讀邊寫(xiě)一次只處理一張圖片寫(xiě)完一頁(yè)就釋放引用。try (PDDocument document new PDDocument()) { for (File imageFile : imageFiles) { BufferedImage image ImageIO.read(imageFile); PDPage page new PDPage(PDRectangle.A4); document.addPage(page); PDImageXObject pdImage LosslessFactory.createFromImage(document, image); try (PDPageContentStream contentStream new PDPageContentStream(document, page)) { contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight); } // 關(guān)鍵主動(dòng)釋放當(dāng)前圖片引用讓GC盡快回收 image.flush(); } document.save(outputPath); }image.flush()這一行很多人會(huì)忽略。BufferedImage的像素?cái)?shù)據(jù)存在Java堆外或者說(shuō)由ImageIO內(nèi)部管理flush之后可以釋放內(nèi)部緩沖區(qū)雖然不保證立刻回收但至少給了GC明確信號(hào)。處理大批量文件時(shí)這行代碼能讓內(nèi)存曲線平穩(wěn)不少。2.2 頁(yè)面尺寸與圖片適配圖片鋪滿頁(yè)面但不裁切每張圖片都要單獨(dú)建一個(gè)PDPage。這里有個(gè)設(shè)計(jì)決策頁(yè)面尺寸怎么定兩種常見(jiàn)策略。第一種固定頁(yè)面大小A4、Letter等圖片等比縮放后居中繪制頁(yè)面統(tǒng)一、適合打印場(chǎng)景。第二種每頁(yè)的尺寸跟隨圖片自身的寬高比生成畫(huà)冊(cè)式PDF文件打開(kāi)時(shí)每頁(yè)大小不一。實(shí)際項(xiàng)目中絕大多數(shù)需求都是第一種——尤其是要做成電子檔案或打印輸出時(shí)統(tǒng)一的頁(yè)面尺寸更符合預(yù)期。但直接在drawImage時(shí)把圖片拉伸到整個(gè)頁(yè)面會(huì)改變圖片比例人臉會(huì)變形表格線會(huì)歪。正確做法是按比例縮放計(jì)算出圖片在頁(yè)面內(nèi)的實(shí)際繪制區(qū)域。假設(shè)頁(yè)面尺寸為寬pageW、高pageH圖片原始尺寸為寬imgW、高imgH縮放比例取Math.min(pageW / imgW, pageH / imgH)這樣圖片在保持比例的前提下至少在一個(gè)維度上鋪滿頁(yè)面然后居中繪制。float scale Math.min(pageW / imgW, pageH / imgH); float drawW imgW * scale; float drawH imgH * scale; float x (pageW - drawW) / 2; float y (pageH - drawH) / 2; contentStream.drawImage(pdImage, x, y, drawW, drawH);再補(bǔ)一個(gè)更貼合真實(shí)需求的設(shè)計(jì)如果希望圖片在頁(yè)面上留一點(diǎn)邊距可以把pageW和pageH先扣除兩側(cè)邊距再參與計(jì)算。比如左右各留20磅那么實(shí)際可用寬度就是pageW - 40。這種細(xì)節(jié)在實(shí)際產(chǎn)出中很能體現(xiàn)差異。2.3 圖片格式與色彩模式透明底、CMYK這些坑怎么填I(lǐng)mageIO在讀取JPEG和PNG上基本沒(méi)問(wèn)題但有兩個(gè)高頻問(wèn)題需要特別注意。第一PNG透明背景。PDFBox在LosslessFactory.createFromImage轉(zhuǎn)換帶Alpha通道的BufferedImage時(shí)透明部分有可能變成黑色。這是因?yàn)镻DF本身支持透明但繪制方式和渲染器的兼容性參差不齊。穩(wěn)妥的做法是先把帶透明的圖片繪制到一張白色底的RGB畫(huà)布上再做轉(zhuǎn)換。BufferedImage rgb new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g rgb.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, src.getWidth(), src.getHeight()); g.drawImage(src, 0, 0, null); g.dispose();第二某些JPEG是CMYK色彩模式。ImageIO在部分JDK版本下讀取這種圖片會(huì)直接拋異常甚至導(dǎo)致整個(gè)進(jìn)程異常退出。如果業(yè)務(wù)中無(wú)法避免上傳這種圖片一個(gè)可靠的兜底方案是用工具庫(kù)做色彩空間轉(zhuǎn)換。建議在讀取圖片時(shí)使用ImageReader并設(shè)置ImageReadParam或者干脆對(duì)異常圖片做二次處理先通過(guò)Toolkit.createImage或者第三方庫(kù)解析成RGB再繼續(xù)。2.4 圖片方向問(wèn)題為什么手機(jī)拍出來(lái)的照片是歪的這是我在實(shí)際項(xiàng)目中踩過(guò)最深的一個(gè)坑。手機(jī)拍攝的照片尤其是iPhone和一些Android機(jī)型JPEG文件里會(huì)寫(xiě)入EXIF信息其中Orientation字段標(biāo)識(shí)了拍攝時(shí)的設(shè)備方向。但I(xiàn)mageIO.read()返回的BufferedImage已經(jīng)丟掉了EXIF信息直接把這張圖寫(xiě)進(jìn)PDF出來(lái)的效果就是豎著拍的圖在PDF里橫躺著。解決思路有兩種。簡(jiǎn)單粗暴的讀取EXIF中的Orientation值如果是旋轉(zhuǎn)過(guò)的比如6表示順時(shí)針旋轉(zhuǎn)90度就先用AffineTransform把圖片旋轉(zhuǎn)回正確方向再交給PDFBox。// 用 ImageIO 的 ImageReader 提取 EXIF ImageReader reader ImageIO.getImageReadersByFormatName(jpeg).next(); reader.setInput(ImageIO.createImageInputStream(imageFile)); IIOMetadata metadata reader.getImageMetadata(0); // 從 metadata 中解析 Orientation 字段標(biāo)準(zhǔn) Exif TIFF tag 0x0112 // 6: 旋轉(zhuǎn)90度3: 旋轉(zhuǎn)180度8: 旋轉(zhuǎn)270度圖片方向處理邏輯不復(fù)雜但它屬于那種不遇到就永遠(yuǎn)不知道存在的問(wèn)題。如果你的項(xiàng)目涉及手機(jī)圖片上傳這步必須提前設(shè)計(jì)好。3. 實(shí)操過(guò)程完整代碼實(shí)現(xiàn)與參數(shù)選擇3.1 工程結(jié)構(gòu)與依賴配置以Maven項(xiàng)目為例只需要一個(gè)核心依賴dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.27/version /dependency2.0.27是目前基于2.x系列的穩(wěn)定版本。PDFBox 3.x也發(fā)布了但包結(jié)構(gòu)和部分API有變動(dòng)如果你是從老項(xiàng)目升級(jí)建議先在測(cè)試環(huán)境驗(yàn)證兼容性再切換。另外PDFBox在處理圖片壓縮時(shí)會(huì)依賴commons-logging一般情況下Maven會(huì)幫我?guī)湍銕氩恍枰獑为?dú)配置。3.2 完整實(shí)現(xiàn)支持本地圖片目錄轉(zhuǎn)PDF我把核心代碼完整貼出來(lái)這個(gè)版本支持目錄下所有圖片按文件名順序合并、自適應(yīng)頁(yè)面等比縮放、透明通道處理、以及基礎(chǔ)的內(nèi)存優(yōu)化。import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; public class ImageToPdfConverter { public static void convert(File imageDir, File outputPdf, float margin) throws IOException { File[] images imageDir.listFiles((dir, name) - { String lower name.toLowerCase(); return lower.endsWith(.jpg) || lower.endsWith(.jpeg) || lower.endsWith(.png); }); if (images null || images.length 0) { throw new IllegalArgumentException(目錄下沒(méi)有找到圖片文件: imageDir.getAbsolutePath()); } // 按文件名排序保證合并順序穩(wěn)定 Arrays.sort(images, Comparator.comparing(File::getName)); try (PDDocument document new PDDocument()) { for (File imageFile : images) { BufferedImage image ImageIO.read(imageFile); if (image null) { System.err.println(跳過(guò)無(wú)法讀取的圖片: imageFile.getName()); continue; } // 透明PNG轉(zhuǎn)白底R(shí)GB避免黑底問(wèn)題 if (image.getColorModel().hasAlpha()) { BufferedImage rgb new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g rgb.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, image.getWidth(), image.getHeight()); g.drawImage(image, 0, 0, null); g.dispose(); image.flush(); image rgb; } // 頁(yè)面大小默認(rèn)A4減去邊距 float pageW PDRectangle.A4.getWidth() - 2 * margin; float pageH PDRectangle.A4.getHeight() - 2 * margin; float imgW image.getWidth(); float imgH image.getHeight(); float scale Math.min(pageW / imgW, pageH / imgH); float drawW imgW * scale; float drawH imgH * scale; float x (PDRectangle.A4.getWidth() - drawW) / 2; float y (PDRectangle.A4.getHeight() - drawH) / 2; PDPage page new PDPage(PDRectangle.A4); document.addPage(page); PDImageXObject pdImage LosslessFactory.createFromImage(document, image); try (PDPageContentStream cs new PDPageContentStream(document, page)) { cs.drawImage(pdImage, x, y, drawW, drawH); } image.flush(); } document.save(outputPdf); } System.out.println(PDF生成成功: outputPdf.getAbsolutePath()); } public static void main(String[] args) throws IOException { File imageDir new File(args.length 0 ? args[0] : ./images); File outputPdf new File(args.length 1 ? args[1] : ./output.pdf); convert(imageDir, outputPdf, 20f); } }幾個(gè)參數(shù)解釋PDRectangle.A4是PDFBox內(nèi)置的A4尺寸常量寬595磅、高842磅。1磅等于1/72英寸A4紙的物理尺寸是210mm x 297mm換算過(guò)來(lái)正是這個(gè)數(shù)值。margin控制頁(yè)邊距我這里默認(rèn)20磅約7mm實(shí)際打印場(chǎng)景建議不小于10磅。排序用了Comparator.comparing(File::getName)字符串排序。如果要按拍攝時(shí)間或自定義序號(hào)排序需要自行擴(kuò)展比較器。3.3 圖片壓縮控制PDF體積的關(guān)鍵把多張高分辨率圖片直接塞進(jìn)PDF生成的文件會(huì)大得離譜。一張4000x3000的JPEG照片不處理直接嵌入PDF體積可能在8~12MB。如果有一百?gòu)堖@種照片PDF就是1GB級(jí)別的怪物。PDFBox提供了一個(gè)簡(jiǎn)單有效的壓縮方式用JPEGFactory替代LosslessFactory。前者會(huì)讓PDFBox對(duì)圖片重新編碼你可以用JPEGFactory.createFromImage(document, image, quality)指定質(zhì)量參數(shù)0.0f ~ 1.0f之間0.75f通常是一個(gè)視覺(jué)質(zhì)量和體積的平衡點(diǎn)。// 替換上一版代碼中的 LosslessFactory 調(diào)用 PDImageXObject pdImage JPEGFactory.createFromImage(document, image, 0.75f);壓縮后單張4000x3000照片的體積能壓到1.5MB以內(nèi)一百?gòu)垐D片的PDF大約在150MB左右已經(jīng)相對(duì)可接受了。但要注意JPEG是有損壓縮如果業(yè)務(wù)要求圖片必須無(wú)損保存比如合同掃描件、醫(yī)療影像那就不能用這個(gè)方案老老實(shí)實(shí)用LosslessFactory。還有一個(gè)可以優(yōu)化的點(diǎn)如果圖片尺寸本身遠(yuǎn)遠(yuǎn)超過(guò)頁(yè)面能顯示的尺寸可以在建立BufferedImage時(shí)就做縮放。比如A4紙頁(yè)面滿幅顯示大約需要1500x2100像素的清晰度超過(guò)這個(gè)數(shù)值的圖片在打印和屏幕上已經(jīng)看不出區(qū)別了提前降采樣能顯著控制內(nèi)存和PDF體積。private BufferedImage scaleToFit(BufferedImage src, int maxWidth, int maxHeight) { int w src.getWidth(); int h src.getHeight(); if (w maxWidth h maxHeight) { return src; } double scale Math.min((double) maxWidth / w, (double) maxHeight / h); int newW (int) (w * scale); int newH (int) (h * scale); BufferedImage resized new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB); Graphics2D g resized.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.drawImage(src, 0, 0, newW, newH, null); g.dispose(); src.flush(); return resized; }實(shí)測(cè)經(jīng)驗(yàn)在圖片壓縮上花費(fèi)的功夫比整個(gè)PDF生成邏輯都要多。因?yàn)镻DF文件的體積直接關(guān)系到后續(xù)的存儲(chǔ)成本和傳輸時(shí)間這個(gè)環(huán)節(jié)省不得。3.4 目標(biāo)生成一個(gè)可復(fù)用的工具類(lèi)上面三段代碼可以組合成一個(gè)完整的工具類(lèi)。注意我把文件讀取、圖片預(yù)處理、縮放、壓縮、PDF寫(xiě)入分成了獨(dú)立步驟這樣每個(gè)環(huán)節(jié)都能單獨(dú)測(cè)試出問(wèn)題時(shí)容易定位。真實(shí)項(xiàng)目中不建議把所有邏輯都堆在一個(gè)main方法里我之前為了趕工這么干過(guò)后續(xù)維護(hù)加需求時(shí)非常痛苦。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 生成PDF時(shí)內(nèi)存溢出OOM這是排行榜第一的問(wèn)題。原因基本集中在一次性讀取全部圖片到內(nèi)存、未釋放BufferedImage、以及沒(méi)有對(duì)超大圖片做降采樣。排查手段很簡(jiǎn)單在JVM啟動(dòng)參數(shù)里加-Xmx512m甚至更小復(fù)現(xiàn)問(wèn)題看異常堆棧指向哪一行。如果是createFromImage附近基本就是圖片太大或者積累太多。處理順序是先做降采樣再做單張?zhí)幚碜詈蠹觙lush()。三個(gè)都做了幾百?gòu)垐D片跑下來(lái)應(yīng)該很穩(wěn)。4.2 生成的PDF里圖片模糊模糊的根源通常是兩種情況一是縮放比例算錯(cuò)了把圖片畫(huà)到了比原始尺寸更大的區(qū)域二是drawImage傳入的寬度高度參數(shù)用了整型截?cái)鄬?dǎo)致比例輕微變形。檢查點(diǎn)確認(rèn)drawW和drawH與原始圖片的寬高比一致確認(rèn)PDF頁(yè)面尺寸沒(méi)有設(shè)置成過(guò)大的大尺寸。另外如果是用JPEGFactory壓縮質(zhì)量參數(shù)低于0.5會(huì)明顯看到壓縮噪點(diǎn)。4.3 圖片旋轉(zhuǎn)90度問(wèn)題定位方法打開(kāi)原始圖片文件如果圖片在系統(tǒng)自帶查看器里正常但在生成的PDF里橫過(guò)來(lái)了99%是EXIF方向信息沒(méi)處理。解決方案就是前面提到的解析Orientation字段并執(zhí)行對(duì)應(yīng)旋轉(zhuǎn)。常見(jiàn)Orientation值對(duì)照1為正常3為旋轉(zhuǎn)180度6為順時(shí)針旋轉(zhuǎn)90度即需要逆時(shí)針旋轉(zhuǎn)90度來(lái)修正8為逆時(shí)針旋轉(zhuǎn)90度需要順時(shí)針旋轉(zhuǎn)90度修正。4.4 頁(yè)面出現(xiàn)黑色或異常色塊基本是Alpha通道問(wèn)題。解決方式就是轉(zhuǎn)白底R(shí)GB代碼里已經(jīng)有了。如果是CMYK的JPEG現(xiàn)象是顏色明顯偏色或者拋異常需要引入額外的色彩轉(zhuǎn)換邏輯。4.5 大文件PDF打開(kāi)很慢原因通常是圖片未做壓縮PDF整體體積太大。上面給的JPEGFactory壓縮方案可以直接緩解。另外PDFBox還支持設(shè)置document.setAllSecurityToBeRemoved()這類(lèi)參數(shù)優(yōu)化打開(kāi)速度不過(guò)對(duì)圖片型PDF幫助有限。問(wèn)題可能原因解決方案OOM一次性讀取過(guò)多圖片、未釋放資源單張?zhí)幚? flush 降采樣圖片模糊縮放比例錯(cuò)誤、壓縮質(zhì)量低檢查寬高比、提高quality參數(shù)圖片橫向EXIF方向未處理解析Orientation字段并旋轉(zhuǎn)黑色背景Alpha通道未處理繪制到白色RGB畫(huà)布文件過(guò)大圖片未壓縮使用JPEGFactory 降采樣5. 場(chǎng)景擴(kuò)展單圖片多頁(yè)面、自定義排版與批量處理能力5.1 單張圖片跨多頁(yè)輸出有些場(chǎng)景下一張圖片非常大比如一張超長(zhǎng)截圖聊天記錄、網(wǎng)頁(yè)全頁(yè)截圖需要切成多頁(yè)輸出。思路是把圖片按固定高度切片每一片生成一個(gè)PDF頁(yè)面。int sliceHeight 1200; // 每頁(yè)顯示1200像素高度 int totalSlices (image.getHeight() sliceHeight - 1) / sliceHeight; for (int i 0; i totalSlices; i) { BufferedImage subImage image.getSubimage(0, i * sliceHeight, image.getWidth(), Math.min(sliceHeight, image.getHeight() - i * sliceHeight)); // 將 subImage 添加到新頁(yè)面 }這種方式生成的PDF每一頁(yè)顯示圖片的一個(gè)連續(xù)片段上下銜接屏幕閱讀很自然。注意getSubimage返回的圖片與原始圖片共享數(shù)據(jù)緩沖區(qū)如果原始圖片在循環(huán)中被flush()會(huì)影響子圖——所以切片完成后立即處理每片或者用BufferedImage復(fù)制一份。5.2 圖片與文字混排如果PDF里需要章節(jié)標(biāo)題、頁(yè)碼或者頁(yè)眉頁(yè)腳可以在PDPageContentStream里同時(shí)畫(huà)圖片和文字PDFBox本身支持showText配合字體配置就能實(shí)現(xiàn)圖文混排。cs.beginText(); cs.setFont(PDType1Font.HELVETICA_BOLD, 16); cs.newLineAtOffset(margin, pageHeight - 30); cs.showText(業(yè)務(wù)文件 - index); cs.endText();對(duì)于中文內(nèi)容需要加載中文字體文件通常是TTF否則中文會(huì)變成亂碼。這個(gè)坑不建議躲直接在資源目錄放一個(gè)字體文件用PDType0Font.load(document, fontFile)加載即可。5.3 批量任務(wù)并行化當(dāng)圖片數(shù)量特別多例如上千張單線程處理時(shí)間可能達(dá)到幾十秒甚至數(shù)分鐘??梢园褕D片列表分片用固定線程池并行處理每個(gè)線程生成獨(dú)立的PDDocument最后再用PDFBox的PDFMergerUtility合并。PDFMergerUtility merger new PDFMergerUtility(); merger.setDestinationFileName(outputPath); for (File tempPdf : tempFiles) { merger.addSource(tempPdf); } merger.mergeDocuments(null);實(shí)測(cè)下來(lái)四核機(jī)器上并行處理的加速比大約在2.5~3倍之間內(nèi)存占用會(huì)比單線程高需要根據(jù)圖片大小調(diào)整分片數(shù)和每個(gè)線程處理的圖片數(shù)量上限。并行化的前提是每張圖片的處理相互獨(dú)立這個(gè)場(chǎng)景天然滿足。6. 實(shí)操總結(jié)與避坑指南項(xiàng)目做到后面我自己的體會(huì)是圖片轉(zhuǎn)PDF不難難的是在真實(shí)業(yè)務(wù)約束下把內(nèi)存、體積、清晰度、順序、方向這些因素同時(shí)控制住。幾個(gè)值得沉淀的經(jīng)驗(yàn)第一圖片處理鏈路中每一個(gè)BufferedImage都是資源不是在用完之后馬上flush()就是在計(jì)算完尺寸后立刻降采樣不要讓大圖在內(nèi)存里停留超過(guò)必要的時(shí)間。第二PDFBox的LosslessFactory與JPEGFactory不是隨便選的。業(yè)務(wù)要求無(wú)損就選前者業(yè)務(wù)強(qiáng)調(diào)體積可控就選后者兩者切換非常容易所以建議把壓縮開(kāi)關(guān)做成一個(gè)可配置參數(shù)而不是寫(xiě)死在代碼里。第三圖片排序邏輯一定要顯式設(shè)計(jì)。按文件名排序看起來(lái)簡(jiǎn)單但如果文件名是1.jpg、2.jpg、10.jpg字符串排序會(huì)得到1、10、2的順序這種問(wèn)題不跑到實(shí)際數(shù)據(jù)上很難發(fā)現(xiàn)。需要自然排序時(shí)可以用Comparator自行比較數(shù)字部分。第四保留原始圖片。PDF生成只是導(dǎo)出動(dòng)作不意味著原始素材可以被刪除。很多項(xiàng)目上線后才發(fā)現(xiàn)用戶需要的是PDF的同時(shí)還需要保留原圖做編輯這個(gè)需求往往等到開(kāi)發(fā)完成后才會(huì)提出來(lái)。這些經(jīng)驗(yàn)不是一兩次就能總結(jié)全的好在這個(gè)方案本身足夠簡(jiǎn)單后續(xù)維護(hù)起來(lái)壓力不大。如果在實(shí)際使用中遇到新的問(wèn)題回到這幾個(gè)關(guān)鍵環(huán)節(jié)排查大概率能找到答案。