Choosing the Best C# Library for Document Generation in 2026
Selecting a C# PDF library affects your project's licensing exposure, deployment flexibility, and long-term maintenance cost. Most libraries that appear suitable during evaluation reveal limitations in production — AGPL requirements you didn't expect, HTML rendering that doesn't match your browser, or memory leaks that only surface on Linux.
This article compares the major options with code examples, documents the trade-offs that matter in practice, and includes a side-by-side code comparison generating the same invoice across three different libraries so you can see the API differences directly.
Quickstart: HTML to PDF in Three Lines
Install via NuGet:
Install-Package IronPDFGenerate a 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")This works on Windows, Linux, macOS, and Docker without additional configuration. The output matches Chrome because IronPDF embeds the same Chromium rendering engine.
Evaluation Criteria
Before comparing libraries, know what to evaluate. These are the questions that surface production issues early:
| Criterion | What to Test | Why It Matters |
|---|---|---|
| HTML/CSS rendering | Feed it your actual templates with Flexbox/Grid | Most libraries claim HTML support but render CSS 2.1 at best |
| JavaScript execution | Test with Chart.js or dynamic table content | Libraries without JS support produce blank sections |
| Licensing model | Read the full license, not the summary | AGPL requires open-sourcing your entire application |
| Platform support | Deploy to your target Linux/Docker/ARM64 environment | Windows success doesn't predict Linux behavior |
| Memory under load | Generate 100+ documents in a loop | Single-document tests hide leaks that crash production servers |
| Published pricing | Check if pricing is on the website | "Contact sales" often means $15K–$210K/year |
Library Comparison
IronPDF — Embedded Chromium, Full CSS/JS Support
IronPDF embeds Chromium directly in the NuGet package. HTML rendering matches Chrome because it is Chrome's engine. CSS Flexbox, Grid, custom properties, and JavaScript all execute as expected.
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")Trade-offs to consider: The embedded Chromium adds ~200MB to the deployment package. For standard server and container deployments, this is a one-time download with no runtime impact. For size-constrained environments like Azure Functions consumption plan, check the deployment size limit. The first PDF generation in a cold process takes 2–5 seconds for Chromium initialization; subsequent generations run in 100–500ms. Memory baseline is ~150–200MB — plan container resources accordingly.
Licensing: Perpetual licenses starting at $749 (1 developer). Published pricing at ironpdf.com. No per-document fees, no AGPL, no revenue thresholds.
iText 7 (iTextSharp) — AGPL Licensing, Limited HTML
iText is a capable PDF manipulation library with a long history. The pdfHTML add-on provides HTML-to-PDF conversion, but it doesn't use a browser engine — it approximates CSS 2.1 with a custom parser.
A production team at a mid-size SaaS company discovered this when they migrated their invoice templates from Razor views. The templates used CSS Flexbox for responsive column layouts. After integrating iText's pdfHTML, every invoice rendered as a single-column vertical stack. The display: flex, gap, and justify-content properties were silently ignored. Three weeks of development time was spent before the team realized pdfHTML couldn't render their existing CSS.
The AGPL reality: iText uses the AGPL license. If your application is network-accessible — which includes every web app, API, and SaaS product — you must release your entire application's source code under AGPL. Not just the PDF module. Everything. iText and parent company Apryse actively enforce this.
Commercial licensing: iText transitioned to subscription-based licensing in 2024. Pricing is not published — you contact sales for a quote. Third-party data suggests $15,000–$210,000 annually depending on usage volume.
PdfSharp — MIT Licensed, No HTML
PdfSharp is genuinely free under the MIT license with 34.9 million NuGet downloads. The trade-off is capability: it provides a coordinate-based drawing API with no HTML parser, no CSS engine, and no template system.
A team building a reporting dashboard chose PdfSharp because it was free and well-known. They spent four months writing coordinate-based layout code — calculating X/Y positions for every text element, drawing table borders pixel by pixel, manually handling page breaks. When they finally compared their output to what the same HTML template produced in a browser, they realized they'd built a worse version of what a Chromium-based library does automatically.
PdfSharp works well for merging PDFs, adding watermarks, and building simple structured documents from data. If you don't need HTML rendering, it remains a legitimate option.
QuestPDF — Elegant API, No HTML, Revenue Threshold
QuestPDF offers a fluent C# API for building documents programmatically. The API design is genuinely good — it's one of the better .NET library APIs in any category.
Two constraints matter: QuestPDF doesn't render HTML (by design — this is a deliberate architectural choice, not a missing feature), and the Community License covers businesses under $1M annual gross revenue. Once your company crosses that threshold, a commercial license becomes mandatory. Companies approaching the threshold should budget for this transition before it becomes urgent.
Despite QuestPDF's clear positioning against HTML, developers regularly discover this after starting implementation because the library appears in "C# PDF library" search results alongside HTML-capable libraries.
wkhtmltopdf Wrappers — Abandoned, Unpatched CVEs
wkhtmltopdf's time has passed. The GitHub organization was archived in July 2024. The underlying QtWebKit engine was deprecated by Qt in 2015. Known CVEs — including CVE-2022-35583(CVSS 9.8, SSRF enabling AWS credential exfiltration) — will never be patched.
C# wrappers like DinkToPdf, NReco.PdfGenerator, and WkHtmlToXSharp all wrap the same abandoned binary. The rendering engine is frozen at approximately Safari 2011 capability: no Flexbox, no Grid, limited JavaScript. This isn't a viable option for new projects.
Puppeteer Sharp — Full Rendering, Operational Complexity
Puppeteer Sharp controls headless Chrome via .NET bindings. Rendering quality matches Chrome because it is Chrome. The trade-off is operational: you manage external browser processes, including downloads, pooling, memory monitoring, and crash recovery.
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 UsingIn production, you also need browser process pooling, memory leak monitoring (Chromium processes can leak), crash recovery, and resource cleanup. Docker deployment requires installing Chromium dependencies — a substantial Dockerfile compared to a standard .NET image. Puppeteer Sharp is viable if your team can absorb the operational overhead.
Aspose.PDF — Extensive Features, Linux Memory Issues
Aspose.PDF offers broad PDF functionality with good documentation. The significant issue is Linux stability: Aspose depends on System.Drawing.Common, which requires libgdiplus on Linux — an unmaintained library with documented memory leaks. Developer reports span years:
"Several dozen requests cause the service to run out of memory in the Unix environment, but this does not occur in the Windows based environment" — Aspose Forum, March 2022
For Windows-only deployments, Aspose remains capable. For cross-platform or containerized deployments, the System.Drawing.Common dependency creates ongoing risk. Commercial licensing starts at approximately $999 per developer.
Feature Comparison
| Feature | IronPDF | iText 7 | PdfSharp | QuestPDF | wkhtmltopdf | Puppeteer | Aspose |
|---|---|---|---|---|---|---|---|
| HTML to PDF | Full (Chromium) | Limited (CSS 2.1) | No | No | Deprecated | Full (Chrome) | Limited |
| CSS Flexbox/Grid | Yes | No | No | No | No | Yes | No |
| JavaScript | Yes | No | No | No | Limited | Yes | No |
| Linux (no libgdiplus) | Yes | Yes | Partial* | Yes | N/A | Yes | No |
| Docker deployment | Standard .NET image | Standard | Partial* | Standard | Complex | Complex | Requires libgdiplus |
| Active maintenance | Yes | Yes | Yes | Yes | Abandoned | Yes | Yes |
| Published pricing | Yes ($749+) | No ($15K–$210K/yr) | Free (MIT) | Yes (free <$1M) | Free | Free (MIT) | Yes ($999+) |
| Perpetual license | Yes | No (subscription) | N/A | N/A | N/A | N/A | Yes |
| AGPL-free | Yes | No (requires commercial) | Yes | Yes | Yes | Yes | Yes |
*PdfSharp has documented platform-specific issues with some configurations.
Performance Comparison
Tested on a mid-tier cloud VM (4 vCPU, 8GB RAM) with a 200-element HTML invoice template, averaged over 50 iterations after warm-up:
| Scenario | IronPDF | Puppeteer Sharp | iText pdfHTML | wkhtmltopdf |
|---|---|---|---|---|
| Simple HTML page | ~150ms | ~500ms | ~200ms | ~200ms |
| Complex CSS layout (Flexbox/Grid) | ~250ms | ~600ms | Fails/Partial | ~400ms (broken) |
| JavaScript-heavy page | ~350ms | ~800ms | Fails | Fails/Partial |
| Memory per operation | ~80MB | ~150MB | ~60MB | ~50MB |
| Cold start (first generation) | 2–5s | 3–8s | <1s | <1s |
iText and wkhtmltopdf show faster cold starts because they don't initialize a browser engine — but they also can't render the same content. The performance comparison is only meaningful for scenarios where all libraries produce correct output.
Code Comparison: Same Invoice, Three Libraries
The differences between these libraries are clearest when building the same document. Here's an invoice generated three ways.
IronPDF — HTML/CSS Approach
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 ClassQuestPDF — Fluent API Approach
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 — Coordinate Drawing Approach
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 ClassThe IronPDF version uses HTML/CSS — skills most developers already have. The QuestPDF version requires learning a domain-specific fluent API but provides structure. The PdfSharp version requires calculating every pixel position manually — every column offset, every row height, every border drawn individually.
Which Library Should I Choose?
When I evaluate these libraries, the decision tree is straightforward:
Need HTML-to-PDF with modern CSS? The practical options are IronPDF or Puppeteer Sharp. IronPDF handles Chromium internally; Puppeteer Sharp requires you to manage external browser processes. wkhtmltopdf is not an option for new projects. iText's pdfHTML can't render Flexbox or Grid.
Building documents programmatically from data, no HTML? QuestPDF's fluent API is productive and well-designed. PdfSharp provides lower-level control but requires significantly more code for equivalent layouts.
Cross-platform deployment (Linux, Docker, cloud)? IronPDF, QuestPDF, and Puppeteer Sharp work on Linux without libgdiplus dependencies. Aspose.PDF has documented memory leaks on Linux. PdfSharp has partial platform support with known issues.
Licensing constraints? PdfSharp (MIT) and Puppeteer Sharp (MIT) are free without conditions. QuestPDF is free under $1M revenue. iText requires either AGPL compliance or commercial licensing ($15K–$210K/year). IronPDF's perpetual license starts at $749. Aspose starts at ~$999.
Before You Commit
Test with your actual content, not "Hello World." Deploy to your target platform early. Measure memory over 100+ documents, not one. Read the full license text with your legal team. Check cold-start latency if you're targeting serverless.
IronPDF offers a trial with full functionality for evaluation against your specific requirements.