The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use a browser-backed renderer when the PDF must preserve existing HTML, CSS, and JavaScript. In ASP.NET, Playwright for .NET with Chromium is the most direct implementation: install Microsoft.Playwright, install its browser binaries, load the page, wait for its real content and assets, and call the PDF API. A direct HTML converter such as SelectPdf can be simpler when its conversion model and licensing fit your application. Choose QuestPDF only when you are willing to define the document as C# layout code; it is not a drop-in renderer for arbitrary existing HTML.
No option is universally best. The right choice depends on HTML complexity, JavaScript behavior, hosting operating system, PDF volume, accessibility requirements, and licensing eligibility.
Choose the rendering model first
| Requirement | Best starting point | Reason |
|---|---|---|
| Keep an existing page, CSS, and client-side rendering | Playwright for .NET with Chromium | A real browser evaluates HTML, stylesheets, fonts, images, and JavaScript before printing. |
| Convert an HTML string or URL with a compact API | SelectPdf | Its C# examples expose direct HTML-string and URL conversion, with page and browser-rendering options. |
| Build a stable document entirely in C# | QuestPDF | Its component model gives explicit layout control, but it does not render arbitrary existing HTML. |
When Playwright is the safer fidelity choice
Use Chromium when the page relies on modern CSS, responsive layout, web fonts, lazy-loaded images, or JavaScript-generated content. The browser must be installed separately from the NuGet package, and the deployment must permit the required Chromium process and resources.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →When a direct converter is more practical
SelectPdf may fit a service that receives an HTML string or URL and wants a direct conversion call. Its documentation shows page size, orientation, margins, web-page width, and a Chromium rendering option. The vendor states that its Community Edition is limited to five pages per document; its commercial edition removes that page limit. Verify the current package, target-framework support, licensing, and terms for your workload before committing.
#1 Best Overall
When code-first layout is the better design
QuestPDF is appropriate when templates can be authored as C# components and you need deterministic document structure. Its ASP.NET example generates PDF bytes and returns them with the application/pdf content type. It should not be presented as an HTML-to-PDF converter. Set its license once during application startup or initialization, using the license option your organization is eligible to use.
Playwright for .NET: a complete HTML-to-PDF path
1. Install the package and Chromium
Add the Microsoft package to the ASP.NET project:
dotnet add package Microsoft.Playwright
The official Playwright setup includes a separate browser-installation step. After restoring the project, run the Playwright install command appropriate to the package version and deployment environment so Chromium is available at runtime. Do not assume that installing the NuGet package alone installs a browser.
2. Create a reusable browser service
Launching a new browser process for every request is a poor default for a busy service. Keep a long-lived browser instance, create isolated contexts or pages per job, and close each page and context in a finally block. Size concurrency for the CPU and memory available in the actual host; measure it rather than copying a fixed number.
using Microsoft.Playwright;
public sealed class PdfRenderer : IAsyncDisposable
{
private readonly IPlaywright _playwright;
private readonly IBrowser _browser;
private PdfRenderer(IPlaywright playwright, IBrowser browser)
{
_playwright = playwright;
_browser = browser;
}
public static async Task<PdfRenderer> CreateAsync()
{
var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true
});
return new PdfRenderer(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.PdfAsync(new PagePdfOptions
{
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true,
Margin = new() { Top = "20mm", Right = "15mm", Bottom = "20mm", Left = "15mm" }
});
return await page.PdfAsync(new PagePdfOptions
{
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true,
Margin = new() { Top = "20mm", Right = "15mm", Bottom = "20mm", Left = "15mm" }
});
}
public async ValueTask DisposeAsync()
{
await _browser.CloseAsync();
_playwright.Dispose();
}
}
In production code, avoid making two PDF calls as in the illustrative method above: create the options once, call PdfAsync once, and return those bytes. The duplication is shown only to make the option object easy to locate; use the corrected implementation below:
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 });
var options = new PagePdfOptions
{
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true,
Margin = new() { Top = "20mm", Right = "15mm", Bottom = "20mm", Left = "15mm" }
};
return await page.PdfAsync(options);
}
3. Register the service and return a PDF
builder.Services.AddSingleton<PdfRenderer>(_ => PdfRenderer.CreateAsync().GetAwaiter().GetResult());
app.MapGet("/reports/{id}/pdf", async (string id, PdfRenderer renderer, CancellationToken cancellationToken) =>
{
var url = $"https://app.example.com/reports/{Uri.EscapeDataString(id)}";
var bytes = await renderer.RenderUrlAsync(url, cancellationToken);
return Results.File(bytes, "application/pdf", $"report-{id}.pdf");
});
For dependency-injection startup, prefer an asynchronous hosted initialization pattern rather than blocking startup if your hosting model permits it. The important design is one controlled browser lifecycle, isolated request contexts, cancellation, and explicit cleanup.
Make print output intentional
Print media versus screen media
Playwright’s PDF operation uses print CSS media by default. If the screen design is the intended output, select screen media before generating the PDF:
Rank #2
await page.EmulateMediaAsync(new PageEmulateMediaOptions { Media = Media.Screen });
var bytes = await page.PdfAsync(new PagePdfOptions { Format = "A4", PrintBackground = true });
Usually, create a dedicated @media print stylesheet instead. Define page breaks, remove navigation, and prevent rows or cards from splitting where possible:
<style>
@page { size: A4; margin: 18mm 15mm; }
@media print {
.no-print { display: none !important; }
table, figure, .card { break-inside: avoid; }
h1, h2, h3 { break-after: avoid; }
}
</style>
Paper size, dimensions, margins, and orientation
The PDF API exposes named paper formats, custom width and height, margins, landscape orientation, headers, footers, and page ranges. Use one strategy consistently: either set a format such as A4 or supply explicit dimensions. If your CSS defines @page, decide whether the browser should prefer that CSS size or the API options.
var options = new PagePdfOptions
{
Landscape = true,
Width = "11in",
Height = "8.5in",
DisplayHeaderFooter = true,
HeaderTemplate = "<span></span>",
FooterTemplate = "<div style='font-size:9px;width:100%;text-align:center'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
PageRanges = "1-3",
PrintBackground = true
};
Header and footer templates run in the browser’s print template context, so keep them self-contained and test their CSS. Page ranges are useful for previews or partial exports, but verify the requested range exists for short documents.
Colors and backgrounds
By default, the PDF operation may adjust colors for printing. If exact screen colors matter, apply -webkit-print-color-adjust: exact in the print stylesheet and enable background printing in the PDF options. Fonts, gradients, transparency, and image color profiles still need testing in the target Chromium build.
Wait for real content
NetworkIdle is not a universal definition of readiness. Pages with analytics, streaming requests, or delayed client rendering may never become idle or may become idle before a chart is drawn. Prefer a page-specific readiness marker:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteawait page.GotoAsync(url, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
await page.WaitForSelectorAsync("#report-ready", new PageWaitForSelectorOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
await page.EvaluateAsync("document.fonts.ready");
For lazy images, scroll or trigger the page’s own loading mechanism before capture. For deterministic output, expose a server-rendered or JavaScript-set marker only after data, fonts, and critical images are ready.
Loading HTML directly instead of navigating to a URL
When the HTML is generated inside the request, create a page and use SetContentAsync. Relative URLs require a meaningful base URL, and remote fonts or images require network access from the server.
await page.SetContentAsync(html, new PageSetContentOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 90_000
});
var pdf = await page.PdfAsync(new PagePdfOptions
{
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true
});
Sanitize untrusted HTML before loading it. A browser renderer can make outbound requests, execute JavaScript, and access credentials that are available to the process or page. Isolate rendering, restrict outbound networking where possible, avoid injecting secrets into page content, and set explicit navigation and resource timeouts.
SelectPdf for direct HTML conversion
SelectPdf’s documented C# model accepts an HTML string or URL and exposes settings for page size, orientation, margins, web-page width, and Chromium rendering. A representative shape is:
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsvar converter = new SelectPdf.HtmlToPdf();
converter.Options.PdfPageSize = SelectPdf.PdfPageSize.A4;
converter.Options.PdfPageOrientation = SelectPdf.PdfPageOrientation.Portrait;
converter.Options.WebPageWidth = 1200;
var document = converter.ConvertHtmlString(html);
document.Save(stream);
document.Close();
Use the exact API names and package version documented for your installed release. The vendor states that Community Edition conversion is limited to five pages per document, while the commercial edition has no such page limit. Treat that as an edition-specific vendor condition, not an independent performance claim. Check current target-framework support and licensing before deployment.
QuestPDF when the document is code-first
A code-first document can be returned from an ASP.NET endpoint as generated bytes:
app.MapGet("/invoice/{id}/pdf", (string id) =>
{
var document = new InvoiceDocument(id);
byte[] bytes = document.GeneratePdf();
return Results.File(bytes, "application/pdf", $"invoice-{id}.pdf");
});
This approach is valuable when the layout is a controlled C# component tree. Recreating an existing marketing page or application view in QuestPDF is a rewrite, not conversion. Configure the library’s license once during startup according to your organization’s eligibility and the current terms.
Rank #4
Performance, reliability, and deployment decisions
- Browser lifecycle: reuse a browser, isolate contexts, and cap concurrent pages based on measured memory and CPU.
- Timeouts: set navigation, selector, and overall request limits; cancel work when the HTTP request is abandoned.
- Fonts and assets: package required fonts or make them reliably reachable; missing fonts change line wrapping and page count.
- Long tables: test repeated headers, row splitting, and page-break CSS with realistic data.
- Security: treat URLs and HTML as untrusted input, prevent server-side request forgery, and restrict access to internal addresses.
- Observability: log renderer choice, URL or document identifier, elapsed time, page count when available, timeout category, and browser errors without logging secrets.
- Capacity: there are no independent latency or throughput benchmarks established here. Measure the actual operating system, Chromium version, HTML, concurrency, and resource limits you will use.
Troubleshooting common failures
Chromium executable is missing
Cause: the NuGet package was installed but browser binaries were not installed in the deployment image. Fix: run the Playwright browser-installation step during image build or deployment and verify the runtime user can read and execute the files.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
PDF is blank or missing JavaScript content
Cause: capture occurred before client rendering completed. Fix: wait for a page-specific selector or readiness flag, then wait for fonts and critical images. Do not rely only on a short arbitrary delay.
CSS looks different from the web page
Cause: print media is active, assets use inaccessible relative URLs, or a print stylesheet hides content. Fix: inspect print rules, choose screen media only when appropriate, set a valid base URL, and verify server-side network access.
Colors are washed out
Cause: print color adjustment. Fix: enable background printing and use -webkit-print-color-adjust: exact where exact colors are required.
Images or fonts are missing
Cause: authentication, CORS, blocked outbound requests, incorrect relative paths, or a race with lazy loading. Fix: provide required cookies or headers in the browser context, use absolute or correctly based URLs, wait for the assets, and capture browser console and network errors.
Requests hang under load
Cause: launching too many browsers, unbounded page concurrency, or pages that keep connections open. Fix: reuse one browser, limit concurrent contexts, use explicit readiness selectors, and enforce cancellation and timeouts.
Best Value
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF, while handling browser setup for you. Its clean-shot pipeline accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.
For an HTML-to-PDF workflow, point the API at the rendered page:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for output and rendering parameters. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Free use includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Recommended Free Tools
Equivalent calls from C# scripts and Node.js
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
How to select after a proof-of-concept
- Capture representative pages: forms, charts, long tables, images, custom fonts, and JavaScript-generated sections.
- Compare print and screen media, page breaks, headers, footers, colors, and page ranges against your acceptance criteria.
- Run the intended operating system and hosting image with realistic concurrency; record memory, CPU, elapsed time, and failure rates.
- Review URL/HTML security, outbound network policy, browser patching, license eligibility, and commercial limits.
- Choose Playwright for browser fidelity, SelectPdf for a direct converter that fits its current terms, or QuestPDF when a C# layout rewrite is acceptable.
Frequently Asked Questions
Can I generate a PDF entirely in an ASP.NET controller?
Yes. The controller or minimal API can await a renderer and return a file response with the application/pdf content type, but browser initialization and concurrency should live in a managed service rather than inside each request.
Should I use a PDF library or the browser’s print dialog?
For server automation, use a documented rendering API such as Playwright or a direct converter. A user’s print dialog is interactive and does not provide a reliable server-side pipeline.
Does HTML-to-PDF preserve accessibility automatically?
Do not assume it does. Test tagged-PDF, reading order, alternative text, and keyboard or screen-reader requirements with the exact renderer and document.
Quick Recap
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.

