C#中的HTML到PDF - 程式庫選項的現實情況
將HTML轉換為PDF在C#中需要一個實際呈現HTML的程式庫——而不是解析部分標籤並逼近CSS 2.1的方式。在Stack Overflow帖子和Reddit討論中推薦的大多數程式庫要么無法呈現現代CSS,要么因為許可限制而不適合商業用途,或者因安全漏洞未修補而被遺棄。
本文比較了開發者在搜索"HTML到PDF C#"時實際遇到的程式庫,記錄了每個程式庫可以和不可以呈現的內容,包括方法學的性能基準,並顯示了各種方法的實際運營成本。
Quickstart:HTML到PDFin 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");Imports IronPdf
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1>")
pdf.SaveAs("output.pdf")通過NuGet安裝:Install-Package IronPdf。 部署到Windows、Linux、macOS和Docker,無需外部依賴。
為什麼HTML到PDF的轉換很困難?
正確地將HTML渲染為PDF需要實現與網頁瀏覽器相同的五個組件:一個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(pdfHTML附加模組)——有限的CSS,AGPL許可
iText的pdfHTML模組使用自定義解析器——而非瀏覽器引擎,將HTML轉換為PDF。它處理基本的HTML/CSS但不能渲染Flexbox、Grid或JavaScript。
失敗模式是默默無聲的:當遇到不支持的CSS時pdfHTML不會拋出異常。 它可以渲染即可的內容,並忽略其餘內容。justify-content: space-between一起呈現為無間距的垂直堆疊元素。 開發者是在整合之後而不是在整合過程中發現這一點。
許可: AGPL——需要開源您的整個網絡可訪問應用程序,或購買商業許可。 價格未發布; 第三方數據顯示每年15,000–210,000美元。
內存使用比較怎麼樣?
iText的pdfHTML將整個文檔加載到內存中進行處理。 對於典型的商務文檔這是可控的,但包含嵌入圖像的大型HTML報告會導致相比流式方法更大的內存壓力。
為什麼PDFSharp不支持HTML?
PdfSharp出現在"HTML轉PDF"搜索結果中是因為其受歡迎程度(34.9百萬個NuGet下載)和頻繁的推薦。 但PdfSharp沒有HTML解析器。 它提供了一個基於坐標的繪圖API:DrawImage()具有明確的X/Y位置。
常見建議的變通方法HtmlRenderer.PdfSharp僅支持HTML 4.01和CSS等級2。 如果您的HTML使用了2010年後引入的任何CSS功能——Flexbox(2012)、Grid(2017)、自定義屬性(2017)、border-radius(2011)——它將不會呈現。
選擇PdfSharp是多餘的。
是什麼讓Puppeteer Sharp資源密集?
Puppeteer Sharp通過.NET綁定控制無頭Chrome。 渲染準確性與Chrome匹配,因為它就是Chrome。 成本則是運營的:您管理外部瀏覽器進程。
這就是生產Puppeteer Sharp部署的實際情況——而不是教程中的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在生成單個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 internallyImports IronPdf
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf(html)
' Browser pooling, process management, crash recovery — handled internally如果您的團隊能夠吸收運行開銷,Puppeteer Sharp是可行的。 對於希望專注於應用程式而不是瀏覽器基礎結構的團隊,IronPDF在內部處理同樣的渲染。
為何QuestPDF無法轉換HTML?
QuestPDF幾乎出現在每次在Reddit和Stack Overflow關於"HTML到PDF C#"的討論中。 這創造了一種一致模式:開發人員購買或整合QuestPDF希望是HTML轉換,然後發現它根本不渲染HTML。
QuestPDF是一個流暢的C# API,用於程序化文件創建。 其定位明確是"停止與HTML到PDF轉換鬥爭"——用C#代碼取代HTML方法。 這是有意的設計選擇。2022年至2024年的GitHub討論顯示開發人員在實施開始後發現這一點。 維護者一致確認不計畫支持HTML。
如果您的現有工作流程使用HTML範本——如發票的Razor視圖、報告的儀表板HTML、設檔用的網頁內容——QuestPDF需要將每個範本重寫為C#流暢API代碼。 對於新專案,您從頭開始使用結構化數據構建文檔佈局,QuestPDF的API設計精良且具生產力。
社區許可涵蓋年收入低於$100萬的企業。 超過這一點需要商業許可。
那Aspose.PDF呢?
Aspose.PDF提供廣泛的PDF功能,具有商業許可(起始價格約為每個開發人員$999) HTML轉換使用自定引擎而非瀏覽器——類似於iText,它處理基本的HTML但不能準確呈現現代CSS功能。
主要問題是平臺穩定性:Aspose依賴System.Drawing.Common,在Linux上需要libgdiplus。 微軟對.NET 6+的非Windows平臺停用了這個組件。 開發者報導特定於Linux部屬的內存洩漏,但在Windows上不會發生。 對於僅Windows環境,Aspose是有能力的。 對於跨平臺或容器化的部屬,依賴鏈會創造持續的風險。
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")這使用CSS Grid與:root選擇器。 這些功能都在iText的pdfHTML中失敗,在wkhtmltopdf中打破,且在PdfSharp或QuestPDF中不存在。
如何從其他程式庫遷移?
對於從iText或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");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")部屬
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安裝,沒有原生庫依賴,沒有沙箱配置。
許可:永久許可從$2,998開始。 公布價格位於ironpdf.com。 無AGPL,無每份文件費用,無收入門檻。
性能基準
在運行Ubuntu 22.04和.NET 8的Standard_D4s_v3 Azure虛擬機(4個vCPU,16GB RAM)上測試。測試文檔:200個元素HTML發票範本,帶有CSS Grid佈局、嵌入圖像和JavaScript生成的圖表。 每項測量在5次熱身後進行50次迭代的平均值。
| 情景 | IronPDF | Puppeteer Sharp | iText pdfHTML | wkhtmltopdf |
|---|---|---|---|---|
| 簡單HTML(無JS) | ~150ms | ~500ms | ~200ms | ~200ms |
| 複雜CSS(Flexbox/Grid) | ~250ms | ~600ms | 壞掉的輸出 | 壞掉的輸出 |
| JavaScript 渲染內容 | ~350ms | ~800ms | 未能(無JS引擎) | 未能/部分 |
| 每次操作的內存 | ~80MB | ~150MB | ~60MB | ~50MB |
| 冷啟動(首次生成) | 2–5秒 | 3–8秒 | <1s | <1s |
iText和wkhtmltopdf顯示首次冷啟動較快,因為它們不初始化瀏覽器引擎。但這種比較僅在所有程式庫生成正確輸出場景中有意義——而對於複雜CSS或JavaScript內容,僅IronPDF和Puppeteer Sharp產生可用結果。
注意:這些代表在指定硬體上的典型觀察。 您的性能會隨著HTML複雜性、文檔長度和伺服器資源而有所不同。 在做決定之前,需使用您的實際工作負載進行測試。
功能比較
| 功能 | IronPDF | iText | Puppeteer Sharp | wkhtmltopdf | PDFSharp | QuestPDF | Aspose |
|---|---|---|---|---|---|---|---|
| HTML到PDF | 是(Chromium) | 有限(CSS 2.1) | 是(Chrome) | 棄用 | 沒有 | 沒有 | 有限 |
| CSS Flexbox/Grid | 有 | 沒有 | 有 | 沒有 | 沒有 | 沒有 | 沒有 |
| JavaScript執行 | 有 | 沒有 | 有 | 有限 | 沒有 | 沒有 | 沒有 |
| 跨平台(無libgdiplus) | 有 | 有 | 有 | 不適用 | 部分 | 有 | 沒有 |
| 公布的價格 | $2,998+ | 無($15K–$210K/年) | 免費(MIT) | 免費 | 免費(MIT) | 免費<$1M | $999+ |
| 積極維護 | 有 | 有 | 有 | 被棄置 | 有 | 有 | 有 |
我應該選擇哪個程式庫?
具有現代CSS的HTML範本→ IronPDF 嵌入Chromium無需外部進程管理。 如果您的團隊可以管理瀏覽器基礎結構,Puppeteer Sharp是個可行的替代方案。
從數據生成程式化文檔,無HTML→ QuestPDF 提供一個優雅的流暢API。 不要選擇它以期待HTML轉換。
簡單的PDF操作(合併、拆分、水印)→ PDFSharp 是免費的,適用於非HTML任務。
避免用於新專案:wkhtmltopdf(廢棄的,CVE)、iText無商業授權(AGPL陷阱)、Linux上的Aspose(內存洩漏)。
關鍵問題是您的工作流程是否使用HTML範本。 如果是,只有基於Chromium的解決方案才能與現代CSS產生正確的輸出。 如果不是,選擇取決於API偏好和許可限制。
