比较

用 C# 将 HTML 转换为 PDF - 库选项的现实意义

在 C# 中将 HTML 转换为 PDF 需要一个能够真正渲染 HTML 的库,而不是一个只能解析部分标签并近似 CSS 2.1 的库。Stack Overflow 帖子和 Reddit 讨论中推荐的大多数库要么无法渲染现代 CSS,要么存在使其无法用于商业用途的许可限制,要么已被弃用且存在未修复的安全漏洞。

本文比较了开发人员在搜索"HTML to PDF C#"时实际遇到的库,记录了每个库可以渲染和不能渲染的内容,包括性能基准测试和方法论,并展示了每种方法的实际运营成本。

快速入门:C# 中的 HTML 转 PDF

using IronPdf;

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1>");
pdf.SaveAs("output.pdf");
using IronPdf;

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1>");
pdf.SaveAs("output.pdf");
Imports IronPdf

Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1>")
pdf.SaveAs("output.pdf")
$vbLabelText   $csharpLabel

通过 NuGet 安装: Install-Package IronPDF 。 无需外部依赖即可部署到 Windows、Linux、macOS 和 Docker。

为什么HTML转PDF很困难?

将 HTML 正确渲染为 PDF 需要实现与 Web 浏览器相同的五个组件:HTML 解析器、CSS 引擎(包括 Flexbox、Grid、层叠、优先级和媒体查询)、JavaScript 运行时、布局引擎以及将所有这些内容以亚像素精度合成到 PDF 的渲染管道。

传统 PDF 库部分实现了前两个功能,完全跳过了 JavaScript。 这就是为什么它们可以处理简单的 HTML,但无法处理任何现代浏览器能够正确渲染的内容。 要想匹配浏览器输出,唯一的办法就是使用浏览器引擎。

哪些库可以将 HTML 转换为 PDF?

wkhtmltopdf封装器 — DLL 加载错误生态系统

开发者们最常搜索到这些文章的关键词是以下几种变体:

System.DllNotFoundException:无法加载 DLL"libwkhtmltox"

平台特定变体包括:

无法加载共享库"wkhtmltox"或其某个依赖项
    (Linux — libwkhtmltox.so not found)

找不到指定的模块。 (0x8007007E)
    (Windows — wkhtmltox.dll path not configured)

dyld:库未加载:libwkhtmltox.dylib
    (macOS — not supported on ARM64/Apple Silicon)

这些错误来自 DinkToPdf、NReco.PdfGenerator、WkHtmlToXSharp 以及其他围绕同一个已废弃二进制文件的 C# 封装程序。 wkhtmltopdf GitHub 组织已于 2024 年 7 月存档。 QtWebKit 底层引擎已于 2015 年被 Qt 弃用。项目状态页面明确将其标记为已弃用。

除了 DLL 加载问题之外,渲染引擎的性能也仅停留在 Safari 2011 的水平。 没有 Flexbox,没有 Grid,CSS3 功能有限,JavaScript 不稳定。 此外,还有一些未修复的严重漏洞: CVE-2022-35583 (CVSS 9.8) 可利用 SSRF 攻击,通过精心构造的 HTML 窃取 AWS 凭证。

wkhtmltopdf 的时代已经过去了。 DLL 加载错误是更深层次问题的征兆:你依赖的软件已经过时,没有出路。

iText 7(pdfHTML 插件)— 有限的 CSS,AGPL 许可

iText 的 pdfHTML 模块使用自定义解析器(而非浏览器引擎)将 HTML 转换为 PDF。它可以处理基本的 HTML/CSS,但不会渲染 Flexbox、Grid 或 JavaScript。

故障模式是静默的:pdfHTML 在遇到不支持的 CSS 时不会抛出异常。 它只渲染能够渲染的部分,忽略其余部分。例如,一个设置了 `display: flexgap: 20pxjustify-content: space-between容器,会被渲染成垂直堆叠的元素,彼此之间没有间距。 开发人员是在集成之后才发现这个问题,而不是在集成过程中发现的。

许可: AGPL — 要求开源您的整个网络可访问应用程序,或购买商业许可。 价格未公布; 第三方数据显示,年收入为 15,000 美元至 210,000 美元。

内存使用情况对比如何?

iText 的 pdfHTML 功能会将整个文档加载到内存中进行处理。 对于一般的商业文档来说,这是可以控制的,但与流式传输方式相比,包含嵌入式图像的大型 HTML 报告可能会造成严重的内存压力。

为什么PdfSharp不支持 HTML?

PdfSharp因其受欢迎程度(3490 万 NuGet 下载量)和频繁推荐,出现在"HTML 转 PDF"的搜索结果中。 但是PdfSharp没有 HTML 解析器。 它提供基于坐标的绘图 API: DrawString()DrawRectangle()DrawImage()并带有明确的 X/Y 位置。

常用的解决方法HtmlRenderer.PdfSharp仅支持 HTML 4.01 和 CSS Level 2。 如果您的 HTML 使用了 2010 年之后引入的任何 CSS 功能——Flexbox(2012 年)、Grid(2017 年)、自定义属性(2017 年)、 border-radius (2011 年)——它将无法渲染。

选择PdfSharp并期望获得 HTML 支持的开发者最终要么需要使用基于坐标的代码手动定位每个元素,要么需要添加第二个库来进行 HTML 渲染——在这种情况下, PdfSharp就显得多余了。

是什么让《傀儡师》成为资源密集型游戏?

Puppeteer Sharp通过 .NET 绑定控制无头 Chrome 浏览器。 渲染精度与 Chrome 一致,因为它就是 Chrome。 成本在于运营方面:您需要管理外部浏览器进程。

以下是 木偶师夏普 生产环境的实际部署示例——不是教程中的 5 行示例代码,而是并发生成 PDF 所需的浏览器池代码:

using PuppeteerSharp;
using System.Collections.Concurrent;

public class PdfBrowserPool : IAsyncDisposable
{
    private readonly ConcurrentBag<IBrowser> _available = new();
    private readonly SemaphoreSlim _semaphore;
    private readonly int _maxBrowsers;

    public PdfBrowserPool(int maxBrowsers = 4)
    {
        _maxBrowsers = maxBrowsers;
        _semaphore = new SemaphoreSlim(maxBrowsers, maxBrowsers);
    }

    public async Task InitializeAsync()
    {
        await new BrowserFetcher().DownloadAsync(); // ~280MB download
        for (int i = 0; i < _maxBrowsers; i++)
        {
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                Args = new[] { "--no-sandbox", "--disable-setuid-sandbox",
                               "--disable-dev-shm-usage" }
            });
            _available.Add(browser);
        }
    }

    public async Task<byte[]> ConvertHtmlToPdf(string html)
    {
        await _semaphore.WaitAsync();
        IBrowser browser = null;
        try
        {
            if (!_available.TryTake(out browser))
                throw new InvalidOperationException("No browser available");

            await using var page = await browser.NewPageAsync();
            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
            });
            var result = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true
            });
            return result;
        }
        catch (Exception ex) when (ex is NavigationException or TargetClosedException)
        {
            // Browser crashed — replace it
            browser?.Dispose();
            browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
            });
            throw; // Re-throw after recovery
        }
        finally
        {
            if (browser != null) _available.Add(browser);
            _semaphore.Release();
        }
    }

    public async ValueTask DisposeAsync()
    {
        foreach (var browser in _available)
        {
            await browser.CloseAsync();
            browser.Dispose();
        }
    }
}
using PuppeteerSharp;
using System.Collections.Concurrent;

public class PdfBrowserPool : IAsyncDisposable
{
    private readonly ConcurrentBag<IBrowser> _available = new();
    private readonly SemaphoreSlim _semaphore;
    private readonly int _maxBrowsers;

    public PdfBrowserPool(int maxBrowsers = 4)
    {
        _maxBrowsers = maxBrowsers;
        _semaphore = new SemaphoreSlim(maxBrowsers, maxBrowsers);
    }

    public async Task InitializeAsync()
    {
        await new BrowserFetcher().DownloadAsync(); // ~280MB download
        for (int i = 0; i < _maxBrowsers; i++)
        {
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                Args = new[] { "--no-sandbox", "--disable-setuid-sandbox",
                               "--disable-dev-shm-usage" }
            });
            _available.Add(browser);
        }
    }

    public async Task<byte[]> ConvertHtmlToPdf(string html)
    {
        await _semaphore.WaitAsync();
        IBrowser browser = null;
        try
        {
            if (!_available.TryTake(out browser))
                throw new InvalidOperationException("No browser available");

            await using var page = await browser.NewPageAsync();
            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
            });
            var result = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true
            });
            return result;
        }
        catch (Exception ex) when (ex is NavigationException or TargetClosedException)
        {
            // Browser crashed — replace it
            browser?.Dispose();
            browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
            });
            throw; // Re-throw after recovery
        }
        finally
        {
            if (browser != null) _available.Add(browser);
            _semaphore.Release();
        }
    }

    public async ValueTask DisposeAsync()
    {
        foreach (var browser in _available)
        {
            await browser.CloseAsync();
            browser.Dispose();
        }
    }
}
Imports PuppeteerSharp
Imports System.Collections.Concurrent
Imports System.Threading

Public Class PdfBrowserPool
    Implements IAsyncDisposable

    Private ReadOnly _available As New ConcurrentBag(Of IBrowser)()
    Private ReadOnly _semaphore As SemaphoreSlim
    Private ReadOnly _maxBrowsers As Integer

    Public Sub New(Optional maxBrowsers As Integer = 4)
        _maxBrowsers = maxBrowsers
        _semaphore = New SemaphoreSlim(maxBrowsers, maxBrowsers)
    End Sub

    Public Async Function InitializeAsync() As Task
        Await (New BrowserFetcher()).DownloadAsync() ' ~280MB download
        For i As Integer = 0 To _maxBrowsers - 1
            Dim browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {
                .Headless = True,
                .Args = New String() {"--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"}
            })
            _available.Add(browser)
        Next
    End Function

    Public Async Function ConvertHtmlToPdf(html As String) As Task(Of Byte())
        Await _semaphore.WaitAsync()
        Dim browser As IBrowser = Nothing
        Try
            If Not _available.TryTake(browser) Then
                Throw New InvalidOperationException("No browser available")
            End If

            Await Using page = Await browser.NewPageAsync()
                Await page.SetContentAsync(html, New NavigationOptions With {
                    .WaitUntil = New WaitUntilNavigation() {WaitUntilNavigation.Networkidle0}
                })
                Dim result = Await page.PdfAsync(New PdfOptions With {
                    .Format = PaperFormat.A4,
                    .PrintBackground = True
                })
                Return result
            End Using
        Catch ex As Exception When TypeOf ex Is NavigationException OrElse TypeOf ex Is TargetClosedException
            ' Browser crashed — replace it
            browser?.Dispose()
            browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {
                .Headless = True,
                .Args = New String() {"--no-sandbox", "--disable-setuid-sandbox"}
            })
            Throw ' Re-throw after recovery
        Finally
            If browser IsNot Nothing Then _available.Add(browser)
            _semaphore.Release()
        End Try
    End Function

    Public Async Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
        For Each browser In _available
            Await browser.CloseAsync()
            browser.Dispose()
        Next
    End Function
End Class
$vbLabelText   $csharpLabel

在生成单个 PDF 文件之前,需要编写大约 60 行基础架构代码。 您还需要内存泄漏监控(Chromium 进程会随着时间的推移积累内存)、健康检查以及包含 20 多个 Chromium 依赖项的 Dockerfile。 Docker 镜像大小增加 300-400MB。

将此与IronPDF的等效方法进行比较:

using IronPdf;

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
// Browser pooling, process management, crash recovery — handled internally
using IronPdf;

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
// Browser pooling, process management, crash recovery — handled internally
Imports IronPdf

Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf(html)
' Browser pooling, process management, crash recovery — handled internally
$vbLabelText   $csharpLabel

如果你的团队能够承担运营成本,那么 木偶师夏普 是一个可行的选择。 对于希望专注于应用程序而不是浏览器基础架构的团队来说,IronPDF 在内部处理相同的渲染工作。

为什么QuestPDF无法转换 HTML?

在 Reddit 和 Stack Overflow 上几乎所有关于"HTML 转 PDF C#"的讨论中都会提到 QuestPDF。 这造成了一种一致的模式:开发者购买或集成QuestPDF时期望它能进行 HTML 转换,然后却发现它根本不渲染 HTML。

QuestPDF是一个流畅的 C# API,用于以编程方式创建文档。 它的定位明确是"停止与 HTML 到 PDF 的转换作斗争"——它用 C# 代码取代了 HTML 方法。 这是有意为之的设计选择。2022年至 2024 年的 GitHub 讨论显示,开发者是在开始实施后才发现这一点的。 维护人员始终确认暂无计划支持 HTML。

如果您的现有工作流程使用 HTML 模板(例如用于发票的 Razor 视图、用于报告的仪表板 HTML、用于归档的 Web 内容),QuestPDF 要求使用 C# 流式 API 代码重写每个模板。 对于使用结构化数据从头开始构建文档布局的新项目,QuestPDF 的 API 设计精良且高效。

社区许可证适用于年总收入低于 100 万美元的企业。 除此之外,还需要商业许可。

Aspose.PDF怎么样?

Aspose.PDF 提供广泛的 PDF 功能,采用商业许可模式(起价约为 999 美元/开发人员)。 HTML 转换使用自定义引擎,而不是浏览器——类似于 iText,它可以处理基本的 HTML,但不能准确地渲染现代 CSS 功能。

主要问题是平台稳定性:Aspose 依赖于 System.Drawing.Common,而 System.Drawing.Common 在 Linux 上需要 libgdiplus。 微软在 .NET 6+ 中弃用了对非 Windows 平台的此功能。 开发人员报告称,Linux 部署中存在特有的内存泄漏问题,而 Windows 上不会出现此类问题。 亚斯 能够满足仅限 Windows 环境的需求。 对于跨平台或容器化部署,依赖链会带来持续的风险。

IronPDF如何处理 HTML 到 PDF 的转换?

IronPDF 将 Chromium 直接嵌入到 NuGet 包中。 CSS Flexbox、Grid、自定义属性、 @font-face 、媒体查询和 JavaScript 的执行方式与在 Chrome 中相同。 输出结果与浏览器一致,因为它们使用了相同的渲染引擎。

using IronPdf;

var renderer = new ChromePdfRenderer();

string html = @"
<!DOCTYPE html>
<html>
<head>
    <style>
        :root { --primary: #2563eb; }
        body { font-family: 'Segoe UI', sans-serif; padding: 40px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; }
        .card {
            background: linear-gradient(135deg, #f8fafc, #e2e8f0);
            border-radius: 12px; padding: 24px; text-align: center;
        }
        .card h3 { color: #6b7280; font-size: 0.8rem; text-transform: uppercase; margin: 0; }
        .card .value { font-size: 2rem; font-weight: 700; color: var(--primary); }
        table { width: 100%; border-collapse: collapse; margin-top: 30px; }
        th { background: var(--primary); color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #e5e7eb; }
    </style>
</head>
<body>
    <div class='grid'>
        <div class='card'><h3>Revenue</h3><div class='value'>$1.2M</div></div>
        <div class='card'><h3>Users</h3><div class='value'>45,230</div></div>
        <div class='card'><h3>Uptime</h3><div class='value'>99.97%</div></div>
    </div>
    <table>
        <tr><th>Product</th><th>Revenue</th><th>Growth</th></tr>
        <tr><td>Enterprise</td><td>$680K</td><td>+12%</td></tr>
        <tr><td>Professional</td><td>$356K</td><td>+8%</td></tr>
    </table>
</body>
</html>";

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("report.pdf");
using IronPdf;

var renderer = new ChromePdfRenderer();

string html = @"
<!DOCTYPE html>
<html>
<head>
    <style>
        :root { --primary: #2563eb; }
        body { font-family: 'Segoe UI', sans-serif; padding: 40px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; }
        .card {
            background: linear-gradient(135deg, #f8fafc, #e2e8f0);
            border-radius: 12px; padding: 24px; text-align: center;
        }
        .card h3 { color: #6b7280; font-size: 0.8rem; text-transform: uppercase; margin: 0; }
        .card .value { font-size: 2rem; font-weight: 700; color: var(--primary); }
        table { width: 100%; border-collapse: collapse; margin-top: 30px; }
        th { background: var(--primary); color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #e5e7eb; }
    </style>
</head>
<body>
    <div class='grid'>
        <div class='card'><h3>Revenue</h3><div class='value'>$1.2M</div></div>
        <div class='card'><h3>Users</h3><div class='value'>45,230</div></div>
        <div class='card'><h3>Uptime</h3><div class='value'>99.97%</div></div>
    </div>
    <table>
        <tr><th>Product</th><th>Revenue</th><th>Growth</th></tr>
        <tr><td>Enterprise</td><td>$680K</td><td>+12%</td></tr>
        <tr><td>Professional</td><td>$356K</td><td>+8%</td></tr>
    </table>
</body>
</html>";

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("report.pdf");
Imports IronPdf

Dim renderer As New ChromePdfRenderer()

Dim html As String = "
<!DOCTYPE html>
<html>
<head>
    <style>
        :root { --primary: #2563eb; }
        body { font-family: 'Segoe UI', sans-serif; padding: 40px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; }
        .card {
            background: linear-gradient(135deg, #f8fafc, #e2e8f0);
            border-radius: 12px; padding: 24px; text-align: center;
        }
        .card h3 { color: #6b7280; font-size: 0.8rem; text-transform: uppercase; margin: 0; }
        .card .value { font-size: 2rem; font-weight: 700; color: var(--primary); }
        table { width: 100%; border-collapse: collapse; margin-top: 30px; }
        th { background: var(--primary); color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #e5e7eb; }
    </style>
</head>
<body>
    <div class='grid'>
        <div class='card'><h3>Revenue</h3><div class='value'>$1.2M</div></div>
        <div class='card'><h3>Users</h3><div class='value'>45,230</div></div>
        <div class='card'><h3>Uptime</h3><div class='value'>99.97%</div></div>
    </div>
    <table>
        <tr><th>Product</th><th>Revenue</th><th>Growth</th></tr>
        <tr><td>Enterprise</td><td>$680K</td><td>+12%</td></tr>
        <tr><td>Professional</td><td>$356K</td><td>+8%</td></tr>
    </table>
</body>
</html>"

Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("report.pdf")
$vbLabelText   $csharpLabel

它使用了 CSS Grid,并结合了auto-fit / minmax 、自定义属性、 linear-gradientborder-radius:root选择器。 这些功能在 iText 的 pdfHTML 中都无法实现,在wkhtmltopdf中也无法实现,而且在PdfSharp或QuestPDF中根本不存在。

如何从其他库迁移?

对于从 iTextSharp 或wkhtmltopdf迁移过来的团队,IronPDF 直接接受 URL——当您现有的工作流程生成 HTML 文件或提供页面时,这非常有用:

using IronPdf;

var renderer = new ChromePdfRenderer();

// Convert from URL — useful when migrating fromwkhtmltopdfURL-based workflows
var pdf = renderer.RenderUrlAsPdf("https://localhost:5001/reports/quarterly");
pdf.SaveAs("report.pdf");

// Convert from local HTML file
var pdfFromFile = renderer.RenderHtmlFileAsPdf("templates/invoice.html");
pdfFromFile.SaveAs("invoice.pdf");
using IronPdf;

var renderer = new ChromePdfRenderer();

// Convert from URL — useful when migrating fromwkhtmltopdfURL-based workflows
var pdf = renderer.RenderUrlAsPdf("https://localhost:5001/reports/quarterly");
pdf.SaveAs("report.pdf");

// Convert from local HTML file
var pdfFromFile = renderer.RenderHtmlFileAsPdf("templates/invoice.html");
pdfFromFile.SaveAs("invoice.pdf");
Imports IronPdf

Dim renderer As New ChromePdfRenderer()

' Convert from URL — useful when migrating from wkhtmltopdf URL-based workflows
Dim pdf = renderer.RenderUrlAsPdf("https://localhost:5001/reports/quarterly")
pdf.SaveAs("report.pdf")

' Convert from local HTML file
Dim pdfFromFile = renderer.RenderHtmlFileAsPdf("templates/invoice.html")
pdfFromFile.SaveAs("invoice.pdf")
$vbLabelText   $csharpLabel

部署

IronPDF 可在 Windows (x64)、Linux (x64, ARM64)、macOS (x64, Apple Silicon) 和 Docker 容器上运行。 Docker 配置是一个标准的 .NET 镜像:

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]

无需安装 Chromium,无需依赖原生库,无需沙箱配置。

许可:永久许可起价 749 美元。价格信息请访问ironpdf.com 。 无AGPL协议,无单件收费,无收入门槛。

性能基准

在运行 Ubuntu 22.04 和 .NET 8 的 Standard_D4s_v3 Azure VM(4 个虚拟 CPU,16GB 内存)上进行了测试。测试文档:一个包含 200 个元素的 HTML 发票模板,采用 CSS Grid 布局、嵌入式图像和 JavaScript 生成的图表。 每次测量均取 50 次迭代的平均值,并在 5 次迭代的预热期后进行。

情景IronPDF木偶师夏普iText pdfHTMLwkhtmltopdf
简单的HTML(不含JS)约150毫秒~500ms~200ms~200ms
复杂的 CSS(Flexbox/Grid)约250毫秒~600ms输出损坏输出损坏
JavaScript渲染的内容约350毫秒约800毫秒失败(无 JS 引擎)失败/部分失败
每次操作内存约80MB约150MB约60MB约50MB
冷启动(第一代)2-5秒3–8秒<1秒<1秒

iText 和wkhtmltopdf的冷启动速度更快,因为它们无需初始化浏览器引擎。但这种比较仅在所有库都能生成正确输出的情况下才有意义——对于复杂的 CSS 或 JavaScript 内容,只有IronPDF和 木偶师夏普 能生成可用的结果。

注:这些是针对指定硬件的典型观察结果。 性能表现会因 HTML 复杂性、文档长度和服务器资源而异。 在做出决定之前,请使用实际工作负载进行测试。

功能对比

特征IronPDFiText 7木偶师夏普wkhtmltopdfPdfSharpQuestPDF亚斯
HTML 至 PDF是的(铬)有限(CSS 2.1)是的(Chrome)已弃用有限的
CSS Flexbox/网格
JavaScript 执行有限的
跨平台(无需 libgdiplus)不适用部分翻译
出版定价749美元以上否(年收入 1.5 万美元至 21 万美元)免费(MIT)免费免费(MIT)免费 <100万美元999美元以上
主动维护放弃

我应该选择哪个库?

采用现代 CSS 的 HTML 模板 → IronPDF提供嵌入式 Chromium,无需外部进程管理。 如果您的团队能够管理浏览器基础架构,那么 木偶师夏普 是一个可行的替代方案。

通过数据进行程序化文档生成,无需 HTML → QuestPDF提供优雅流畅的 API。 不要指望它能转换成 HTML 格式。

简单的 PDF 操作(合并、拆分、添加水印)→ PdfSharp免费且功能强大,可处理非 HTML 任务。

新项目应避免使用: wkhtmltopdf(已弃用,存在 CVE 漏洞)、无商业许可的 iText(AGPL 陷阱)、Linux 上的 Aspose(内存泄漏)。

关键问题在于你的工作流程是否使用了 HTML 模板。 如果确实如此,只有基于 Chromium 的解决方案才能使用现代 CSS 生成正确的输出。 如果没有,则选择取决于 API 偏好和许可限制。