# Convert HTML to image in C#: PuppeteerSharp, Playwright or an API

> HTML to PNG in C# and .NET with PuppeteerSharp or Microsoft.Playwright, what Chromium needs in Docker, and the same image via HttpClient.

Published 2026-11-04 · https://shareglint.com/en/guides/html-to-image-csharp

To convert HTML to an image in C#, drive headless Chromium with **PuppeteerSharp** or **Microsoft.Playwright**: load the markup with `SetContentAsync(html)` and call `ScreenshotAsync` on the page or on one element. Both give you Chrome's real rendering — grid, gradients, web fonts — in about fifteen lines. The work is in deployment: Chromium is a few hundred megabytes with its own system dependencies and sandbox rules, which bites in containers. If you would rather not ship a browser, an HTML to image API takes the same job with one `HttpClient` call.

Every sample below was compiled and run on .NET 10: PuppeteerSharp 25.15.0 and Microsoft.Playwright 1.64.1 inside Microsoft's Playwright Noble image, the API call against a live server.

## The HTML

```csharp
// Card.cs
static class Card
{
    public const string Html = """
        <!doctype html>
        <html>
        <head>
          <meta charset="utf-8">
          <style>
            body { margin: 0; }
            .card {
              width: 1200px; height: 630px; box-sizing: border-box;
              display: grid; align-content: center; gap: 16px; padding: 80px;
              background: linear-gradient(135deg, #0f172a, #1e3a8a);
              color: #f8fafc; font: 700 64px/1.15 system-ui, sans-serif;
            }
            .card small { font-size: 28px; font-weight: 400; color: #93c5fd; }
          </style>
        </head>
        <body>
          <div class="card">
            <small>Release notes · v2.4</small>
            HTML to image in C#, rendered by Chromium
          </div>
        </body>
        </html>
        """;
}
```

## PuppeteerSharp

```bash
dotnet add package PuppeteerSharp
```

```csharp
using PuppeteerSharp;

// Downloads a matching Chrome on first run, into the app's directory.
await new BrowserFetcher().DownloadAsync();

await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = true,
    // Docker's default seccomp profile blocks Chrome's sandbox. Drop it only
    // for HTML you wrote yourself; for anyone else's HTML, fix the sandbox.
    Args = new[] { "--no-sandbox" },
});
await using var page = await browser.NewPageAsync();

await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 630, DeviceScaleFactor = 2 });
await page.SetContentAsync(Card.Html, new SetContentOptions
{
    WaitUntil = new[] { WaitUntilNavigation.Load },
    Timeout = 15_000,
});
await page.EvaluateExpressionAsync("document.fonts.ready");

var card = await page.QuerySelectorAsync(".card");
await card.ScreenshotAsync("card.png");                 // 2400×1260, the element only

byte[] jpeg = await page.ScreenshotDataAsync(new ScreenshotOptions
{
    Type = ScreenshotType.Jpeg,
    Quality = 85,
    FullPage = true,
});
```

Two version notes. In PuppeteerSharp 25 the overload `SetContentAsync(string, NavigationOptions)` is obsolete — the compiler warns that "the networkidle0 and networkidle2 wait conditions never worked reliably with SetContent" — so use `SetContentOptions` as above. And `BrowserFetcher` saves the browser next to your binaries: on our first run it wrote about 670 MB (Chrome plus chrome-headless-shell) into the output directory and took about a minute. Download it at image build time, not on the first request. The [project README](https://github.com/hardkoded/puppeteer-sharp) shows the same fetch-launch-screenshot sequence.

## Microsoft.Playwright

```bash
dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net10.0/playwright.ps1 install chromium   # match your target framework
```

```csharp
using Microsoft.Playwright;

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

var page = await browser.NewPageAsync(new()
{
    ViewportSize = new() { Width = 1200, Height = 630 },
    DeviceScaleFactor = 2,
});
await page.SetContentAsync(Card.Html, new() { WaitUntil = WaitUntilState.Load });

await page.Locator(".card").ScreenshotAsync(new() { Path = "card.png" });

byte[] png = await page.ScreenshotAsync(new()
{
    Clip = new() { X = 0, Y = 0, Width = 600, Height = 315 },
    OmitBackground = true,   // transparent where the page paints nothing
});
```

Playwright needs no `--no-sandbox`, because its `chromiumSandbox` launch option [defaults to false](https://playwright.dev/docs/api/class-browsertype) — which also means Chromium runs unsandboxed unless you turn it on. The browsers live outside your build output (`playwright.ps1 install`, see the [.NET install docs](https://playwright.dev/dotnet/docs/intro)).

## Chromium in containers

PuppeteerSharp failed twice for us before it worked, in ways you will meet in production:

- **Running as root.** PuppeteerSharp in a container as root: "Running as root without --no-sandbox is not supported."
- **No usable sandbox.** As a normal user, Docker's default seccomp profile blocked Chrome's sandbox ("No usable sandbox!"). Microsoft's [Playwright Docker guidance](https://playwright.dev/dotnet/docs/docker): for trusted pages root is acceptable; for pages you do not control, run as a separate user with a seccomp profile that enables the sandbox.
- **Libraries and fonts.** Chromium needs dozens of shared libraries and real fonts, including emoji and CJK if your text has them. Microsoft publishes `mcr.microsoft.com/playwright/dotnet:v1.64.1-noble` with everything installed; pin the tag to your package version, or Playwright cannot find its browser. Alpine is not supported (musl, not glibc).
- **Flags.** The same page recommends `--ipc=host` (Chromium can run out of shared memory) and `--init` (zombie processes).

Then the usual browser operations: keep one browser alive and open a page per render, cap concurrent pages, time out every render, restart the browser now and then. If users supply the HTML, block outbound requests (`page.RouteAsync` in Playwright, request interception in PuppeteerSharp) so the markup cannot reach internal addresses.

## The same image from an API with HttpClient

An [HTML to image API](https://shareglint.com/en/html-to-image) removes Chromium from your deployment. With Shareglint you save the HTML and CSS once as a template with named fields, then each image is one request with the values:

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://shareglint.com/v1/"),   // trailing slash matters
    Timeout = TimeSpan.FromSeconds(60),
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("IMAGE_API_KEY"));

// 1. Save the template once (a deploy step or a migration).
var created = await http.PostAsJsonAsync("templates", new
{
    name = "Article card",
    slug = "article-card",
    width = 1200,
    height = 630,
    fields = new[] { "kicker", "title", "author" },
    html = """<div class="card"><p class="kicker">{{ kicker }}</p><div class="fitbox"><h1 class="fit" data-autofit>{{ title }}</h1></div><p class="by">{{ author }}</p></div>""",
    css = ".card{height:100%;padding:72px;display:grid;grid-template-rows:auto 1fr auto;gap:24px;background:#0f172a;color:#f8fafc;font-family:Roboto,sans-serif} .kicker{color:#fbbf24;font-size:26px;text-transform:uppercase} h1{font-size:76px;line-height:1.08} .by{font-size:28px;color:#94a3b8}",
});
created.EnsureSuccessStatusCode();

// 2. One request per image.
var response = await http.PostAsJsonAsync("images?wait=true", new
{
    template = "article-card",
    format = "png",
    modifications = new
    {
        kicker = "Engineering",
        title = "How we cut our build time in half",
        author = "Ada Lovelace",
    },
});
response.EnsureSuccessStatusCode();

var render = await response.Content.ReadFromJsonAsync<Render>();
Console.WriteLine(render!.Url);

record Render(string Id, string Status, string? Url);
```

`ReadFromJsonAsync` uses System.Text.Json with web defaults, so the camel-case JSON fields bind to the `Render` record without attributes. In ASP.NET Core, register the client with `IHttpClientFactory` instead of creating it per call.

How the API behaves: step 1 runs once — a second run answers `422` because the slug is taken (update with `PATCH /v1/templates/{id}`), and built-in templates such as `blog-og` need no step 1. The same fields again return the existing file and use no quota. A `503` with an `id` means the synchronous wait ran out; the render continues and `GET /v1/images/{id}` collects it. The sandbox has a price: no scripts in templates, no external URLs in the HTML or CSS (images arrive through image fields) and fonts only from the installed list — Playwright or PuppeteerSharp have none of those limits. Free accounts get 100 images a month with a small watermark; Starter is $9 a month for 2,000 ([pricing](https://shareglint.com/en/pricing)).

## Which one to choose

- **Microsoft.Playwright** if you already ship browsers (end-to-end tests, a worker service) — official images make containers simplest.
- **PuppeteerSharp** if you want the browser download in code and Puppeteer's API.
- **An API** for Azure, serverless or slim containers where a few hundred megabytes of Chromium and its sandbox are not worth it.

The same task in [Python](https://shareglint.com/en/guides/html-to-image-python), [JavaScript](https://shareglint.com/en/guides/html-to-image-javascript) and [PHP](https://shareglint.com/en/guides/html-to-image-php), and [which npm package](https://shareglint.com/en/guides/html-to-image-npm) renders what.

## FAQ

### How do I convert HTML to an image in C#?

Use PuppeteerSharp or Microsoft.Playwright. Both drive headless Chromium: SetContentAsync(html) loads the markup and ScreenshotAsync on the page or an element writes a PNG or JPEG. Set DeviceScaleFactor = 2 for a retina-sharp file.

### PuppeteerSharp or Playwright for .NET?

Both render with Chromium and give the same pixels. Playwright is maintained by Microsoft, has locators that wait automatically and official Docker images; PuppeteerSharp is a community port of Puppeteer whose BrowserFetcher downloads Chrome from code, which some deployments find simpler.

### Why does Chrome fail to start in my .NET Docker container?

Usually one of three reasons: missing system libraries, running as root (Chrome refuses to start sandboxed as root), or Docker's default seccomp profile blocking the sandbox. Use an image with Chromium's dependencies, a non-root user, and either a seccomp profile that allows the sandbox or --no-sandbox for HTML you wrote yourself.

### Can I render HTML to an image in .NET without a browser?

Not with full CSS support: grid, flexbox and web fonts need a browser engine. Without bundling Chromium, post the template fields to an HTML to image API with HttpClient and store the returned URL.
