Iron Suite for .NET
10個 .NET 程式庫。一個完整的文件套件。
SSRS與IronPDF:技術比較指南
選擇C# PDF程式庫影響您的專案授權曝險、部署靈活性和長期維護成本。大多數看似在評估時適合的程式庫在生產環境中暴露出限制——如您未預期的AGPL需求、與瀏覽器不匹配的HTML渲染,或僅在Linux上出現的記憶體洩漏。
本文比較了主要選擇,包含程式碼範例,記錄了實際操作中重要的取捨,並包括並排程式碼比較以在三個不同程式庫中產生相同的發票,讓您可以直觀地看到API差異。
快速入門:三行程式碼實現HTML到PDF
通過NuGet安裝:
生成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")Imports IronPdf
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1>")
pdf.SaveAs("output.pdf")這在Windows、Linux、macOS和Docker上運行,而無需額外配置。 輸出與Chrome匹配,因為IronPDF嵌入了相同的Chromium渲染引擎。
評估標準
在比較程式庫之前,要知道評估的內容。 這些是早期暴露生產問題的問題:
| 評估標準 | 測試內容 | 為什麼重要 |
|---|---|---|
| HTML/CSS渲染 | 使用您的實際模板與Flexbox/Grid測試 | 大多數程式庫聲稱支援HTML,但最多只能渲染CSS 2.1 |
| JavaScript執行 | 測試使用Chart.js或動態表格內容 | 沒有JS支援的程式庫會生成空白部分 |
| 授權模式 | 閱讀完整授權,而不是摘要 | AGPL要求公開整個應用程式的源程式碼 |
| 平台支援 | 部署到您的目標Linux/Docker/ARM64環境 | Windows成功不預示Linux行為 |
| 高負載下的記憶體 | 在迴圈中生成100+個文件 | 單文件測試掩蓋會導致生產伺服器崩潰的洩漏 |
| 發佈的價格 | 檢查網站是否有報價 | "聯繫銷售"通常意味著每年$15K–$210K |
程式庫比較
IronPDF - 嵌入式Chromium,完整CSS/JS支援
IronPDF直接在NuGet包中嵌入Chromium。 HTML渲染匹配Chrome,因為它是Chrome的引擎。CSS Flexbox、Grid、自定義屬性和JavaScript都按預期執行。
using IronPdf;
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
var pdf = renderer.RenderHtmlAsPdf(@"
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.card { background: linear-gradient(135deg, #f8fafc, #e2e8f0);
border-radius: 8px; padding: 20px; text-align: center; }
</style>
</head>
<body>
<div class='grid'>
<div class='card'><h3>Revenue</h3><p>$1.2M</p></div>
<div class='card'><h3>Users</h3><p>45,230</p></div>
<div class='card'><h3>Uptime</h3><p>99.97%</p></div>
</div>
</body>
</html>");
pdf.SaveAs("dashboard.pdf");using IronPdf;
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
var pdf = renderer.RenderHtmlAsPdf(@"
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.card { background: linear-gradient(135deg, #f8fafc, #e2e8f0);
border-radius: 8px; padding: 20px; text-align: center; }
</style>
</head>
<body>
<div class='grid'>
<div class='card'><h3>Revenue</h3><p>$1.2M</p></div>
<div class='card'><h3>Users</h3><p>45,230</p></div>
<div class='card'><h3>Uptime</h3><p>99.97%</p></div>
</div>
</body>
</html>");
pdf.SaveAs("dashboard.pdf");Imports IronPdf
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print
Dim pdf = renderer.RenderHtmlAsPdf("
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.card { background: linear-gradient(135deg, #f8fafc, #e2e8f0);
border-radius: 8px; padding: 20px; text-align: center; }
</style>
</head>
<body>
<div class='grid'>
<div class='card'><h3>Revenue</h3><p>$1.2M</p></div>
<div class='card'><h3>Users</h3><p>45,230</p></div>
<div class='card'><h3>Uptime</h3><p>99.97%</p></div>
</div>
</body>
</html>")
pdf.SaveAs("dashboard.pdf")Imports IronPdf
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print
Dim pdf = renderer.RenderHtmlAsPdf("
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.card { background: linear-gradient(135deg, #f8fafc, #e2e8f0);
border-radius: 8px; padding: 20px; text-align: center; }
</style>
</head>
<body>
<div class='grid'>
<div class='card'><h3>Revenue</h3><p>$1.2M</p></div>
<div class='card'><h3>Users</h3><p>45,230</p></div>
<div class='card'><h3>Uptime</h3><p>99.97%</p></div>
</div>
</body>
</html>")
pdf.SaveAs("dashboard.pdf")考慮的取捨:嵌入的Chromium會增加約200MB到部署包。 對於標準伺服器和容器部署,這是一次性下載,對運行時沒有影響。 對於像Azure Functions消費計畫這樣受大小限制的環境,需要檢查部署大小限制。 在冷啟動過程中的第一次PDF生成需要2至5秒來初始化Chromium; 後續生成在100至500毫秒之間運行。 記憶體基線約為150–200MB - 請相應計劃容器資源。
授權:永久授權起價$999(1位開發人員)。 發佈的價格在ironpdf.com上。 無文件費用,無AGPL,無收入門檻。
iText(iText) - AGPL授權,有限HTML
iText是一個功能健全的PDF操作程式庫,其歷史久遠。 pdfHTML外掛提供HTML到PDF轉換,但它不使用瀏覽器引擎——它用自定義解析器近似CSS 2.1。
一家中型SaaS公司的生產團隊在遷移他們的發票模板從Razor視圖時發現了這一點。 他們的模板使用CSS Flexbox進行響應式列佈局。 在整合iText的pdfHTML後,每個發票都顯示為單列垂直堆疊。 justify-content屬性被默默忽略。 在團隊了解pdfHTML無法渲染他們現有的CSS之前,花費了三周的開發時間。
AGPL現實:iText使用AGPL授權。 如果您的應用程式是網路可存取的——這包括每個Web應用、API和SaaS產品——您必須根據AGPL發布您的整個應用程式的源程式碼。 不僅僅是PDF模組。 而是所有內容。 iText及其母公司Apryse積極執行這一點。
商業授權:iText在2024年過渡到基於訂閱的授權。未公佈價格——您需要聯繫銷售獲取報價。 第三方資料顯示每年約$15,000–$210,000,取決於使用量。
PDFSharp - MIT授權,無HTML
PdfSharp在MIT授權下真正免費,NuGet下載量達到3490萬。 取捨在於功能性:它提供了一個基於座標的繪圖API,沒有HTML解析器,沒有CSS引擎,沒有模板系統。
一個用於建立報表儀表板的團隊選擇了PdfSharp,因為它免費且知名。 他們花了四個月撰寫基於座標的佈局程式碼——計算每個文字元素的X/Y位置,逐位繪製表格邊框,手動處理頁面分頁。 當他們最終將輸出與相同HTML模板在瀏覽器中生成的相比時,意識到他們建立了一個不如基於Chromium的程式庫自動執行的版本。
PdfSharp在合併PDF、新增浮水印和從資料生成簡單結構化文件方面效果良好。 如果您不需要HTML渲染,它仍然是一個合法的選擇。
QuestPDF - 優雅API,無HTML,收入門檻
QuestPDF提供了一個流暢的C# API來程式化的建立文件。 API設計確實很好——它是任何類別中更好的.NET程式庫API之一。
兩個約束事項:QuestPDF不渲染HTML(因其設計——這是一個刻意的架構選擇,不是缺失功能),而且社群授權適用於每年總收入不足100萬美元的企業。 一旦您的公司超過那個門檻,就必須購買商業授權。 接近門檻的公司應在緊急之前為這一過渡制定預算。
儘管QuestPDF明確定位為不支持HTML,開發者經常在實施後才發現這一點,因為該程式庫在"C# PDF程式庫"搜尋結果中出現,與HTML功能的程式庫並列。
wkhtmltopdf包裝器 - 已棄用,未修補的CVE
wkhtmltopdf的時代已經過去。 GitHub組織於2024年7月被歸檔。 QtWebKit引擎在2015年被Qt棄用。已知的CVE——包括CVE-2022-35583(CVSS 9.8,允許AWS憑證外洩的SSRF)——永遠不會被修補。
像DinkToPdf、NReco.PdfGenerator、WkHtmlToXSharp這樣的C#包裝器都包裝了同一棄用的二進制文件。 渲染引擎已凍結在大約Safari 2011的能力上:沒有Flexbox、沒有Grid、有限的JavaScript。 這對新專案來說不是一個可行的選擇。
Puppeteer Sharp - 完整渲染,操作複雜
Puppeteer Sharp通過.NET綁定控制無頭Chrome。 渲染品質匹配Chrome,因為它是Chrome。 取捨在於操作性:您需要管理外部瀏覽器進程,包括下載、池管理、記憶體監控和崩潰恢復。
using PuppeteerSharp;
// Downloads ~280MB Chromium on first run
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(
new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html);
return await page.PdfAsync(new PdfOptions { Format = PaperFormat.A4, PrintBackground = true });using PuppeteerSharp;
// Downloads ~280MB Chromium on first run
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(
new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html);
return await page.PdfAsync(new PdfOptions { Format = PaperFormat.A4, PrintBackground = true });Imports PuppeteerSharp
' Downloads ~280MB Chromium on first run
Await (New BrowserFetcher()).DownloadAsync()
Await Using browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {.Headless = True})
Await Using page = Await browser.NewPageAsync()
Await page.SetContentAsync(html)
Return Await page.PdfAsync(New PdfOptions With {.Format = PaperFormat.A4, .PrintBackground = True})
End Using
End UsingImports PuppeteerSharp
' Downloads ~280MB Chromium on first run
Await (New BrowserFetcher()).DownloadAsync()
Await Using browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {.Headless = True})
Await Using page = Await browser.NewPageAsync()
Await page.SetContentAsync(html)
Return Await page.PdfAsync(New PdfOptions With {.Format = PaperFormat.A4, .PrintBackground = True})
End Using
End Using在生產環境中,您還需要瀏覽器進程池管理、記憶體洩漏監控(Chromium進程可能洩漏)、崩潰恢復和資源清理。Docker部署需要安裝Chromium依賴——與標準.NET映像相比是一個相當大的Dockerfile。 如果您的團隊能夠承受運營開銷,Puppeteer Sharp是可行的。
Aspose.PDF - 廣泛功能,Linux記憶體問題
Aspose.PDF提供廣泛的PDF功能,且文件良好。 重大問題是Linux穩定性:Aspose依賴於System.Drawing.Common,這需要Linux上的libgdiplus——一個未維護的、具有記憶體洩漏文件的庫。 開發者報告跨越多年:
"數十個請求導致服務在Unix環境下耗盡記憶體,但在基於Windows的環境中不會發生" — Aspose論壇,2022年3月
對於僅在Windows上部署,Aspose仍然有能力。 對於跨平台或容器化部署,System.Drawing.Common依賴會造成持續風險。 商業授權起價約為999美元/開發人員。
功能比較
| 功能 | IronPDF | iText | PDFSharp | QuestPDF | wkhtmltopdf | Puppeteer | Aspose |
|---|---|---|---|---|---|---|---|
| HTML到PDF | 完整 (Chromium) | 有限 (CSS 2.1) | 無 | 無 | 已棄用 | 完整 (Chrome) | 有限 |
| CSS Flexbox/Grid | 是 | 無 | 無 | 無 | 無 | 是 | 無 |
| JavaScript | 是 | 無 | 無 | 無 | 有限 | 是 | 無 |
| Linux (無libgdiplus) | 是 | 是 | 部分* | 是 | 不適用 | 是 | 無 |
| Docker部署 | 標準.NET映像 | 標準 | 部分* | 標準 | 複雜 | 複雜 | 需要libgdiplus |
| 活躍維護 | 是 | 是 | 是 | 是 | 已廢棄 | 是 | 是 |
| 發佈的價格 | 是 ($999+) | 否 ($15K–$210K/年) | 免費 (MIT) | 是 (免費 < $1M) | 免費 | 免費 (MIT) | 是 ($999+) |
| 永久授權 | 是 | 否 (訂閱制) | 不適用 | 不適用 | 不適用 | 不適用 | 是 |
| AGPL免除 | 是 | 否 (需要商業授權) | 是 | 是 | 是 | 是 | 是 |
*PDFSharp在一些配置中存在平台特定問題的文件。
性能比較
在一個中等階級的雲VM上測試(4個vCPU,8GB RAM),使用一個200元素的HTML發票模板,經過暖身後平均50次迭代:
| 情景 | IronPDF | Puppeteer Sharp | iText pdfHTML | wkhtmltopdf |
|---|---|---|---|---|
| 簡單HTML頁面 | 約150ms | 約500ms | 約200ms | 約200ms |
| 複雜CSS佈局(Flexbox/Grid) | 約250ms | 約600ms | 失敗/部分完成 | 約400ms (損壞) |
| JavaScript密集頁面 | 約350ms | 約800ms | 失敗 | 失敗/部分完成 |
| 每次操作的記憶體 | 約80MB | 約150MB | 約60MB | 約50MB |
| 冷啟動(第一次生成) | 2–5秒 | 3–8秒 | <1s | <1s |
iText和wkhtmltopdf顯示冷啟動較快,因為它們不初始化瀏覽器引擎——但它們也無法渲染相同的內容。 性能比較僅在所有程式庫產生正確輸出的情況下具有意義。
程式碼比較:相同發票,三個程式庫
這些程式庫之間的差異最明顯的是構建相同的文件時。 這是一個三種方式生成的發票。
IronPDF - HTML/CSS方法
using IronPdf;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var renderer = new ChromePdfRenderer();
string html = $@"
<!DOCTYPE html>
<html>
<head>
<style>
body {{font-family: 'Segoe UI', sans-serif; margin: 40px;}}
h1 {{color: #2c3e50;}}
table {{width: 100%; border-collapse: collapse;}}
th {{background: #3498db; color: white; padding: 12px; text-align: left;}}
td {{border-bottom: 1px solid #e0e0e0; padding: 10px;}}
.total {{font-weight: bold; font-size: 1.2em; text-align: right; margin-top: 20px;}}
</style>
</head>
<body>
<h1>Invoice #{data.InvoiceNumber}</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
{string.Join("", data.Items.Select(i =>
$"<tr><td>{i.Name}</td><td>{i.Quantity}</td><td>${i.Price:F2}</td></tr>"))}
</table>
<p class='total'>Total: ${data.Total:F2}</p>
</body>
</html>";
var pdf = renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
}using IronPdf;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var renderer = new ChromePdfRenderer();
string html = $@"
<!DOCTYPE html>
<html>
<head>
<style>
body {{font-family: 'Segoe UI', sans-serif; margin: 40px;}}
h1 {{color: #2c3e50;}}
table {{width: 100%; border-collapse: collapse;}}
th {{background: #3498db; color: white; padding: 12px; text-align: left;}}
td {{border-bottom: 1px solid #e0e0e0; padding: 10px;}}
.total {{font-weight: bold; font-size: 1.2em; text-align: right; margin-top: 20px;}}
</style>
</head>
<body>
<h1>Invoice #{data.InvoiceNumber}</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
{string.Join("", data.Items.Select(i =>
$"<tr><td>{i.Name}</td><td>{i.Quantity}</td><td>${i.Price:F2}</td></tr>"))}
</table>
<p class='total'>Total: ${data.Total:F2}</p>
</body>
</html>";
var pdf = renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
}Imports IronPdf
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim renderer = New ChromePdfRenderer()
Dim html As String = $"
<!DOCTYPE html>
<html>
<head>
<style>
body {{font-family: 'Segoe UI', sans-serif; margin: 40px;}}
h1 {{color: #2c3e50;}}
table {{width: 100%; border-collapse: collapse;}}
th {{background: #3498db; color: white; padding: 12px; text-align: left;}}
td {{border-bottom: 1px solid #e0e0e0; padding: 10px;}}
.total {{font-weight: bold; font-size: 1.2em; text-align: right; margin-top: 20px;}}
</style>
</head>
<body>
<h1>Invoice #{data.InvoiceNumber}</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
{String.Join("", data.Items.Select(Function(i) $"<tr><td>{i.Name}</td><td>{i.Quantity}</td><td>${i.Price:F2}</td></tr>"))}
</table>
<p class='total'>Total: ${data.Total:F2}</p>
</body>
</html>"
Dim pdf = renderer.RenderHtmlAsPdf(html)
Return pdf.BinaryData
End Function
End ClassImports IronPdf
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim renderer = New ChromePdfRenderer()
Dim html As String = $"
<!DOCTYPE html>
<html>
<head>
<style>
body {{font-family: 'Segoe UI', sans-serif; margin: 40px;}}
h1 {{color: #2c3e50;}}
table {{width: 100%; border-collapse: collapse;}}
th {{background: #3498db; color: white; padding: 12px; text-align: left;}}
td {{border-bottom: 1px solid #e0e0e0; padding: 10px;}}
.total {{font-weight: bold; font-size: 1.2em; text-align: right; margin-top: 20px;}}
</style>
</head>
<body>
<h1>Invoice #{data.InvoiceNumber}</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
{String.Join("", data.Items.Select(Function(i) $"<tr><td>{i.Name}</td><td>{i.Quantity}</td><td>${i.Price:F2}</td></tr>"))}
</table>
<p class='total'>Total: ${data.Total:F2}</p>
</body>
</html>"
Dim pdf = renderer.RenderHtmlAsPdf(html)
Return pdf.BinaryData
End Function
End ClassQuestPDF - 流暢API方法
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(40);
page.DefaultTextStyle(x => x.FontFamily("Segoe UI"));
page.Header()
.Text($"Invoice #{data.InvoiceNumber}")
.FontSize(24).FontColor(Colors.Blue.Darken2);
page.Content().Column(column =>
{
column.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(3);
cols.RelativeColumn(1);
cols.RelativeColumn(1);
});
table.Header(header =>
{
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Item").FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Qty").FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Price").FontColor(Colors.White);
});
foreach (var item in data.Items)
{
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text(item.Name);
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text(item.Quantity.ToString());
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text($"${item.Price:F2}");
}
});
column.Item().AlignRight().PaddingTop(20)
.Text($"Total: ${data.Total:F2}").FontSize(16).Bold();
});
});
});
using var stream = new MemoryStream();
document.GeneratePdf(stream);
return stream.ToArray();
}
}using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(40);
page.DefaultTextStyle(x => x.FontFamily("Segoe UI"));
page.Header()
.Text($"Invoice #{data.InvoiceNumber}")
.FontSize(24).FontColor(Colors.Blue.Darken2);
page.Content().Column(column =>
{
column.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(3);
cols.RelativeColumn(1);
cols.RelativeColumn(1);
});
table.Header(header =>
{
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Item").FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Qty").FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Medium).Padding(8)
.Text("Price").FontColor(Colors.White);
});
foreach (var item in data.Items)
{
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text(item.Name);
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text(item.Quantity.ToString());
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(8).Text($"${item.Price:F2}");
}
});
column.Item().AlignRight().PaddingTop(20)
.Text($"Total: ${data.Total:F2}").FontSize(16).Bold();
});
});
});
using var stream = new MemoryStream();
document.GeneratePdf(stream);
return stream.ToArray();
}
}Imports QuestPDF.Fluent
Imports QuestPDF.Infrastructure
Imports System.IO
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim document = Document.Create(Sub(container)
container.Page(Sub(page)
page.Size(PageSizes.A4)
page.Margin(40)
page.DefaultTextStyle(Function(x) x.FontFamily("Segoe UI"))
page.Header() _
.Text($"Invoice #{data.InvoiceNumber}") _
.FontSize(24).FontColor(Colors.Blue.Darken2)
page.Content().Column(Sub(column)
column.Item().Table(Sub(table)
table.ColumnsDefinition(Sub(cols)
cols.RelativeColumn(3)
cols.RelativeColumn(1)
cols.RelativeColumn(1)
End Sub)
table.Header(Sub(header)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Item").FontColor(Colors.White)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Qty").FontColor(Colors.White)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Price").FontColor(Colors.White)
End Sub)
For Each item In data.Items
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text(item.Name)
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text(item.Quantity.ToString())
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text($"${item.Price:F2}")
Next
End Sub)
column.Item().AlignRight().PaddingTop(20) _
.Text($"Total: ${data.Total:F2}").FontSize(16).Bold()
End Sub)
End Sub)
End Sub)
Using stream As New MemoryStream()
document.GeneratePdf(stream)
Return stream.ToArray()
End Using
End Function
End ClassImports QuestPDF.Fluent
Imports QuestPDF.Infrastructure
Imports System.IO
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim document = Document.Create(Sub(container)
container.Page(Sub(page)
page.Size(PageSizes.A4)
page.Margin(40)
page.DefaultTextStyle(Function(x) x.FontFamily("Segoe UI"))
page.Header() _
.Text($"Invoice #{data.InvoiceNumber}") _
.FontSize(24).FontColor(Colors.Blue.Darken2)
page.Content().Column(Sub(column)
column.Item().Table(Sub(table)
table.ColumnsDefinition(Sub(cols)
cols.RelativeColumn(3)
cols.RelativeColumn(1)
cols.RelativeColumn(1)
End Sub)
table.Header(Sub(header)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Item").FontColor(Colors.White)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Qty").FontColor(Colors.White)
header.Cell().Background(Colors.Blue.Medium).Padding(8) _
.Text("Price").FontColor(Colors.White)
End Sub)
For Each item In data.Items
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text(item.Name)
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text(item.Quantity.ToString())
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) _
.Padding(8).Text($"${item.Price:F2}")
Next
End Sub)
column.Item().AlignRight().PaddingTop(20) _
.Text($"Total: ${data.Total:F2}").FontSize(16).Bold()
End Sub)
End Sub)
End Sub)
Using stream As New MemoryStream()
document.GeneratePdf(stream)
Return stream.ToArray()
End Using
End Function
End ClassPDFSharp - 座標繪圖方法
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
var titleFont = new XFont("Arial", 24);
var headerFont = new XFont("Arial", 12, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 12);
double y = 40;
gfx.DrawString($"Invoice #{data.InvoiceNumber}", titleFont,
XBrushes.DarkBlue, 40, y);
y += 50;
// Table header — manually positioned
double[] colX = { 40, 300, 400 };
double rowHeight = 30;
gfx.DrawRectangle(XBrushes.SteelBlue, 40, y, 500, rowHeight);
gfx.DrawString("Item", headerFont, XBrushes.White, colX[0] + 10, y + 20);
gfx.DrawString("Qty", headerFont, XBrushes.White, colX[1] + 10, y + 20);
gfx.DrawString("Price", headerFont, XBrushes.White, colX[2] + 10, y + 20);
y += rowHeight;
// Each row drawn individually with explicit coordinates
foreach (var item in data.Items)
{
gfx.DrawRectangle(XPens.LightGray, 40, y, 500, rowHeight);
gfx.DrawString(item.Name, bodyFont, XBrushes.Black, colX[0] + 10, y + 20);
gfx.DrawString(item.Quantity.ToString(), bodyFont, XBrushes.Black, colX[1] + 10, y + 20);
gfx.DrawString($"${item.Price:F2}", bodyFont, XBrushes.Black, colX[2] + 10, y + 20);
y += rowHeight;
}
y += 20;
gfx.DrawString($"Total: ${data.Total:F2}", headerFont, XBrushes.Black, 440, y);
using var stream = new MemoryStream();
document.Save(stream);
return stream.ToArray();
}
}using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
public class InvoiceGenerator
{
public byte[] GenerateInvoice(InvoiceData data)
{
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
var titleFont = new XFont("Arial", 24);
var headerFont = new XFont("Arial", 12, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 12);
double y = 40;
gfx.DrawString($"Invoice #{data.InvoiceNumber}", titleFont,
XBrushes.DarkBlue, 40, y);
y += 50;
// Table header — manually positioned
double[] colX = { 40, 300, 400 };
double rowHeight = 30;
gfx.DrawRectangle(XBrushes.SteelBlue, 40, y, 500, rowHeight);
gfx.DrawString("Item", headerFont, XBrushes.White, colX[0] + 10, y + 20);
gfx.DrawString("Qty", headerFont, XBrushes.White, colX[1] + 10, y + 20);
gfx.DrawString("Price", headerFont, XBrushes.White, colX[2] + 10, y + 20);
y += rowHeight;
// Each row drawn individually with explicit coordinates
foreach (var item in data.Items)
{
gfx.DrawRectangle(XPens.LightGray, 40, y, 500, rowHeight);
gfx.DrawString(item.Name, bodyFont, XBrushes.Black, colX[0] + 10, y + 20);
gfx.DrawString(item.Quantity.ToString(), bodyFont, XBrushes.Black, colX[1] + 10, y + 20);
gfx.DrawString($"${item.Price:F2}", bodyFont, XBrushes.Black, colX[2] + 10, y + 20);
y += rowHeight;
}
y += 20;
gfx.DrawString($"Total: ${data.Total:F2}", headerFont, XBrushes.Black, 440, y);
using var stream = new MemoryStream();
document.Save(stream);
return stream.ToArray();
}
}Imports PdfSharpCore.Drawing
Imports PdfSharpCore.Pdf
Imports System.IO
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim document As New PdfDocument()
Dim page = document.AddPage()
Dim gfx = XGraphics.FromPdfPage(page)
Dim titleFont As New XFont("Arial", 24)
Dim headerFont As New XFont("Arial", 12, XFontStyleEx.Bold)
Dim bodyFont As New XFont("Arial", 12)
Dim y As Double = 40
gfx.DrawString($"Invoice #{data.InvoiceNumber}", titleFont, XBrushes.DarkBlue, 40, y)
y += 50
' Table header — manually positioned
Dim colX As Double() = {40, 300, 400}
Dim rowHeight As Double = 30
gfx.DrawRectangle(XBrushes.SteelBlue, 40, y, 500, rowHeight)
gfx.DrawString("Item", headerFont, XBrushes.White, colX(0) + 10, y + 20)
gfx.DrawString("Qty", headerFont, XBrushes.White, colX(1) + 10, y + 20)
gfx.DrawString("Price", headerFont, XBrushes.White, colX(2) + 10, y + 20)
y += rowHeight
' Each row drawn individually with explicit coordinates
For Each item In data.Items
gfx.DrawRectangle(XPens.LightGray, 40, y, 500, rowHeight)
gfx.DrawString(item.Name, bodyFont, XBrushes.Black, colX(0) + 10, y + 20)
gfx.DrawString(item.Quantity.ToString(), bodyFont, XBrushes.Black, colX(1) + 10, y + 20)
gfx.DrawString($"${item.Price:F2}", bodyFont, XBrushes.Black, colX(2) + 10, y + 20)
y += rowHeight
Next
y += 20
gfx.DrawString($"Total: ${data.Total:F2}", headerFont, XBrushes.Black, 440, y)
Using stream As New MemoryStream()
document.Save(stream)
Return stream.ToArray()
End Using
End Function
End ClassImports PdfSharpCore.Drawing
Imports PdfSharpCore.Pdf
Imports System.IO
Public Class InvoiceGenerator
Public Function GenerateInvoice(data As InvoiceData) As Byte()
Dim document As New PdfDocument()
Dim page = document.AddPage()
Dim gfx = XGraphics.FromPdfPage(page)
Dim titleFont As New XFont("Arial", 24)
Dim headerFont As New XFont("Arial", 12, XFontStyleEx.Bold)
Dim bodyFont As New XFont("Arial", 12)
Dim y As Double = 40
gfx.DrawString($"Invoice #{data.InvoiceNumber}", titleFont, XBrushes.DarkBlue, 40, y)
y += 50
' Table header — manually positioned
Dim colX As Double() = {40, 300, 400}
Dim rowHeight As Double = 30
gfx.DrawRectangle(XBrushes.SteelBlue, 40, y, 500, rowHeight)
gfx.DrawString("Item", headerFont, XBrushes.White, colX(0) + 10, y + 20)
gfx.DrawString("Qty", headerFont, XBrushes.White, colX(1) + 10, y + 20)
gfx.DrawString("Price", headerFont, XBrushes.White, colX(2) + 10, y + 20)
y += rowHeight
' Each row drawn individually with explicit coordinates
For Each item In data.Items
gfx.DrawRectangle(XPens.LightGray, 40, y, 500, rowHeight)
gfx.DrawString(item.Name, bodyFont, XBrushes.Black, colX(0) + 10, y + 20)
gfx.DrawString(item.Quantity.ToString(), bodyFont, XBrushes.Black, colX(1) + 10, y + 20)
gfx.DrawString($"${item.Price:F2}", bodyFont, XBrushes.Black, colX(2) + 10, y + 20)
y += rowHeight
Next
y += 20
gfx.DrawString($"Total: ${data.Total:F2}", headerFont, XBrushes.Black, 440, y)
Using stream As New MemoryStream()
document.Save(stream)
Return stream.ToArray()
End Using
End Function
End ClassIronPDF版本使用HTML/CSS——大多數開發者已具備的技能。 QuestPDF版本需要學習一個特定於領域的流暢API,但提供結構。 PdfSharp版本需要手動計算每個像素位置——每個列位移、每個行高、每個單獨繪製的邊框。
我應該選擇哪個程式庫?
當我評估這些程式庫時,決策樹很簡單:
需要現代CSS的HTML到PDF轉換嗎? 實際選擇是IronPDF或Puppeteer Sharp。IronPDF內部處理Chromium; Puppeteer Sharp要求您管理外部瀏覽器進程。 wkhtmltopdf不是新專案的選擇。 iText的pdfHTML無法渲染Flexbox或Grid。
從資料程式化的構建文件,不需HTML? QuestPDF的流暢API效率高且設計良好。 PdfSharp提供較低層次的控制,但需要大量的程式碼來實現等效佈局。
跨平台部署(Linux、Docker、雲)? IronPDF、QuestPDF和Puppeteer Sharp在Linux上運行,無libgdiplus依賴。 Aspose.PDF在Linux上有記憶體洩漏的文件。 PdfSharp有部分平台支援,且已知問題。
授權限制? PdfSharp(MIT)和Puppeteer Sharp(MIT)免費且無條件。 QuestPDF在收入低於100萬美元時是免費的。 iText需要符合AGPL或購買商業授權(每年$15K–$210K)。 IronPDF的永久授權起價$999。 Aspose起價約999美元。
在您承諾之前
用您的實際內容測試,而不是"Hello World"。儘早部署到您的目標平台。 測試100多個文件的記憶體,而不是一個。 與您的法律團隊閱讀完整的授權文字。 如果您打算針對無伺服器,請檢查冷啟動延遲。
IronPDF提供試用,具備完整功能可對您的特定需求進行評估。