PDFsharp 指南:使用 MigraDoc 和 PdfSharpCore 创建 PDF 文件
PDFsharp 是一个开源的 .NET 库,用于通过 C# 代码创建和编辑 PDF 文档。您只需在页面上绘制文本、线条和图像,调用 Save 方法,即可获得一个 PDF 文件。无需浏览器引擎,也无需原生二进制文件。该项目始于 2005 年,如今许多 .NET 后端生成的发票、报告和证书仍然需要经过它。
我写这篇文章是为了帮助那些需要在服务器上使用 PDF 的开发者,让他们了解自己将要面对的问题。我们将介绍 PDFsharp 和 MigraDoc 如何协同工作、应该安装哪个版本、如何创建第一个可用的文件、Linux 系统上的字体问题、最近的许可变更,以及何时应该选择其他解决方案。
什么是PDFsharp?它能做什么?
PDFsharp库的幕后功臣是empira Software GmbH,一家德国小型软件公司,他们让这个项目得以延续近二十年。你的大部分时间都花在XGraphics类上。如果你曾经使用System.Drawing.Graphics进行过WinForms绘图,那么十分钟之内你就能上手:字体、画笔、x轴和y轴,开始吧!
开箱即用:
- 使用文本、矢量形状、线条和图像从头开始创建 PDF 文档
- 打开现有的 PDF 文件,并向其页面添加内容,例如图章、水印或页码。
- 通过将页面从一个文件导入到另一个文件,合并和拆分文档。
- 嵌入并筛选字体,使文件在每台机器上看起来都一样。
- 添加书签、链接和文档元数据,例如标题、作者和关键词
- 使用密码和权限标志保护文件
- 自 6.2 版本起,支持电子签名。
部署过程很枯燥,我这么说其实是赞扬。所有代码都是托管的,所以运维人员再也不用在服务器上安装无头 Chrome 浏览器,也不用在凌晨两点到处找 Ghostscript DLL 文件了。你只需要引用一个 NuGet 包,推送一下,就搞定了。
现在问题来了。你得到的只是一个画布,仅此而已。长段落超出底部边距?那是你的问题。表格超出第一页?也是你的问题,除非你引入 MigraDoc。
PDFsharp 和 MigraDoc:你需要哪个层?
同一家供应商,同样的发布周期,但工作内容却截然不同。MigraDoc Foundation 的办公地点就在 PDFsharp 楼上。你无需告诉它字符串应该放在哪里,只需提供章节、段落、表格、页眉、页脚、样式以及一些图表,它就能自动计算出所有内容的位置。最终生成 PDF 文件。或者,如果你的同事坚持要用 Word 打开所有文件,它也会生成 RTF 文件。
我的简写是:PDFsharp 是“把这段文本放在 x=40,y=120 的位置”。MigraDoc 是“这里有 30 行和一个重复的页眉,你自己整理页面”。
| 任务 | 最佳匹配 | 为什么 |
|---|---|---|
| 单页证书或票据,位置固定 | PDFsharp | 完全控制每个坐标 |
| 包含表格和页眉的多页报告 | MigraDoc | 自动分页和样式 |
| 发票包含可变数量的行项目 | MigraDoc | 表格会跨页扩展和分割 |
| 在现有 PDF 文件上添加印章、水印或页码 | PDFsharp | MigraDoc 仅创建新文档 |
| 合并或拆分现有文件 | PDFsharp | 页面导入是 PDFsharp 的一项功能。 |
| 报告内的业务图表 | MigraDoc | 内置图表支持 |
| 混合:生成的报告加上自定义绘制的封面 | 两个都 | MigraDoc 可以渲染到 PDFsharp 页面上 |
实际项目中往往两者都会用到。正文通过 MigraDoc 生成,而手绘封面或供应商提供的 PDF 上的印章则通过底层实现。无论如何,你都无法避免安装 PDFsharp,因为 MigraDoc 软件包会将其作为依赖项引入。
PDFsharp 构建详解:核心构建、GDI+ 和 WPF
对于新手来说,在 NuGet 包列表中,PDFsharp 6 通常会排在下一个位置。它有三个版本,每个版本都对应一个 MigraDoc 版本。
| NuGet 包 | 建造 | 平台 | 字体和图像 |
|---|---|---|---|
| PDFsharp / PDFsharp-MigraDoc | 核 | Windows、Linux、macOS | 需要字体解析器;支持 BMP、PNG、JPEG 图像 |
| PDFsharp-GDI / PDFsharp-MigraDoc-GDI | GDI+ | 仅限 Windows 系统 | 通过 System.Drawing 使用已安装的字体 |
| PDFsharp-WPF / PDFsharp-MigraDoc-WPF | WPF | 仅限 Windows 系统 | 通过 WPF 使用已安装的字体 |
目前的软件包面向 .NET 8、9 和 10,以及 .NET Standard 2.0 和 .NET Framework 4.6.2。因此,几乎所有仍在开发中的项目都涵盖在内,从尘封已久的 WinForms 工具到全新的 ASP.NET Core API。
控制台应用、Web API、Docker、Azure Functions,以及任何基于 Linux 的应用?核心构建即可。GDI+ 或 WPF 仅适用于 Windows 桌面应用,例如需要使用系统字体或在原生控件中预览的应用。Stack Overflow 上有人问过他们的控制台应用是否需要 WPF 版本,答案是否定的。

如何在 Visual Studio 中通过 NuGet 安装 PDFsharp
你仍然会看到一些来自 WinForms 时代的论坛帖子,教你下载 ZIP 文件,找出其中的 DLL 文件,然后自己添加引用。忽略它们。通过 NuGet 安装 PDFsharp;包管理器会自动跟踪版本,所以你无需手动操作。
- 在 Visual Studio 中打开您的解决方案,然后在解决方案资源管理器中右键单击该项目。
- 选择“管理 NuGet 程序包” ,切换到“浏览”选项卡,然后搜索“PDFsharp”。
- 如果您还需要文档排版功能,请选择PDFsharp (核心版本)或PDFsharp-MigraDoc 。请确认发行商为 empira Software GmbH。
- 点击“安装”并接受许可提示。
- 请构建一次项目,以确认软件包已正确恢复。
终端用户可以跳过点击步骤:
bash dotnet add package PDFsharp dotnet add package PDFsharp-MigraDoc
在包管理器控制台中,同样的操作是运行 `Install-Package PDFsharp`。另外,请不要混用不同的构建版本。我亲眼见过一个团队因为类型冲突浪费了一整个下午的时间,原因就是一个项目引用了 PDFsharp,而它的邻近项目却引入了 PDFsharp-GDI。
使用 C 语言中的 PDFsharp 创建您的第一个 PDF 文档
下面介绍的是一个可以生成单页 PDF 文件的最小程序。只要设置了字体解析器(下一节),它就可以在任何操作系统上的 Core 版本上运行。
```csharp
使用 PdfSharp.Drawing;
使用 PdfSharp.Pdf;
// PDFsharp 6.5+ 首次使用前需要选择许可证
PdfSharpLicense.SetAgplLicense();
var document = new PdfDocument();
document.Info.Title = "Hello PDFsharp";
PdfPage page = document.AddPage();
XGraphics gfx = XGraphics.FromPdfPage(page);
var font = new XFont("Arial", 20, XFontStyleEx.Bold);
gfx.DrawString("你好,PDFsharp!", font, XBrushes.Black,
new XRect(0, 0, page.Width.Point, page.Height.Point),
XStringFormats.Center);
document.Save("hello.pdf");
```
所有 PDFsharp 程序都遵循以下结构:文档、页面、XGraphics、绘制、保存。单位为点(1/72 英寸),原点位于左上角。在 6.2.x 版本中,删除许可行。该 API 仅在 6.5.0 版本中出现。
MigraDoc 版本:
```csharp
使用 MigraDoc.DocumentObjectModel;
使用 MigraDoc.Rendering;
var doc = new Document();
Section section = doc.AddSection();
section.AddParagraph("你好,MigraDoc!");
var renderer = new PdfDocumentRenderer { Document = doc };
renderer.RenderDocument();
renderer.PdfDocument.Save("hello-migradoc.pdf");
```
没有任何坐标。随便塞500段文字进去,就能得到相应的页数,而且你这边完全不需要任何分页代码。
PDFsharp核心构建中的字体:解析器问题
随便问问就知道,字体问题是最常见的抱怨。GDI+ 和 WPF 构建只能读取 Windows 系统中已安装的 TrueType 字体。而 Core 构建则无法做任何假设,因为精简的 Linux 容器通常根本没有可用的字体。因此,首次 Docker 部署往往会因为找不到合适的字体而抛出异常。
解决方法是使用字体解析器,这是一个实现了 IFontResolver 接口的类。它有两个方法:
- ResolveTypeface 将请求的字体系列和样式(例如,“Arial”,粗体)映射到字体名称
- GetFont 返回该字体名称对应的 TTF 文件的原始字节。
在启动时通过 GlobalFontSettings.FontResolver 注册一次,在创建第一个 XFont 之前。
```csharp
public class AppFontResolver : IFontResolver
{
public FontResolverInfo? ResolveTypeface(string family, bool bold, bool italic)
=> new FontResolverInfo(bold ? "Inter-Bold" : "Inter-Regular");
public byte[]? GetFont(string faceName) => File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Fonts", faceName + ".ttf"));
}
GlobalFontSettings.FontResolver = new AppFontResolver();
```
在你亲身经历之前,有必要了解以下几点:
- 将 TTF 文件保存在项目的 Fonts 文件夹中,并设置将其复制到输出目录,或者将其作为资源嵌入,以便容器镜像始终拥有它们。
- 6.2.0 版本移除了旧的 CoreBuildFontResolver。在 Windows 或 WSL2 上,您可以使用 GlobalFontSettings.UseWindowsFontsUnderWindows 恢复类似功能,但这在 Linux 或 macOS 上无效。
- 官方示例库中包含一个 SubstitutingFontResolver,您可以将其复制作为起点。
- 在将商业字体嵌入生成的文件之前,请务必检查字体许可。像 Inter、Roboto 或 Noto 这样的开源字体则完全避免了这个问题。
有了解析器之后,在 Windows、Linux 和 macOS 上的输出看起来完全相同。
使用 PDFsharp 6 合并、签名和保护文件
很多团队从来不从头开始创建文档。他们使用库来粘贴、添加标记或锁定已有的文件。合并文档只需几行代码:以导入模式打开每个源文件,然后复制页面即可。
csharp var output = new PdfDocument(); foreach (var path in new[] { "part1.pdf", "part2.pdf" }) { using var input = PdfReader.Open(path, PdfDocumentOpenMode.Import); foreach (PdfPage p in input.Pages) output.AddPage(p); } output.Save("merged.pdf");
拆分操作与此循环相反,每个页面或范围生成一个输出文件。为了保护数据安全,`document.SecuritySettings` 接受用户密码、所有者密码和权限标志(例如打印、复制文本等)。
两个用户期待已久的功能终于在 6.2 版本中实现了:
- 基于 CMS 的数字签名。默认签名器包含在一个单独的 PDFsharp.Cryptography 程序集中,您可以通过实现 IDigitalSigner 接口来插入您自己的签名器。时间戳服务器支持在现代 .NET 版本中有效,但在 .NET Framework 版本中无效。
- 用于归档的PDF/A 输出。官方文档称其“存在一些局限性”且仍在开发中,因此在依赖其合规性之前,请使用 PDF/A 检测器验证结果。
该版本还引入了彩色表情符号字形和受管理的 MD5 实现,如果您在 FIPS 规则下或在 Blazor 中运行,这将非常重要。
PDFsharp 代码库、PdfSharpCore 分支和 1.x 版本
在 GitHub 上搜索 PDFsharp,你会发现一些名称几乎相同的项目。这都怪历史遗留问题。
旧版 1.x 系列(1.50、1.51)仅支持 .NET Framework。.NET Core 发布后,人们不愿等待,便自行移植。其中最广为人知的当属 Stefan Steiger 创建的 PdfSharpCore,它后来被其他分支继承和完善。PdfSharpCore 移除了 GDI+,并在官方发布前数年就实现了在 Linux 上的运行。
随后 PDFsharp 6 问世:它基于现代 .NET 进行了彻底重写,并提供了官方的跨平台核心版本。各种分支版本失去了存在的意义。
| 项目 | 维护者 | 平台 | 地位 |
|---|---|---|---|
| PDFsharp 6.x(empira/PDFsharp 存储库) | empira Software GmbH | .NET 8–10、.NET Standard 2.0、.NET Framework 4.6.2 | 积极维护 |
| PDFsharp 1.5x | empira Software GmbH | 仅限 .NET Framework | 源代码保存在 GitHub 上,但已停止维护。 |
| PdfSharpCore 及其分支 | 社区 | .NET Core / .NET Standard 2.0 | 因分支而异,更新速度较慢 |
| PDFsharp-MigraDoc 6.x | empira Software GmbH | 与 PDFsharp 6 相同 | 积极维护 |
所以,Reddit 上反复出现的“PdfSharp vs PdfSharpCore”帖子呢?新项目,官方软件包。只有当旧代码已经依赖于它,并且现在迁移不值得投入大量时间时,才保留分支。如果觉得文档不够完善,可以去 empira 的独立示例仓库。它的文件夹和文件都是可运行的示例,比猜测要好得多。
PDFsharp是免费的吗?PDFsharp 6.5的许可变更
是的,在其大部分生命周期内都是如此。它采用的是 MIT 许可证,这意味着可以几乎不受任何限制地用于商业用途。包括 6.2.4 在内的所有版本仍然如此。
6.5.0 版本改变了这一点。该库现在采用双重许可:
- AGPL(免费)。您需要在使用库之前调用 `PdfSharpLicense.SetAgplLicense()` 来启用它。AGPL 要求您将源代码提供给用户,包括通过网络与您的软件交互的用户,因此它不太适合闭源 SaaS。
- 商业许可。您从供应商处购买许可密钥,并使用 PdfSharpLicense.LoadLicense(...) 加载它。这将解除 AGPL 协议的约束。
从 6.5.0 版本开始,你的代码必须明确地选择一个版本。7.0 版本的预览版也采用相同的方式。
实际操作中:
- 与 AGPL 兼容的开源项目可以继续免费使用最新版本。
- 闭源产品可以选择购买商业许可,也可以继续使用 6.2.x 版本,该版本仍遵循 MIT 许可协议。继续使用旧版本意味着会错过未来的修复,因此请认真权衡利弊。
- 无论您选择哪种方案,请务必仔细阅读许可协议原文,并请您的法律团队签字确认。本指南不构成法律建议。

PDFsharp 的不足之处以及与之搭配使用的软件
这个库的功能很完善,只是功能范围比较窄,而且每个论坛帖子都会提到同样的问题。
- PDFsharp 不支持HTML 转 PDF。它本身并不渲染 HTML 或 CSS。虽然有一个名为 HtmlRenderer.PdfSharp 的社区插件,但它仅支持 HTML 4.01 和 CSS Level 2,因此使用 flexbox 或 grid 等现代布局的 PDFsharp 将无法正常工作。需要 HTML 模板的团队通常会选择使用无头浏览器渲染器。
- 不支持 JavaScript。无法捕获客户端构建的图表或内容。
- 不支持图像渲染。PDFsharp无法将 PDF 页面转换为 PNG 格式以生成缩略图或预览图。您需要使用单独的栅格化工具才能实现此功能。
- 文本提取功能有限。虽然可以遍历内容流,但没有高级 API 能够返回带有位置信息的干净文本。PdfPig 是一个独立的开源库,专门用于读取和提取文本。
- 部分表单支持。简单的 AcroForm 表单填写功能正常;但包含脚本的复杂表单则另当别论。
- 表格需要使用 MigraDoc。如果使用普通的 PDFsharp,则需要自行计算每一行的高度。
但这并不意味着它是个糟糕的选择。发票、账单、标签和证书等数据驱动型文档正是它的优势所在,而且它占用空间极小。但要完美复制网页则不然。
PDFsharp 加密货币发票和付款收据
收据可能是人们使用 PDFsharp 最常创建的内容。加密货币收据比银行卡收据稍微复杂一些。好消息是:一旦支付网关收到你的 webhook(支付状态变更时发送的 HTTP 回调),你就拥有了所有数据。无需额外查找。
我会在上面写些什么:
- 订单或发票编号以及商家详情
- 支付时的加密货币金额及其等值法币金额
- 币种和网络(例如,TRON 上的 USDT 与以太坊上的 USDT)
- 交易哈希值,以便客户可以在区块浏览器上进行验证。
- 缩短的接收钱包地址和确认时间
- 指向交易或发票页面的二维码
哈希值和地址使用等宽字体。顾客会眯着眼睛一个个地看,而 Arial 字体中,数字 0 和大写字母 O 看起来就像双胞胎。如果哈希值太宽,请换行;6 磅的字体对顾客没有任何帮助。嵌入字体,将文件与订单一起保存,并在文件上签名,以防会计需要收据。
以上内容并未涵盖实际收款环节。要实现这一功能,你需要接入像Plisio这样的支付网关。它可以接收 BTC、USDT 以及其他多种加密货币,生成发票,并在状态每次变更时调用你的端点。你的处理程序会将有效负载传递给 MigraDoc,买家在感谢页面上就能收到带有你公司 logo 的收据。
结论
我一直选择 PDFsharp 的原因只有一个:它体积小巧,不会占用太多空间。Core 版本可以在任何支持 .NET 的平台上运行,而当画布不足以满足需求时,MigraDoc 可以接管布局工作。有三件事需要尽早确定:选择哪个版本、字体加载方式以及你使用的许可证(因为 6.5 版本现在采用的是 AGPL 许可证)。它用于打印数据,而不是网页,因此可以稳定运行多年。