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

ApproachBest forTradeoff
PuppeteerSharpJS-rendered pages, web fonts, full CSS 3, URLsChromium ~150MB; ~2s startup; needs sandbox flags in Docker
iText 7 + pdfHTMLServer-side invoice/report templates, no browser neededCSS 2.1 subset only; no JavaScript; AGPL for closed-source
ChangeThisFile APINo Chromium in container, URL conversion, free tierNetwork latency; 25MB file limit on free tier

Production tips

  • Reuse the Puppeteer browser as a singleton. Register IBrowser in DI as a singleton. Each page gets a new IPage tab — the browser itself stays warm.
  • Use WaitUntil Networkidle0 for JS-heavy pages. If the page content loads via fetch/XHR, DOMContentLoaded fires before the data arrives. Networkidle0 waits 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. Call EmulateMediaTypeAsync(MediaType.Print) before PdfAsync.
  • 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.