How to Render a Webpage to PDF in a .NET Application
Convert a webpage or HTML to PDF in .NET with Playwright for .NET, including print styles, page sizing, fonts, troubleshooting, and a ScreenshotNeo API option.
Use Playwright for .NET to render a webpage as a PDF: launch a browser, navigate to the page, and call Page.PdfAsync. PDF rendering uses print media by default. Set the paper size, margins, background printing, and readiness conditions explicitly so the output matches your needs.
Render a webpage to PDF with Playwright for .NET
The example below targets a .NET console application. Add the Playwright package, install its browser, then run the program. The first browser installation is a separate setup step; deploy the required browser with your application environment.
dotnet new console -n WebToPdf
cd WebToPdf
dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net*/playwright.ps1 install chromium
Replace Program.cs with:
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(
new BrowserTypeLaunchOptions { Headless = true });
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 60_000
});
await page.PdfAsync(new PagePdfOptions
{
Path = "page.pdf",
Format = "A4",
PrintBackground = true,
Margin = new Margin
{
Top = "12mm",
Right = "12mm",
Bottom = "12mm",
Left = "12mm"
}
});
await browser.CloseAsync();
Run it with dotnet run. The PDF is written to page.pdf in the current working directory. For production code, catch navigation and rendering exceptions, log the target URL and failure stage, and choose timeouts that match your pages.
Use generated HTML instead of a URL
If your application already has HTML, use SetContentAsync rather than navigating to a hosted page. Include absolute URLs for external images, stylesheets, and fonts when the HTML will not be served from a location that can resolve relative paths.
var html = """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 12mm; }
body { font: 14px Arial, sans-serif; }
</style>
</head>
<body>
<h1>Invoice</h1>
<p>Generated from application HTML.</p>
</body>
</html>
""";
await page.SetContentAsync(html, new PageSetContentOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 60_000
});
await page.PdfAsync(new PagePdfOptions { Path = "invoice.pdf", Format = "A4" });
Choose print or screen styling
Playwright’s PDF output uses print CSS media by default. That means @media print rules can change visibility, colors, and layout, and screen-only styling may not appear. This is usually appropriate for documents designed to print.
To render the page with its screen styles instead, emulate screen media before creating the PDF:
await page.EmulateMediaAsync(new PageEmulateMediaOptions
{
Media = Media.Screen
});
await page.PdfAsync(new PagePdfOptions { Path = "screen-style.pdf" });
Pick one mode deliberately. Switching to screen media can also mean print-specific page breaks and document layout rules no longer apply. Verify the result with representative pages.
PDF options that affect the result
| Option | What it controls | When to set it |
|---|---|---|
Format |
Standard paper format, such as A4 or Letter. |
Use for conventional documents when CSS does not need to define paper dimensions. |
Width and Height |
Custom page dimensions with units such as mm, cm, in, or pixels. |
Use for labels, receipts, or other nonstandard page sizes. |
Margin |
Top, right, bottom, and left page margins. | Set explicitly when content should align consistently or avoid clipping. |
PrintBackground |
Whether background graphics and colors are printed. Off by default. | Enable when the design relies on background fills or images. |
Scale |
Scales page content from 0.1 to 2. |
Use sparingly to fit content; check legibility after scaling. |
PreferCSSPageSize |
Whether CSS @page size takes precedence over the PDF paper settings. |
Enable when the document’s CSS defines its intended page size. |
PageRanges |
Selects which pages to include. | Use when exporting a subset of a long document. |
Tagged |
Requests tagged PDF output. | Consider when downstream accessibility workflows require document tags; inspect the resulting file for your use case. |
Do not set both a paper format and custom dimensions expecting both to control the output. Choose the intended sizing source. When using @page, enable PreferCSSPageSize if the CSS dimensions should win.
await page.PdfAsync(new PagePdfOptions
{
Path = "selected-pages.pdf",
Width = "210mm",
Height = "297mm",
PreferCSSPageSize = true,
PageRanges = "1-3",
Scale = 1,
PrintBackground = true
});
For the complete current option list and types, see the Playwright .NET Page API.
Wait for page content, images, and fonts
A navigation event does not guarantee that every visual asset is ready. Pages may load images lazily, fetch content after navigation, or use web fonts. Choose a readiness strategy appropriate to the page:
- Use a navigation condition such as
NetworkIdlewhen the page settles its network activity. Some applications keep connections open, so this condition may time out. - Wait for a known content selector when the key content appears asynchronously.
- For generated HTML, wait for required resources before calling
PdfAsync. - For font-sensitive output, wait for
document.fonts.readyand check that the font files are reachable.
await page.GotoAsync("https://example.com/report", new PageGotoOptions
{
WaitUntil = WaitUntilState.DOMContentLoaded,
Timeout = 60_000
});
await page.Locator("main article").WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 30_000
});
await page.EvaluateAsync("() => document.fonts.ready");
await page.PdfAsync(new PagePdfOptions { Path = "report.pdf", Format = "A4" });
The PuppeteerSharp project documentation also waits for document.fonts.ready in its PDF example. Treat font readiness as a page-specific concern: confirm the actual fonts and content rendered in your output.
Use cURL, Python, or Node.js for a direct API capture
If you need an image or PDF from a URL without managing a browser in your .NET application, ScreenshotNeo offers a screenshot API. Its request model also applies from other languages. These examples use the product’s documented API endpoint; see the ScreenshotNeo API documentation for options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com",
"format": "pdf",
},
timeout=90,
)
r.raise_for_status()
with open("page.pdf", "wb") as pdf:
pdf.write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('page.pdf', Buffer.from(await res.arrayBuffer()))
);
C#
Call the same endpoint from .NET with HttpClient. Keep the key in configuration or a secret store rather than source control.
using System.Net.Http;
var accessKey = Environment.GetEnvironmentVariable("SCREENSHOTNEO_API_KEY")
?? throw new InvalidOperationException("Set SCREENSHOTNEO_API_KEY.");
var target = Uri.EscapeDataString("https://example.com");
var endpoint = $"https://api.screenshotneo.com/v1/shot?access_key={Uri.EscapeDataString(accessKey)}&url={target}&format=pdf";
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
using var response = await http.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
await using var output = File.Create("page.pdf");
await response.Content.CopyToAsync(output);
Or skip the browser setup
For a one-call capture from a URL, ScreenshotNeo returns a PDF or screenshot without requiring you to install and operate a browser in your application. Use format=pdf for a PDF response.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
ScreenshotNeo accepts cookie banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card. See the API documentation for request options.
Alternatives and selection criteria
Playwright for .NET is a direct fit when your application is already in .NET and you need browser navigation, print styling, and PDF controls. Other options have different tradeoffs:
- PuppeteerSharp: a .NET browser automation alternative with a PDF API. Its NuGet documentation describes .NET 8 and .NET Standard 2.0 flavors; verify the package release and target-framework compatibility you plan to use.
- wkhtmltopdf: a separate HTML-to-PDF tool whose project page identifies Qt WebKit. The available project information does not establish modern CSS compatibility or current maintenance status, so validate its output and support suitability before adopting it.
- Commercial .NET PDF SDKs: some vendors document rendering HTML webpages. Confirm licensing, target frameworks, deployment platforms, and current pricing directly with the vendor before selecting one.
There is no supported benchmark here that establishes a universally fastest or most accurate renderer. Test representative pages from your own workload, including print styles, long pages, custom fonts, and dynamic content.
Production, performance, and cost considerations
- Reuse browser processes carefully: launching a browser for every document adds startup work. A service can reuse a browser process and create isolated pages per job, while setting limits so concurrent work does not exhaust memory.
- Bound the work: set navigation and selector timeouts, cap document size and job duration, and close pages after each job. A page that never becomes idle should not hold a worker indefinitely.
- Control output size: large images, long documents, and background graphics can increase render time and PDF size. Export only needed page ranges and use suitable source assets.
- Keep dependencies available: install a compatible browser in the deployment environment and ensure network access to required page resources. Browser and package versions should be managed together.
- Handle failure explicitly: record whether failure occurred during navigation, readiness waiting, or PDF writing. Retry only failures that may be transient, and avoid uncontrolled retries against pages that consistently fail.
- Account for costs: self-hosted Playwright has no per-capture API price established by the cited sources, but it uses compute, memory, storage, and engineering time. A hosted capture API shifts browser operations to a service and may charge by plan or usage; check current terms and measure your own workload.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF looks different from the browser | PDF uses print media by default, activating print CSS. | Inspect @media print rules. Emulate screen media before PDF generation if screen styling is desired. |
| Background colors or images are missing | Background printing is off by default. | Set PrintBackground = true. |
| Text uses a fallback font or is missing | Fonts have not loaded, or font URLs are inaccessible. | Wait for document.fonts.ready, verify font requests, and ensure the browser can reach the font files. |
| Images or async content are absent | Capture starts before the content appears, or lazy content has not been triggered. | Wait for a meaningful selector or application-ready signal; scroll or otherwise trigger lazy loading if the page requires it. |
| Navigation times out on an active site | Long polling, analytics, or persistent network activity can prevent a network-idle condition. | Use a less strict navigation condition such as DOM content loaded, then wait for the specific content you need. |
| Content is clipped or too small | Paper dimensions, margins, scale, or page CSS do not match the content. | Set format or custom dimensions and margins deliberately; inspect @page rules and use scale only after checking legibility. |
| Browser executable is missing | The browser was not installed in the runtime environment or the deployed package expects a different browser revision. | Install the browser required by the Playwright package during environment setup and keep package and browser versions aligned. |
| Relative images or stylesheets fail with generated HTML | Content supplied with SetContentAsync has no useful base URL for relative references. |
Use absolute resource URLs or serve the HTML from an address whose base path resolves those references. |
FAQ
Can I render HTML that is not hosted on a website?
Yes. Set the page content with SetContentAsync, wait for needed resources, then call PdfAsync.
Does Playwright return PDF bytes as well as save to a file?
Yes. PdfAsync returns PDF bytes; its options also support writing the result to a path.
Can I use CSS to define the page size?
Yes. Define @page size in CSS and enable PreferCSSPageSize when that CSS sizing should take precedence.
Which .NET renderer is most accurate?
The cited sources do not establish a fidelity ranking. Compare output against your own pages and required PDF behavior.


