该页面可以包含自动翻译的文本。
如何创建 PDF 文档 in C# 和 VB.NET
本文介绍如何借助 Docotic.Pdf 库在 .NET 中创建 PDF 文档的不同方式。它是一个高性能、纯 C# .NET 库,无需外部依赖,可用于创建、编辑、转换和处理 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 图像格式。

当 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 在空白区域放置文本和图像。在简单情况下,例如只需添加姓名和照片,这种方法效果很好。你需要知道这些区域的坐标和尺寸;为了正确放置文本,可能还需要先测量文本,再据此对齐。
处理可变长度或多行文本更具挑战性,但仍然可行。通过组合使用 DrawText、DrawString 以及文本测量方法,你可以按需换行并定位文本。如果模板中这类区域不止几个,建议考虑其他方案,例如使用 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字典)

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 对象的 Save、SignAndSave 和 TimestampAndSave 方法会使用默认设置。这些默认值经过精心选择,适用于大多数场景,但你可能仍需进行调整。
有关可用选项及其默认值的详细信息,请参阅 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 会生成更小的文件(按字节计):RemoveUnusedObjects、OptimizeIndirectObjects、WriteWithoutFormatting 和 UseObjectStreams。
下面是如何生成不包含未引用对象和额外空白、并将数据紧密打包到对象流中的 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。当 WriteIncrementally 为 true 时,库会将更改附加到现有文件,而不是重写文件。之前的交叉引用和对象数据保持不变。附加的数据称为增量更新,而当前更新与所有先前更新一起构成文件的新修订版。
对于新建文档,增量更新不可用,因为没有可追加的先前修订版。该库会忽略新文档上的此选项,并以非增量模式写入。
何时需要增量更新
当向已包含签名的文档添加新数字签名时,必须以增量方式保存文件。对先前已签名的文件添加新批注或表单数据时,同样适用。在这些情况下重写整个文件会使现有签名失效。
同时,在应用第一个数字签名之前,最好先执行一次非增量(完整)保存,以便已签名的基线是一个干净、完全重写的文件。如果对包含早期修订结构问题的文档进行签名,可能会导致意外的签名验证问题。
在必须保留可审计修订历史或强制仅追加文档存储的工作流中,也需要增量附加。
使用增量更新的好处
增量更新允许同一文件包含多个签名,并允许在不使现有签名失效的前提下进行有限的签后修改,例如填写表单字段。
这种方法还支持更快地保存小改动,因为只写入修改过的数据。它还会保留文档的修订历史,这对于审计和其他合规驱动的工作流至关重要。
需要避免的问题和陷阱
增量更新不能对整个文件应用全局压缩,也不能移除过时对象,因为它们只附加已修改的对象。因此,与完整重写相比,它们通常会生成更大的、优化程度更低的文件。
文件大小会随着每次修订增长,即使没有未使用的对象也是如此,因为所有先前修订都会保留在文件中并继续占用空间。
早期修订中的敏感或错误信息仍可被恢复,并且通过附加新数据不会修正现有 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 输出的方法,以便你应用中的更改不会引入回归或意外差异。