How to Generate a PDF from HTML in C#
Generate reliable HTML-to-PDF files in C# with Playwright and Chromium, including print CSS, waiting, assets, page options, troubleshooting, and alternatives.
Use Playwright for .NET with Chromium when you need browser-faithful HTML, CSS, and JavaScript rendered as a PDF. Install the Microsoft.Playwright package, install Chromium, navigate to or load the HTML, wait for the page to finish rendering, then call Page.PdfAsync. Playwright uses print CSS media by default, so choose print or screen media deliberately.
1. Create a PDF from HTML with Playwright
Playwright’s .NET API exposes PdfAsync, which returns a PDF buffer or writes directly to a path. The official setup requires both the NuGet package and the browser binaries. See the Playwright .NET library setup and the Page API reference.
Install the package and Chromium
dotnet new console -n HtmlToPdf
cd HtmlToPdf
dotnet add package Microsoft.Playwright
dotnet build
# Run the Playwright install script generated in the build output.
# On Windows PowerShell, for example:
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
Use the framework directory produced by your project if it is not net8.0. Install the browser during image creation or deployment rather than on every request in production.
Complete C# example for an HTML string
using Microsoft.Playwright;
await using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true
});
var html = """
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: Arial, sans-serif; color: #222; }
h1 { margin-bottom: 8px; }
.total { margin-top: 32px; font-size: 20px; font-weight: 700; }
</style>
</head>
<body>
<h1>Invoice 1001</h1>
<p>Prepared from a server-rendered HTML template.</p>
<p class='total'>Total: $125.00</p>
</body>
</html>
""";
var page = await browser.NewPageAsync();
await page.SetContentAsync(html, new PageSetContentOptions
{
WaitUntil = WaitUntilState.Load
});
await page.PdfAsync(new PagePdfOptions
{
Path = "invoice.pdf",
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true
});
PrintBackground preserves background colors and images. PreferCSSPageSize lets an @page rule control the paper size when one is present.
2. Convert a URL or a page that uses JavaScript
For a web page, use GotoAsync, wait for the application’s own ready condition, and then create the PDF. Waiting for load only means the load event fired; client-side data may still be rendering.
using Microsoft.Playwright;
await using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com/invoice/1001", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 60_000
});
// Prefer a selector that your application sets after data and charts are ready.
await page.Locator("[data-pdf-ready='true']").WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 30_000
});
await page.PdfAsync(new PagePdfOptions
{
Path = "invoice-1001.pdf",
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true
});
Use a selector wait when possible. A fixed delay is useful for a known animation or third-party widget, but it is less reliable than an application-controlled ready marker.
3. Choose print or screen styles
PdfAsync generates the page with print CSS media by default. If your design is written for the screen, switch media before generating the file:
await page.EmulateMediaAsync(new PageEmulateMediaOptions
{
Media = Media.Screen
});
await page.PdfAsync(new PagePdfOptions
{
Path = "screen-styled.pdf",
PrintBackground = true
});
Define print-specific rules with @media print, and define paper size and margins with @page. Check both media modes when the same template serves browser viewing and PDF output.
4. PDF options that affect the output
| Option | Use |
|---|---|
Format |
Named paper such as A4 or Letter. |
Width, Height |
Explicit paper dimensions. |
PrintBackground |
Include CSS background colors and images. |
PreferCSSPageSize |
Prefer the document’s @page size. |
Landscape |
Rotate the paper orientation. |
Scale |
Scale page content before printing. |
PageRanges |
Print selected pages, such as 1-3. |
Margin |
Set top, right, bottom, and left margins. |
HeaderTemplate, FooterTemplate |
Add repeating header or footer markup. |
Set only the dimensions you need. A CSS @page rule and a conflicting Format can produce surprising results unless PreferCSSPageSize is chosen intentionally.
Headers and footers
Playwright supports header and footer templates, but the API documentation notes that scripts in those templates are not evaluated and page styles are not visible inside them. Put the required markup and inline styles in the template itself.
5. Make assets, fonts, and charts render consistently
- Ensure the rendering process can reach every stylesheet, image, font, and API endpoint.
- Use absolute HTTPS URLs or a correctly configured base URL for relative assets.
- Wait for application data and chart rendering, not only the browser’s load event.
- Use web fonts that are available to Chromium and wait for them when typography matters.
- Keep print rules from hiding content required in the PDF.
- For local files, serve the template and assets from a testable HTTP origin when relative paths or module scripts are involved.
6. A reusable service method
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();
return new HtmlPdfRenderer(playwright, browser);
}
public async Task RenderAsync(string html, string outputPath, CancellationToken cancellationToken = default)
{
await using var page = await _browser.NewPageAsync();
await page.SetContentAsync(html, new PageSetContentOptions
{
WaitUntil = WaitUntilState.Load,
Timeout = 60_000
});
await page.PdfAsync(new PagePdfOptions
{
Path = outputPath,
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true
});
}
public async ValueTask DisposeAsync()
{
await _browser.DisposeAsync();
_playwright.Dispose();
}
}
Keep one browser process alive and create an isolated page per job. Dispose pages after each conversion and close the browser during application shutdown.
7. Alternatives and when they fit
| Approach | Best fit | Trade-offs |
|---|---|---|
| Playwright .NET + Chromium | Modern HTML, CSS, and JavaScript with browser-faithful output | Browser binaries, startup cost, and memory management |
| WebView2 | Windows desktop applications already hosting Edge | Windows scope and embedded runtime management |
| wkhtmltopdf | Existing CLI pipelines and simple HTML | Qt WebKit rendering behavior and separate process packaging |
| iText pdfHTML | Library-oriented reports, invoices, and structured PDF workflows | Different HTML/CSS support, licensing, and deployment model |
WebView2’s PrintToPdf silently prints the current top-level document with custom print settings. wkhtmltopdf is an open-source LGPL command-line renderer based on Qt WebKit. iText pdfHTML is a .NET add-on for converting HTML and CSS to PDF.
8. Or skip the browser setup
If you only need a screenshot or PDF from a URL, ScreenshotNeo provides a hosted request instead of requiring Chromium packaging in your application. Its PDF endpoint uses the same capture API:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -d format=pdf -o page.pdf
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"}, timeout=90)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com', format: 'pdf' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('page.pdf', Buffer.from(await res.arrayBuffer()));
See the ScreenshotNeo documentation for the full option list. Cookie banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status; and an MCP server lets AI agents take screenshots or capture PDFs. The free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Chromium executable not found | Browser binaries were not installed in the deployment image. | Run the generated Playwright install script during setup and verify its cache or installation path. |
| PDF is blank | The page was printed before client-side content rendered, or navigation failed. | Check navigation errors, wait for a ready selector, and confirm the page has content before calling PdfAsync. |
| Styles or images are missing | Relative URLs, blocked requests, or inaccessible private assets. | Use reachable absolute URLs, authenticate asset requests, and inspect the page in the same runtime. |
| Background colors disappear | Background printing is disabled. | Set PrintBackground = true and check print CSS rules. |
| Screen layout is different | PDF generation uses print media by default. | Call EmulateMediaAsync with Media.Screen, or add print-specific CSS. |
| Content is cut off | Fixed heights, overflow rules, or incorrect paper sizing. | Remove restrictive heights, review @page margins, and test PreferCSSPageSize. |
| Fonts change in production | Fonts are unavailable or still loading. | Package or serve the fonts reliably and wait for the document’s font and data readiness. |
| Header or footer is unstyled | Page styles are not visible in templates. | Use inline styles and static markup in the header/footer template. |
| Conversion times out | A request, script, or third-party resource never completes. | Set realistic navigation and selector timeouts, remove unnecessary resources, and use an application ready marker. |
10. Performance, reliability, and cost
- Reuse the browser: launch Chromium once per worker and create short-lived pages per conversion.
- Control concurrency: limit simultaneous pages according to available CPU and memory; measure your own templates because output size and JavaScript vary.
- Reduce work: avoid unnecessary third-party scripts, animations, and high-resolution assets in print templates.
- Use deterministic readiness: a server-rendered document or explicit ready selector is more reliable than an arbitrary sleep.
- Retry carefully: retry transient navigation failures, but avoid duplicate business actions in page scripts.
- Plan deployment: include Chromium and its system dependencies in the container or host image, and keep the Playwright package and browser version aligned.
- Measure your workload: track conversion time, memory, PDF size, timeout rate, and failed asset requests for representative documents. No universal benchmark applies to every HTML template.
- Hosted option: ScreenshotNeo charges only for clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Choose a plan based on measured volume: 1,000 free shots monthly, then $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000, or $249 for 1,000,000. Yearly billing gives two months free.
11. Checklist before shipping
- Chromium is installed in every runtime that renders PDFs.
- Print or screen media is selected intentionally.
- Paper size, margins, backgrounds, and page ranges are explicit.
- Fonts, images, stylesheets, and API data are reachable.
- The application waits for a real ready condition.
- Large documents are tested for page breaks and memory use.
- Timeouts, logging, retries, and cleanup are implemented.
- Representative PDFs are compared after browser or template changes.
FAQ
Does Playwright generate the PDF without opening a visible browser?
Yes. Chromium runs headlessly when launched with its default headless configuration.
Can I convert Razor views?
Render the Razor view to an HTML string or reachable route, then pass that HTML to SetContentAsync or navigate with GotoAsync.
Why does my PDF have different colors than the browser?
PDF generation uses print media by default and may omit backgrounds unless PrintBackground is enabled. Check both your media rules and that option.
Should I use a fixed delay?
Use a selector or application ready signal when possible. A fixed delay is a fallback for content whose completion cannot be observed directly.
Is a browser renderer always the right choice?
No. WebView2 suits Windows desktop applications, wkhtmltopdf suits existing CLI workflows, and iText pdfHTML suits library-oriented PDF pipelines. Compare rendering needs, deployment, licensing, and operational cost.


