Three ways to turn HTML into a real, text-selectable PDF from C#, all run on the same invoice template on .NET 10, plus the NuGet packages you should skip. Tested with PuppeteerSharp 25.14 and Microsoft.Playwright 1.63.
| Option | Pick it when |
|---|---|
| PuppeteerSharp .NET port of Puppeteer | You want the most direct Chrome control and an in-code browser download (BrowserFetcher). |
| Playwright for .NET By Microsoft, same Chromium print engine | You already write Playwright tests in .NET, or want built-in auto-waiting. Same PDF options. |
| An API Chromium on someone else's server | Azure Functions, App Service, slim containers, or apps where a ~150 MB browser per instance isn't worth running. |
Skip these: DinkToPdf, Rotativa and other wkhtmltopdf wrappers ship a 2012-era WebKit whose project is archived (wkhtmltopdf alternative); iTextSharp's old HTMLWorker is deprecated and ignores most CSS; PdfSharp and QuestPDF are good libraries but build PDFs from code and don't read HTML. Commercial HTML-to-PDF libraries (IronPDF, SelectPdf and others) work but need a paid license per developer or deployment, so read their terms first.
Save this as invoice.html next to your project:
<!doctype html>
<html><head><meta charset="utf-8"><style>
body { font-family: sans-serif; }
.top { display: flex; justify-content: space-between; background: #eef2ff; padding: 12px; }
table { width: 100%; border-collapse: collapse; }
td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
tr { break-inside: avoid; }
</style></head><body>
<div class="top"><h1>Invoice #1042</h1><p>Due: 2026-11-01</p></div>
<table><tr><th>Item</th><th>Price</th></tr>
<tr><td>Consulting (3h)</td><td>€450.00</td></tr>
<tr><td>Hosting, Oct.</td><td>€19.00</td></tr></table>
</body></html>
dotnet new console -n InvoicePdf && cd InvoicePdf
dotnet add package PuppeteerSharp
Replace Program.cs and run dotnet run:
using PuppeteerSharp;
using PuppeteerSharp.Media;
await new BrowserFetcher().DownloadAsync(); // downloads Chrome once, then reuses it
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true, Args = ["--no-sandbox"] });
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(await File.ReadAllTextAsync("invoice.html"));
await page.PdfAsync("invoice.pdf", new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
MarginOptions = new MarginOptions { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" },
DisplayHeaderFooter = true,
HeaderTemplate = "<span></span>",
FooterTemplate = "<div style='font-size:9px;width:100%;text-align:right;padding-right:15mm'>" +
"Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
});
Result: an A4 PDF with the blue header bar, title and due date side by side, selectable text and "Page 1 of 1" at the bottom right. For a live page, use await page.GoToAsync("https://example.com", WaitUntilNavigation.Networkidle0) instead of SetContentAsync. Things that trip people up:
PrintBackground = true is off by default, so background colors and images vanish without it.HeaderTemplate or Chrome prints the date and title there.--no-sandbox: Chrome fails with "No usable sandbox" on Ubuntu 23.10+, in Docker and as root. In containers also install fonts (fonts-noto-core, fonts-noto-cjk); empty boxes in the PDF mean a missing font.PdfDataAsync(options) returns a byte[] instead of writing a file, which is what you want in a web app.dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net10.0/playwright.ps1 install chromium
No PowerShell? Call the installer from code once: Microsoft.Playwright.Program.Main(["install", "chromium"]);. Then:
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.SetContentAsync(await File.ReadAllTextAsync("invoice.html"));
await page.PdfAsync(new PagePdfOptions
{
Path = "invoice-pw.pdf",
Format = "A4",
PrintBackground = true,
Margin = new Margin { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" },
DisplayHeaderFooter = true,
HeaderTemplate = "<span></span>",
FooterTemplate = "<div style='font-size:9px;width:100%;text-align:right;padding-right:15mm'>" +
"Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
});
The output matches PuppeteerSharp's: both drive Chromium's print-to-PDF, so the options map one to one. PDF export only works in Chromium, not in Firefox or WebKit. Playwright passes --no-sandbox itself. Using a .NET 10 file-based app (dotnet run app.cs)? Those disable reflection-based JSON, which Playwright needs, so add #:property JsonSerializerIsReflectionEnabledByDefault=true at the top, or it fails with "Reflection-based serialization has been disabled".
Shotmint renders in the same Chromium, but your app only makes an HTTP call: no browser download, no fonts, nothing to keep alive. No NuGet packages needed:
using System.Text;
using System.Text.Json.Nodes;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("SHOTMINT_KEY"));
var body = new JsonObject
{
["html"] = await File.ReadAllTextAsync("invoice.html"),
["format"] = "A4",
["margin"] = "20mm",
}.ToJsonString();
var res = await http.PostAsync("https://shotmint.creasework.com/v1/pdf",
new StringContent(body, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
throw new Exception($"Render failed ({(int)res.StatusCode}): {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("invoice-api.pdf", await res.Content.ReadAsByteArrayAsync());
JsonObject escapes the HTML for you, so a Razor-rendered string can go straight into html, and it also works in trimmed and Native AOT builds. For a live page, send url instead. Other options: landscape, format (A3, A4, A5, Letter, Legal, Tabloid), scale, print_background (on by default) and screen_media. There is no header/footer template option: for page numbers, use CSS @page { @bottom-right { content: "Page " counter(page) " of " counter(pages) } }, which current Chromium supports. In ASP.NET Core, register the client with IHttpClientFactory and return the bytes with Results.File(bytes, "application/pdf", "invoice.pdf"). Images and fonts must be reachable from the internet (or inlined as data URIs). Screenshots from C#: website screenshots in C#.
| Feature | Support |
|---|---|
| Rendering | All three: Chromium, flexbox, grid, JavaScript, web fonts |
| Page numbers | PuppeteerSharp and Playwright: FooterTemplate or CSS API: CSS @page margin boxes |
| Install | API: nothing (built-in HttpClient) PuppeteerSharp and Playwright: plus a ~150 MB Chromium and system libraries |
| Azure Functions, App Service | API: any plan Self-hosted: needs a container image with Chromium and fonts |
| Private pages | PuppeteerSharp and Playwright: can reach localhost and your VPN API: public URLs or HTML you send |
If your app can run a browser, PuppeteerSharp or Playwright are free and give you full control; pick the one your team already knows. If it can't (consumption-plan Functions, a small container) or you'd rather not keep Chromium patched and fed with fonts, use the API. Want to see the output first? Paste your HTML into the free HTML to PDF converter; no key needed. Other languages: Node.js, Python.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).
Get a free API key Full API reference