HTML-to-PDF in .NET has two fundamentally different use cases: rendering a web page (with JS, CSS, web fonts) and building a PDF document from an HTML template. PuppeteerSharp solves the first by driving a real Chromium browser. iText solves the second with a .NET HTML renderer inside the PDF engine. The ChangeThisFile API handles both via a single POST.
Method 1: PuppeteerSharp (full Chrome rendering, JavaScript support)
PuppeteerSharp is the .NET port of Google Puppeteer. It downloads and manages Chromium automatically on first run — no manual browser install needed.
dotnet add package PuppeteerSharp
using PuppeteerSharp;
using PuppeteerSharp.Media;
public static class HtmlToPdf
{
private static IBrowser? _browser;
/// <summary>Call once at startup (e.g., in Program.cs) to reuse the browser.</summary>
public static async Task InitAsync()
{
var fetcher = new BrowserFetcher();
await fetcher.DownloadAsync();
_browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = new[] { "--no-sandbox", "--disable-dev-shm-usage" }
});
}
/// <summary>Convert an HTML file or URL to PDF.</summary>
public static async Task ConvertAsync(
string htmlPathOrUrl,
string outputPath,
CancellationToken ct = default)
{
if (_browser == null) await InitAsync();
await using var page = await _browser!.NewPageAsync();
bool isUrl = htmlPathOrUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase);
if (isUrl)
await page.GoToAsync(htmlPathOrUrl, WaitUntilNavigation.Networkidle0);
else
{
var html = await File.ReadAllTextAsync(htmlPathOrUrl, ct);
await page.SetContentAsync(html, new NavigationOptions
{ WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
}
await page.PdfAsync(outputPath, new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
MarginOptions = new MarginOptions
{ Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" }
});
}
}
// Usage
await HtmlToPdf.InitAsync();
await HtmlToPdf.ConvertAsync("report.html", "report.pdf");
Reuse the IBrowser instance across requests — Chromium startup takes ~2 seconds. In ASP.NET, register it as a singleton service. The --no-sandbox flag is required in Docker; for Windows servers running as a real user, you can omit it.
Method 2: iText 7 (programmatic PDF from HTML, no browser)
iText 7 with the pdfHTML add-on renders HTML to PDF using its own CSS engine. No browser needed — ideal for server-side invoice/report generation from HTML templates.
dotnet add package itext7
dotnet add package itext7.pdfhtml
using iText.Html2pdf;
using iText.Kernel.Pdf;
public static class HtmlToPdfItext
{
public static void Convert(string htmlPath, string outputPath)
{
using var htmlReader = new StreamReader(htmlPath);
using var pdfWriter = new PdfWriter(outputPath);
using var pdfDoc = new PdfDocument(pdfWriter);
var converterProps = new ConverterProperties();
// Set base URI so relative CSS/image references resolve
converterProps.SetBaseUri(Path.GetDirectoryName(
Path.GetFullPath(htmlPath)) + Path.DirectorySeparatorChar);
HtmlConverter.ConvertToPdf(
htmlReader.BaseStream, pdfDoc, converterProps);
}
}
iText supports most CSS 2.1 and a subset of CSS 3. JavaScript is not executed — if your HTML relies on JS to render content, use PuppeteerSharp instead. iText's AGPL license means the free version requires your project to be open-source; for closed-source commercial use, purchase an iText license.
Method 3: ChangeThisFile API (HttpClient, URL or file)
Pass an HTML file or send the raw HTML string. The API renders with a real browser engine server-side. Free tier: 1,000 conversions/month.
# Convert a local HTML file
curl -X POST https://changethisfile.com/v1/convert \
-H "Authorization: Bearer ctf_sk_your_key" \
-F "file=@report.html" \
-F "target=pdf" \
--output report.pdf
using System.Net.Http;
using System.Net.Http.Headers;
public class HtmlToPdfService
{
private readonly HttpClient _http;
private const string ApiKey = "ctf_sk_your_key_here";
public HtmlToPdfService(IHttpClientFactory factory)
=> _http = factory.CreateClient("ctf");
public async Task ConvertFileAsync(
string htmlPath,
string outputPath,
CancellationToken ct = default)
{
await using var fileStream = File.OpenRead(htmlPath);
using var form = new MultipartFormDataContent();
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue("text/html");
form.Add(fileContent, "file", Path.GetFileName(htmlPath));
form.Add(new StringContent("pdf"), "target");
using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/convert")
{
Content = form,
Headers = { Authorization =
new AuthenticationHeaderValue("Bearer", ApiKey) }
};
using var response = await _http.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
await using var outStream = File.Create(outputPath);
await response.Content.CopyToAsync(outStream, ct);
}
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| PuppeteerSharp | JS-rendered pages, web fonts, full CSS 3, URLs | Chromium ~150MB; ~2s startup; needs sandbox flags in Docker |
| iText 7 + pdfHTML | Server-side invoice/report templates, no browser needed | CSS 2.1 subset only; no JavaScript; AGPL for closed-source |
| ChangeThisFile API | No Chromium in container, URL conversion, free tier | Network latency; 25MB file limit on free tier |
Production tips
- Reuse the Puppeteer browser as a singleton. Register
IBrowserin DI as a singleton. Each page gets a newIPagetab — the browser itself stays warm. - Use WaitUntil Networkidle0 for JS-heavy pages. If the page content loads via fetch/XHR,
DOMContentLoadedfires before the data arrives.Networkidle0waits until there are no pending network requests. - Pass CancellationToken. PuppeteerSharp's async methods accept a cancellation token from .NET 9+. For earlier versions, wrap with
ct.Register(() => page.CloseAsync()). - Set page.EmulateMediaTypeAsync(MediaType.Print) for print CSS. Many HTML templates use
@media print— Puppeteer renders in screen mode by default. CallEmulateMediaTypeAsync(MediaType.Print)beforePdfAsync. - Use IHttpClientFactory for the API. Never
new HttpClient()inside a service method. Set a 3-minute timeout for large HTML pages with many embedded resources.
PuppeteerSharp is the right default for most HTML-to-PDF needs in .NET — full browser rendering, JS support, and CSS 3. iText is better for building PDFs programmatically from templates without the Chromium overhead. For CI/CD pipelines and containers that need to stay lean, the ChangeThisFile API removes both dependencies. Free tier: 1,000 conversions/month.