Skip to main content

Generate PDFs in C# Using QuestPDF: Complete Guide

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

PDF document generation with QuestPDF library in C#/.NET

PDF generation in .NET has always been a bit of a pain – absolute positioning, manual pagination, fiddly styling calculations. QuestPDF takes a different approach with a layout-based system and a fluent API that feels natural to C# developers. This guide walks through how QuestPDF handles PDF creation, from basic setup to building a full invoice with QR codes.

What is QuestPDF?

QuestPDF is an open-source, modern .NET library built specifically for programmatic PDF document creation. It provides a full-featured layout engine powered by a concise C# Fluent API that handles the complexities of document structure automatically.

Key Features and Capabilities

QuestPDF offers a wide set of features that distinguish it from other C# PDF generation libraries:

  • Container-based layout system that eliminates the need for absolute positioning and coordinates.
  • Automatic pagination with content overflow handling between pages.
  • Dynamic data binding using standard C# patterns (loops, conditionals).
  • Rich text formatting with full control over fonts, styles, and alignment.
  • Responsive tables and grids with flexible column definitions.
  • SVG and image support with multiple sizing options.

From invoices and reports to contracts and data exports, QuestPDF's layout approach cuts development time while producing polished documents.

This guide walks you through everything from basic setup to advanced techniques for implementing PDF document generation in your C#/.NET applications.

Important Note

QuestPDF is not an HTML to PDF converter. It provides a programming interface for creating and manipulating PDF documents directly in C#.

If you're looking to convert HTML to PDF in C#, you'll need a separate tool or library such as DinkToPdf, PuppeteerSharp, or Playwright. See HTML to PDF Conversion Options for a full comparison.

Getting Started with QuestPDF in C#/.NET Projects

Installation

Add QuestPDF to your .NET project using one of the following methods:

Using the .NET CLI:

dotnet add package QuestPDF

Using the Package Manager Console:

Install-Package QuestPDF

You can also add a reference in your .csproj file:

<PackageReference Include="QuestPDF" Version="2025.12.4" />

Setting Up the License

QuestPDF offers a tiered licensing model:

  • Community License: Free for individuals, non-profits, open-source projects, and businesses with annual revenue below $1M USD. Suitable for most use cases.
  • Professional/Enterprise Licenses: Required for larger organizations.

Add this line at startup to set the license:

QuestPDF.Settings.License = LicenseType.Community;
// or LicenseType.Professional or LicenseType.Enterprise
License Details

For detailed licensing information, visit the QuestPDF License and Pricing page.

Our First PDF Document

Let's create a simple PDF with some text:

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

// Create and generate a PDF document
Document.Create(container =>
{
container.Page(page =>
{
// Configure page settings
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(12));

// Add a header
page.Header()
.Text("Our First QuestPDF Document")
.SemiBold()
.FontSize(34)
.FontColor(Colors.Pink.Medium);

// Add content
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(column =>
{
column.Item().Text("Grab a coffee ☕ and let's make some PDFs!")
.FontSize(20)
.SemiBold();
});
});
})
.GeneratePdf("first-doc.pdf");

Mission accomplished – take a look at the result:

Simple PDF document created using QuestPDF in C#/.NET

Working with Text in PDFs Using QuestPDF

QuestPDF provides a variety of text formatting options for creating well-styled documents.

Text Formatting Code Example
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(14));

page.Header()
.Text("Text Formatting Options in QuestPDF")
.SemiBold()
.FontSize(24);

page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(column =>
{
column.Spacing(25);

column.Item()
.Text("Standard text with purple color")
.FontColor(Colors.Purple.Medium);

column.Item()
.Text("Bold text with orange color")
.Bold()
.FontColor(Colors.Orange.Medium);

column.Item()
.Text("Italic text with right alignment")
.Italic()
.AlignRight();

column.Item()
.Text("Underlined text with letter spacing")
.Underline()
.LetterSpacing((float)0.5);

column.Item()
.Text("Strikethrough text with large font")
.Strikethrough()
.FontSize(18);

column.Item()
.Text("Centered, bold and italic text")
.Bold()
.Italic()
.AlignCenter();


column.Item()
.Text(text =>
{
text.Span("Mixed ").FontSize(12);
text.Span("formatting ").Bold().FontColor(Colors.Blue.Medium);
text.Span("in ").Italic();
text.Span("one ").Underline();
text.Span("line").FontSize(18).FontColor(Colors.Red.Medium);
});

column.Item()
.Text(
"Justified text with longer content. This text demonstrates how QuestPDF handles justified alignment with multiple lines of content in a single text block.")
.Justify();
});
});
})
.GeneratePdf("text-formatting.pdf");

This example demonstrates a variety of text formatting options available in QuestPDF:

OptionDescriptionExample
Font ColorsSets the color of text..FontColor()
Font WeightsControls the thickness of the text..Bold()
.SemiBold()
Font StylesCreates slanted text (italic)..Italic()
Text DecorationsAdds lines to text, such as underlining or strikethrough..Underline()
.Strikethrough()
Text AlignmentControls the positioning of the text..AlignLeft()
.AlignRight()
.AlignCenter()
.Justify()
Font SizingChanges the size of the text in points..FontSize()
Letter SpacingAdjusts the space between characters..LetterSpacing()
Mixed FormattingCombines multiple styles in one line using Span()..Text(text => { text.Span()... })

Here is the output:

Working with Text Formatting using QuestPDF in C#/.NET

Working with Images in PDFs Using QuestPDF

QuestPDF makes it easy to embed images in your documents with flexible positioning and sizing options.

Image Code Example
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(12));

page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(column =>
{
// Standard image - will scale to fit width
column.Item()
.Padding(10)
.Column(imageSection =>
{
imageSection.Item().Text("Default Image (FitWidth)").Bold();
imageSection.Item().PaddingBottom(10);
imageSection.Item().Image("image.jpg");
});

column.Spacing(10);

// Image with specific width
column.Item()
.Padding(10)
.Column(imageSection =>
{
imageSection.Item().Text("Image with Specific Width").Bold();
imageSection.Item().PaddingBottom(10);
imageSection.Item().Width(150).Image("image.jpg");
});

column.Spacing(10);

// Row of images
column.Item()
.Padding(10)
.Column(imageSection =>
{
imageSection.Item().Text("Multiple Images in a Row").Bold();
imageSection.Item().PaddingBottom(10);

imageSection.Item().Row(row =>
{
row.Spacing(10);
row.RelativeItem().Image("image.jpg");
row.RelativeItem().Image("image.jpg");
row.RelativeItem().Image("image.jpg");
});
});
});
});
})
.GeneratePdf("image-examples.pdf");

PDF preview:

PDF with images using QuestPDF in C#/.NET


Supported Image Formats

QuestPDF supports the most common image formats including JPEG, PNG, BMP, and WEBP. The library handles image scaling and positioning automatically.

Image Sizing

In QuestPDF, specify only Width() or Height() for images, not both. The library preserves aspect ratio by default and handles the pixel-to-point conversion automatically.

container
.Width(1, Unit.Inch)
.Image("image.jpg")

Working with Tables in PDFs Using QuestPDF

Tables in QuestPDF offer great flexibility for displaying structured data.

Table Code Example
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(12));

page.Header()
.Text("Simple Table")
.SemiBold()
.FontSize(20);

page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(180); // Fixed width column
columns.RelativeColumn(3); // Takes 3 parts of remaining width
columns.RelativeColumn(1); // Takes 1 part of remaining width
});

// Header row spanning all columns
table.Cell().ColumnSpan(3)
.Background(Colors.Grey.Lighten2).Element(CellStyle)
.Text("Custom Column Widths")
.FontSize(14)
.Bold();

// Column description row
table.Cell().Element(CellStyle).Text("Fixed Width");
table.Cell().Element(CellStyle).Text("Relative Width (75%)");
table.Cell().Element(CellStyle).Text("Relative Width (25%)");

// Example width values
table.Cell().Element(CellStyle).Text("Banana 🍌");
table.Cell().Element(CellStyle).Text("Coffee ☕");
table.Cell().Element(CellStyle).Text("Deadline 😱");

// Helper method for consistent cell styling
static IContainer CellStyle(IContainer container)
=> container.Border(1).BorderColor(Colors.Grey.Medium).Padding(10);
});
});
})
.GeneratePdf("table-example.pdf");

Preview of the Generated PDF:

PDF with table layout using QuestPDF in C#/.NET

Working with Headers and Footers in PDFs Using QuestPDF

QuestPDF makes it easy to create consistent headers and footers across multiple pages.

Headers and Footers Code Example
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(11));

// Add a header
page.Header().Element(ComposeHeader);

// Add content
page.Content().Element(ComposeContent);

// Add a footer
page.Footer().Element(ComposeFooter);
});
})
.GeneratePdf("header-footer-example.pdf");

// Simple header composition
void ComposeHeader(IContainer container)
{
container.Border(1).BorderColor(Colors.Grey.Medium).Padding(10)
.Row(row =>
{
row.RelativeItem().Column(column =>
{
column.Item().Text("Document Title")
.FontSize(16)
.Bold();

column.Item().Text($"Generated: {DateTime.Now:d}")
.FontSize(10)
.FontColor(Colors.Grey.Medium);
});
});
}

// Main content composition
void ComposeContent(IContainer container)
{
container.PaddingVertical(10).Column(column =>
{
// Add some content for demonstration
column.Item().Text("First Page Content").FontSize(14).Bold();
column.Item().PaddingTop(5).Text(Placeholders.LoremIpsum());
column.Item().PaddingTop(10).Text(Placeholders.LoremIpsum());

// Add a line break to demonstrate multi-page content
column.Item().PageBreak();

// Continue content on next page
column.Item().Text("Second Page Content").FontSize(14).Bold();
column.Item().PaddingTop(5).Text(Placeholders.LoremIpsum());
column.Item().PaddingTop(10).Text(Placeholders.LoremIpsum());
});
}

// Simple footer composition
void ComposeFooter(IContainer container)
{
container.Border(1).BorderColor(Colors.Grey.Medium)
.PaddingVertical(5).PaddingHorizontal(10)
.Row(row =>
{
row.RelativeItem().AlignLeft().Text("Company Name")
.FontSize(10);

row.RelativeItem().AlignRight().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" of ");
text.TotalPages();
});
});
}

The header and footer appear on every page automatically. QuestPDF repeats them across all pages without any extra configuration.

Output Preview

Document with headers and footers generated using QuestPDF in C#/.NET
Document with headers and footers generated using QuestPDF in C#/.NET

Adding Barcodes and QR Codes to PDFs with QuestPDF

QuestPDF supports generating various types of barcodes through integration with the ZXing.Net library. This allows you to embed both linear barcodes and QR codes directly in your PDF documents.

Important Note

Before using any of these examples, you must install the ZXing.Net package:

dotnet add package ZXing.Net

Here's a simple example showing how to create both linear barcodes and QR codes in your documents.

Barcode and QR Code Example
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using ZXing;
using ZXing.OneD;
using ZXing.QrCode;
using ZXing.Rendering;

// Set license type at startup
QuestPDF.Settings.License = LicenseType.Community;

Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(14));

page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(column =>
{
// 1. Linear Barcode Example (Code 128)
column.Item()
.Background(Colors.Grey.Lighten4)
.Padding(15)
.Column(barcodeSection =>
{
barcodeSection.Item().Text("Code 128 Barcode").Bold();
barcodeSection.Item().PaddingBottom(10);

// Product code to encode
var productCode = "PRD12345";

barcodeSection.Item()
.Height(80)
.AlignCenter()
.Background(Colors.White)
.Padding(10)
.Svg(size =>
{
var writer = new Code128Writer();
var matrix = writer.encode(productCode, BarcodeFormat.CODE_128,
(int)size.Width, (int)size.Height);
var renderer = new SvgRenderer();
return renderer.Render(matrix, BarcodeFormat.CODE_128, productCode).Content;
});

barcodeSection.Item().PaddingBottom(10);
barcodeSection.Item()
.AlignCenter()
.Text(productCode);
});

column.Spacing(20);

// 2. QR Code Example
column.Item()
.Background(Colors.Grey.Lighten4)
.Padding(15)
.Column(qrSection =>
{
qrSection.Item().Text("QR Code").Bold();
qrSection.Item().Height(10);

// URL to encode
var url = "https://pdfbolt.com";

qrSection.Item()
.Height(150)
.AlignCenter()
.Background(Colors.White)
.Padding(10)
.Svg(size =>
{
var writer = new QRCodeWriter();
var matrix = writer.encode(url, BarcodeFormat.QR_CODE,
(int)size.Width, (int)size.Height);
var renderer = new SvgRenderer();
return renderer.Render(matrix, BarcodeFormat.QR_CODE, null).Content;
});

qrSection.Item().PaddingBottom(10);
qrSection.Item()
.AlignCenter()
.Text(url)
.FontColor(Colors.Blue.Medium);
});
});
});
})
.GeneratePdf("barcode-qrcode-examples.pdf");

These examples demonstrate:

  1. Linear Barcode (Code 128): Suitable for product IDs, tracking numbers, and other alphanumeric data.
  2. QR Code: Ideal for encoding URLs, contact information, or larger amounts of text.

Both examples use SVG rendering for high-quality output at any resolution, so your barcodes stay sharp and scannable in the final PDF.

Customization
  • You can adjust the height and padding of the barcodes to fit your needs.
  • QR codes can encode much more data than shown in this example (URLs, contact info, etc.).

See the Result:

PDF with Barcode and QR Code using QuestPDF in C#/.NET

Step-by-Step: Creating an Invoice PDF with QR Code in C#

Let's create a complete invoice example with QuestPDF, including a company logo, structured layout, and a QR code for payment information.

Step 1: Project Setup

First, Let's Create the Project Structure

InvoiceGenerator/

├── Program.cs # Main application entry point
├── InvoiceDocument.cs # Invoice document definition
├── InvoiceModel.cs # Data model for the invoice
├── Assets/
│ └── logo.png # Company logo
└── Helpers/
└── InvoiceStyle.cs # Styling helpers for the invoice

Make Sure Your Project Has the Necessary Dependencies Installed

dotnet list package

You should see output that includes entries like:

  • QuestPDF
  • ZXing.Net

If they’re not listed, install them using:

dotnet add package QuestPDF
dotnet add package ZXing.Net

Step 2: Create the Invoice Model

Now let's create our data model that will hold all the invoice information.

Create a file named InvoiceModel.cs with the following code:

InvoiceModel.cs
namespace InvoiceGenerator
{
public class InvoiceModel
{
// Invoice metadata
public string? InvoiceNumber { get; set; }
public DateTime IssueDate { get; set; } = DateTime.Now;
public DateTime DueDate { get; set; } = DateTime.Now.AddDays(14);

// Company information
public CompanyInfo? Seller { get; set; }
public CompanyInfo? Customer { get; set; }

// Invoice content
public List<InvoiceItem> Items { get; set; } = new();
public string? Comments { get; set; }

// Payment details for QR code
public string? PaymentAccountNumber { get; set; }
public string? PaymentReference { get; set; }

// Calculated properties
public decimal Subtotal => Items.Sum(x => x.TotalPrice);
public decimal TaxRate { get; set; } = 0.23m; // Default 23% VAT
public decimal TaxAmount => Subtotal * TaxRate;
public decimal GrandTotal => Subtotal + TaxAmount;
}

public class CompanyInfo
{
public string? Name { get; set; }
public string? Street { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
}

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

Step 3: Create the Invoice Styling Helper

Next, we'll create a helper class to maintain consistent styling throughout our invoice document. This centralizes our colors and text styles in one place.

Create a file named Helpers/InvoiceStyle.cs with common styling methods:

InvoiceStyle.cs
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace InvoiceGenerator.Helpers
{
public static class InvoiceStyle
{
// Colors
public static Color PrimaryColor => Color.FromHex("#916E56");
public static Color TextColor => Colors.Black;

// Text Styles
public static TextStyle Title => TextStyle.Default.FontSize(24).SemiBold().FontColor(PrimaryColor);
public static TextStyle Subtitle => TextStyle.Default.FontSize(13).SemiBold().FontColor(TextColor);
public static TextStyle Normal => TextStyle.Default.FontSize(11).FontColor(TextColor).LineHeight((float?)1.4);
}
}

Step 4: Create the Invoice Document Class

Now we'll create the core class that implements the QuestPDF IDocument interface. This class will define the overall structure of our invoice and handle the rendering of all components.

Create a file named InvoiceDocument.cs:

InvoiceDocument.cs
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using ZXing;
using ZXing.QrCode;
using ZXing.Rendering;
using InvoiceGenerator.Helpers;
using System.Globalization;

namespace InvoiceGenerator
{
public class InvoiceDocument : IDocument
{
public InvoiceModel Model { get; }

public InvoiceDocument(InvoiceModel model) => Model = model;

public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

// Main document composition
public void Compose(IDocumentContainer container)
{
// Set US culture for $ currency formatting
var culture = new CultureInfo("en-US");
Thread.CurrentThread.CurrentCulture = culture;

container
.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(50);
page.DefaultTextStyle(x => x.FontSize(10));

page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().Element(ComposeFooter);
});
}

// Simple footer composition
void ComposeFooter(IContainer container)
{
container.Border(1).BorderColor(Colors.Grey.Lighten2)
.PaddingVertical(5).PaddingHorizontal(10)
.Row(row =>
{
row.RelativeItem().AlignLeft().Text(Model.Seller?.Name)
.FontSize(10);

row.RelativeItem().AlignRight().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" of ");
text.TotalPages();
});
});
}

// Document header with logo, invoice details and QR code
private void ComposeHeader(IContainer container)
{
container.Row(row =>
{
// Left column: Logo and basic invoice info
row.RelativeItem().Column(column =>
{
column.Item().Height(80).Image("Assets/logo.png");
column.Item().PaddingBottom(10);
column.Item().Text($"Invoice Number: {Model.InvoiceNumber}").Style(InvoiceStyle.Normal);
column.Item().Text($"Issue Date: {Model.IssueDate:d}").Style(InvoiceStyle.Normal);
column.Item().Text($"Due Date: {Model.DueDate:d}").Style(InvoiceStyle.Normal);
});

// Right column: Title and QR code
row.RelativeItem().Column(column =>
{
column.Item().AlignRight().Text("INVOICE").Style(InvoiceStyle.Title);
column.Item().PaddingBottom(10);

// Payment QR code for scanning
column.Item().AlignRight().Width(90).AspectRatio(1).Svg(GenerateQrCode);
});
});
}

// Generate QR code with payment information
private string GenerateQrCode(Size size)
{
string qrData =
$"Recipient:{Model.Seller?.Name}|AccNo:{Model.PaymentAccountNumber}|Ref:{Model.PaymentReference}|Amount:{Model.GrandTotal:F2}";
var matrix = new QRCodeWriter().encode(qrData, BarcodeFormat.QR_CODE, (int)size.Width, (int)size.Height);
return new SvgRenderer().Render(matrix, BarcodeFormat.QR_CODE, null).Content;
}

// Main content area
private void ComposeContent(IContainer container)
{
container.Column(column =>
{
column.Item().PaddingTop(20).Element(ComposeBillingAndShippingInfo);
column.Item().PaddingTop(20).Element(ComposeInvoiceItems);
column.Item().PaddingTop(10).AlignRight().Width(250).Element(ComposeTotals);

// Only show comments if present
if (!string.IsNullOrEmpty(Model.Comments))
column.Item().PaddingTop(20).Element(ComposeComments);
});
}

// Billing and shipping information section
private void ComposeBillingAndShippingInfo(IContainer container)
{
container.Row(row =>
{
// Bill To section
ComposeAddressSection(row.RelativeItem(), "BILL TO", Model.Customer);

row.ConstantItem(50); // Spacing between sections

// Ship To section
ComposeAddressSection(row.RelativeItem(), "SHIP TO", Model.Seller);
});
}

// Reusable address section formatter
private void ComposeAddressSection(IContainer container, string title, CompanyInfo? company)
{
container.Column(column =>
{
column.Item().Text(title).Style(InvoiceStyle.Subtitle).FontColor(InvoiceStyle.PrimaryColor);
column.Item().PaddingBottom(5);
column.Item().LineHorizontal(1).LineColor(Colors.Grey.Lighten1);
column.Item().PaddingBottom(5);

if (company != null)
{
column.Item().Text(company.Name).Bold().Style(InvoiceStyle.Normal);
column.Item().Text(company.Street).Style(InvoiceStyle.Normal);
column.Item().Text($"{company.City}, {company.State} {company.Zip}").Style(InvoiceStyle.Normal);
column.Item().Text($"Phone: {company.Phone}").Style(InvoiceStyle.Normal);
column.Item().Text($"Email: {company.Email}").Style(InvoiceStyle.Normal);
}
});
}

// Invoice items table
private void ComposeInvoiceItems(IContainer container)
{
container.Table(table =>
{
// Table columns definition
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(3); // Description
columns.ConstantColumn(100); // Unit Price
columns.ConstantColumn(80); // Quantity
columns.ConstantColumn(100); // Total
});

// Table header
AddTableHeader(table);

// Table rows with alternating colors
bool isAlternateRow = false;
foreach (var item in Model.Items)
{
AddTableRow(table, item, isAlternateRow);
isAlternateRow = !isAlternateRow;
}
});
}

// Table header helper
private void AddTableHeader(TableDescriptor table)
{
table.Header(header =>
{
header.Cell().Background(Colors.Grey.Lighten3)
.Padding(5).Text("Description")
.Style(InvoiceStyle.Subtitle);

header.Cell().Background(Colors.Grey.Lighten3)
.Padding(5).AlignRight().Text("Unit Price")
.Style(InvoiceStyle.Subtitle);

header.Cell().Background(Colors.Grey.Lighten3)
.Padding(5).AlignRight().Text("Quantity")
.Style(InvoiceStyle.Subtitle);

header.Cell().Background(Colors.Grey.Lighten3)
.Padding(5).AlignRight().Text("Amount")
.Style(InvoiceStyle.Subtitle);
});
}

// Table row helper
private void AddTableRow(TableDescriptor table, InvoiceItem item, bool isAlternateRow)
{
var bgColor = isAlternateRow ? Colors.White : Colors.Grey.Lighten5;

// Description cell
table.Cell().Background(bgColor)
.Border(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).AlignRight().AlignMiddle()
.Text($"{item.Name}");

// Unit price cell
table.Cell().Background(bgColor)
.Border(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).AlignRight().AlignMiddle()
.Text($"{item.UnitPrice:C}");

// Quantity cell
table.Cell().Background(bgColor)
.Border(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).AlignRight().AlignMiddle()
.Text($"{item.Quantity}");

// Total cell
table.Cell().Background(bgColor)
.Border(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).AlignRight().AlignMiddle()
.Text($"{item.TotalPrice:C}");
}

// Invoice totals section
private void ComposeTotals(IContainer container)
{
container.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.ConstantColumn(180);
});

// Subtotal row
table.Cell().PaddingBottom(8).AlignRight().Text("Subtotal:").Style(InvoiceStyle.Normal);
table.Cell().Padding(5).AlignRight().Text($"{Model.Subtotal:C}");

// Tax row
table.Cell().PaddingBottom(8).AlignRight()
.Text($"Tax ({Model.TaxRate:P0}):").Style(InvoiceStyle.Normal);
table.Cell().Padding(5).AlignRight().Text($"{Model.TaxAmount:C}");

// Total row
table.Cell().BorderLeft(3).BorderColor(InvoiceStyle.PrimaryColor)
.Background(Colors.Grey.Lighten4).Padding(8).AlignRight()
.Text("TOTAL:").Style(InvoiceStyle.Subtitle);
table.Cell().Background(Colors.Grey.Lighten4).Padding(8)
.AlignRight().Text($"{Model.GrandTotal:C}").Bold().FontSize(12);
});
}

// Optional comments section
private void ComposeComments(IContainer container)
{
container.Background(Colors.Grey.Lighten4)
.BorderLeft(4).BorderColor(InvoiceStyle.PrimaryColor).Padding(12)
.Column(column =>
{
column.Item().Text("Note").Style(InvoiceStyle.Subtitle);
column.Spacing(10);
column.Item().Text(Model.Comments).Style(InvoiceStyle.Normal);
});
}
}
}

Step 5: Create the Program Entry Point

Finally, let's create the main program that ties everything together. This file will set up the QuestPDF license, create sample invoice data, and generate the final PDF document.

Create a file named Program.cs:

Program.cs
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

namespace InvoiceGenerator
{
class Program
{
static void Main(string[] args)
{
// Set license
QuestPDF.Settings.License = LicenseType.Community;

// Create and generate the invoice
var document = new InvoiceDocument(GetSampleInvoice());
document.GeneratePdf("invoice.pdf");

Console.WriteLine("Invoice generated successfully: invoice.pdf");
}

// Create a sample invoice for testing
static InvoiceModel GetSampleInvoice() => new()
{
InvoiceNumber = $"INV-{DateTimeOffset.Now.ToUnixTimeSeconds()}",
IssueDate = DateTime.Now,
DueDate = DateTime.Now.AddDays(30),
PaymentAccountNumber = "9876543210",
PaymentReference = "INV-PAYMENT",

Seller = new CompanyInfo
{
Name = "Coffee Point",
Street = "404 Bean Not Found",
City = "Latteville",
State = "NY",
Zip = "12345",
Email = "beans@example.coffee",
Phone = "(555) 123-4567"
},

Customer = new CompanyInfo
{
Name = "Latte Lovers",
Street = "101 Cappuccino St.",
City = "Milktown",
State = "NY",
Zip = "10001",
Email = "accounts@latte.love",
Phone = "(555) 987-6543"
},

Items = new List<InvoiceItem>
{
new()
{
Name = "Custom Coffee Blend",
UnitPrice = 15.00m,
Quantity = 20
},
new()
{
Name = "Monthly Bean Subscription",
UnitPrice = 59.00m,
Quantity = 3
},
new()
{
Name = "Espresso Machine Maintenance",
UnitPrice = 115.00m,
Quantity = 1
},
new()
{
Name = "Espresso Support Package",
UnitPrice = 195.00m,
Quantity = 1
}
},

Comments =
"Please use the QR code for quick payment or include the invoice number with your payment. Thank you for your business!",
};
}
}
  1. Create an Assets folder in your project directory.
  2. Add your company logo as logo.png in this folder.

Step 7: Run the Application

dotnet run

This will generate an invoice.pdf file in your project directory.

The generated invoice will have:

  1. Header Section:

    • Company logo.
    • Invoice number and dates.
    • QR code containing payment information.
  2. Main Content:

    • Billing and shipping information sections.
    • Detailed table of invoice items.
    • Subtotal, tax, and grand total calculations.
    • Optional comments section.
  3. Footer:

    • Company name.
    • Page numbering

Coffee in hand ☕ – time to check out our Invoice PDF preview: Invoice PDF generated using QuestPDF in C#/.NET

Best Practices for Efficient PDF Generation with QuestPDF in .NET

To keep your PDF generation fast and maintainable, follow these best practices when working with QuestPDF in your .NET applications. For a broader look at approaches to generating PDFs in C#, see our library comparison guide. If you prefer a coordinate-based approach over QuestPDF's layout engine, our PDFsharp guide covers that alternative.

PracticeDescription
Modular DesignBreak down complex documents into reusable components and methods.
Memory ManagementUse streams for large documents to reduce memory usage.
Error HandlingWrap PDF generation in try-catch blocks with meaningful error messages.
Image OptimizationOptimize image size and resolution before adding to documents.
Font ManagementUse standard fonts or properly embed custom fonts.
Code OrganizationSeparate document structure, data, and styling concerns.
Hot-ReloadUse ShowInPreviewer() during development for real-time preview.

HTML to PDF Conversion Options

QuestPDF is designed for programmatic PDF creation through C# code, not HTML conversion. If you already have HTML content that needs to become a PDF, here are your options:

ApproachToolsBest ForConsLearn More
HTML ParsingiText 7Complex HTML documents with CSS styling support.Commercial license required for most use cases.HTML to PDF with iText 7 in C#
HTML to PDF APIsPDFBoltFull-featured conversions with complex layouts.External service.HTML to PDF API Guide
Browser AutomationPuppeteerSharp
Playwright
High-fidelity rendering of modern web apps.Higher resource usage, more complex setupGenerate PDF Using Playwright in C#/.NET
HTML to PDF in C#/.NET Using PuppeteerSharp

API-Based Alternative: Template-Driven PDF Generation

While QuestPDF gives you full programmatic control over PDF layout, it requires you to build every element in C# code – headers, tables, footers, and styling all live in your application. For teams that prefer designing templates visually or need to generate PDFs from existing HTML, a PDF generation API like PDFBolt offers a different workflow.

  • Design templates visually: Create invoice or report layouts in a template designer or pick from the template gallery.
  • Separate design from logic: Template changes do not require code deployments.
  • Simple integration: A single HTTP request from your C# application returns a finished PDF (see also our API integration essentials guide).
  • Full HTML/CSS support: Use standard web technologies for styling and layout.
Template-based API call in C#
using System;
using System.Net.Http;
using System.IO;
using System.Threading.Tasks;
using System.Text.Json;

public class PdfApiExample {
public static async Task Main(string[] args) {
using var client = new HttpClient();

var requestData = new {
templateId = "your-template-id",
templateData = new {
invoice_number = "INV-2025-503",
invoice_date = "May 3, 2025",
due_date = "June 2, 2025",
company_name = "Coffee Point",
company_address = "404 Bean Not Found, Latteville, NY 12345",
company_email = "beans@example.coffee",
company_phone = "(555) 123-4567",
company_logo = "https://img.pdfbolt.com/logo.png",
client_name = "Latte Lovers",
client_address = "101 Cappuccino St., Milktown, NY 10001",
client_email = "accounts@latte.love",
line_items = new object[] {
new {
description = "Custom Coffee Blend",
quantity = 20,
unit_price = 15.00
},
new {
description = "Monthly Bean Subscription",
quantity = 3,
unit_price = 59.00
}
}
}
};

var request = new HttpRequestMessage {
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.pdfbolt.com/v1/direct"),
Content = new StringContent(
JsonSerializer.Serialize(requestData),
System.Text.Encoding.UTF8,
"application/json"
)
};

request.Headers.Add("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");

try {
using var response = await client.SendAsync(request);

if (!response.IsSuccessStatusCode) {
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine($"Error Message: {errorContent}");
return;
}

var pdfBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("invoice_pdfbolt.pdf", pdfBytes);
Console.WriteLine("PDF generated successfully");
} catch (Exception ex) {
Console.WriteLine($"Error: {ex.Message}");
}
}
}

This approach works well when you need consistent document output without building every layout element in code, or when non-developers need to update PDF designs without touching the C# codebase.

Conclusion

QuestPDF works well for C# PDF generation. Its layout-based approach and fluent API make document creation much simpler than traditional libraries that rely on absolute positioning, and automatic pagination handles multi-page layouts without extra effort.

Between its text formatting, table support, and image handling, QuestPDF covers most programmatic PDF needs in .NET. The community around it is active too.

For HTML to PDF conversion, consider a dedicated tool like PDFBolt's HTML to PDF API – it handles rendering with headless Chrome and returns PDFs that preserve your CSS layouts and styles.

Thanks for reading – you've earned a coffee break (or three)! ☕☕☕