该页面可以包含自动翻译的文本。

在 C# 和 VB.NET 中合并 PDF 文档

业务中经常会合并 PDF 文件用于文档归档。虽然 PDF 合并看起来像一项简单的任务,但这里有许多陷阱。你应该正确合并表单字段、书签、图层和其他 PDF 对象。你还应避免重复对象,以获得紧凑的输出文件。

Docotic.Pdf 库 处理了所有合并细节。它允许你仅用几行 C# 或 VB.NET 代码合并 PDF 文档。

合并 PDF 文档

Docotic.Pdf 提供免费和付费许可证。请在 下载 C# .NET PDF 库 页面获取该库和免费的限时许可证密钥。

PDF 合并基础

PdfDocument.Append 方法允许你从文件、流或字节数组中追加 PDF 文档。对于追加受保护文件和合并表单字段,也有相应选项。

合并两个 PDF 文件

此示例代码演示如何在 C# 中合并 PDF 文件:

using var pdf = new PdfDocument("first.pdf");
pdf.Append("second.pdf");
pdf.Save("merged.pdf");

请尝试 GitHub 上的 合并两个 PDF 文档 代码示例。

合并 PDF 流

很容易将前面的示例改为使用流而不是文件路径。以下是用于合并流的辅助方法:

void Merge(Stream first, Stream second, Stream result)
{
    using var pdf = new PdfDocument(first);
    pdf.Append(second);
    pdf.Save(result);
}

合并多个 PDF 文件

你可以重复调用 Append 方法来追加多个 PDF 文件:

string[] filesToMerge = ..;
using var pdf = new PdfDocument();
foreach (string file in filesToMerge)
    pdf.Append(file);

// 删除由 PdfDocument() 调用添加的空白页
pdf.RemovePage(0);

pdf.Save(pathToFile);

合并加密的 PDF 文件

有用于合并加密文档的 Append 重载:

using var pdf = new PdfDocument();
pdf.Append("encrypted.pdf", new PdfStandardDecryptionHandler("password"));
pdf.Save("merged.pdf");

你可以在在 C# 和 VB.NET 中解密 PDF 文档一文中找到更多信息。

合并 PDF 表单

PDF 文档中的每个表单字段都必须具有唯一名称。当要合并的文档包含同名字段时,这可能会导致问题。Docotic.Pdf 为冲突的表单控件提供以下合并策略:

  • 当附加控件与现有控件冲突时重命名附加的控件
  • 将附加控件合并到现有控件
  • 展平附加控件
  • 不追加任何控件
  • 按原样追加控件

默认情况下,库会在冲突时重命名附加的控件。你可以使用 PdfMergingOptions 类选择其他策略:

using var pdf = new PdfDocument("form.pdf");

var decryptionHandler = new PdfStandardDecryptionHandler(string.Empty);
var mergingOptions = new PdfMergingOptions()
{
    ControlMergingMode = PdfControlMergingMode.CopyAsKids
};
pdf.Append("form.pdf", decryptionHandler, mergingOptions);

pdf.Save("merged.pdf");

CopyAsKids 模式下,库会合并并同步发生冲突的控件。即:当你更改一个控件时,第二个控件将具有相同的值。

减小合并后的 PDF 文件

PDF 文档可能包含相同的对象,例如字体或图像。当你合并这类文档时,生成的文档会包含相同对象的副本。使用 PdfDocument.ReplaceDuplicateObjects() 方法优化合并结果:

using var pdf = new PdfDocument("2024-05-28.pdf");
pdf.Append("2024-05-29.pdf");

pdf.ReplaceDuplicateObjects();

pdf.Save("merged.pdf");

你还可以进一步减小输出文件大小。例如,你可以删除未使用的字体字形或压缩图像。有关支持的压缩选项,请参阅 在 C# 和 VB.NET 中压缩 PDF 文档 一文。

自定义 PDF 合并

Docotic.Pdf 提供用于提取、重新排序或删除 PDF 页面的方法。你可以将它们与 Append 方法结合使用,实现自定义 PDF 合并任务。

追加指定的 PDF 页面

Docotic.Pdf 也允许你合并 PDF 文档的一部分。实现方式有多种。例如,你可以拆分添加的 PDF 文档并追加提取的页面。以下 C# 辅助方法会将所选页面追加到 PdfDocument

private static void AppendPart(PdfDocument pdf, string filePath, params int[] pagesToAppend)
{
    using var streamToAppend = new MemoryStream();
    using var other = new PdfDocument(filePath);
    using var extracted = other.CopyPages(pagesToAppend);
    var options = new PdfSaveOptions
    {
        UseObjectStreams = false
    };
    extracted.Save(streamToAppend, options);

    pdf.Append(streamToAppend);
}

或者,你也可以追加整个 PDF 文档并删除不必要的页面。以下代码示例会追加 second.pdf 的前两页:

using var pdf = new PdfDocument(@"first.pdf");

int pageCountBefore = pdf.PageCount;
pdf.Append(@"second.pdf");
pdf.RemovePages(pageCountBefore + 2);

pdf.Save(pathToFile);

另一种解决方案与 PDF 拼版有关。你可以在相应部分中阅读相关内容。

前置 PDF

Append 方法总是将页面追加到当前文档末尾。如何以不同顺序合并 PDF 文件?有时你可以调整 Append 调用的顺序。即,使用

pdf.Append("first.pdf");
pdf.Append("second.pdf");

而不是

pdf.Append("second.pdf");
pdf.Append("first.pdf");

或者,你也可以在合并后重新排序页面。以下 C# 代码将追加的 PDF 文档移动到开头:

using var pdf = new PdfDocument(@"second.pdf");

int pageCountBefore = pdf.PageCount;
pdf.Append(@"first.pdf");
pdf.MovePages(pageCountBefore, pdf.PageCount - pageCountBefore, 0);

pdf.Save(pathToFile);

有关重新排序 PDF 页面的更多信息,请参阅:

PDF 拼版

Docotic.Pdf 允许你在单个页面上组合多个 PDF 页面。使用 PdfDocument.CreateXObject(PdfPage) 方法基于现有页面创建 PdfXObject 对象。然后,以所需缩放比例绘制该对象。示例代码:

using var src = new PdfDocument(@"src.pdf");
using var dest = new PdfDocument();
PdfXObject firstXObject = dest.CreateXObject(src.Pages[0]);
PdfXObject secondXObject = dest.CreateXObject(src.Pages[1]);

PdfPage page = dest.Pages[0];
page.Orientation = PdfPaperOrientation.Landscape;
double halfOfPage = page.Width / 2;
page.Canvas.DrawXObject(firstXObject, 0, 0, halfOfPage, page.Height, 0);
page.Canvas.DrawXObject(secondXObject, halfOfPage, 0, halfOfPage, page.Height, 0);

dest.Save("result.pdf");

请在 GitHub 上测试相关的 从页面创建 XObject 示例项目。

作为附件合并

有时,你可能需要将一个 PDF 文件作为附件嵌入到另一个文件中。这也是可以的。你还可以在 PDF 页面上为嵌入的文件添加链接:

using var pdf = new PdfDocument();

PdfFileSpecification first = pdf.CreateFileAttachment("first.pdf");
pdf.SharedAttachments.Add(first);

var bounds = new PdfRectangle(20, 70, 100, 100);
PdfFileSpecification fs = pdf.CreateFileAttachment("second.pdf");
pdf.Pages[0].AddFileAnnotation(bounds, fs);

pdf.Save("attachments.pdf");

你可以在 GitHub 上的 PDF 附件 组中找到相关代码示例。

在并行线程中合并

当合并许多 PDF 文件时,可以将代码并行化。PdfDocument 类不是线程安全的。因此,我们需要在线程间使用独立的 PdfDocument 对象。有关更多细节,请查看 在并行线程中合并 PDF 文档 代码示例。

以下代码展示如何并行合并 PDF 流:

Stream[] documentsToMerge = ..;

int rangeSize = 50;
while (documentsToMerge.Length > rangeSize)
{
    int partitionCount = (int)Math.Ceiling(documentsToMerge.Length / (double)rangeSize);
    var result = new Stream[partitionCount];

    var partitioner = Partitioner.Create(0, documentsToMerge.Length, rangeSize);
    Parallel.ForEach(partitioner, range =>
    {
        int startIndex = range.Item1;
        int count = range.Item2 - range.Item1;
        result[startIndex / rangeSize] = MergeToStream(documentsToMerge, startIndex, count);
    });
    documentsToMerge = result;
}

using PdfDocument final = GetMergedDocument(documentsToMerge, 0, documentsToMerge.Length);
final.Save("merged.pdf");


private static Stream MergeToStream(Stream[] streams, int startIndex, int count)
{
    using PdfDocument pdf = GetMergedDocument(streams, startIndex, count);

    var result = new MemoryStream();

    var options = new PdfSaveOptions
    {
        UseObjectStreams = false // 加速写入中间文档
    };
    pdf.Save(result, options);
    return result;
}

private static PdfDocument GetMergedDocument(Stream[] streams, int startIndex, int count)
{
    var pdf = new PdfDocument();
    try
    {
        for (int i = 0; i < count; ++i)
        {
            var s = streams[startIndex + i];
            pdf.Append(s);
            s.Dispose();
        }

        pdf.RemovePage(0);

        pdf.ReplaceDuplicateObjects();

        return pdf;
    }
    catch
    {
        pdf.Dispose();
        throw;
    }
}

上面的代码将输入文档按 rangeSize 大小分组。然后,代码并行将每组合并为中间文档。该过程持续进行,直到输入文档数量足够少,可以进行简单合并。

并行方案不一定比单线程版本更快。结果可能会因输入文档的数量及其大小而异。在示例代码中,rangeSize 参数的最佳值可能更大也可能更小。你应该对应用程序进行基准测试,以找到最有效的实现。

结论

你可以使用 Docotic.Pdf 库 在 C# 和 VB.NET 中合并 PDF 文档。它允许你合并文件、流或字节数组。你可以合并加密文件、PDF 表单和特定 PDF 页面。Docotic.Pdf 还能帮助你压缩生成的文件并节省磁盘空间。

请尝试 GitHub 上 Docotic.Pdf 示例仓库 中的代码示例。你可以在 Docotic.Pdf 下载页面获取评估版许可证密钥并下载该库。