Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a browser-backed renderer when the existing HTML, CSS, and JavaScript must remain intact. For ASP.NET applications, Playwright for .NET with Chromium is the most direct way to reproduce a browser’s print output. A direct HTML converter such as SelectPdf can be simpler when its API and licensing fit your workload. If you can redesign the document as C# components, QuestPDF is a code-first alternative, not an HTML converter.

The correct choice depends on HTML fidelity, JavaScript requirements, deployment constraints, throughput, page limits, and licensing eligibility. No option is universally best.

Choose the rendering model first

Requirement Best starting point Important qualification
Keep existing HTML, CSS, fonts, images, and JavaScript Playwright for .NET and Chromium Installs and runs a browser; test the target hosting environment.
Convert an HTML string or URL through a focused .NET API SelectPdf The vendor documents a free Community Edition limited to five pages per document; commercial terms and current framework support must be checked.
Create a stable document layout in C# QuestPDF Code-first; it does not render arbitrary existing HTML.

Option 1: Render HTML with Playwright for .NET

Install the package and Chromium

  1. Add the package:
    dotnet add package Microsoft.Playwright
  2. Build the project so the Playwright installation script is generated.
  3. Run that script to install Chromium, for example from the project output:
    pwsh bin/Debug/net8.0/playwright.ps1 install chromium

    Adjust the framework directory to your project. Chromium can run headlessly.

Browser binaries are a deployment dependency. Include the installation step in your image or release process, and verify that the hosting account can start Chromium and write to its temporary directories.

Minimal conversion service

using Microsoft.Playwright;

public sealed class HtmlPdfRenderer : IAsyncDisposable
{
    private readonly IPlaywright _playwright;
    private readonly IBrowser _browser;

    private HtmlPdfRenderer(IPlaywright playwright, IBrowser browser)
    {
        _playwright = playwright;
        _browser = browser;
    }

    public static async Task<HtmlPdfRenderer> CreateAsync()
    {
        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
        return new HtmlPdfRenderer(playwright, browser);
    }

    public async Task<byte[]> RenderUrlAsync(string url, CancellationToken cancellationToken = default)
    {
        await using var context = await _browser.NewContextAsync();
        var page = await context.NewPageAsync();
        await page.GotoAsync(url, new PageGotoOptions
        {
            WaitUntil = WaitUntilState.NetworkIdle,
            Timeout = 90_000
        });
        await page.EmulateMediaAsync(new PageEmulateMediaOptions
        {
            Media = Media.Screen
        });
        return await page.PdfAsync(new PagePdfOptions
        {
            Format = "A4",
            PrintBackground = true,
            Margin = new Margin
            {
                Top = "16mm", Bottom = "16mm", Left = "14mm", Right = "14mm"
            }
        });
    }

    public async ValueTask DisposeAsync()
    {
        await _browser.CloseAsync();
        _playwright.Dispose();
    }
}

Register one renderer or browser process for the application lifetime rather than launching a new browser for every request. Create isolated contexts or pages per job, limit concurrency, and close them in a finally path. The appropriate pooling and queue limits depend on your CPU, memory, PDF size, and hosting platform.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Convert HTML already held by the application

public async Task<byte[]> RenderHtmlAsync(string html)
{
    await using var context = await _browser.NewContextAsync();
    var page = await context.NewPageAsync();
    await page.SetContentAsync(html, new PageSetContentOptions
    {
        WaitUntil = WaitUntilState.NetworkIdle,
        Timeout = 90_000
    });
    await page.EmulateMediaAsync(new PageEmulateMediaOptions { Media = Media.Print });
    return await page.PdfAsync(new PagePdfOptions
    {
        Format = "Letter",
        PrintBackground = true,
        PreferCSSPageSize = true,
        DisplayHeaderFooter = true,
        HeaderTemplate = "",
        FooterTemplate = "/",
        Margin = new Margin { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" }
    });
}

Use Media.Print for print styles (the default PDF behavior). Call EmulateMediaAsync with Media.Screen when the screen stylesheet is the intended design. The PDF API supports paper formats, explicit width and height, margins, headers and footers, and page ranges such as 1-3 or 5. By default, page.pdf() generates a PDF with modified colors for printing; use CSS -webkit-print-color-adjust when exact color reproduction is required.

Make the page deterministic before capture

  • Wait for a meaningful selector, not only navigation completion, when JavaScript builds the content: await page.WaitForSelectorAsync("#invoice-ready");
  • Wait for fonts and images where necessary: await page.EvaluateAsync("document.fonts.ready"); and inspect image loading in page code.
  • Use absolute, reachable asset URLs or a controlled base URL for relative links.
  • Define print page breaks with CSS such as break-before, break-inside, and break-after; test long tables and repeating headers.
  • Use a fixed timezone, locale, cookies, authorization headers, or user agent when the page changes by visitor context.

ASP.NET endpoint

[ApiController]
[Route("pdf")]
public sealed class PdfController : ControllerBase
{
    private readonly HtmlPdfRenderer _renderer;
    public PdfController(HtmlPdfRenderer renderer) => _renderer = renderer;

    [HttpGet]
    public async Task<IActionResult> Get(CancellationToken cancellationToken)
    {
        var bytes = await _renderer.RenderUrlAsync("https://your-site.example/report", cancellationToken);
        return File(bytes, "application/pdf", "report.pdf");
    }
}

Validate or allow-list destination URLs if users can supply them. Otherwise a screenshot endpoint can become a server-side request forgery path. Restrict outbound networks, protect credentials, and avoid placing secrets in query strings or rendered HTML.

Option 2: SelectPdf for direct HTML or URL conversion

SelectPdf documents C# conversion from an HTML string and from a URL. Its documented settings include page size, orientation, margins, web-page width, and a Chromium rendering option. This model can be attractive when you want a conversion object instead of managing browser contexts directly.

var converter = new SelectPdf.HtmlToPdf();
converter.Options.PdfPageSize = SelectPdf.PdfPageSize.A4;
converter.Options.PdfPageOrientation = SelectPdf.PdfPageOrientation.Portrait;
converter.Options.MarginTop = 16;
converter.Options.MarginBottom = 16;
converter.Options.WebPageWidth = 1024;
converter.Options.RenderingEngine = SelectPdf.RenderingEngine.Chromium;

var document = converter.ConvertHtmlString(html);
document.Save("output.pdf");
document.Close();

The vendor states that its Community Edition permits five pages per document and that the commercial edition has no such page limit. Treat that as a vendor-published distinction, check the current package and target-framework support, and confirm licensing before production use. A five-page limit is per document, not a monthly allowance.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option 3: QuestPDF when the layout can be rewritten in C#

QuestPDF is appropriate when your team can express the document as C# components: rows, columns, tables, text, images, and page decorations. It is not a drop-in renderer for arbitrary HTML and CSS.

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

QuestPDF.Settings.License = LicenseType.Community; // choose only when eligible

var document = Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(30);
        page.Content().Column(column =>
        {
            column.Item().Text("Monthly report").FontSize(22);
            column.Item().Text("Generated in C# with a code-first layout.");
        });
    });
});
var pdfBytes = document.GeneratePdf();
return Results.File(pdfBytes, "application/pdf", "report.pdf");

Set the library license once during application startup or initialization, using the license type your organization and deployment actually qualify for. Review current terms before release.

What to test before choosing

  • HTML fidelity: CSS grids, web fonts, SVG, canvas, JavaScript-generated sections, lazy images, and external assets.
  • Print behavior: paper size, orientation, margins, print versus screen media, colors, page ranges, headers, footers, and page breaks.
  • Operations: browser installation, sandbox permissions, memory growth, concurrency limits, timeouts, cancellation, and recovery after a crashed browser.
  • Security: URL allow-lists, SSRF protection, authentication handling, untrusted HTML isolation, and outbound-request restrictions.
  • Commercial fit: page limits, license eligibility, target-framework support, and total terms rather than only an initial package price.

There are no independent latency or fidelity benchmarks established here. Measure representative documents in the exact operating system, browser version, container, and traffic pattern you will deploy.

Troubleshooting

Chromium will not launch

Confirm that the browser binaries were installed for the deployed user, that executable dependencies exist in the image, and that the process has a writable temporary directory. Capture browser stderr and avoid assuming a developer workstation matches production.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The PDF is blank or missing dynamic content

Navigation completion may occur before the application finishes rendering. Wait for a stable selector or an application-specific ready signal, then verify fonts, images, API calls, cookies, and authorization inside the browser context.

The colors differ from the web page

PDF generation uses print media by default and may adjust colors. Choose screen media when appropriate, set PrintBackground, and apply -webkit-print-color-adjust: exact to elements whose colors must be preserved.

Pages break in the wrong places

Set the intended page size and margins, use print-specific CSS break rules, avoid oversized unbreakable containers, and test tables with unusually long rows.

Requests time out or overload the server

Set navigation and PDF timeouts, cancel abandoned requests, cap concurrent jobs, reuse the browser, and queue work when PDFs are large. Record URL, duration, page count, failure category, and browser errors without logging secrets.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status.

For a one-call image, see the ScreenshotNeo documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/report -o report.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://your-site.example/report"}, timeout=90)
r.raise_for_status()
open("report.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://your-site.example/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());

ScreenshotNeo also supports PDF capture, full-page and element shots, device and viewport settings, JavaScript and CSS, waits, request blocking, cookies and headers, geolocation, resizing, caching, signed links, asynchronous webhooks, bulk capture, and an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can Playwright generate a PDF without displaying a browser window?

Yes. Launch Chromium with headless mode enabled; the browser still needs to be installed on the host.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use HTML or QuestPDF for a new report?

Keep HTML when browser rendering and existing templates matter. Choose QuestPDF when the document can be authored and maintained as C# layout components.

Is SelectPdf Community Edition suitable for a 20-page invoice pack?

The vendor-stated five-page-per-document Community Edition limit means you must verify commercial licensing or choose another approach.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.