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

如何创建 PDF 文档 in C# 和 VB.NET

本文介绍如何借助 Docotic.Pdf 库在 .NET 中创建 PDF 文档的不同方式。它是一个高性能、纯 C# .NET 库,无需外部依赖,可用于创建、编辑、转换和处理 PDF 文档。

展示使用 Docotic.Pdf 创建 PDF 和文档自动化的示意图。

在以下章节中,我将介绍使用 Docotic.Pdf 创建 PDF 的主要方法:

  • 使用 Core API,它可对文本、图形和 PDF 内部结构提供低级控制。此选项最适合自定义布局、图形密集型文档和高级功能。
  • 使用高层 Layout API,它支持段落、表格、页眉、页脚和自动分页。当你希望生成结构化文档,而无需手动计算位置时,这个 API 最合适。
  • 使用 HTML 转 PDF 并支持 SVG 和其他 Web 格式。当你的解决方案已经生成 HTML 文档,并且需要这些 HTML 和 CSS 文件的 PDF 版本时,这种方式尤其有用。
  • 图像创建 PDF。此方法适用于扫描文档、基于图像的报告、收据,以及任何以光栅图像开始的工作流。
  • 合并或拆分 PDF。这是整理报告、处理用户上传、合并相关文档以及重组大型 PDF 的不错选择。
  • 模板创建 PDF。当批量生成收据、税表、劳动合同和其他可重复文档类型,并且需要一致格式时,这种方式效果很好。

本指南还涵盖以下附加主题:

使用 Core API 创建 PDF

Core API 是 Docotic.Pdf 中 PDF 创建的基础。它通过其 Canvas API 为你提供在 PDF 画布上放置文本、图像和矢量图形的完全低级控制。这个绘图 API 是 Core API 的子集,提供用于在画布上向页面和其他对象添加内容的方法和属性。除了渲染之外,Core API 还支持批注、表单字段、图层、书签和其他 PDF 功能。

下面是一个使用三项基本操作创建简单 PDF 的 C# 代码:绘制文本、放置图像以及在页面画布上渲染矢量图形。

using var pdf = new PdfDocument();
var canvas = pdf.Pages[0].Canvas;

canvas.Font = pdf.CreateFont(PdfBuiltInFont.HelveticaBold);
canvas.FontSize = 14;
canvas.DrawString(40, 100, "Core API demo: text, images, and vector graphics");

var image = pdf.CreateImage("image.png");
canvas.DrawImage(image, 40, 180, 120, 120, 0);

canvas.Pen.Color = new PdfRgbColor(30, 60, 160);
canvas.Pen.Width = 2;
canvas.Brush.Color = new PdfRgbColor(200, 230, 255);
canvas.DrawRectangle(new PdfRectangle(200, 200, 150, 80), PdfDrawMode.FillAndStroke);

pdf.Save("core-api-demo.pdf");

这个概览只介绍了 Core API 功能的一小部分。有关高级主题,请参阅使用 Core API 构建 PDF 的详细文章。该文章涵盖文本测量、色彩空间、裁剪、使用图案填充区域、处理透明度以及其他功能。

使用 Layout API 生成 PDF

Layout API 是一个高层文档构建引擎,提供生成复杂、内容丰富的 PDF 的最简单、最高效的方法。

使用该 API 时,你通过页面、容器、文本片段、图像、表格、链接、页眉、页脚等结构元素来组合 PDF。你无需计算坐标或手动管理分页,只需描述文档结构,其余工作交给布局引擎处理。

此示例演示如何使用 Layout API 创建 PDF,采用声明式布局,而不是手动定位。

PdfDocumentBuilder.Create()
    .Info(info => info.Title = "Docotic.Pdf Layout API demo")
    .Generate("layout-api-demo.pdf", doc => doc.Pages(pages =>
    {
        pages.Content().Padding(100).Text(text =>
        {
            text.Span("The Layout API lets you compose PDFs from structural elements ");
            text.Line("without manually calculating coordinates or handling pagination.")
                .Style(s => s.Strong);
        });
    }));

查看这份深入指南,了解如何在.NET 应用程序中生成 PDF时使用 Layout API。

使用 HTML 转 PDF API 转换 Web 内容

Docotic.Pdf 与其免费的 HtmlToPdf 附加组件配合后,提供了现代、高质量、基于 Chrome 的 HTML 转 PDF 引擎。你可以使用该附加组件提供的 API,将现代 HTML 和其他 Web 内容(如 SVG 或 WebP 图像)转换为高质量的 PDF 文档。

HTML 转 PDF API 可以从完整 HTML 页面或 HTML 片段创建 PDF。你可以从 URL、原始 HTML 字符串和本地 HTML 文件转换内容。后两种方式便于从 HTML 模板生成 PDF。

下面是一个从 HTML 模板生成 PDF 的示例:

public static async Task HelloHtmlTemplate()
{
    static string GetUserName()
    {
        // 替换为真实逻辑:表单输入、API 调用、配置等。
        return "World";
    }

    string html = $@"
        <h1>Hello, {GetUserName()}!</h1>
        <p>This PDF was generated from an HTML template.</p>";

    using var converter = await HtmlConverter.CreateAsync();
    using var pdf = await converter.CreatePdfFromStringAsync(html);
    pdf.Save("hello-html-template.pdf");
}

更多细节和示例,请查看我们的深入 HTML 转 PDF 概述

从图像创建 PDF

Docotic.Pdf 提供了一种灵活、对开发者友好的方式将图像转换为 PDF。该库通过 Core API 支持 JPEG、BMP、GIF、PNG、TIFF 和 JPEG 2000 图像格式。

展示 Docotic.Pdf 如何将多个图像文件转换为单个 PDF 文档的示意图。

当 PDF 格式支持时,Docotic.Pdf 会按原样嵌入图像字节,避免像素解码和重新编码,以保留原始压缩。该库还会在可能时保留色彩空间。

此外,借助 HTML 转 PDF API 还支持 SVG 和 WebP 格式。当你需要在图像旁边放置标签或说明文字时,Layout API 可帮助你以最少的工作量排列和对齐元素。

如何将多张图像合并为一个 PDF

使用 Docotic.Pdf,你可以轻松将一组图像转换为单个 PDF,并将一张图像放在一页上。

下面的示例从文件加载图像,并将每张图像绘制到各自的页面上。每张图像都会按比例缩放以适配页面,并居中显示,以获得简洁、一致的布局。

public static void ImagesOnToPdf(string[] imagePaths, string outputPath)
{
    using var pdf = new PdfDocument();

    foreach (string path in imagePaths)
    {
        var image = pdf.CreateImage(path);

        var page = pdf.AddPage();
        var pageWidth = page.Width;
        var pageHeight = page.Height;

        var scale = Math.Min(pageWidth / image.Width, pageHeight / image.Height);
        var drawWidth = image.Width * scale;
        var drawHeight = image.Height * scale;
        var x = (pageWidth - drawWidth) / 2;
        var y = (pageHeight - drawHeight) / 2;

        page.Canvas.DrawImage(image, x, y, drawWidth, drawHeight, 0);
    }

    pdf.RemovePage(0);

    pdf.Save(outputPath);
}

处理多页 TIFF 和 GIF 图像

Docotic.Pdf 完全支持多页 TIFF 和 GIF 文件。当向 PDF 添加图像时,如果某些图像可能包含多页,请使用 OpenImage 方法,而不是 CreateImage

下面的代码演示如何将 TIFF 转换为 PDF;它对单页和多页图像都有效:

public static void OddFramesToPdf(string[] imagePaths, string outputPath)
{
    using var pdf = new PdfDocument();
    foreach (string path in imagePaths)
    {
        var imageFrames = pdf.OpenImage(path);
        for (int i = 0; i < imageFrames.Count; i++)
        {
            if (i % 2 != 0)
                continue;

            var image = pdf.CreateImage(imageFrames[i]);
            var page = pdf.AddPage();

            page.Width = image.Width;
            page.Height = image.Height;

            page.Canvas.DrawImage(image, 0, 0, image.Width, image.Height, 0);
        }
    }

    pdf.RemovePage(0);

    pdf.Save(outputPath);
}

你也可以使用同样的方法将 GIF 转换为 PDF。它同样适用于其他图像格式,不过对于只有单帧的格式来说,这种方式比必要的要复杂一些。

合并和拆分 PDF

作为一个功能齐全的 .NET 库,Docotic.Pdf 可以通过合并、提取和重组现有文档中的页面来创建新的 PDF。

当你合并 PDF 时,库不仅会添加另一个文档中的页面,还会附加图层、书签、页面标签、共享 JavaScript、目标(链接目标)和嵌入文件。更多细节以及关于如何减小合并后 PDF 大小的指导,请参阅合并 PDF的文章。

带数字签名的 PDF 不能在不使现有签名失效的情况下合并。为了保留签名,请改为创建 PDF portfolio,而不是附加文档。另一种选择是先合并 PDF,然后对合并后的文档应用新的数字签名

Docotic.Pdf 还允许你将页面复制并提取到新文档中。与复制页面关联的所有内容都会保留,包括批注、表单控件、结构化内容、图层和其他相关数据。有关实际示例,请参阅在 .NET 中拆分 PDF的文章。该文章还解释了如何提取或删除页面。

使用 PDF 模板

PDF 模板是预先设计好的 PDF 文件,作为创建新文档的基础结构。当你需要在提供不同数据的同时生成具有一致布局的 PDF 时,它们很有用。如果你希望将视觉设计与数据本身分离,PDF 模板也是一个不错的选择。

模板可以是基于表单的 PDF,也可以是不含表单的静态 PDF。两种类型用途相同。此外,基于表单的模板包含交互元素,如果不展平,它们可以收集用户信息。

从基于表单的模板创建 PDF

基于表单的模板通常包含 AcroForms,这是标准且广泛支持的交互式 PDF 表单类型。要从此类模板生成 PDF,通常需要:

  • 填充每个占位字段
  • 展平字段以防止进一步编辑
  • 将结果保存为新的 PDF

下面是一个 C# 代码示例:按名称查找占位文本字段,为其赋值,展平该字段,并保存结果,从而从模板创建 PDF:

var nameOnCertificate = "Eva Marin";
using var pdf = new PdfDocument("certificate-template.pdf");
if (pdf.TryGetControl("name", out var field))
{
    if (field is PdfTextBox nameField)
    {
        nameField.Text = nameOnCertificate;
        nameField.Flatten();
    }
}

pdf.Save($"certificate-{nameOnCertificate}.pdf");

如果模板包含许多占位符,你可以导入 FDF 数据,而不是逐个填充每个字段。你也可以使用 PdfDocument.FlattenControls 一次性展平所有字段。

从不含表单的静态模板创建 PDF

如果模板不包含表单字段,你会直接在页面画布上绘制姓名和其他数据。静态 PDF 模板通常包含固定的视觉占位符,例如文本、图像或空白区域。要从模板生成 PDF,你需要填充这些空白区域,并以编程方式替换占位文本和图像。

空白区域

利用 Canvas API 在空白区域放置文本和图像。在简单情况下,例如只需添加姓名和照片,这种方法效果很好。你需要知道这些区域的坐标和尺寸;为了正确放置文本,可能还需要先测量文本,再据此对齐。

处理可变长度或多行文本更具挑战性,但仍然可行。通过组合使用 DrawTextDrawString 以及文本测量方法,你可以按需换行并定位文本。如果模板中这类区域不止几个,建议考虑其他方案,例如使用 Layout API 生成 PDF。

占位文本

Docotic.Pdf 还提供查找和替换文本的方法。不过,将文本搜索用作模板机制通常并不比处理空白占位区域更简单。在插入新内容之前,你必须定位准确的文本片段并干净地将其移除。

占位图像

静态模板可能包含用于用户头像或产品照片的占位图像。要找到占位图像,请枚举绘制在每页上的图像集合。对于每个已绘制图像,你都可以获得其可见尺寸和位置。要替换占位图像,请使用 PdfImage.ReplaceWith

using var pdf = new PdfDocument("invoice-template.pdf");
var paintedImages = pdf.Pages[0].GetPaintedImages();

var placeholder = paintedImages.First();
placeholder.Image.ReplaceWith("company-logo.jpg");

pdf.Save($"invoice.pdf");

另一种选择是在占位图像所在区域之上绘制新图像,但这样通常会在没有充分理由的情况下增加最终 PDF 的大小。

为便于替换而设计占位符

对于静态模板,为文本和图像设计具有可预测、边界清晰的区域会很有帮助。为包含可变长度内容的区域预留足够的内边距,并使用与预期后续插入的纵横比匹配的中性占位图像。

如果模板使用你计划替换的占位文本,可以通过使用文本框而不是原始文本来简化工作流。向模板中添加一个只读、无边框的文本框,并将占位文本放入其中。生成最终 PDF 时,打开模板,按名称定位该文本框,然后直接通过 box.Text = "new text"; 赋值。随后展平文本框,以防止进一步编辑。

添加交互元素

交互功能会将静态 PDF 转变为动态、易于导航的文档,并附加批注和标记。动作和 JavaScript 可直接在 PDF 内实现自动化。

批注

批注是附加到页面上的对象,用于表示评论、高亮、文件附件和其他交互式控件。它们显示在页面内容中,并支持审阅流程和协作。

下面的 C# 示例演示如何使用 Docotic.Pdf 向 PDF 页面添加文本批注,也称为便签。

using var pdf = new PdfDocument("example.pdf");
var page = pdf.Pages[0];

var textAnnot = page.AddTextAnnotation(new PdfPoint(50, 100), "Reviewer comment");
textAnnot.Contents = "Please check the figures on this page.";

pdf.Save("text-annotation.pdf");

下一个示例演示如何高亮文本和其他内容,以吸引对文档关键部分的注意。

using var pdf = new PdfDocument("example.pdf");
var page = pdf.Pages[0];

var color = new PdfRgbColor(255, 255, 120);
var annotationText = "Please confirm this part.";
var bounds = new PdfRectangle(50, 250, 120, 40);
page.AddHighlightAnnotation(annotationText, bounds, color);

pdf.Save("highlight-annotation.pdf");

PDF 标准定义了几种类型的 PDF 链接。其中最重要、最常用的是内部链接和超链接。

内部链接,也称为 GoTo 动作,允许跳转到同一 PDF 内的页面或命名目标。它们适用于交叉引用和内部导航。

下面是一个 C# 代码示例,它创建了一个从第一页跳转到索引等于 5 的页面的链接:

using var pdf = new PdfDocument();
var page = pdf.Pages[0];

int targetPageIndex = 5;
for (int i = 0; i < targetPageIndex; i++)
    pdf.AddPage();

var rect = new PdfRectangle(50, 50, 100, 40);
page.Canvas.DrawRectangle(rect);
page.AddLinkToPage(rect, targetPageIndex);

pdf.Pages[targetPageIndex].Canvas.DrawString(50, 50, "Glad to have you here.");

pdf.Save("link-to-page.pdf");

Layout API 提供了另一种方式来创建内部链接,无需绝对定位。

外部链接,也称为 URI 动作,会打开一个 Web URL。你可以使用 PdfPage.AddHyperlink 方法为 PDF 页面添加超链接。除此之外,方法与内部链接相同。

书签

书签,也称为大纲,是特殊的快捷方式或链接,帮助读者快速导航到特定章节或页面。读者单击书签时,查看器应用会跳转到文档的指定部分。

大纲显示在查看器的书签面板中,并提供类似纸质书目录的层级导航树,但它是交互式的。PDF 大纲可以包含主书签和嵌套书签,这使大型文档更易于组织。

下面的示例演示如何使用 C# 和 Docotic.Pdf 在 PDF 中创建书签。代码创建了三个顶级书签。第二个书签包含一个嵌套书签。

using var pdf = new PdfDocument();

for (int i = 0; i < 5; i++)
{
    var page = i == 0 ? pdf.Pages[0] : pdf.AddPage();

    var canvas = page.Canvas;
    canvas.FontSize = 14;
    canvas.DrawString(50, 50, $"Page {i + 1}");
}

var root = pdf.OutlineRoot;
root.AddChild("Getting Started", 1);

var child = root.AddChild("Things You Can Do", 2);
child.AddChild("Making Quick Improvements", 3);

root.AddChild("Keeping Everything Running Smoothly", 4);

pdf.PageMode = PdfPageMode.UseOutlines;

pdf.Save("bookmarks.pdf");

书签不同于你可能在纸质书页面上看到或在 PDF 中显示的目录。你可以通过测量标题并写入带页码的条目,以编程方式创建目录。

要了解使用 Layout API 创建目录的另一种方式,请查看我们的示例仓库中的相关代码。

PDF 脚本

JavaScript 动作是最强大的交互功能之一。PDF JavaScript 是 JavaScript 的一个子集,它暴露了文档和查看器 API。它用于表单验证、计算、用户界面对话框和小型自动化任务。

你可以将脚本附加到批注、书签、表单控件或打开动作上。使用 Docotic.Pdf,你可以将 JavaScript 代码嵌入 PDF。代码可以验证表单输入、计算值、显示或隐藏字段,或执行查看器交互。

共享 JavaScript 集合包含存储在文档级别的脚本。这些脚本可以被多个动作复用。换句话说,共享脚本适用于工具函数和共享逻辑。它们有助于减少重复并简化维护。

下面的代码定义了一个共享脚本,用于在 PDF 查看器中显示警告消息,然后演示如何通过将其分配给按钮的单击动作来触发该脚本。

using var pdf = new PdfDocument();

pdf.SharedScripts.Add(
    pdf.CreateJavaScriptAction("function messageBox(message) { app.alert(message,3); }")
);

var button = pdf.Pages[0].AddButton(50, 50, 100, 40);
button.Text = "Click me";
button.OnMouseUp = pdf.CreateJavaScriptAction("messageBox('Hello, dear!');");

pdf.Save("shared-javascript.pdf");

示例中的脚本很简单,但你可以构建任意复杂度的 JavaScript 动作。Adobe JavaScript API 参考提供了许多可用方法。请记住,非 Adobe 查看器通常只支持该 API 的一部分。

打开动作

打开动作是 PDF 查看器在文档打开时执行的动作。典型用途包括在特定页面打开、运行 JavaScript 初始化例程,或设置查看器首选项。打开动作的类型没有限制。

下面的示例演示如何创建一个 GoTo 打开动作。代码向第二页添加文本,并设置一个打开动作,使查看器在 PDF 打开时自动导航到该页面。

using var pdf = new PdfDocument();

var canvas = pdf.AddPage().Canvas;
canvas.FontSize = 14;

var message =
    "If you see this immediately after opening the file, " +
    "your PDF viewer supports open actions.";
var options = new PdfTextDrawingOptions(new PdfRectangle(100, 100, 100, 150));
canvas.DrawText(message, options);

pdf.OnOpenDocument = pdf.CreateGoToPageAction(1, 0);

pdf.Save("open-action.pdf");

请注意,并非所有查看器都会执行 JavaScript 打开动作。有些会忽略它们,或先提示用户。有些查看器会完全阻止打开动作。

要检查 PDF 是否包含打开动作,请将其加载到 PdfDocument 中并检查 OnOpenDocument 属性。如果该属性为 null,则文档未定义打开动作。

应用加密和数字签名

加密和数字签名针对你创建的 PDF 的安全性提供了两个互补方面。加密控制谁可以打开文档以及可以对其执行哪些操作,而签名证明是谁创建或批准了该文件,并确认其未被篡改。

密码保护允许你在创建时设置访问规则。你可以设置打开密码以限制查看,并设置所有者密码以定义打印、复制、编辑或填写表单等权限。证书加密提供更强、按接收者区分的保护,并且在向多人分发机密 PDF 而不依赖共享密码时效果很好。有关更多细节,请参阅使用密码和证书加密 PDF的文章。

数字签名在创建时添加真实性和完整性。Docotic.Pdf 可以使用文件中的证书、Windows 存储、硬件令牌、HSM 或云密钥服务签名 PDF。你可以包含时间戳和长期验证数据,以便签名在文档生成后很长时间仍可验证。还支持外部签名工作流,包括 PKCS#11 和云 KMS。

设置 PDF 元数据

PDF 元数据是嵌入文档中的描述信息,例如标题、作者、主题、关键字、创建日期及类似字段。它帮助软件、搜索引擎和文档管理系统在不打开文件的情况下理解文件内容。

PDF 文档可以在两个并存的系统中携带元数据:

  • XMP 元数据
  • 文档信息字典(Info 字典)

使用 Docotic.Pdf 向 PDF 文档添加 XMP 元数据的过程示意图。

XMP 是更丰富、结构化且标准化的描述性元数据嵌入格式。Info 字典简单且广泛支持,但功能有限,并且在 PDF 2.0 标准(ISO 32000‑2)中已弃用,转而推荐 XMP 元数据。Docotic.Pdf 可以读写这两个系统,并提供一个帮助方法来保持它们同步。

Docotic.Pdf 在保存 PDF 文件之前会自动更新某些元数据。例如,该库默认会设置 Producer 和 Creator 值。使用保存选项可以更改此行为,并保留显式设置的元数据值。

XMP 元数据

使用 PdfDocument.Metadata 属性访问和修改 PDF 中的 XMP 元数据。通过此属性,你可以处理 XMP Core、Dublin Core 和 PDF schema 等常见 schema,也可以管理自己的自定义元数据。

using var pdf = new PdfDocument();
var xmp = pdf.Metadata;

xmp.Pdf.Creator = new XmpString("Second-line authoring terminal");
xmp.Pdf.Title = new XmpString("Quarterly Report");

var creators = new XmpArray(XmpArrayType.Ordered);
creators.Values.Add(new XmpString("Second-line authoring terminal"));
creators.Values.Add(new XmpString("Assistive authoring terminal"));
xmp.DublinCore.Creators = creators;

var descriptions = new XmpArray(XmpArrayType.Alternative);
descriptions.Values.Add(new XmpLanguageAlternative("x-default", "Quarterly Report"));
descriptions.Values.Add(new XmpLanguageAlternative("fr", "Rapport trimestriel"));
descriptions.Values.Add(new XmpLanguageAlternative("de", "Quartalsbericht"));
xmp.DublinCore.Descriptions = descriptions;

var author1 = new XmpString("First Author");
author1.Qualifiers.Add("role", "main author");

var author2 = new XmpString("Second Author");
author2.Qualifiers.Add("role", "co-author");

var authors = new XmpArray(XmpArrayType.Unordered);
authors.Values.Add(author1);
authors.Values.Add(author2);
xmp.Custom.Properties.Add("authors", authors);

pdf.Save("with-xmp-metadata.pdf");

XMP 支持数组、结构和有类型的值,因此非常适合丰富的元数据。上面的代码还展示了如何在自定义 XMP schema 中存储应用特定属性。

文档信息字典

Info 字典主要存储文本字符串值。它体积小、支持广泛,但功能有限。对于需要与旧工具兼容的场景,请使用 Info 字典;其他情况下则优先使用 XMP。

同步元数据

保持两个元数据系统同步是个好习惯,可避免不一致而使读者和自动化工具产生混淆。

使用 PdfDocument.SyncMetadata 可以对齐 XMP 和 Info 值,使对应字段保持一致。该方法会用 XMP 补齐缺失的 Info 属性,反之也会用 Info 填充缺失的 XMP 字段。当 XMP 是权威来源时,将 preferXmp: true;当应以 Info 字典为准时,则设为 false

pdf.SyncMetadata(preferXmp: true);

有关该方法同步哪些属性的详细信息,请参阅 SyncMetadata 文档中的 Remarks 部分。

配置页面标签和查看器首选项

新创建的 PDF 可以受益于显式页码、精细调整的查看器首选项,以及选择合适的页面布局,以更有效地呈现文档内容。这些设置会影响读者首次查看和导航文件的方式。

页面标签

页面标签是告诉 PDF 查看器为每页显示什么标签的元数据。当可见编号必须与物理页索引不同时时使用它们。例如,当你希望 PDF 的前言使用 i, ii, iii,正文使用 1, 2, 3 时。

下面的 C# 代码演示如何为前 3 页使用小写罗马数字标签,并从第 4 页开始使用阿拉伯数字 1。

using var pdf = new PdfDocument();

for (int i = 0; i < 8; i++)
    pdf.AddPage();

pdf.PageLabels.AddRange(0, 2, PdfPageNumberingStyle.LowercaseRoman);
pdf.PageLabels.AddRange(3, PdfPageNumberingStyle.DecimalArabic);

pdf.Save("with-page-labels.pdf");

PDF 查看器首选项

PDF 查看器首选项是嵌入文档中的建议,用于提示查看器如何呈现文档。例如,你可以指定查看器应隐藏工具栏、居中窗口或使窗口适配页面。查看器首选项可与页面布局和打开动作设置互补。

下面是在 Docotic.Pdf 中更改 PDF 查看首选项的方法:

using var pdf = new PdfDocument();

pdf.ViewerPreferences.DisplayTitle = false;
pdf.ViewerPreferences.FitWindow = true;
pdf.ViewerPreferences.HideToolBar = true;
pdf.ViewerPreferences.HideMenuBar = true;
pdf.ViewerPreferences.HideWindowUI = true;
pdf.ViewerPreferences.CenterWindow = true;

pdf.Save("with-viewer-prefs.pdf");

请注意,具体取决于配置,Adobe Acrobat 和其他查看器可能会忽略这些首选项。

页面布局和页面模式

页面布局决定文档打开时页面如何排列:单页、单栏连续,还是双页展开。页面模式控制打开时显示哪些 UI 面板:书签/大纲、附件、缩略图,或不显示任何面板。

下面演示如何指定生成的 PDF 以双页展开方式显示,左页在前,并在打开时显示缩略图面板:

using var pdf = new PdfDocument();

for (int i = 0; i < 7; i++)
{
    var page = i > 0 ? pdf.AddPage() : pdf.Pages[0];
    page.Canvas.FontSize = 36;
    page.Canvas.DrawString(100, 100, $"Page {i + 1}");
}

pdf.PageLayout = PdfPageLayout.TwoPageLeft;
pdf.PageMode = PdfPageMode.UseThumbs;

pdf.Save("with-layout-and-mode.pdf");

保存 PDF

Docotic.Pdf 可以从你创建或编辑的同一文档生成不同的 PDF 文件或流。这些输出可以符合不同版本的 PDF 格式,字节长度不同,生成所需内存也不同。

该库生成 PDF 字节的方式取决于保存选项。当你没有显式指定保存选项时,PdfDocument 对象的 SaveSignAndSaveTimestampAndSave 方法会使用默认设置。这些默认值经过精心选择,适用于大多数场景,但你可能仍需进行调整。

有关可用选项及其默认值的详细信息,请参阅 PdfSaveOptions 类文档。下面各节突出介绍了一些更重要的选项,并提供实用建议。

PDF 版本

Docotic.Pdf 默认使用对象流,以更好地压缩其生成的文件。因此,库默认创建 PDF 1.5 文件和流。

PDF 1.5 需要 Adobe Reader 6(2003 年发布)或更高版本才能查看生成的文档。通常这不是问题,除非你必须支持旧工具、旧查看器或仅接受较旧 PDF 版本的嵌入式设备。

下面是如何保存为较旧 PDF 文件版本的方法:

using var pdf = new PdfDocument();

var options = new PdfSaveOptions
{
    Version = PdfVersion.Pdf14,
    UseObjectStreams = false,
};
pdf.Save("version-1.4.pdf", options);

要保存为 PDF 1.4 版本,也必须禁用对象流。如果文档包含后续版本引入的功能,库不会使用更旧的版本。

缩小文件大小

当以下保存选项设为 true 时,Docotic.Pdf 会生成更小的文件(按字节计):RemoveUnusedObjectsOptimizeIndirectObjectsWriteWithoutFormattingUseObjectStreams

下面是如何生成不包含未引用对象和额外空白、并将数据紧密打包到对象流中的 PDF:

using var pdf = new PdfDocument();

var options = new PdfSaveOptions
{
    UseObjectStreams = true,
    RemoveUnusedObjects = true,
    OptimizeIndirectObjects = true,
    WriteWithoutFormatting = true,
};
pdf.Save("optimized.pdf", options);

当 PDF 被完全重写时,这些选项最有效。在增量保存期间,它们只应用于新添加的修订版,无法清理或优化文件的早期部分。

增量更新

Docotic.Pdf 可以以增量方式更新 PDF。当 WriteIncrementallytrue 时,库会将更改附加到现有文件,而不是重写文件。之前的交叉引用和对象数据保持不变。附加的数据称为增量更新,而当前更新与所有先前更新一起构成文件的新修订版。

对于新建文档,增量更新不可用,因为没有可追加的先前修订版。该库会忽略新文档上的此选项,并以非增量模式写入。

何时需要增量更新

当向已包含签名的文档添加新数字签名时,必须以增量方式保存文件。对先前已签名的文件添加新批注或表单数据时,同样适用。在这些情况下重写整个文件会使现有签名失效。

同时,在应用第一个数字签名之前,最好先执行一次非增量(完整)保存,以便已签名的基线是一个干净、完全重写的文件。如果对包含早期修订结构问题的文档进行签名,可能会导致意外的签名验证问题。

在必须保留可审计修订历史或强制仅追加文档存储的工作流中,也需要增量附加。

使用增量更新的好处

增量更新允许同一文件包含多个签名,并允许在不使现有签名失效的前提下进行有限的签后修改,例如填写表单字段。

这种方法还支持更快地保存小改动,因为只写入修改过的数据。它还会保留文档的修订历史,这对于审计和其他合规驱动的工作流至关重要。

需要避免的问题和陷阱

增量更新不能对整个文件应用全局压缩,也不能移除过时对象,因为它们只附加已修改的对象。因此,与完整重写相比,它们通常会生成更大的、优化程度更低的文件。

文件大小会随着每次修订增长,即使没有未使用的对象也是如此,因为所有先前修订都会保留在文件中并继续占用空间。

早期修订中的敏感或错误信息仍可被恢复,并且通过附加新数据不会修正现有 PDF 格式问题或先前修订中的结构缺陷。

最后,一些查看器和处理工具难以处理多修订版 PDF。在依赖增量更新之前,请确保所有文档使用方都能处理包含多个修订版的文件。

测试 PDF 输出

自动化 PDF 测试通过将生成的 PDF 与存储在仓库或制品存储中的基线 PDF 进行比较,来防止发布出现回归。基线有助于检测文本、字体、图像或布局中的意外变化,并减少每次构建都需要手动 QA 的工作量。

将结构检查、文本提取和视觉比较结合起来,可获得最可靠的结果。

方法快速对比

方法 速度 敏感度 最适合
结构比较 高:检测对象级变更 需要确认同一文档两个版本在结构上完全一致的回归测试
文本提取 中:通常忽略布局变化 验证语义内容和表格
视觉差异比对 较慢 高:检测内容以及渲染/布局变化 捕获视觉回归

比较文档结构

使用 PdfDocument.DocumentsAreEqual 比较 PDF 对象图、PDF 版本和文档安全存储(DSS),同时忽略与时间相关的文档属性。该方法还会忽略文档元数据、trailer IDs 和其他自动生成的属性。

对于必须确保没有添加或删除意外对象的 PDF 文档测试工作流,此方法非常适合。DocumentsAreEqual 支持文件和流重载,并且可以比较加密的 PDF。

Docotic.Pdf 示例中提供了一个演示此技术的完整示例。除了展示如何在常规 .NET 应用程序中使用该方法外,该示例还演示了如何在 Native AOT 应用程序中使用 DocumentsAreEqual

通过提取文本验证 PDF

从整个文档一次性提取文本,或逐页提取,并比较字符串。你可以使用文本提取选项来微调提取过程,例如排除包含页脚的矩形区域。为便于比较,你还可以将提取出的文本拆分为行或单词。

对于结构化检查,先提取每个文本块、单词或字符的文本,并包含位置、字体和其他详细信息。然后将每个提取元素与对应的基线元素进行比较。

检测视觉差异

首先将 PDF 页面渲染为图像,并将每张图像与基线图像进行比较。使用 ImageSharp.Compare 或 Magick.NET 等专用库来检测图像差异。

优先采用严格的逐像素比较,以确保两张图像中每个对应像素都必须匹配。如果你的要求允许少量渲染差异,可以调整比较逻辑以容忍轻微差别,但精确像素相等能提供最可靠的结果。

可以考虑使用哈希作为快速预检,以判断两张图像是否很可能相同,而无需进行完整的逐像素比较。为每张渲染后的图像计算一个 SHA-256 哈希,如果哈希匹配,则这些图像几乎肯定相同。如果哈希不同,再执行完整的逐像素比较。

结论

Docotic.Pdf 为在 .NET 中创建和处理 PDF 提供了一个全面、多层次的工具包。开发者可以在使用 Core API 进行低级控制、使用 Layout API 进行高层文档生成,或为已围绕 Web 技术构建的工作流使用 HTML 转 PDF 转换之间进行选择。

该库还支持基于图像的 PDF、模板驱动生成,以及丰富的交互功能集,例如批注、链接、书签、JavaScript 动作和打开动作。

为确保可靠性,Docotic.Pdf 还提供了测试 PDF 输出的方法,以便你应用中的更改不会引入回归或意外差异。