ScreenshotNeo

BlogHow-to

How to Print HTML to PDF with C#

Convert HTML strings, files, or URLs to PDFs in C# with Playwright, PuppeteerSharp, IronPDF, or a hosted API.

By the ScreenshotNeo team1 October 20268 min read

Use a browser rendering engine when the PDF must match HTML and CSS. In .NET, Microsoft Playwright is a practical default: load an HTML string or URL, wait for the page to be ready, then call Page.PdfAsync. Playwright uses print CSS media by default; call EmulateMediaAsync with screen when the page is designed for screen styles. Install the Playwright browser binaries as part of setup.

1. Choose the right C# approach

Approach Rendering model Use it when Operational notes
Microsoft Playwright Chromium lays out HTML and CSS You need browser-like output, JavaScript, modern CSS, or URL rendering Add the NuGet package and install browser binaries. PdfAsync API and browser setup.
PuppeteerSharp Headless Chromium You already use Puppeteer-style automation Launch Chromium, navigate, and call its PDF API. See the PuppeteerSharp documentation.
IronPDF Integrated Chromium-based library You want a packaged .NET API around HTML rendering The quickstart includes license-key setup. Verify the current license, platform support, and deployment requirements in the IronPDF documentation. Its API supports a base URL for relative assets.
QuestPDF Code-first PDF layout Your team wants to define the document in C# rather than convert HTML The cited examples compose documents through layout APIs. Check current license eligibility in the QuestPDF documentation.

Playwright, PuppeteerSharp, and IronPDF render HTML. QuestPDF is a different model: you describe the PDF structure in C#. Do not select it expecting a browser HTML conversion API.

2. Set up Playwright for .NET

  1. Create or open a .NET application.
  2. Add the package:
dotnet add package Microsoft.Playwright

After the package is installed, install the browser binaries required by your deployment environment. The exact generated script can vary by project and SDK; follow Playwright’s current browser installation instructions as part of your build or container setup.

3. Convert an HTML string to PDF

This complete console example creates a page from an HTML string and writes output.pdf. The PDF operation returns a buffer and can also save directly to a path.

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(new BrowserNewPageOptions
{
    ViewportSize = new ViewportSize { Width = 1280, Height = 900 }
});

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-top: 0; }
    .total { font-size: 20px; font-weight: 700; }
  </style>
</head>
<body>
  <h1>Invoice 1042</h1>
  <p>Generated from an HTML string in C#.</p>
  <p class='total'>Total: $125.00</p>
</body>
</html>
""";

await page.SetContentAsync(html, new PageSetContentOptions
{
    WaitUntil = WaitUntilState.NetworkIdle
});

await page.PdfAsync(new PagePdfOptions
{
    Path = "output.pdf",
    Format = "A4",
    PrintBackground = true,
    PreferCSSPageSize = true,
    Margin = new Margin
    {
        Top = "18mm",
        Right = "18mm",
        Bottom = "18mm",
        Left = "18mm"
    }
});

4. Convert an HTML file

using Microsoft.Playwright;

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();

var fileUrl = new Uri(Path.GetFullPath("invoice.html")).AbsoluteUri;
await page.GotoAsync(fileUrl, new PageGotoOptions
{
    WaitUntil = WaitUntilState.NetworkIdle,
    Timeout = 60_000
});

await page.PdfAsync(new PagePdfOptions
{
    Path = "invoice.pdf",
    Format = "A4",
    PrintBackground = true,
    PreferCSSPageSize = true
});

For local CSS, fonts, and images, use correct relative paths or absolute file:// URLs. A web server is often simpler when the document has many assets or application routes.

5. Convert a webpage URL

using Microsoft.Playwright;

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", new PageGotoOptions
{
    WaitUntil = WaitUntilState.NetworkIdle,
    Timeout = 90_000
});

await page.PdfAsync(new PagePdfOptions
{
    Path = "webpage.pdf",
    Format = "A4",
    PrintBackground = true,
    PreferCSSPageSize = true
});

For authenticated pages, create a browser context with the required cookies or headers before opening the page. Avoid putting secrets in a URL because URLs can be logged.

6. Control CSS media, paper, margins, and pagination

PdfAsync uses print media by default. If your layout is written for screen media, emulate it before generating the PDF:

await page.EmulateMediaAsync(new PageEmulateMediaOptions
{
    Media = Media.Screen
});

await page.PdfAsync(new PagePdfOptions
{
    Path = "screen-styled.pdf",
    PrintBackground = true
});

Page size and margins

Use Format such as A4 or Letter, or provide explicit Width and Height. Playwright accepts unit-bearing values such as mm, in, and px; values without units are interpreted as pixels. CSS @page rules can define the page size, and PreferCSSPageSize tells the renderer to prefer that CSS size.

await page.PdfAsync(new PagePdfOptions
{
    Path = "letter.pdf",
    Format = "Letter",
    Landscape = false,
    PrintBackground = true,
    Margin = new Margin
    {
        Top = "0.6in",
        Right = "0.6in",
        Bottom = "0.6in",
        Left = "0.6in"
    }
});

Useful print CSS

@page { size: A4; margin: 16mm; }

@media print {
  .screen-only { display: none; }
  a { color: inherit; text-decoration: none; }
}

.invoice-row { break-inside: avoid; }
h2 { break-after: avoid; }

For long reports, design and validate page breaks deliberately. Tables, flex layouts, and large images can move together or split in ways that differ from the screen view.

7. Wait for fonts, images, and JavaScript

NetworkIdle is useful for many pages, but it is not a guarantee that every visual asset is ready. For application pages, wait for a selector that proves rendering is complete, or wait for a known delay after your own readiness signal.

await page.GotoAsync(url, new PageGotoOptions
{
    WaitUntil = WaitUntilState.DOMContentLoaded,
    Timeout = 90_000
});

await page.WaitForSelectorAsync("#report-ready", new PageWaitForSelectorOptions
{
    State = WaitForSelectorState.Visible,
    Timeout = 30_000
});

await page.EvaluateAsync("document.fonts.ready");
await page.PdfAsync(new PagePdfOptions
{
    Path = "report.pdf",
    PrintBackground = true
});

For external images, use absolute HTTPS URLs, serve a correct content type, and make sure the rendering environment can reach them. Inline critical CSS and fonts when deterministic output matters.

8. PuppeteerSharp and IronPDF alternatives

PuppeteerSharp follows the same browser pattern: launch headless Chromium, create a page, navigate or set content, and call the PDF method. Consult its current API for browser download and launch options.

IronPDF packages Chromium rendering behind a .NET API. Its documentation shows installing the IronPdf NuGet package, configuring a license key, and rendering HTML. When your HTML uses relative CSS, scripts, images, or links, supply the base URL supported by the renderer. Verify license terms and deployment support for your exact version and operating system.

9. Common errors and fixes

Error or symptom Likely cause Fix
Browser executable not found Playwright package is installed but browser binaries are missing Run the documented Playwright browser installation step during development and deployment.
PDF is blank Navigation failed, content is rendered after the PDF call, or the page requires authentication Check the response and console logs, increase the navigation timeout, wait for a ready selector, and provide required cookies or headers.
CSS or images are missing Relative URLs have no usable base, assets are blocked, or the renderer cannot reach the host Use absolute URLs or a base URL, serve assets with valid MIME types, and verify network access from the runtime.
Background colors disappear Print backgrounds are disabled Set PrintBackground = true and check print CSS.
Screen layout differs from the PDF Print media is the default Call EmulateMediaAsync with Media.Screen, or add deliberate print styles.
Fonts change between machines Font is not installed or the web font has not loaded Bundle or serve the font, wait for document.fonts.ready, and use a fallback stack.
Content is cut off Fixed heights, overflow rules, or unsuitable page breaks Remove restrictive heights for print, use break-inside rules, and test representative long content.
Timeout on a dynamic page Third-party requests never become idle Wait for your own readiness selector instead of relying only on network idle; block unnecessary requests where appropriate.
Works locally but fails in a container Missing browser dependencies, sandbox permissions, fonts, or certificates Install the browser and OS dependencies in the image, include required fonts, and review the runtime’s Chromium security configuration.

10. Reliability, performance, and cost considerations

  • Reuse browsers: keep one browser process and create separate contexts or pages per job. Launching Chromium for every document adds startup overhead.
  • Set bounded timeouts: use navigation and selector timeouts, cancel abandoned work, and record the URL, duration, and failure stage.
  • Limit concurrency: too many simultaneous pages can exhaust CPU, memory, file descriptors, or network bandwidth. Start conservatively and measure your own workload.
  • Make output deterministic: pin browser and library versions, bundle fonts, set a timezone where dates matter, and use fixed paper and margin settings.
  • Control untrusted HTML: isolate rendering, restrict access to internal networks where necessary, and avoid exposing secrets through page content or URLs.
  • Cache when appropriate: cache PDFs for immutable inputs, but include content, template, asset, and renderer versions in the cache key.
  • Validate representative documents: test long tables, large images, right-to-left text, missing assets, slow JavaScript, and pages with cookies or authentication. The cited documentation does not establish a universal performance winner.

11. Or skip the browser setup

ScreenshotNeo provides a hosted capture API that can return a PDF from a URL, so your C# service does not need to install or operate Chromium. See the ScreenshotNeo API documentation for PDF options such as paper size, margins, landscape mode, and page ranges.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can take screenshots, plus bulk capture, signed links, custom headers and cookies, waits, blocking controls, and PDF settings. 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 and start with 1,000 screenshots each month at no charge.

12. Short FAQ

Does Playwright print CSS or screen CSS?

Print CSS by default. Use EmulateMediaAsync with screen media when the document is designed for screen styles.

Can I convert an HTML string without hosting it?

Yes. Create a page and call SetContentAsync, then generate the PDF.

Why is QuestPDF listed separately?

QuestPDF composes PDFs through C# layout APIs. It is useful when you want code-defined documents, but it is not evidence of an HTML conversion API in the cited examples.

Should I use a hosted service?

Use one when you want to avoid browser installation and maintenance, or when your application needs URL capture, consent cleanup, billing headers, and an API or MCP workflow. Review the service’s PDF options and data-handling requirements before production use.