C# 中的 HTML 轉 PDF - 庫選項的現實
在 C# 中將 HTML 轉換為 PDF 需要一個能夠真正渲染 HTML 的函式庫,而不是一個只能解析部分標籤並近似 CSS 2.1 的函式庫。 Stack Overflow 貼文和 Reddit 討論中推薦的大多數函式庫要么無法渲染現代 CSS,要么存在使其無法用於商業用途的許可限制,要么已被棄用且存在未修復的安全漏洞。
本文比較了開發人員在搜尋"HTML to PDF C#"時實際遇到的函式庫,記錄了每個函式庫可以渲染和不能渲染的內容,包括效能基準測試和方法論,並展示了每種方法的實際運作成本。
Quickstart: HTML 轉 PDF in C
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");透過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: flex 和 justify-content: space-between 的 display: flex 容器會被渲染成垂直堆疊的元素,沒有間距。 開發人員是在整合之後才發現這個問題,而不是在整合過程中發現的。
許可: AGPL — 要求開源您的整個網路可存取應用程序,或購買商業許可。 價格未公佈; 第三方數據顯示,年收入為 15,000 美元至 210,000 美元。
記憶體使用情況比較如何?
iText 的 pdfHTML 功能會將整個文件載入到記憶體中進行處理。 對於一般的商業文件來說,這是可以控制的,但與串流方式相比,包含嵌入式影像的大型 HTML 報告可能會造成嚴重的記憶體壓力。
為什麼 PdfSharp 不支援 HTML?
PdfSharp因其受歡迎程度(3,490 萬NuGet下載)和頻繁推薦而出現在"HTML 轉 PDF"搜尋結果中。 但 PdfSharp 沒有 HTML 解析器。 它提供基於座標的繪圖 API: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》成為資源密集遊戲?
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();
}
}
}在產生單一 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 internallyusing IronPdf;
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
// Browser pooling, process management, crash recovery — handled internally如果你的團隊能夠承擔營運成本,那麼 木偶師夏普 是一個可行的選擇。 對於希望專注於應用程式而不是瀏覽器基礎架構的團隊來說, 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 上不會出現此類問題。 Aspose 能夠滿足僅限 Windows 環境的需求。 對於跨平台或容器化部署,依賴鏈會帶來持續的風險。
IronPDF如何處理 HTML 到 PDF 的轉換?
IronPDF將 Chromium 直接嵌入NuGet包中。 CSS Flexbox、Grid、自訂屬性、媒體查詢和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");它使用 CSS Grid,帶有 border-radius 和 :root 選擇器。 這些功能在 iText 的 pdfHTML 中都無法實現,在 wkhtmltopdf 中會出錯,並且在 PdfSharp 或 QuestPDF 中不存在。
如何從其他庫遷移?
對於從 iTextSharp 或 wkhtmltopdf 遷移過來的團隊, IronPDF直接接受 URL——當您現有的工作流程產生 HTML 檔案或提供頁面時,這非常有用:
using IronPdf;
var renderer = new ChromePdfRenderer();
// Convert from URL — useful when migrating from wkhtmltopdf URL-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 from wkhtmltopdf URL-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");部署
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 。 無AGPL協議,無單件收費,無收入門檻。
性能基準
在執行 Ubuntu 22.04 和.NET 8 的 Standard_D4s_v3 Azure VM(4 個虛擬 CPU,16GB 記憶體)上進行了測試。測試文件:一個包含 200 個元素的 HTML 發票模板,採用 CSS Grid 佈局、嵌入式圖片和 JavaScript 產生的圖表。 每次測量取 50 次迭代的平均值,並在 5 次迭代的預熱期後進行。
| 設想 | IronPDF | 木偶師夏普 | iText pdfHTML | wkhtmltopdf |
|---|---|---|---|---|
| 簡單的HTML(不含JS) | 約150毫秒 | 約500毫秒 | 約200毫秒 | 約200毫秒 |
| 複雜 CSS(Flexbox/Grid) | 約250毫秒 | 約600毫秒 | 輸出損壞 | 輸出損壞 |
| JavaScript渲染的內容 | 約350毫秒 | 約800毫秒 | 失敗(無 JS 引擎) | 失敗/部分失敗 |
| 每次操作內存 | 約80MB | 約150MB | 約60MB | 約50MB |
| 冷啟動(第一代) | 2-5秒 | 3–8秒 | <1s | <1s |
iText 和 wkhtmltopdf 的冷啟動速度更快,因為它們無需初始化瀏覽器引擎。但這種比較僅在所有函式庫都能產生正確輸出的情況下才有意義——對於複雜的 CSS 或JavaScript內容,只有IronPDF和 木偶師夏普 能產生可用的結果。
註:這些是針對指定硬體的典型觀察。 效能表現會因 HTML 複雜性、文件長度和伺服器資源而異。 在做出決定之前,請使用實際工作負載進行測試。
功能對比
| 特徵 | IronPDF | iText 7 | 木偶師夏普 | wkhtmltopdf | PdfSharp | QuestPDF | Aspose |
|---|---|---|---|---|---|---|---|
| HTML 轉 PDF | 是的(鉻) | 有限(CSS 2.1) | 是的(Chrome) | 已棄用 | 不 | 不 | 有限的 |
| CSS Flexbox/Grid | 是的 | 不 | 是的 | 不 | 不 | 不 | 不 |
| 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 偏好和授權限制。
