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");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")這在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")考慮的取捨:嵌入的Chromium會增加約200MB到部署包。 對於標準伺服器和容器部署,這是一次性下載,對運行時沒有影響。 對於像Azure Functions消費計畫這樣受大小限制的環境,需要檢查部署大小限制。 在冷啟動過程中的第一次PDF生成需要2至5秒來初始化Chromium; 後續生成在100至500毫秒之間運行。 記憶體基線約為150–200MB — 請相應計劃容器資源。
授權:永久授權起價$2,998(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 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 |
| 活躍維護 | 是 | 是 | 是 | 是 | 已廢棄 | 是 | 是 |
| 發佈的價格 | 是 ($2,998+) | 否 ($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 As 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 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 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的永久授權起價$2,998。 Aspose起價約999美元。
在您承諾之前
用您的實際內容測試,而不是"Hello World"。儘早部署到您的目標平台。 測試100多個文件的記憶體,而不是一個。 與您的法律團隊閱讀完整的授權文字。 如果您打算針對無伺服器,請檢查冷啟動延遲。
IronPDF提供試用,具備完整功能可對您的特定需求進行評估。
