Part 4.2: Generate PDF Using Puppeteer Sharp in ASP.NET Web Forms

Jan 3, 2026
Updated Jun 26, 2026
adriancs

This is part of the series of introducing Vanilla ASP.NET Web Forms Architecture. Read more about this at the main menu: Main Article: [Introducing Vanilla ASP.NET Web Forms Architecture]

Part 4: Generate PDF for The Generated Print Content

Introduction

In Part 4.1, we covered using Chrome.exe directly via command line to generate PDFs. While that approach works well, it has limitations:

  • Requires Chrome installed on the server
  • Limited to dedicated servers / VPS (not shared hosting)
  • Less programmatic control over PDF options

Puppeteer Sharp solves these problems. It’s a .NET port of Google’s Puppeteer library that provides a high-level API to control Chromium browsers programmatically.

What is Puppeteer Sharp?

Puppeteer Sharp is a .NET library that:

  • Downloads Chromium automatically — No need to install Chrome manually
  • Provides full programmatic control — Fine-grained PDF options via C# API
  • Works on more hosting environments — Including shared hosting (with some configurations)
  • Supports async/await — Modern C# patterns for better performance
  • Cross-platform — Works on Windows, Linux, and macOS

Official Resources:

Comparison: Chrome.exe vs Puppeteer Sharp

AspectChrome.exe (Part 4.1)Puppeteer Sharp (Part 4.2)
Chrome InstallationRequired on serverAuto-downloads Chromium
HostingDedicated/VPS onlyMore flexible
ControlCommand-line args onlyFull C# API
PDF OptionsLimitedExtensive (margins, headers, footers, scale, etc.)
Async SupportNo (spawns process)Yes (native async/await)
Download SizeUses existing Chrome~150MB Chromium download
Best ForSimple setupsProduction applications

Step 1: Install the NuGet Package

Puppeteer Sharp is available on NuGet. It supports:

  • .NET Standard 2.0 — For .NET Framework 4.6.1+ and .NET Core 2.0+
  • .NET 8.0 — For latest .NET applications

Install via Package Manager Console:

Install-Package PuppeteerSharp

Or via .NET CLI:

dotnet add package PuppeteerSharp

Or add to your .csproj:

<PackageReference Include="PuppeteerSharp" Version="20.2.4" />

Step 2: Download Chromium (First Run)

Before generating PDFs, Puppeteer Sharp needs to download the Chromium browser. This only happens once and is stored locally.

using PuppeteerSharp;

// ============================================
// Download Chromium browser (first run only)
// This downloads ~150MB and stores it locally
// ============================================
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();

Where is Chromium stored?

By default, Chromium is downloaded to:

  • Windows: %USERPROFILE%\.puppeteer\local-chromium
  • Linux/macOS: ~/.puppeteer/local-chromium

Step 3: Basic PDF Generation

Here’s the simplest example of generating a PDF from a URL:

using PuppeteerSharp;

public class PdfGenerator
{
    /// <summary>
    /// Generate PDF from a URL using Puppeteer Sharp
    /// </summary>
    public static async Task<bool> GeneratePdfFromUrlAsync(string url, string outputPdfPath)
    {
        try
        {
            // ============================================
            // Step 1: Ensure Chromium is downloaded
            // ============================================
            var browserFetcher = new BrowserFetcher();
            await browserFetcher.DownloadAsync();

            // ============================================
            // Step 2: Launch the browser in headless mode
            // ============================================
            await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true  // Run without visible window
            });

            // ============================================
            // Step 3: Create a new page (tab)
            // ============================================
            await using var page = await browser.NewPageAsync();

            // ============================================
            // Step 4: Navigate to the URL
            // ============================================
            await page.GoToAsync(url);

            // ============================================
            // Step 5: Generate PDF and save to file
            // ============================================
            await page.PdfAsync(outputPdfPath);

            return true;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"PDF generation failed: {ex.Message}");
            return false;
        }
    }
}

Step 4: PDF Generation with Options

Puppeteer Sharp provides extensive PDF options through the PdfOptions class:

using PuppeteerSharp;
using PuppeteerSharp.Media;

public class PdfGenerator
{
    /// <summary>
    /// Generate PDF with custom options
    /// </summary>
    public static async Task<bool> GeneratePdfWithOptionsAsync(string url, string outputPdfPath)
    {
        try
        {
            // Ensure Chromium is downloaded
            var browserFetcher = new BrowserFetcher();
            await browserFetcher.DownloadAsync();

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

            // Create page
            await using var page = await browser.NewPageAsync();

            // Navigate to URL
            await page.GoToAsync(url);

            // ============================================
            // Generate PDF with custom options
            // ============================================
            await page.PdfAsync(outputPdfPath, new PdfOptions
            {
                // Paper format (A4, Letter, Legal, etc.)
                Format = PaperFormat.A4,

                // Paper orientation
                Landscape = false,

                // Print background colors and images
                PrintBackground = true,

                // Page margins
                MarginOptions = new MarginOptions
                {
                    Top = "10mm",
                    Right = "10mm",
                    Bottom = "10mm",
                    Left = "10mm"
                },

                // Scale of the webpage rendering (0.1 to 2.0)
                Scale = 1.0m,

                // Display header and footer
                DisplayHeaderFooter = false,

                // Give CSS @page size priority over Format
                PreferCSSPageSize = true
            });

            return true;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"PDF generation failed: {ex.Message}");
            return false;
        }
    }
}

Step 5: Generate PDF from HTML String

Instead of navigating to a URL, you can generate PDF directly from an HTML string:

using PuppeteerSharp;
using PuppeteerSharp.Media;

public class PdfGenerator
{
    /// <summary>
    /// Generate PDF from HTML content string
    /// </summary>
    public static async Task<bool> GeneratePdfFromHtmlAsync(string htmlContent, string outputPdfPath)
    {
        try
        {
            // Ensure Chromium is downloaded
            var browserFetcher = new BrowserFetcher();
            await browserFetcher.DownloadAsync();

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

            // Create page
            await using var page = await browser.NewPageAsync();

            // ============================================
            // Set the HTML content directly
            // No need to navigate to a URL
            // ============================================
            await page.SetContentAsync(htmlContent);

            // Generate PDF
            await page.PdfAsync(outputPdfPath, new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                PreferCSSPageSize = true,
                MarginOptions = new MarginOptions
                {
                    Top = "0mm",
                    Right = "0mm",
                    Bottom = "0mm",
                    Left = "0mm"
                }
            });

            return true;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"PDF generation failed: {ex.Message}");
            return false;
        }
    }
}

Step 6: Get PDF as Byte Array

For web applications, you often need the PDF as a byte array to send as a download:

using PuppeteerSharp;
using PuppeteerSharp.Media;

public class PdfGenerator
{
    /// <summary>
    /// Generate PDF and return as byte array
    /// Useful for web downloads and email attachments
    /// </summary>
    public static async Task<byte[]> GeneratePdfBytesAsync(string htmlContent)
    {
        // Ensure Chromium is downloaded
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

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

        // Create page
        await using var page = await browser.NewPageAsync();

        // Set HTML content
        await page.SetContentAsync(htmlContent);

        // ============================================
        // Generate PDF as byte array using PdfDataAsync
        // ============================================
        byte[] pdfBytes = await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            PreferCSSPageSize = true
        });

        return pdfBytes;
    }
}

Step 7: PDF with Headers and Footers

Puppeteer Sharp supports custom headers and footers with dynamic values:

using PuppeteerSharp;
using PuppeteerSharp.Media;

public class PdfGenerator
{
    /// <summary>
    /// Generate PDF with custom header and footer
    /// </summary>
    public static async Task<byte[]> GeneratePdfWithHeaderFooterAsync(string htmlContent)
    {
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

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

        await using var page = await browser.NewPageAsync();
        await page.SetContentAsync(htmlContent);

        // ============================================
        // Generate PDF with header and footer
        // Available CSS classes for dynamic values:
        // - date: formatted print date
        // - title: document title
        // - url: document location
        // - pageNumber: current page number
        // - totalPages: total pages in document
        // ============================================
        byte[] pdfBytes = await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,

            // Enable header and footer
            DisplayHeaderFooter = true,

            // Header template (HTML)
            HeaderTemplate = @"
                <div style='font-size: 10px; width: 100%; text-align: center; color: #666;'>
                    <span class='title'></span>
                </div>
            ",

            // Footer template (HTML)
            FooterTemplate = @"
                <div style='font-size: 10px; width: 100%; text-align: center; color: #666;'>
                    Page <span class='pageNumber'></span> of <span class='totalPages'></span>
                </div>
            ",

            // Margins to make room for header/footer
            MarginOptions = new MarginOptions
            {
                Top = "20mm",     // Space for header
                Right = "10mm",
                Bottom = "20mm",  // Space for footer
                Left = "10mm"
            }
        });

        return pdfBytes;
    }
}

Complete ASP.NET Web Forms Example

Here’s a complete example integrating Puppeteer Sharp with ASP.NET Web Forms:

apiInvoicePdf.aspx (Frontend – just the page directive):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiInvoicePdf.aspx.cs" 
    Inherits="myweb.apiInvoicePdf" Async="true" %>

Note: The Async="true" attribute is required for async operations in Web Forms.

apiInvoicePdf.aspx.cs (Backend):

using System;
using System.IO;
using System.Threading.Tasks;
using System.Web;
using System.Web.UI;
using PuppeteerSharp;
using PuppeteerSharp.Media;

public partial class apiInvoicePdf : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Call async method from sync Page_Load
        RegisterAsyncTask(new PageAsyncTask(GeneratePdfAsync));
    }

    private async Task GeneratePdfAsync()
    {
        // ============================================
        // Step 1: Authentication check
        // ============================================
        if (Session["login_user"] == null)
        {
            Response.StatusCode = 401;
            return;
        }

        // ============================================
        // Step 2: Get invoice ID from query string
        // ============================================
        int invoice_id = 0;
        if (!int.TryParse(Request["id"], out invoice_id) || invoice_id <= 0)
        {
            Response.StatusCode = 400;
            return;
        }

        try
        {
            // ============================================
            // Step 3: Generate the invoice HTML
            // autoprint = 0 to disable JavaScript print
            // ============================================
            string htmlContent = engineInvoice.GenerateInvoice(invoice_id, 0);

            if (string.IsNullOrEmpty(htmlContent))
            {
                Response.StatusCode = 404;
                Response.Write("Invoice not found");
                return;
            }

            // ============================================
            // Step 4: Generate PDF using Puppeteer Sharp
            // ============================================
            byte[] pdfBytes = await GeneratePdfBytesAsync(htmlContent);

            // ============================================
            // Step 5: Send PDF to browser for download
            // ============================================
            Response.Clear();
            Response.ContentType = "application/pdf";
            Response.AddHeader("Content-Disposition", $"attachment; filename=\"invoice-{invoice_id}.pdf\"");
            Response.AddHeader("Content-Length", pdfBytes.Length.ToString());
            Response.BinaryWrite(pdfBytes);
            Response.End();
        }
        catch (Exception ex)
        {
            Response.StatusCode = 500;
            Response.Write($"Error: {ex.Message}");
        }
    }

    /// <summary>
    /// Generate PDF bytes from HTML content
    /// </summary>
    private async Task<byte[]> GeneratePdfBytesAsync(string htmlContent)
    {
        // Ensure Chromium is downloaded
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

        // Launch browser in headless mode
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[] 
            { 
                "--no-sandbox",           // May be required on some servers
                "--disable-setuid-sandbox" 
            }
        });

        // Create page
        await using var page = await browser.NewPageAsync();

        // Set HTML content
        await page.SetContentAsync(htmlContent);

        // Generate PDF as byte array
        byte[] pdfBytes = await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            PreferCSSPageSize = true,
            MarginOptions = new MarginOptions
            {
                Top = "0mm",
                Right = "0mm",
                Bottom = "0mm",
                Left = "0mm"
            }
        });

        return pdfBytes;
    }
}

PdfOptions Reference

Here’s a complete reference of all PdfOptions properties:

PropertyTypeDefaultDescription
FormatPaperFormatnullPaper format (A4, Letter, Legal, etc.)
WidthobjectnullPaper width (e.g., “8.5in”, “210mm”)
HeightobjectnullPaper height (e.g., “11in”, “297mm”)
LandscapeboolfalsePaper orientation
Scaledecimal1Scale of webpage (0.1 to 2.0)
PrintBackgroundboolfalsePrint background colors/images
DisplayHeaderFooterboolfalseShow header and footer
HeaderTemplatestringnullHTML template for header
FooterTemplatestringnullHTML template for footer
MarginOptionsMarginOptionsnullPage margins (Top, Right, Bottom, Left)
PageRangesstring“”Pages to print (e.g., “1-5, 8”)
PreferCSSPageSizeboolfalseGive CSS @page size priority
OmitBackgroundboolfalseHide default white background
TaggedboolfalseGenerate tagged (accessible) PDF
OutlineboolfalseGenerate document outline

Available Paper Formats:

PaperFormat.Letter   // 8.5in x 11in
PaperFormat.Legal    // 8.5in x 14in
PaperFormat.Tabloid  // 11in x 17in
PaperFormat.Ledger   // 17in x 11in
PaperFormat.A0       // 33.1in x 46.8in
PaperFormat.A1       // 23.4in x 33.1in
PaperFormat.A2       // 16.54in x 23.4in
PaperFormat.A3       // 11.7in x 16.54in
PaperFormat.A4       // 8.27in x 11.7in
PaperFormat.A5       // 5.83in x 8.27in
PaperFormat.A6       // 4.13in x 5.83in

Performance Tips

1. Reuse Browser Instance

For high-volume PDF generation, reuse the browser instance instead of launching a new one for each PDF:

public class PdfService : IDisposable
{
    private IBrowser _browser;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task InitializeAsync()
    {
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

        _browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await _semaphore.WaitAsync();
        try
        {
            await using var page = await _browser.NewPageAsync();
            await page.SetContentAsync(html);
            return await page.PdfDataAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true
            });
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public void Dispose()
    {
        _browser?.Dispose();
    }
}

2. Pre-download Chromium

Download Chromium during application startup rather than on first PDF request:

// Global.asax.cs
protected void Application_Start(object sender, EventArgs e)
{
    // Pre-download Chromium in background
    Task.Run(async () =>
    {
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();
    });
}

Troubleshooting

Common Issues and Solutions

1. “Failed to launch browser” on Linux

Add sandbox flags:

await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = true,
    Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
});

2. Timeout errors

Increase navigation timeout:

await page.GoToAsync(url, new NavigationOptions
{
    Timeout = 60000,  // 60 seconds
    WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});

3. Missing fonts

Install fonts on the server or embed them in CSS using base64:

@font-face {
    font-family: 'MyFont';
    src: url(data:font/woff2;base64,<base64-encoded-font>) format('woff2');
}

4. External resources not loading

Wait for network to be idle before generating PDF:

await page.GoToAsync(url, new NavigationOptions
{
    WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});

Summary

In this part, we covered:

  1. What is Puppeteer Sharp — A .NET port of Google’s Puppeteer library
  2. Installation — Via NuGet package
  3. Basic PDF Generation — From URL and HTML string
  4. PDF Options — Format, margins, scale, headers/footers
  5. ASP.NET Integration — Complete Web Forms example with async support
  6. Performance Tips — Browser reuse, pre-downloading Chromium
  7. Troubleshooting — Common issues and solutions

Key Advantages of Puppeteer Sharp:

  • Auto-downloads Chromium — No manual installation required
  • Full programmatic control — Extensive C# API
  • Async/await support — Modern patterns
  • More hosting flexibility — Works in more environments than Chrome.exe

When to use Puppeteer Sharp vs Chrome.exe:

  • Use Chrome.exe (Part 4.1) for simple setups where Chrome is already installed
  • Use Puppeteer Sharp (Part 4.2) for production applications requiring more control and flexibility

Additional Resources