Skip to main content

How to Generate PDF from HTML Using Playwright in C# and .NET

· 19 min read
Michał Szymanowski
Michał Szymanowski
PDFBolt Co-Founder

Generate PDF from HTML using Playwright in C#/.NET

This detailed guide shows you how to use Microsoft Playwright for .NET and C# to generate high-quality PDFs with perfect rendering. Learn how to implement a complete solution that combines Scriban templates with Playwright's powerful browser automation to create pixel-perfect documents. Whether you're building invoices, reports, or any business documents, this step-by-step tutorial provides all the code and explanation you need to integrate PDF generation into your C#-based .NET applications.

What is Playwright for .NET?

Microsoft Playwright for .NET is a powerful browser automation library that provides a unified API to control Chromium, Firefox, and WebKit browsers. Originally developed for Node.js by Microsoft, Playwright has been ported to the .NET ecosystem, offering C# developers a robust tool for browser automation, testing, and PDF generation.

Key Features of Playwright for PDF Generation

When implementing HTML to PDF generation in .NET, Playwright offers these significant advantages:

  • Cross-Browser Support: Works with multiple browser engines (though Chromium is typically used for PDF generation).
  • Headless Mode: Runs without visible UI for efficient server-side processing.
  • Rich API: Provides extensive configuration options for PDF output.
  • JavaScript Support: Fully executes JavaScript, enabling dynamic content in generated PDFs.
  • Resource Handling: Efficiently manages resource loading and rendering.

Step-by-Step Implementation: PDF Invoice Generator

Let's build a complete solution for generating PDF invoices using Playwright and Scriban templates.

Step 1: Set Up Your .NET Development Environment

Before writing any code, you need to establish a proper development environment. Playwright requires specific dependencies to control browser engines effectively and generate high-quality PDFs.

First, ensure you have the necessary tools and prerequisites:

  1. IDE: Visual Studio, Visual Studio Code with C# extensions, or JetBrains Rider.
  2. .NET SDK: .NET 6.0 or newer (this tutorial uses .NET 8.0).

Confirm your .NET installation by running:

dotnet --version

Step 2: Create a New .NET Project

In this step, we're setting up the basic structure for our application. A console application provides a simple entry point for our PDF generation code.

Let's start with a new console application:

mkdir PlaywrightPdfGenerator
cd PlaywrightPdfGenerator
dotnet new console

Step 3: Set Up Your Project Structure

A well-organized directory structure is essential for maintaining and scaling your application. Following separation of concerns principles, we'll divide our code into logical components:

  • Models for our data structures (invoice data).
  • Templates for our Scriban HTML templates.
  • Services for core functionality (template rendering and PDF generation).
  • Output as the destination for generated files.

This organization makes the code more maintainable and follows good software design principles.

Create a well-organized directory structure:

PlaywrightPdfGenerator/

├── Program.cs # Main application entry point

├── Models/ # Data models
│ └── InvoiceModel.cs # Invoice data structure

├── Templates/ # Template files
│ └── Invoice.scriban # Invoice template

├── Services/ # Service classes
│ ├── TemplateService.cs # Template rendering service
│ └── PdfService.cs # PDF generation service

└── Output/ # Generated PDFs

Create these directories:

mkdir Models Templates Services Output

Step 4: Install Required NuGet Packages

Our solution depends on two primary external libraries:

  1. Microsoft.Playwright - This browser automation library leverages real browser engines to render HTML exactly as it would appear in a browser, then captures this rendering as a PDF.
  2. Scriban - A lightweight and high-performance templating engine that offers an easy-to-use syntax that's powerful enough for complex templates.

Additionally, we need to install the actual browser binaries that Playwright will control. This is handled through the Playwright CLI tool.

Add the necessary packages to your project:

dotnet add package Microsoft.Playwright
dotnet add package Scriban

Install Playwright CLI:

dotnet tool install --global Microsoft.Playwright.CLI

Install browser binaries:

playwright install

Step 5: Create the Invoice Data Model for PDF Templates

The data model:

  • Represents our business object – an invoice – with all its properties and relationships.
  • Provides structure and ensures type safety through a strongly-typed approach.
  • Enables business logic using calculated properties.
  • Serves as the data source for Scriban template binding.

Create the file Models/InvoiceModel.cs with the following content:

InvoiceModel.cs
namespace PlaywrightPdfGenerator.Models
{
public class InvoiceModel
{
// Basic invoice properties
public string? InvoiceNumber { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime DueDate { get; set; }

// Company information
public string? CompanyLogo { get; set; }
public string? CompanyName { get; set; }
public string? CompanyAddress { get; set; }
public string? CompanyEmail { get; set; }
public string? CompanyPhone { get; set; }

// Client information
public string? ClientName { get; set; }
public string? ClientAddress { get; set; }
public string? ClientEmail { get; set; }

// Financial details
public decimal SubTotal => Items.Sum(item => item.Total);
public decimal TaxRate { get; set; }
public decimal TaxAmount => Math.Round(SubTotal * (TaxRate / 100), 2);
public decimal Total => SubTotal + TaxAmount;
public string Currency { get; set; } = "$";

// Invoice items
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();

// Additional information
public string? Notes { get; set; }
public string? PaymentTerms { get; set; }
}

public class InvoiceItem
{
public string? Description { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
}

This model represents the structure of our invoice data. It includes properties for company and client details, invoice items, and calculated properties for totals, taxes, and other financial information.

Step 6: Create a Template with Scriban for C# PDF Generation

The template defines the visual design and structure of our PDF document using HTML and CSS.

Scriban uses double curly braces ({{ variable }}) for data insertion and constructs like {{ for }} and {{ if }} for dynamic content.

Create the file Templates/Invoice.scriban:

Invoice.scriban
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice {{ invoice_number }}</title>
<style>
:root {
--primary-color: #1e293b;
--accent-color: #f97316;
--light-bg: #f8fafc;
--table-row-alt: #f1f5f9;
--text-color: #0f172a;
--border-color: #e2e8f0;
}

@page {
size: A4;
margin: 0;
}

body {
font-family: 'Segoe UI', sans-serif;
color: var(--text-color);
margin: 0;
padding: 0;
}

.invoice-container {
max-width: 850px;
margin: 20px auto;
padding: 15px 50px;
}

.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 3px solid var(--accent-color);
padding-bottom: 20px;
margin-bottom: 30px;
}

.company-info img {
max-height: 70px;
margin-bottom: 10px;
}

.company-info p {
margin: 4px 0;
font-size: 14px;
line-height: 1.4;
}


.invoice-meta h1 {
margin: 0;
font-size: 42px;
color: var(--primary-color);
text-align: right;
margin-bottom:25px;
}

.invoice-meta p {
margin: 6px 0;
font-size: 14px;
}

.section-title {
font-size: 18px;
color: var(--accent-color);
font-weight: 600;
border-bottom: 1px solid var(--border-color);
margin-bottom: 12px;
padding-bottom: 5px;
}

.invoice-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}

.client-details,
.invoice-details-info {
width: 48%;
}

table.invoice-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}

table.invoice-table th {
background: var(--primary-color);
color: #ffffff;
padding: 10px 14px;
font-size: 16px;
text-align: left;
}

table.invoice-table td {
padding: 10px 14px;
font-size: 14px;
border-bottom: 1px solid var(--border-color);
}

table.invoice-table tr:nth-child(even) {
background: var(--table-row-alt);
}

.text-right {
text-align: right;
}

.summary {
display: flex;
justify-content: flex-end;
}

.summary-table {
width: 320px;
border-collapse: collapse;
}

.summary-table td {
padding: 12px;
font-size: 14px;
border-bottom: 1px solid var(--border-color);
}

.summary-table .total {
font-weight: 700;
background-color: var(--light-bg);
font-size: 16px;
border-left: 3px solid var(--accent-color);
}

.notes {
background-color: var(--light-bg);
border-left: 4px solid var(--accent-color);
padding: 16px 20px;
margin-top: 30px;
font-size: 14px;
}

.footer {
text-align: center;
font-size: 12px;
color: #64748b;
margin-top: 40px;
border-top: 1px solid var(--border-color);
padding-top: 20px;
}
</style>
</head>
<body>
<div class="invoice-container">
<div class="header">
<div class="company-info">
<img src="{{ company_logo }}" alt="{{ company_name }} Logo" />
<p><strong>{{ company_name }}</strong></p>
<p>{{ company_address }}</p>
<p>{{ company_email }}</p>
<p>{{ company_phone }}</p>
</div>
<div class="invoice-meta">
<h1>INVOICE</h1>
<p><strong>Invoice Number:</strong> {{ invoice_number }}</p>
<p><strong>Invoice Date:</strong> {{ invoice_date | date.to_string '%B %d, %Y' }}</p>
<p><strong>Due Date:</strong> {{ due_date | date.to_string '%B %d, %Y' }}</p>
</div>
</div>

<div class="invoice-details">
<div class="invoice-details-info">
<div class="section-title">Invoice Details</div>
<p><strong>Invoice Number:</strong> {{ invoice_number }}</p>
<p><strong>Invoice Date:</strong> {{ invoice_date | date.to_string '%B %d, %Y' }}</p>
<p><strong>Due Date:</strong> {{ due_date | date.to_string '%B %d, %Y' }}</p>
</div>
<div class="client-details">
<div class="section-title">Bill To</div>
<p><strong>{{ client_name }}</strong></p>
<p>{{ client_address }}</p>
<p>{{ client_email }}</p>
</div>
</div>

<table class="invoice-table">
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
{{ for item in items }}
<tr>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
<td>{{ currency }}{{ item.unit_price | math.format '0.00' }}</td>
<td class="text-right">{{ currency }}{{ item.total | math.format '0.00' }}</td>
</tr>
{{ end }}
</tbody>
</table>

<div class="summary">
<table class="summary-table">
<tr>
<td>Subtotal</td>
<td class="text-right">{{ currency }}{{ sub_total | math.format '0.00' }}</td>
</tr>
<tr>
<td>Tax ({{ tax_rate }}%)</td>
<td class="text-right">{{ currency }}{{ tax_amount | math.format '0.00' }}</td>
</tr>
<tr class="total">
<td>Total</td>
<td class="text-right">{{ currency }}{{ total | math.format '0.00' }}</td>
</tr>
</table>
</div>

{{ if notes != '' }}
<div class="notes">
<div class="section-title">Notes</div>
<p>{{ notes }}</p>
</div>
{{ end }}

<div class="footer">
<p>Thank you for your business!</p>
</div>
</div>
</body>
</html>

The template includes:

  • Professional styling with CSS.
  • Structured layout for company and client information.
  • Tabular format for line items.
  • Summary section with calculations.
  • Notes section for additional information.
  • Footer with payment terms.

Step 7: Create the Template Engine Service

Now let's create the service that renders our Scriban templates.

Create Services/TemplateService.cs:

TemplateService.cs
using System.Collections;
using Scriban;
using Scriban.Runtime;
using System.Diagnostics.CodeAnalysis;

namespace PlaywrightPdfGenerator.Services
{
public class TemplateService
{
// Cache for templates to improve performance
private static readonly Dictionary<string, Template> _templateCache = new();

// Renders a Scriban template with the provided model
public async Task<string> RenderTemplateAsync<T>(string templatePath, T model)
{
try
{
// Load and cache the template
if (!_templateCache.TryGetValue(templatePath, out var template))
{
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"Template file not found: {templatePath}");
}

string templateContent = await File.ReadAllTextAsync(templatePath);
template = Template.Parse(templateContent);

if (template.HasErrors)
{
throw new Exception($"Template parsing error: {string.Join(", ", template.Messages)}");
}

_templateCache[templatePath] = template;
}

// Convert model to snake_case for Scriban (which uses this by convention)
var scribanModel = ConvertToScribanObject(model);

// Create a template context with the model
var context = new TemplateContext();
var scriptObject = new ScriptObject();
foreach (var kvp in (scribanModel as ScriptObject).ToList())
{
scriptObject.Add(kvp.Key, kvp.Value);
}
context.PushGlobal(scriptObject);
// Render the template
return await template.RenderAsync(context);
}
catch (Exception ex)
{
Console.WriteLine($"Template rendering error: {ex.Message}");
throw;
}
}

// Converts C# model to snake_case properties for Scriban templates
private static ScriptObject ConvertToScribanObject([AllowNull] object model)
{
if (model == null)
return new ScriptObject();

var scriptObject = new ScriptObject();
var properties = model.GetType().GetProperties();

foreach (var property in properties)
{
// Convert property name to snake_case
string snakeCaseName = ConvertToSnakeCase(property.Name);
object value = property.GetValue(model);

if (value == null)
{
// Null values remain null
scriptObject[snakeCaseName] = null;
}
else if (value is IEnumerable enumerable && !(value is string))
{
// Handle collections (e.g., List<InvoiceItem>)
var list = new List<object>();
foreach (var item in enumerable)
{
list.Add(ConvertToScribanObject(item));
}
scriptObject[snakeCaseName] = list;
}
else if (value.GetType().IsClass && value.GetType() != typeof(string))
{
// Handle nested objects recursively
scriptObject[snakeCaseName] = ConvertToScribanObject(value);
}
else
{
// Primitive types (e.g., DateTime, int, string) are added as-is
scriptObject[snakeCaseName] = value;
}
}

return scriptObject;
}

// Converts a string from PascalCase to snake_case
private static string ConvertToSnakeCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;

// Special case for ID -> id instead of i_d
if (input == "ID")
return "id";

// Build the snake_case string
var result = new System.Text.StringBuilder();

for (int i = 0; i < input.Length; i++)
{
var currentChar = input[i];

// If it's the first character, just make it lowercase
if (i == 0)
{
result.Append(char.ToLower(currentChar));
continue;
}

// If it's uppercase and not the first character, add an underscore before it
if (char.IsUpper(currentChar))
{
result.Append('_');
result.Append(char.ToLower(currentChar));
}
else
{
result.Append(currentChar);
}
}

return result.ToString();
}
}
}

Key features include of this service:

  1. Template Caching: Improves performance by caching parsed templates.
  2. Model Conversion: Transforms C# PascalCase properties to snake_case for Scriban templates.
  3. Error Handling: Provides clear error messages if template parsing or rendering fails.
  4. Asynchronous Processing: Uses async/await for non-blocking I/O operations.

The service is designed to be reusable with any model type, making it flexible for different document types.

Step 8: Create the PDF Generation Service with Playwright

The PDF Service is the core component for generating documents. It leverages Playwright to render HTML into high-quality PDFs using actual browser technology.

Create Services/PdfService.cs:

PdfService.cs
using Microsoft.Playwright;

namespace PlaywrightPdfGenerator.Services
{
public class PdfService : IAsyncDisposable
{
private IPlaywright? _playwright;
private IBrowser? _browser;
private bool _initialized = false;

// Initializes Playwright and launches a browser instance
public async Task InitializeAsync()
{
if (_initialized)
return;

try
{
// Create Playwright instance
_playwright = await Playwright.CreateAsync();

// Launch the browser (Chromium is best for PDF generation)
_browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true
});

_initialized = true;
Console.WriteLine("Playwright initialized successfully");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to initialize Playwright: {ex.Message}");
throw;
}
}

// Generates a PDF from HTML content
public async Task<string> GeneratePdfFromHtmlAsync(
string html,
string outputPath,
PagePdfOptions? pdfOptions = null)
{
if (!_initialized)
await InitializeAsync();

// Create output directory if it doesn't exist
string directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}

// Handle page creation and PDF generation
try
{
// Create a new page
var page = await _browser.NewPageAsync();

try
{
// Set content and wait for it to load completely
await page.SetContentAsync(html, new PageSetContentOptions
{
WaitUntil = WaitUntilState.NetworkIdle
});

// Default PDF options if none provided
pdfOptions ??= new PagePdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
Margin = new Margin
{
Top = "20px",
Bottom = "20px",
Left = "20px",
Right = "20px"
}
};

if (page == null)
{
throw new InvalidOperationException("Page is null.");
}
var pdfBuffer = await page.PdfAsync(pdfOptions);

// Save PDF buffer to file
await File.WriteAllBytesAsync(outputPath, pdfBuffer);

Console.WriteLine($"PDF generated successfully: {Path.GetFullPath(outputPath)}");
return Path.GetFullPath(outputPath);
}
finally
{
// Close page manually
await page.CloseAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"PDF generation error: {ex.Message}");
throw;
}
}

// Disposes Playwright resources
public async ValueTask DisposeAsync()
{
if (_browser != null)
await _browser.DisposeAsync();

_playwright?.Dispose();
_initialized = false;
}
}
}

This PDF service uses Playwright to convert HTML content to PDF. Key features include:

  1. Resource Management: Implements IAsyncDisposable for proper cleanup of Playwright resources.
  2. Browser Initialization: Handles browser setup with appropriate configuration.
  3. PDF Configuration: Allows customization of PDF output with format, margins, and other options.
  4. Error Handling: Provides detailed error information if PDF generation fails.
  5. File System Management: Creates output directories as needed.

The service is designed to be efficient by reusing the browser instance across multiple PDF generation requests.

Step 9: Implement the Main Program

Now let's tie everything together in the main program.

Update Program.cs:

Program.cs
using Microsoft.Playwright;
using PlaywrightPdfGenerator.Models;
using PlaywrightPdfGenerator.Services;

namespace PlaywrightPdfGenerator
{
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("Starting HTML to PDF conversion with Playwright");

try
{
// Define paths for template and output
string templatePath = Path.Combine("Templates", "Invoice.scriban");

// Set output directory
string outputDir = Path.Combine(Directory.GetCurrentDirectory(), "Output");
Directory.CreateDirectory(outputDir);

Console.WriteLine($"Output directory: {outputDir}");

// 1. Create sample invoice data
var invoice = CreateSampleInvoice();
Console.WriteLine($"Created sample invoice with number: {invoice.InvoiceNumber}");

// 2. Initialize template service
var templateService = new TemplateService();
Console.WriteLine("Template service initialized");

// 3. Initialize PDF service
await using var pdfService = new PdfService();
await pdfService.InitializeAsync();
Console.WriteLine("PDF service initialized");

// 4. Render HTML from the template
string html = await templateService.RenderTemplateAsync(templatePath, invoice);
Console.WriteLine("Template rendered successfully");

// 5. Save HTML for debugging (optional)
string invoiceId = $"INV-{DateTime.Now:yyyyMMdd-HHmmssfff}";
string htmlOutputPath = Path.Combine(outputDir, $"Invoice_{invoiceId}.html");
await File.WriteAllTextAsync(htmlOutputPath, html);
Console.WriteLine($"HTML saved to: {htmlOutputPath}");

// 6. Configure PDF options
var pdfOptions = new PagePdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
Margin = new Margin
{
Top = "30px",
Right = "30px",
Bottom = "30px",
Left = "30px"
},
DisplayHeaderFooter = true,
FooterTemplate = "<div style='width: 100%; text-align: center; font-size: 10px; font-family: Arial; padding: 5px;'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"
};

// 7. Generate the PDF
string pdfOutputPath = Path.Combine(outputDir, $"Invoice_{invoiceId}.pdf");
await pdfService.GeneratePdfFromHtmlAsync(html, pdfOutputPath, pdfOptions);

Console.WriteLine("PDF generation completed successfully!");
Console.WriteLine($"PDF saved to: {Path.GetFullPath(pdfOutputPath)}");
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine(ex.StackTrace);
Console.ResetColor();
}
}

// Helper method to create a sample invoice
private static InvoiceModel CreateSampleInvoice()
{
// Generate invoice number with year and random digits
string invoiceNumber = $"INV-{DateTime.Now:yyyy}-{new Random().Next(1000, 9999)}";

return new InvoiceModel
{
// Invoice information
InvoiceNumber = invoiceNumber,
InvoiceDate = DateTime.Now,
DueDate = DateTime.Now.AddDays(30),

// Company details
CompanyLogo = "https://img.pdfbolt.com/logo-design-example.png",
CompanyName = "NullPointer Solutions",
CompanyAddress = "8 Null Street, Bugsville",
CompanyEmail = "[email protected]",
CompanyPhone = "+0 (123) 404-0000",

// Client information
ClientName = "404 Innovations",
ClientAddress = "789 Business Avenue, Compiletown",
ClientEmail = "[email protected]",

// Financial details
Currency = "$",
TaxRate = 9m,

// Notes
Notes = "Payment due in 30 days. After that, our finance bot will start sending reminders using CAPS LOCK.",

// Invoice items
Items = new List<InvoiceItem>
{
new InvoiceItem
{
Description = "Code Review by AI Intern",
Quantity = 5,
UnitPrice = 89.00m
},
new InvoiceItem
{
Description = "Legacy Code Translator",
Quantity = 1,
UnitPrice = 1199.00m
},
new InvoiceItem
{
Description = "404 Page Generator",
Quantity = 2,
UnitPrice = 75.00m
},
new InvoiceItem
{
Description = "AI-Powered Time Optimization",
Quantity = 1,
UnitPrice = 999.00m
}
}
};
}
}
}

The main program orchestrates the entire PDF generation process:

  1. Data Preparation: Creates a sample invoice with example data.
  2. Services Initialization: Sets up the template and PDF services.
  3. Template Rendering: Converts the invoice data into HTML using Scriban.
  4. Debug Output: Saves the generated HTML for debugging (optional).
  5. PDF Configuration: Sets page size, margins, headers, footers, and other PDF options.
  6. PDF Generation: Creates the final PDF document using Playwright.
  7. Error Handling: Provides error reporting if any step fails.

All operations use async/await for efficient handling of I/O operations.

Step 10: Run the Application

Now you're ready to run the application and generate our invoice PDF:

dotnet run

When successful, you'll see output confirming each step of the process, and the generated PDF will be saved in the Output directory.

Here's what your generated PDF will look like:

PDF Invoice Generated with Playwright in C#/.NET

Playwright PDF Configuration Options

Playwright provides extensive options for customizing your generated PDFs.

Here are the most useful configuration settings:

OptionDescriptionExample Values
FormatStandard paper size"A4", "Letter", "Legal" etc.
Width/HeightCustom page dimensionsWidth = "8.5in"
Height = "11in"
MarginSpace around contentTop = "20px"
Bottom = "20px"
Right = "20px"
Left = "20px"
PrintBackgroundInclude background colors/imagestrue
false
LandscapePage orientationtrue (landscape)
false (portrait)
ScaleContent scaling factor1.0 (100%), 0.75 (75%)
PageRangesSpecific pages to include"1-5, 8, 11-13"
DisplayHeaderFooterShow headers and footerstrue
false
HeaderTemplateHTML for page headers"<div>Company Confidential</div>"
FooterTemplateHTML for page footers"<div>Page <span class='pageNumber'></span></div>"
PreferCSSPageSizeUse CSS @page sizetrue
false

Here's how to apply these options in code:

var pdfOptions = new PagePdfOptions
{
Format = PaperFormat.Letter,
PrintBackground = true,
Landscape = true,
Margin = new Margin
{
Top = "35px",
Right = "30px",
Bottom = "25px",
Left = "30px"
},
DisplayHeaderFooter = true,
HeaderTemplate =
"<div style='width: 100%; text-align: center; font-size: 10px; padding: 5px;'>CONFIDENTIAL</div>",
FooterTemplate =
"<div style='width: 100%; text-align: center; font-size: 10px; padding: 5px;'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
Scale = (float?)0.8
};

Performance Optimization Strategies for Playwright PDF Generation in .NET

When implementing PDF generation with Playwright in production environments, consider these optimization techniques:

Browser Management

Browser initialization is the most time-consuming part of the process. Optimize it with these strategies:

  1. Browser Pooling: Maintain a pool of browser instances for handling concurrent requests.
  2. Persistent Browser Context: Use persistent contexts to reduce initialization overhead.

Template Optimization

Improve template rendering performance with these techniques:

  1. Template Precompilation: Parse templates during application startup.
  2. Caching: Implement multi-level caching for templates and rendered HTML.
  3. Minimal External Resources: Avoid external dependencies in HTML templates.
  4. CSS Optimization: Simplify CSS and use efficient selectors.

Asynchronous Processing

For high-volume PDF generation:

  1. Background Processing: Generate PDFs in background tasks.
  2. Queue System: Implement a job queue for PDF generation requests.
  3. Progress Tracking: Provide status updates for long-running operations.
  4. Resource Limits: Set timeouts and resource constraints to prevent runaway processes.

Alternative Approaches for PDF Generation in C#

While Playwright offers excellent modern browser rendering for PDFs, you may want to consider these alternatives based on your specific needs:

SolutionAdvantagesBest Use CaseLearn More
PuppeteerSharpSimilar to Playwright but focused on Chrome.Ideal when you need full Chrome compatibility and advanced browser features.PuppeteerSharp HTML to PDF Guide
iText 7Fine-grained layout control, server-side only.For complex documents with precise layout requirements.iText 7 HTML to PDF in C#/.NET
PDFsharpLightweight, no browser dependency.For simple documents or restricted environments.
DinkToPdfOpen-source, simple wrapper around wkhtmltopdf.For straightforward HTML to PDF conversion.
HTML to PDF APICloud-based service like PDFBolt, no local dependencies.For serverless applications or managed services.Convert HTML to PDF Using API

Conclusion

In this comprehensive guide, you've learned how to create a robust PDF generation solution using Microsoft Playwright for .NET and Scriban templates.

This solution is ideal for generating invoices, reports, statements, certificates, and other business documents that require a professional appearance and reliable output.

For situations where maintaining browser instances isn't ideal, or when scaling to handle high-volume document generation, consider using a dedicated HTML to PDF API service, which handles all the infrastructure and optimization for you.

Now you have all the knowledge you need to confidently implement robust PDF generation in your C#/.NET applications.

Stay strong – and don’t let PDF generation break your spirit! 🫠