DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to take screenshots and generate PDFs in C# and .NET

How to Take Screenshots and Generate PDFs in C# and .NET

The usual .NET options for screenshots and PDFs are PuppeteerSharp or Playwright for .NET — both download a bundled Chromium binary on first run (~200MB), require a writable filesystem, and don't work in restricted environments like Azure Functions on the Consumption plan or Lambda.

Here's the simpler path: HttpClient, System.Text.Json, binary response. No browser download.

Screenshot from a URL

using System.Net.Http;
using System.Text;
using System.Text.Json;

public class PageBoltClient
{
    private static readonly HttpClient _http = new();
    private readonly string _apiKey = Environment.GetEnvironmentVariable("PAGEBOLT_API_KEY")!;
    private const string BaseUrl = "https://pagebolt.dev/api/v1";

    public async Task<byte[]> ScreenshotAsync(string url)
    {
        var body = JsonSerializer.Serialize(new
        {
            url,
            fullPage = true,
            blockBanners = true
        });

        using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/screenshot");
        request.Headers.Add("x-api-key", _apiKey);
        request.Content = new StringContent(body, Encoding.UTF8, "application/json");

        using var response = await _http.SendAsync(request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsByteArrayAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

PDF from a URL

public async Task<byte[]> PdfFromUrlAsync(string url)
{
    var body = JsonSerializer.Serialize(new { url, blockBanners = true });

    using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/pdf");
    request.Headers.Add("x-api-key", _apiKey);
    request.Content = new StringContent(body, Encoding.UTF8, "application/json");

    using var response = await _http.SendAsync(request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsByteArrayAsync();
}
Enter fullscreen mode Exit fullscreen mode

PDF from HTML (replacing PuppeteerSharp / wkhtmltopdf)

public async Task<byte[]> PdfFromHtmlAsync(string html)
{
    var body = JsonSerializer.Serialize(new { html });

    using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/pdf");
    request.Headers.Add("x-api-key", _apiKey);
    request.Content = new StringContent(body, Encoding.UTF8, "application/json");

    using var response = await _http.SendAsync(request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsByteArrayAsync();
}
Enter fullscreen mode Exit fullscreen mode

System.Text.Json.JsonSerializer.Serialize handles all escaping — no manual string-building required.

Register as a singleton in DI (recommended)

// Program.cs
builder.Services.AddHttpClient<PageBoltClient>();
builder.Services.AddSingleton<PageBoltClient>();
Enter fullscreen mode Exit fullscreen mode
// PageBoltClient.cs — constructor injection version
public class PageBoltClient
{
    private readonly HttpClient _http;
    private readonly string _apiKey;
    private const string BaseUrl = "https://pagebolt.dev/api/v1";

    public PageBoltClient(HttpClient http, IConfiguration config)
    {
        _http = http;
        _apiKey = config["PageBolt:ApiKey"] ?? throw new InvalidOperationException("PageBolt:ApiKey not configured");
    }

    public async Task<byte[]> ScreenshotAsync(string url) { /* ... */ }
    public async Task<byte[]> PdfFromHtmlAsync(string html) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core controller

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/invoices")]
public class InvoiceController : ControllerBase
{
    private readonly PageBoltClient _pagebolt;
    private readonly IInvoiceService _invoices;
    private readonly IRazorViewRenderer _renderer;

    public InvoiceController(
        PageBoltClient pagebolt,
        IInvoiceService invoices,
        IRazorViewRenderer renderer)
    {
        _pagebolt = pagebolt;
        _invoices = invoices;
        _renderer = renderer;
    }

    [HttpGet("{id}/pdf")]
    public async Task<IActionResult> DownloadPdf(int id)
    {
        var invoice = await _invoices.GetByIdAsync(id);
        var html = await _renderer.RenderAsync("Invoice", invoice);

        var pdf = await _pagebolt.PdfFromHtmlAsync(html);

        return File(pdf, "application/pdf", $"invoice-{id}.pdf");
    }

    [HttpGet("{id}/screenshot")]
    public async Task<IActionResult> GetScreenshot(int id)
    {
        var invoice = await _invoices.GetByIdAsync(id);
        var image = await _pagebolt.ScreenshotAsync($"https://yourapp.com/invoices/{id}");

        return File(image, "image/png", $"invoice-{id}.png");
    }
}
Enter fullscreen mode Exit fullscreen mode

Azure Functions (Consumption plan)

This is where the no-browser approach matters most. PuppeteerSharp and Playwright fail on Azure Functions Consumption plan because there's no writable directory for the Chromium binary download. An HTTP call works fine:

[Function("GenerateInvoicePdf")]
public async Task<HttpResponseData> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "invoices/{id}/pdf")] HttpRequestData req,
    int id)
{
    var html = await _renderer.RenderAsync("Invoice", id);
    var pdf = await _pagebolt.PdfFromHtmlAsync(html);

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "application/pdf");
    response.Headers.Add("Content-Disposition", $"attachment; filename=\"invoice-{id}.pdf\"");
    await response.WriteBytesAsync(pdf);
    return response;
}
Enter fullscreen mode Exit fullscreen mode

No browser binary. No filesystem writes. Works on Consumption plan.


Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

Top comments (0)