PDF 合併是報表系統、歸檔服務、電子簽章平台的高頻需求。本文提供基於 iText 5(com.lowagie.text)的完整可用代碼,並針對大文件、內存控制、異常處理、版本兼容性給出生產級優化建議。
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
import java.io.FileOutputStream;
import java.io.IOException;
public class PdfMergeUtil {
/**
* 合併多個 PDF 文件
* @param sourceFiles 源文件絕對路徑數組
* @param targetFile 合併後輸出文件絕對路徑
* @return 成功返回 true,失敗返回 false
*/
public static boolean mergePdfFiles(String[] sourceFiles, String targetFile) {
if (sourceFiles == null || sourceFiles.length == 0) {
throw new IllegalArgumentException("源文件列表不能為空");
}
Document document = null;
PdfCopy copy = null;
FileOutputStream fos = null;
try {
// 1. 以第一個文件的頁面大小創建 Document
PdfReader firstReader = new PdfReader(sourceFiles[0]);
document = new Document(firstReader.getPageSize(1));
firstReader.close(); // 僅獲取頁面大小,及時關閉
// 2. 創建 PdfCopy 寫入輸出流
fos = new FileOutputStream(targetFile);
copy = new PdfCopy(document, fos);
document.open();
// 3. 遍歷所有源文件,逐頁導入
for (String srcFile : sourceFiles) {
PdfReader reader = null;
try {
reader = new PdfReader(srcFile);
int pageCount = reader.getNumberOfPages();
for (int pageNum = 1; pageNum <= pageCount; pageNum++) {
document.newPage(); // 必須調用,創建新頁面
PdfImportedPage importedPage = copy.getImportedPage(reader, pageNum);
copy.addPage(importedPage);
}
} finally {
if (reader != null) {
reader.close(); // 關鍵:釋放文件句柄,防止句柄洩漏
}
}
}
return true;
} catch (DocumentException | IOException e) {
e.printStackTrace();
return false;
} finally {
// 4. 逆序關閉資源
if (document != null) {
document.close(); // 會自動關閉 copy
}
if (fos != null) {
try { fos.close(); } catch (IOException ignored) {}
}
}
}
public static void main(String[] args) {
String[] files = {"e:\\1.pdf", "e:\\2.pdf", "e:\\3.pdf"};
String output = "e:\\merged.pdf";
boolean success = mergePdfFiles(files, output);
System.out.println("合併" + (success ? "成功" : "失敗") + ":" + output);
}
}
PdfCopy 專為合併/拆分設計,直接複製頁面內容流,不重新解析渲染,性能極高;PdfWriter 用於創建新內容。getImportedPage(reader, 1) 為第一頁。PdfCopy.setMergeFields() 處理表單字段)。iText 5(Lowagie)已停止維護,新項目建議使用 iText 7(AGPL 協議)或 Apache PDFBox(Apache 2.0 協議)。iText 7 核心 API 變更:
// iText 7 風格
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfPage;
public static void mergePdfFilesV7(String[] sources, String target) throws IOException {
try (PdfWriter writer = new PdfWriter(target);
PdfDocument pdfDoc = new PdfDocument(writer)) {
for (String src : sources) {
try (PdfReader reader = new PdfReader(src);
PdfDocument srcDoc = new PdfDocument(reader)) {
srcDoc.copyPagesTo(1, srcDoc.getNumberOfPages(), pdfDoc);
}
}
}
}
| 優化點 | 方案 |
|---|---|
| 大文件內存壓力 | 使用 PdfReader.unethicalReading()(iText 5)或流式處理(iText 7/PDFBox),避免全量載入內存 |
| 加密 PDF 處理 | 構造 PdfReader 時傳入密碼:new PdfReader(src, password.getBytes()) |
| 表單字段合併 | copy.setMergeFields() 保留/重命名字段,避免同名衝突 |
| 書籤/目錄保留 | copy.setOutlines() 合併書籤樹 |
| 異常恢復 | 臨時文件寫入成功後原子重命名(Files.move(..., REPLACE_EXISTING, ATOMIC_MOVE)),防止輸出損壞 |
| 並發合併 | 線程池並行處理多組合併任務,注意臨時目錄隔離 |
// PDFBox 3.x 風格
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import java.io.File;
public static void mergeWithPdfBox(String[] sources, String target) throws IOException {
PDFMergerUtility merger = new PDFMergerUtility();
for (String src : sources) {
merger.addSource(new File(src));
}
merger.setDestinationFileName(target);
merger.mergeDocuments(null); // null 表示不加載到內存,流式處理
}
PDF 合併核心在於頁面級流式複製而非重新渲染。iText 5 代碼簡潔高效,但新項目建議遷移至 iText 7 或 PDFBox 獲得長期維護與許可證合規。生產環境務必關注:資源及時釋放、加密文件支持、大文件內存控制、輸出原子性、書籤/表單保留。