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
- Part 1: Printing Static Layout Content (Tickets, PVC ID Card, etc…)
- Part 2: Printing Semi Dynamic Layout Content (Invoices, Bills, etc…)
- Part 3: Printing Full Dynamic Layout Content (Reports, Data Grids, etc…)
- Part 4.1: Generate PDF Using Chrome.exe in ASP.NET Web Forms
- Part 4.2: Generate PDF Using Puppeteer Sharp in ASP.NET Web Forms
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:
- GitHub Repository: https://github.com/hardkoded/puppeteer-sharp
- Official Website: https://www.puppeteersharp.com
- API Documentation: https://www.puppeteersharp.com/api/index.html
- NuGet Package: https://www.nuget.org/packages/PuppeteerSharp
Comparison: Chrome.exe vs Puppeteer Sharp
| Aspect | Chrome.exe (Part 4.1) | Puppeteer Sharp (Part 4.2) |
|---|---|---|
| Chrome Installation | Required on server | Auto-downloads Chromium |
| Hosting | Dedicated/VPS only | More flexible |
| Control | Command-line args only | Full C# API |
| PDF Options | Limited | Extensive (margins, headers, footers, scale, etc.) |
| Async Support | No (spawns process) | Yes (native async/await) |
| Download Size | Uses existing Chrome | ~150MB Chromium download |
| Best For | Simple setups | Production 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 PuppeteerSharpOr via .NET CLI:
dotnet add package PuppeteerSharpOr 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:
| Property | Type | Default | Description |
|---|---|---|---|
Format | PaperFormat | null | Paper format (A4, Letter, Legal, etc.) |
Width | object | null | Paper width (e.g., “8.5in”, “210mm”) |
Height | object | null | Paper height (e.g., “11in”, “297mm”) |
Landscape | bool | false | Paper orientation |
Scale | decimal | 1 | Scale of webpage (0.1 to 2.0) |
PrintBackground | bool | false | Print background colors/images |
DisplayHeaderFooter | bool | false | Show header and footer |
HeaderTemplate | string | null | HTML template for header |
FooterTemplate | string | null | HTML template for footer |
MarginOptions | MarginOptions | null | Page margins (Top, Right, Bottom, Left) |
PageRanges | string | “” | Pages to print (e.g., “1-5, 8”) |
PreferCSSPageSize | bool | false | Give CSS @page size priority |
OmitBackground | bool | false | Hide default white background |
Tagged | bool | false | Generate tagged (accessible) PDF |
Outline | bool | false | Generate 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.83inPerformance 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:
- What is Puppeteer Sharp — A .NET port of Google’s Puppeteer library
- Installation — Via NuGet package
- Basic PDF Generation — From URL and HTML string
- PDF Options — Format, margins, scale, headers/footers
- ASP.NET Integration — Complete Web Forms example with async support
- Performance Tips — Browser reuse, pre-downloading Chromium
- 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
- Official Website: https://www.puppeteersharp.com
- GitHub Repository: https://github.com/hardkoded/puppeteer-sharp
- API Documentation: https://www.puppeteersharp.com/api/index.html
- NuGet Package: https://www.nuget.org/packages/PuppeteerSharp
