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

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