合併 DOCX、XLSX 與 PDF 成單一綑綁 — 示範執行

The thing that kept eating my Fridays

每個星期五下午,持續大約一年,我都有同樣的小儀式。會收到三個檔案的合約——Word 的主協議、Excel 的價格附件,以及合作夥伴的條款 PDF——我必須把它們合併成一個乾淨的 PDF。這事本身不難。開啟 Word,匯出成 PDF;開啟 Excel,匯出成 PDF;打開某個免費的 PDF 合併工具,拖入三個檔案,檢查順序,儲存。

大約需要八分鐘。若每週有十五份合約,光是移動滑鼠就浪費了兩小時。更糟的是,每隔幾週就會有人把附件放在第一頁,因為合併工具會依檔名字母順序排序。

如果你有類似的經驗,接下來的內容就是我最終用程式碼取代這個儀式的下午。

真正的成本不是時間——而是五十份合約中有一份頁面順序錯誤,且在客戶簽署錯誤版本前沒人發現。

What I actually wanted

不是「華麗的文件管線」。只要三件事:

  1. 給方法一個檔案清單(任意混合 DOCX、XLSX、PDF),回傳一個 PDF。
  2. 把同樣的邏輯指向一個資料夾,讓它自行找出檔案清單。
  3. 從完成的綑綁檔中抽取頁碼範圍,而不必重新合併整個檔案。

這就是全部需求。如果函式庫無法乾淨地完成這三件事,我不想知道它有什麼功能。

Setup

  • .NET 6.0 或更新版本
  • GroupDocs.Merger for .NET 24.10+(取得暫時授權,避免輸出帶有評估浮水印)
  • 一個資料夾,裡面放著你平常會手動合併的各種文件
dotnet add package GroupDocs.Merger

就這樣,沒有外部轉換器、沒有無頭 Office 安裝、也不需要額外的 PDF 操作函式庫。

Step 1 — Let a folder be the input

我總是從這裡開始,因為它是最實際的入口點。實務上,會有其他程式(上傳處理器、電子郵件擷取工作、財務夜間匯出)把一堆檔案放到目錄中,我的程式必須處理它們。

// Pick up every supported file in the drop folder; the PDF wins
// the tie-break for position 0 so the merger keeps the output
// as a PDF regardless of how files are named.
string[] extensions = { ".pdf", ".docx", ".xlsx" };
var files = Directory.EnumerateFiles(folderPath)
    .Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
    .OrderBy(f => Path.GetExtension(f).ToLowerInvariant() == ".pdf" ? 0 : 1)
    .ThenBy(f => f)
    .ToArray();

if (files.Length == 0)
    throw new InvalidOperationException(
        $"No supported documents found in '{folderPath}'.");

OrderBy 的技巧是關鍵。GroupDocs.Merger 會根據你最先開啟的檔案決定輸出格式——如果我把 DOCX 當作主要文件,輸出就會是 DOCX。因為我的管線永遠想要 PDF 輸出,我確保資料夾中任何現有的 PDF 都排在第 0 位。

兩件值得一提的事:

  • ToLowerInvariant() 因為有一天合作夥伴會傳來 REPORT.PDF,若只用小寫過濾就會悄悄把它排除。
  • ThenBy(f) 只為了讓輸出具備決定性。沒有它,同一資料夾的兩次執行可能因檔案系統的隨機性而產生不同順序。

Step 2 — The merge itself

取得排序好的路徑清單後,合併的程式碼比描述本身還要短。

Console.WriteLine($"Primary source: {sourcePaths[0]}");
using var merger = new Merger(sourcePaths[0]);

var joinOptions = new JoinOptions();
for (int i = 1; i < sourcePaths.Length; i++)
{
    Console.WriteLine($"Joining: {sourcePaths[i]}");
    merger.Join(sourcePaths[i], joinOptions);
}

merger.Save(outputPath);
Console.WriteLine($"Unified PDF binder saved to: {Path.GetFullPath(outputPath)}");

使用時的幾點說明:

  • using 很重要。Merger 會保留來源檔案的檔案句柄;若忘記釋放,資料夾的工作程式最終會無法刪除自己的輸入檔。
  • JoinOptions 這裡是空的,因為預設值已滿足我 95% 的需求。若需要自訂,頁碼範圍、旋轉、插入位置都在這裡設定。
  • 當 Excel 被加入綑綁時,工作表到頁面的版面配置由來源活頁簿的列印區決定。如果你的 XLSX 結果是 38 頁而你只想要三頁,必須在試算表本身調整,而不是在 JoinOptions 裡。

儲存後我總會加上一段 sanity check:

using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");

這兩行程式碼捕捉到的「悄悄遺失附件」的錯誤,比我寫過的任何測試都多。

Step 3 — Extract a slice later

我每次都會收到的後續需求:「能不能只給我封面頁?」或「客戶只要簽名頁。」重新組合整個綑綁只為了兩頁實在太蠢——直接抽取就好。

using var merger = new Merger(binderPath);
merger.ExtractPages(new ExtractOptions(pages));
merger.Save(outputPath);
Console.WriteLine($"Extracted pages [{string.Join(",", pages)}] to " +
    Path.GetFullPath(outputPath));

pages 是一個 int[],裡面放的是你想保留的 1 起始頁碼。其他頁全部捨棄。速度快,因為結果已經是 PDF——不需要再做轉換。

Before vs. after, honestly

What I used to do With Merger.Join
Per-contract time 5–10 minutes of clicking under 30 seconds end-to-end
Typical failure Pages in the wrong order, nobody notices Whatever order the file list says, repeatably
Scaling to 100/day Doesn’t — you hire a person One worker, bored most of the time
Code you maintain A Confluence page titled “Binder Process v4” One class, ~70 lines
Output Three PDFs and a prayer One binder, with page count you can log

我最在意的是「失敗」那一行。手動合併會靜默失敗;程式碼若未達到預期會直接拋出錯誤。

A real story from a tiny legal-tech team

我曾合作的兩人新創公司裡,有位法律助理的早晨就是組合合約。Word 協議、Excel 價格、PDF 附錄,透過一個應用程式串接後上傳至 DocuSign。每套大約八分鐘,若一天要處理 30 套,基本上佔滿她整個早上。

他們把資料夾掃描的方式嵌入已經在監聽收件郵件的後端服務。每套只要二十秒,加上一行頁數日誌。法律助理因此可以改為審閱合約,而不是組合合約。再也沒有人因為頁面順序錯誤而寄出錯誤的綑綁——不是因為函式庫有魔法,而是檔案清單寫在程式碼裡,且可以 diff。

string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";

var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");

就這樣完成整個整合。上游(電子郵件監聽、儲存路徑)早已就緒。

Stuff I didn’t need today but will tomorrow

同一個函式庫還能做許多我此篇未提及的功能,因為寫完文章會拖太久。大致上依我使用的先後順序:

  • Watermarks on the output 用於在未簽署的副本上加上「DRAFT」印章。
  • Page rotation 處理側向掃描的文件。
  • Custom page ordering 當來源順序與交付順序不一致時。
  • PDF encryption 用於傳送給外部對象的檔案。

所有功能都在同一個 Merger API 之下。完整清單請參考 docs——我只想說「合併」是最便宜的入門,其餘功能在需要時隨時可用。

What I’d tell past-me

如果你正打算自己寫一個 DOCX 轉 PDF 的步驟,因為「只要一個方法」就好,請停下。轉換是最容易悄悄腐爛的部分——新 Office 功能、掃描圖像處理、內嵌字型,種種問題層出不窮。讓別的工具負責這層表面工作,然後把你的星期五下午留給不需要只靠檔名排序的事。

Where to go next: