這年頭要合併 PDF,現成軟體或線上服務多如牛毛。考量線上服務必須上傳 PDF 檔到雲端我不愛,免費又順手的軟體得花時間尋找評估,想到就懶。不寫程式碼手會癢的我,決定省下找軟體學軟體的時間,依據使用習慣自己寫工具來用,這才符合程式硬漢作風。(謎之聲:可憐吶,看來病得不輕呀)

講到用 .NET 合併 PDF 檔,過去我研究過 PdfPigPdfSharp / PdfSharpCoreTelerik PDF,每一套都能勝任。評估後我選擇只需單一組件檔(dll)的老牌 .NET PDF 元件 - PdfSharp (.NET Framework) / PdfSharpCore (.NET 6+)。

這些年下來,隨著愈來愈常開終端機操作 Linux、用 PowerShell 管理及部署 IIS,加上用 git、dotnet 下指令已是家常便飯,我已愛上用 CLI 敲指令,不複雜的操作根本不想開啟 GUI。因此,我理想中的 PDF 合併工具,也應該寫成 CLI,用起來像這樣:

LitePdfMerger sample.pdf prince\invoice.pdf prince\textbook.pdf
LitePdfMerger sample.pdf prince\*.pdf
LitePdfMerger sample.pdf prince\*.pdf -o:merged.pdf

專案我選用 .NET 8 Console,並打算啟用 Native AOT 編譯成輕巧的單一原生 EXE 檔,不需安裝 .NET 8 Runtime 或 SDK,Copy-and-Play。

要啟用 Native AOT,.csproj 需加入 PublishAotIsAotCompatible

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <IsAotCompatible>true</IsAotCompatible>    
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PdfSharpCore" Version="1.3.63" />
  </ItemGroup>

</Project>

程式碼不多,不到 70 行搞定。(註:其中有用到 Console.ReadLine() 偵測 Ctrl-C 中斷的技巧)

using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;

Console.WriteLine("PDF Merge Tools v0.9b");
List<PdfDocument> sources = new List<PdfDocument>();
var outputPath = string.Empty;
foreach (string fn in args)
{
    if (fn.StartsWith("-o:"))
    {
        outputPath = fn.Substring(3);
        continue;
    }
    if (fn.Contains("*"))
    {
        var folder = Path.GetDirectoryName(fn);
        if (string.IsNullOrEmpty(folder)) folder = ".";
        foreach (var found in Directory.GetFiles(folder, Path.GetFileName(fn), SearchOption.AllDirectories))
        {
            Console.WriteLine($" * {found}");
            var srcPdf = PdfReader.Open(found, PdfDocumentOpenMode.Import);
            sources.Add(srcPdf);
        }
    }
    else if (!File.Exists(fn))
    {
        Console.WriteLine("Can't find " + fn);
        return;
    }
    else
    {
        Console.WriteLine(" * " + fn);
        var srcPdf = PdfReader.Open(fn, PdfDocumentOpenMode.Import);
        sources.Add(srcPdf);
    }
}
if (sources.Count == 0)
{
    Console.WriteLine("No pdf to merge");
    return;
}
var merged = new PdfDocument();
foreach (var pdf in sources)
{
    for (int i = 0; i < pdf.PageCount; i++)
    {
        merged.AddPage(pdf.Pages[i]);
    }
}
var mergedFilePath = $"{DateTime.Now:yyyyMMdd-HHmmss}.pdf";
if (!string.IsNullOrEmpty(outputPath))
{
    mergedFilePath = outputPath;
}
else
{
    Console.Write($"Output filename [default: {mergedFilePath}]: "); 
    var assignFilePath = Console.ReadLine();
    if (assignFilePath == null) return; // user press Ctrl+C or Ctrl-Z
    Console.WriteLine("assignFilePath: " + assignFilePath);
    if (!string.IsNullOrEmpty(assignFilePath))
    {
        if (!assignFilePath.EndsWith(".pdf")) assignFilePath += ".pdf";
        mergedFilePath = assignFilePath;
    }
}
Console.WriteLine($"Saving to {mergedFilePath}");
merged.Save(mergedFilePath);

編譯出來的 EXE 檔很小,大約 3.3MB,複製到其他電腦可以直接用,不需要先安裝 .NET 8,這是 .NET Native AOT 技術的一大優勢。

我從網路上找了一些 PDF 範例檔來做測試:

測試一,輸入要合併的 PDF 路徑合併成單一 PDF,懶得想輸出檔名可按 Enter 接受用日期時間自動命名,或者可輸入合併檔名。

測試二,來源 PDF 路徑還支援 * 萬用字元,並可用 -o:outputPath 事先指定輸出檔名,方便批次作業。

實測成功!

範例專案我放上 Github 了,大家若已安裝 Git for Windows 跟 .NET 8 SDK,執行以下指令便能做出這個好用的 PDF 合併小工具。

git clone https://github.com/darkthread/LitePdfMerger.git
cd LitePdfMerger
dotnet publish -c Release
dir .\bin\Release\net8.0\win-x64\publish\

This post is about creating a custom CLI tool in .NET to merge PDF files using PdfSharpCore. I explain the benefits of using a CLI tool over online services and provides a detailed implementation, including project setup and code. The tool leverages .NET 8 and Native AOT for lightweight, standalone execution.


Comments

# by Sephi

typo, 編譯出來的 EXE 檔很小,大約 "3.3MB" :)

# by Jeffrey

to Sephi, 謝謝,已修正。

# by Alex

Typo, 我已愛上用 CLI 敲指令,不複雜的操作根本(不)想開 GUI。—少了個(不)

# by Hank

請問下 要編譯 Linux 用的 Native AOT 原生程式庫需要用 Linux 平台上編譯(編譯平台與目標平台需相同) 這也包含 CPU 架構對嗎? 例如想要在 Windows Arm 架構跑,不能用 Windows x64 編譯?

# by Jeffrey

to Alex, 哈,對,謝謝~

# by Jeffrey

to Hank, 是的,編譯 Native AOT 需要有 C++ 編譯環境,對作業系統依賴很深,甚至你用 Ubuntu 20.04 編譯的 Native AOT 檔案,只能在 Ubuntu 20.04 以後的作業系統執行,無法在 Ubuntu 18.04 用,對 Windows 不同 CPU 架構也是如此。

# by yoyo

不知道以後會不會有Native AOT 的cross compiler 哈

Post a comment