ScreenshotNeo

BlogHow-to

Convert HTML to Image in C# .NET Core

Convert HTML or web pages to PNG, JPEG, or WebP in C# with CoreHtmlToImage, Playwright, or PuppeteerSharp, including deployment fixes.

By the ScreenshotNeo team1 October 202611 min read

Direct answer: In a C#/.NET application, render the HTML with a Chromium-based browser and save the resulting screenshot. For a compact API, CoreHtmlToImage accepts an HTML string or URL and returns image bytes. For browser navigation, interaction, custom viewports, full-page captures, or element screenshots, use Playwright for .NET or PuppeteerSharp.

Choose the right approach

Requirement Recommended approach Reason
Convert a small HTML string or URL with little setup CoreHtmlToImage It exposes asynchronous HTML-string and URL methods and wraps headless Chromium.
Control navigation, waiting, clicks, headers, or cookies Playwright .NET The browser API exposes page actions and screenshot options.
Use a Puppeteer-style API PuppeteerSharp It is a .NET port of Puppeteer’s browser automation API.
Render from a hosted URL without managing Chromium in your app ScreenshotNeo One HTTP request returns a PNG, JPEG, WebP, or PDF and handles browser setup for you.

1. Check your target framework

The current CoreHtmlToImage 2.0.0 package listing targets .NET 10.0. Do not assume that a package called “.NET Core” supports every .NET Core or .NET release. Check the package’s target framework against your project before installing it. The project repository explains that version 2 replaced the older wkhtmltoimage engine with headless Chromium; version 1.x targeted .NET Standard 2.0. See the repository documentation for version details.

dotnet --info
dotnet add package CoreHtmlToImage

If your application targets .NET 5–9 or an older .NET Core version, select a package release that explicitly supports that target or use Playwright .NET/PuppeteerSharp. Verify the selected version in NuGet before deployment.

2. Convert an HTML string with CoreHtmlToImage

CoreHtmlToImage’s documented minimal flow is asynchronous: create an HtmlConverter, call FromHtmlStringAsync, and write the returned bytes. The package documentation describes JPEG as the default and PNG and WebP as available options; inspect the current package API for the exact format option names in the version you install.

using CoreHtmlToImage;

await using var converter = new HtmlConverter();

var html = """
<!doctype html>
<html>
  <head>
    <meta charset=\"utf-8\">
    <style>
      body { margin: 0; font-family: Arial, sans-serif; }
      .card { width: 640px; padding: 32px; background: #f5f7fb; }
      h1 { margin: 0 0 12px; color: #172033; }
      p { margin: 0; color: #4b5563; }
    </style>
  </head>
  <body>
    <main class=\"card\">
      <h1>Invoice ready</h1>
      <p>Rendered from an HTML string.</p>
    </main>
  </body>
</html>
""";

byte[] bytes = await converter.FromHtmlStringAsync(html);
await File.WriteAllBytesAsync("invoice.jpg", bytes);

For a page available at an HTTP or HTTPS URL, use the URL method:

using CoreHtmlToImage;

await using var converter = new HtmlConverter();
var bytes = await converter.FromUrlAsync("https://example.com");
await File.WriteAllBytesAsync("example.jpg", bytes);

Use the package’s documented options for output format, dimensions, transparency, and other rendering settings. Confirm the output dimensions and alpha behavior with the actual HTML and installed package version; browser layout depends on CSS, loaded fonts, images, and the viewport.

3. Use Playwright when you need browser control

Playwright is a better fit when conversion includes navigation, authentication, interaction, waiting for a page state, a selected element, or a full-page screenshot. Its .NET documentation covers saving screenshots to a file or receiving bytes, full-page capture, element screenshots, and image format and quality settings.

Install and install the browser

dotnet add package Microsoft.Playwright
dotnet build
# Run the generated browser installer from your build output:
dotnet tool install --global Microsoft.Playwright.CLI
playwright install chromium

Render an HTML string

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 = 1200, Height = 800 },
    DeviceScaleFactor = 1
});

var html = """
<html>
  <head>
    <style>
      body { margin: 0; font-family: Arial, sans-serif; }
      .report { width: 900px; padding: 40px; background: white; }
    </style>
  </head>
  <body>
    <section class=\"report\"><h1>Monthly report</h1><p>Generated by Playwright.</p></section>
  </body>
</html>
""";

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

await page.ScreenshotAsync(new PageScreenshotOptions
{
    Path = "report.png",
    FullPage = true,
    Type = ScreenshotType.Png
});

Capture a URL, element, or full page

using Microsoft.Playwright;

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync(new BrowserNewPageOptions
{
    ViewportSize = new ViewportSize { Width = 1440, Height = 900 },
    DeviceScaleFactor = 2
});

await page.GotoAsync("https://example.com", new PageGotoOptions
{
    WaitUntil = WaitUntilState.NetworkIdle,
    Timeout = 60_000
});

// Optional: wait for an application-specific ready marker.
await page.Locator("main").WaitForAsync(new LocatorWaitForOptions
{
    State = WaitForSelectorState.Visible,
    Timeout = 30_000
});

// Full document.
await page.ScreenshotAsync(new PageScreenshotOptions
{
    Path = "page.webp",
    FullPage = true,
    Type = ScreenshotType.Webp,
    Quality = 85
});

// One element only.
await page.Locator("main").ScreenshotAsync(new LocatorScreenshotOptions
{
    Path = "main.png",
    Type = ScreenshotType.Png
});

Use FullPage = true for the complete document. Use a locator screenshot when the required output is a card, chart, invoice, or other component. A larger device scale factor increases pixel dimensions and memory use.

Control rendering state

await page.EmulateMediaAsync(new PageEmulateMediaOptions
{
    ColorScheme = ColorScheme.Dark
});

await page.AddStyleTagAsync(new PageAddStyleTagOptions
{
    Content = """
      * { animation: none !important; transition: none !important; }
      .cookie-banner, .chat-widget { display: none !important; }
    """
});

await page.EvaluateAsync("window.scrollTo(0, document.body.scrollHeight)");
await page.WaitForTimeoutAsync(500);
await page.ScreenshotAsync(new PageScreenshotOptions
{
    Path = "stable.png",
    FullPage = true
});

Prefer a selector that means “the page is ready” over an arbitrary delay. Use a short delay only for effects that have no observable DOM or network state. Disable animations when repeatable pixels matter.

4. Use PuppeteerSharp as an alternative browser API

PuppeteerSharp provides a Puppeteer-style .NET API. The basic sequence is to launch a headless browser, open a page, set a viewport, navigate, and take a screenshot.

using PuppeteerSharp;

await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = true
});

await using var page = await browser.NewPageAsync();
await page.SetViewportAsync(new ViewPortOptions
{
    Width = 1280,
    Height = 800,
    DeviceScaleFactor = 1
});

await page.GoToAsync("https://example.com", WaitUntilNavigation.Networkidle0);
await page.ScreenshotAsync("example.png", new ScreenshotOptions
{
    FullPage = true,
    Type = ScreenshotType.Png
});

Browser download and launch behavior varies by package version. Pin the package version, follow its browser installation instructions, and make the browser executable available in the deployment environment.

5. HTML, assets, fonts, and page state

Inline HTML versus a URL

  • Inline HTML: use SetContentAsync in Playwright or FromHtmlStringAsync in CoreHtmlToImage. Absolute URLs are safer for external images, stylesheets, and fonts.
  • Hosted URL: use navigation when the page’s JavaScript, routing, authentication, or server-rendered content must run.
  • Local files: ensure the browser process can read the file and that relative asset paths resolve. A small local HTTP server is often more predictable than a file:// URL.

Images and fonts

Wait for the page’s own ready condition and for important images to finish loading. If you control the page, expose a marker such as window.renderReady = true or a visible data-render-complete element. Use web-safe fonts or package the required font files; font substitution changes line wrapping and therefore image dimensions.

await page.WaitForFunctionAsync("() => window.renderReady === true", null,
    new PageWaitForFunctionOptions { Timeout = 30_000 });

await page.EvaluateAsync("""
  async () => {
    const images = Array.from(document.images);
    await Promise.all(images.map(img => img.complete
      ? Promise.resolve()
      : new Promise(resolve => { img.addEventListener('load', resolve); img.addEventListener('error', resolve); })));
    if (document.fonts) await document.fonts.ready;
  }
""");

Viewport, scale, and output format

Setting Effect Trade-off
Viewport width and height Controls responsive breakpoints and visible area Different widths can produce different layouts.
Device scale factor Produces more pixels per CSS pixel Increases file size and memory use.
Full-page capture Includes content below the viewport Very tall pages can create large images.
JPEG quality Reduces file size for photographic content Lossy compression and no transparency.
PNG Lossless output and transparency Often larger for photographic pages.
WebP Modern compressed image format Confirm every downstream consumer supports it.

6. Deployment checklist

  1. Pin the .NET package version and verify its target framework.
  2. Install or package the matching Chromium browser during image build or startup.
  3. Allow the runtime to write to the browser cache directory.
  4. Give the process enough memory for the selected viewport, scale factor, and page length.
  5. Allow outbound access to every required stylesheet, image, font, and API endpoint, or serve those assets locally.
  6. Set explicit navigation and rendering timeouts.
  7. Close pages, contexts, and browsers with await using or equivalent cleanup.
  8. Record the browser, library, viewport, scale factor, URL, and output format with each generated asset so changes can be reproduced.

CoreHtmlToImage’s package listing says its compatible Chromium binary is downloaded automatically on first use and cached afterward, with a download of about 200 MB. Account for that first-start network access, cache persistence, container layer size, and runtime permissions. Playwright and PuppeteerSharp also require a compatible browser installation according to their package workflows.

7. Troubleshooting

Symptom Likely cause Fix
Package will not install Target framework is unsupported by the selected version. Inspect the NuGet target frameworks, choose a compatible version, or use Playwright/PuppeteerSharp.
First request fails while launching Chromium Browser download is blocked or the cache directory is not writable. Install the browser during deployment, allow download access, set a writable cache path, and verify file permissions.
Blank or partially rendered image Capture occurred before JavaScript, images, or fonts finished. Wait for a ready selector or function, then wait for images and fonts; increase the navigation timeout when needed.
Wrong responsive layout Viewport width differs from the intended device. Set an explicit viewport before navigation and test each required breakpoint.
Text wraps differently between machines Fonts are missing or a different font version is installed. Package the fonts, wait for document.fonts.ready, and keep browser and font versions consistent.
Animations appear mid-transition The screenshot was taken during an animation. Inject CSS that disables transitions and animations or wait for an application-ready state.
External images are missing Relative URLs, blocked requests, authentication, or CORS-related page logic. Use absolute URLs, supply the required cookies or headers, and inspect failed requests in the browser.
Very large images cause crashes Full-page capture and high device scale factor consume too much memory. Lower the scale factor, capture sections, reduce unnecessary page height, or allocate more memory.
Navigation times out The page never reaches the selected load state or a dependency is slow. Use an application-specific ready selector, set a bounded timeout, and handle the timeout as a failed capture.

8. Performance, reliability, and cost

Performance

  • Reuse a browser process when generating many images, but create isolated contexts or pages for separate jobs.
  • Reuse cached browser binaries; downloading Chromium for every request adds startup time and requires network access.
  • Capture only the required element when a full document is unnecessary.
  • Keep viewport and device scale factor as low as the output requirement permits.
  • Wait for a specific ready condition instead of using a long fixed delay.
  • Limit concurrent pages according to available CPU and memory; a browser page is not a free thread.

Reliability

Rendering is dependent on the page’s network resources, JavaScript, browser version, fonts, and current content. Use bounded retries for transient navigation failures, record the URL and rendering settings, and treat a timeout or missing ready marker as a failed job. Do not describe output as pixel-identical across machines without testing the exact deployment.

Cost

Self-hosted libraries have no per-image ScreenshotNeo charge, but your application pays for compute, memory, browser storage, bandwidth, and engineering time. A browser download of about 200 MB on first use can affect container size and cold-start behavior. If you need a hosted endpoint, compare the complete operational cost with an API plan and the time required to maintain browser workers.

9. Security considerations

Rendering arbitrary HTML or URLs is an application security boundary. Treat user-supplied HTML as untrusted, isolate browser processes where appropriate, restrict outbound network access, avoid exposing cloud metadata endpoints, limit navigation time and response sizes, and do not pass sensitive cookies or headers to untrusted destinations. These controls belong in your application’s threat model; the libraries do not make arbitrary browsing safe by themselves.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API for a URL. It accepts a cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. 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 X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. A basic request looks like this:

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);

ScreenshotNeo supports full-page and element capture, dark mode, device presets or custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, and a usage API. The parameter names used by other screenshot APIs also work, which can simplify migration.

There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Can I convert HTML without installing a browser?

A Chromium-based renderer must run somewhere. CoreHtmlToImage, Playwright, and PuppeteerSharp manage or use a local browser; ScreenshotNeo runs the browser through its hosted API.

Which format should I choose?

Use PNG for lossless UI and transparency, JPEG for smaller photographic output, and WebP when your consumers support it and you want modern compression. Confirm the exact encoder options in the library version you install.

Should I use CoreHtmlToImage or Playwright?

Use CoreHtmlToImage for a compact HTML-string or URL conversion. Choose Playwright when you need explicit waits, interactions, headers, cookies, viewport control, full-page capture, or element screenshots.

Why does the current CoreHtmlToImage example not work in an older .NET Core app?

The current 2.0.0 package listing targets .NET 10.0. Select a release that supports your target framework or use a browser API whose package supports it.

Can I render a page that requires login?

With Playwright or PuppeteerSharp, establish the authenticated browser context and provide the required cookies or headers before capture. For a hosted API, use the service’s documented header, cookie, or authorization options and avoid sending credentials to an untrusted URL.