COMPARISON

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 IronPDF

Generate 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")
$vbLabelText   $csharpLabel

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:

CriterionWhat to TestWhy It Matters
HTML/CSS renderingFeed it your actual templates with Flexbox/GridMost libraries claim HTML support but render CSS 2.1 at best
JavaScript executionTest with Chart.js or dynamic table contentLibraries without JS support produce blank sections
Licensing modelRead the full license, not the summaryAGPL requires open-sourcing your entire application
Platform supportDeploy to your target Linux/Docker/ARM64 environmentWindows success doesn't predict Linux behavior
Memory under loadGenerate 100+ documents in a loopSingle-document tests hide leaks that crash production servers
Published pricingCheck 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")
$vbLabelText   $csharpLabel

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 Using
$vbLabelText   $csharpLabel

In 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

FeatureIronPDFiText 7PdfSharpQuestPDFwkhtmltopdfPuppeteerAspose
HTML to PDFFull (Chromium)Limited (CSS 2.1)NoNoDeprecatedFull (Chrome)Limited
CSS Flexbox/GridYesNoNoNoNoYesNo
JavaScriptYesNoNoNoLimitedYesNo
Linux (no libgdiplus)YesYesPartial*YesN/AYesNo
Docker deploymentStandard .NET imageStandardPartial*StandardComplexComplexRequires libgdiplus
Active maintenanceYesYesYesYesAbandonedYesYes
Published pricingYes ($749+)No ($15K–$210K/yr)Free (MIT)Yes (free <$1M)FreeFree (MIT)Yes ($999+)
Perpetual licenseYesNo (subscription)N/AN/AN/AN/AYes
AGPL-freeYesNo (requires commercial)YesYesYesYesYes

*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:

ScenarioIronPDFPuppeteer SharpiText pdfHTMLwkhtmltopdf
Simple HTML page~150ms~500ms~200ms~200ms
Complex CSS layout (Flexbox/Grid)~250ms~600msFails/Partial~400ms (broken)
JavaScript-heavy page~350ms~800msFailsFails/Partial
Memory per operation~80MB~150MB~60MB~50MB
Cold start (first generation)2–5s3–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 Class
$vbLabelText   $csharpLabel

QuestPDF — 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 Class
$vbLabelText   $csharpLabel

PdfSharp — 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 Class
$vbLabelText   $csharpLabel

The 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.