DEV Community

IronSoftware
IronSoftware

Posted on

Convert HTML to PDF in C# (.NET Guide)

Converting HTML to PDF in .NET should be straightforward — you have HTML, you want a PDF, there should be a library that handles the conversion. In practice, every approach I've tried over the years has had significant issues. CSS doesn't render correctly, fonts are missing, layouts break in ways that don't happen in browsers, or the library is so complex that generating a simple invoice requires 100+ lines of code.

I started with iTextSharp because it was the most recommended solution on Stack Overflow. The problem is iTextSharp doesn't actually convert HTML to PDF directly — it builds PDFs programmatically from code. The HTML add-on exists but it's limited, expensive, and the API is unintuitive. Generating an invoice meant writing code to create document objects, add paragraphs, manage fonts, calculate layouts manually. Changing the invoice design required modifying dozens of lines of PDF construction code.

Then I tried wkhtmltopdf through various .NET wrappers (Rotativa, DinkToPdf, NReco). This finally worked — actual HTML-to-PDF conversion using a real rendering engine. The catch is wkhtmltopdf uses a WebKit snapshot from 2015 and hasn't been maintained since 2019. Modern CSS features don't work. Flexbox is buggy. Grid layouts fail entirely. JavaScript support is limited to ES5. Security vulnerabilities accumulate with no patches. It's a dead project.

Playwright and PuppeteerSharp are browser automation tools that can generate PDFs. They work well for testing scenarios where you're already managing browser instances, but for dedicated PDF generation they're overkill. You're launching full headless Chrome browsers, navigating to pages, waiting for loads, then extracting PDFs. Each PDF takes 800ms+ due to browser overhead. Memory consumption is high because each browser instance needs 100-200MB RAM.

IronPDF solved these problems by using Chromium's rendering engine directly without the browser wrapper. It's purpose-built for PDF generation rather than being a testing tool adapted for PDFs. If your HTML renders correctly in Chrome, it generates correctly as a PDF. No layout quirks, no CSS limitations, no JavaScript failures. Generation times are 100-300ms per PDF instead of 800ms+ with Playwright because there's no browser initialization overhead.

I've migrated invoice systems, report generators, and receipt systems from wkhtmltopdf and iTextSharp to IronPDF. The migration eliminated "PDF debugging" sessions where stakeholders complained that elements were misaligned or missing in PDFs despite looking correct in browsers. With Chromium rendering, what you see in Chrome is what you get in the PDF. This predictability is essential for production systems generating thousands of PDFs daily.

using IronPdf;
// Install via NuGet: Install-Package IronPdf

var renderer = new [ChromePdfRenderer](https://ironpdf.com/blog/videos/how-to-render-html-string-to-pdf-in-csharp-ironpdf/)();
var html = "<h1>Invoice #12345</h1><p>Total: $1,234.56</p>";
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
Enter fullscreen mode Exit fullscreen mode

That's the minimal case — create a renderer, pass HTML, save the PDF. The renderer handles Chromium initialization, HTML parsing, CSS application, layout calculation, and PDF construction. For production systems, create one renderer instance and reuse it across multiple PDFs to amortize the initialization cost.

Why Is Converting HTML to PDF in C# So Difficult?

HTML and PDF are fundamentally different technologies. HTML is designed for dynamic, responsive layouts that adapt to screen sizes and user interaction. PDF is designed for fixed-page documents with precise positioning, pagination, and print output. Bridging these paradigms requires a rendering engine that understands modern HTML/CSS/JavaScript but can output fixed-page documents.

The difficulty comes from the complexity of web standards. Modern HTML5, CSS3, and JavaScript specifications are massive. Implementing them correctly requires browser-grade rendering engines maintained by large teams. This is why lightweight PDF libraries struggle — they implement subsets of HTML/CSS from years ago but can't keep up with evolving standards.

iTextSharp doesn't even attempt full HTML rendering. It's a PDF manipulation library that can construct PDFs programmatically or convert very simple HTML. For complex documents, you're writing code to build PDF structure directly. This worked when PDFs were simple reports with text and tables, but modern documents need brand styling, responsive layouts, charts, and dynamic content.

wkhtmltopdf was revolutionary when it launched because it used WebKit, a real browser engine. The problem is the developers forked WebKit in 2015 and stopped updating. Web standards evolved dramatically since then — CSS Grid, Flexbox improvements, modern JavaScript, new HTML elements. wkhtmltopdf can't render these features because its engine is frozen in time.

Browser automation tools (Playwright, Puppeteer) solve the standards problem by using actual browsers. They support everything Chrome supports because they are Chrome. The trade-off is overhead. Browsers are designed for user interaction, not headless PDF generation. Starting browsers, navigating pages, and managing browser lifecycle adds latency and resource consumption that purpose-built PDF libraries avoid.

What Are the Common Approaches to HTML-to-PDF Conversion?

I've used four main approaches over the years, each with different trade-offs.

Programmatic PDF construction (iTextSharp, PDFsharp) means writing code that creates PDF objects — pages, text blocks, images, layout containers. You're not converting HTML; you're building PDFs from scratch using APIs. This gives precise control but is labor-intensive. Changing a document layout requires code changes rather than editing HTML templates. I use this approach only when I need low-level PDF manipulation unavailable in HTML conversion libraries.

Legacy HTML conversion (wkhtmltopdf wrappers) uses the outdated WebKit engine. It works for simple HTML but fails with modern CSS or JavaScript. I actively migrate systems away from wkhtmltopdf because it's unmaintained, insecure, and becoming increasingly incompatible with modern web development practices. If you're starting a new project in 2025, skip this approach entirely.

Browser automation (Playwright, PuppeteerSharp) launches headless Chrome instances to render pages and export PDFs. This works excellently for testing or occasional PDF generation but has performance overhead for high-volume scenarios. Each PDF requires browser startup (300-500ms), page navigation, and browser shutdown. Resource consumption is high. I use browser automation when I'm already managing browser instances for testing and PDF generation is secondary.

Purpose-built Chromium rendering (IronPDF) uses Chromium's rendering engine without the browser wrapper. It supports modern web standards like browser automation but is optimized for PDF generation specifically. Generation times are 3-5x faster than browser automation, resource consumption is lower, and the API is designed around PDF workflows rather than testing scenarios. This is my default choice for production PDF generation systems.

How Does IronPDF Simplify HTML to PDF Conversion?

IronPDF's API abstracts the complexity of Chromium rendering and PDF generation into simple method calls. You work with high-level concepts — HTML content, rendering options, PDF output — rather than managing browsers, processes, or PDF structure.

The simplest case is rendering HTML strings:

var renderer = new ChromePdfRenderer();

var html = @"
<html>
<head>
    <style>
        body { font-family: Arial; margin: 40px; }
        .header { color: #333; font-size: 24px; }
        .total { font-weight: bold; }
    </style>
</head>
<body>
    <div class='header'>Invoice #12345</div>
    <p>Customer: Acme Corp</p>
    <p class='total'>Total: $1,234.56</p>
</body>
</html>
";

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("invoice.pdf");
Enter fullscreen mode Exit fullscreen mode

The HTML is standard web HTML with embedded styles. CSS applies normally — fonts, colors, margins, layouts. This is HTML you'd write for a web page, and it renders identically in the PDF. No special PDF-specific markup, no layout limitations, no style restrictions.

For documents with external assets (images, CSS files), use base URLs to resolve relative paths:

renderer.RenderingOptions.BaseUrlPath = @"C:\Templates\";

var html = @"
<html>
<head>
    <link rel='stylesheet' href='invoice.css' />
</head>
<body>
    <img src='logo.png' />
    <h1>Invoice</h1>
</body>
</html>
";

var pdf = renderer.RenderHtmlAsPdf(html);
Enter fullscreen mode Exit fullscreen mode

The base URL tells the renderer where to find invoice.css and logo.png. It resolves relative URLs against this path. I use this pattern for template-based document generation where HTML templates, stylesheets, and images are managed as separate files in a templates directory.

Can I Convert URLs to PDF?

Yes, IronPDF can fetch and render URLs directly. This is useful for archiving web content, capturing dashboard snapshots, or generating PDFs from existing web applications.

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderUrlAsPdf("https://example.com/report");
pdf.SaveAs("report.pdf");
Enter fullscreen mode Exit fullscreen mode

The renderer handles the HTTP request, waits for page load, executes JavaScript, applies CSS, and renders the final page state. This is identical to how Chrome would display the page, ensuring accurate rendering of dynamic content.

For pages with asynchronous content loading (React apps, dashboards with API calls, charts that render after page load), add rendering delays:

renderer.RenderingOptions.WaitFor.RenderDelay(2000);  // Wait 2 seconds
var pdf = renderer.RenderUrlAsPdf("https://example.com/dashboard");
Enter fullscreen mode Exit fullscreen mode

This delays PDF generation for 2 seconds after initial page load, giving JavaScript time to fetch data and render content. I use this for dashboards where the initial HTML is minimal and JavaScript populates everything dynamically.

For more precise control, wait for specific elements:

renderer.RenderingOptions.WaitFor.HtmlElement("#content-loaded");
Enter fullscreen mode Exit fullscreen mode

This waits until an element with ID content-loaded appears in the DOM. Your JavaScript adds this element after async operations complete, signaling the page is ready for PDF generation. More reliable than fixed delays because it responds to actual content state rather than guessing timing.

How Do I Convert HTML Files to PDF?

Converting HTML files is useful for template-based PDF generation — loading template files, performing substitutions, generating PDFs. This separates document design (HTML files) from generation logic (C# code).

var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlFileAsPdf("invoice-template.html");
pdf.SaveAs("output.pdf");
Enter fullscreen mode Exit fullscreen mode

The file path can be absolute or relative to the working directory. The renderer loads the HTML and resolves asset references (images, CSS) relative to the file's location. This makes templates portable — put HTML, CSS, and images in a folder, and relative references work automatically.

For batch generation with placeholders:

var template = File.ReadAllText("invoice-template.html");

foreach (var invoice in invoices)
{
    var html = template
        .Replace("{{INVOICE_NUMBER}}", invoice.Number)
        .Replace("{{CUSTOMER}}", invoice.CustomerName)
        .Replace("{{TOTAL}}", invoice.Total.ToString("C"));

    var pdf = renderer.RenderHtmlAsPdf(html);
    pdf.SaveAs($"invoice-{invoice.Number}.pdf");
}
Enter fullscreen mode Exit fullscreen mode

This loads the template once, performs string replacements per invoice, and generates PDFs. It's simple and works well for straightforward substitutions. For complex templates with loops or conditionals, use a templating engine like Handlebars.NET or Razor instead of string replacement.

Does IronPDF Support Modern CSS and JavaScript?

Yes, because it uses Chromium's rendering engine. If your HTML renders correctly in Chrome, it generates correctly as a PDF. This includes modern CSS features (Flexbox, Grid, transforms, custom properties), JavaScript frameworks (React, Vue, Angular), and web fonts.

CSS example with modern features:

var html = @"
<html>
<head>
<style>
    .container {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 20px;
    }
    .item {
        background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);
        padding: 20px;
        border-radius: 8px;
    }
</style>
</head>
<body>
    <div class='container'>
        <div class='item'>Item 1</div>
        <div class='item'>Item 2</div>
    </div>
</body>
</html>
";

var pdf = renderer.RenderHtmlAsPdf(html);
Enter fullscreen mode Exit fullscreen mode

CSS Grid, gradients, and border-radius all render correctly. With wkhtmltopdf, Grid wouldn't work and gradients might fail. With IronPDF, if Chrome supports it, the PDF supports it.

JavaScript example:

var html = @"
<html>
<head>
<script src='https://cdn.jsdelivr.net/npm/chart.js'></script>
</head>
<body>
    <canvas id='chart'></canvas>
    <script>
        new Chart(document.getElementById('chart'), {
            type: 'bar',
            data: { labels: ['A', 'B', 'C'], datasets: [{ data: [10, 20, 30] }] }
        });
        document.body.innerHTML += '<div id=\"chart-ready\"></div>';
    </script>
</body>
</html>
";

renderer.RenderingOptions.WaitFor.HtmlElement("#chart-ready");
var pdf = renderer.RenderHtmlAsPdf(html);
Enter fullscreen mode Exit fullscreen mode

The JavaScript loads Chart.js, renders a chart, then adds a #chart-ready element. The renderer waits for this element before generating the PDF, ensuring the chart is fully rendered. I've used this pattern for dashboards with D3.js visualizations, charts, and dynamic tables.

How Do I Add Headers, Footers, and Page Numbers?

IronPDF generates headers and footers from HTML with support for placeholders like page numbers and dates.

var renderer = new ChromePdfRenderer();

renderer.RenderingOptions.HtmlHeader = new IronPdf.Rendering.HtmlHeaderFooter
{
    HtmlFragment = "<div style='text-align: center;'>Company Confidential</div>",
    DrawDividerLine = true
};

renderer.RenderingOptions.HtmlFooter = new IronPdf.Rendering.HtmlHeaderFooter
{
    HtmlFragment = "<div style='text-align: center;'>Page {page} of {total-pages}</div>"
};

var pdf = renderer.RenderHtmlAsPdf("<h1>Document Content</h1>");
Enter fullscreen mode Exit fullscreen mode

The {page} and {total-pages} placeholders are replaced automatically. The divider line draws a horizontal rule separating header from content. I use this for reports where page numbers help readers navigate printed copies.

For complex headers with multiple elements:

renderer.RenderingOptions.HtmlHeader = new IronPdf.Rendering.HtmlHeaderFooter
{
    HtmlFragment = @"
        <div style='display: flex; justify-content: space-between; padding: 0 20px;'>
            <div>Invoice Report</div>
            <div>{date:yyyy-MM-dd}</div>
        </div>
    "
};
Enter fullscreen mode Exit fullscreen mode

This creates a two-column header with document title on the left and current date on the right. The {date} placeholder supports .NET date format strings. Full CSS works in headers and footers, allowing complex designs.

Can I Control Margins and Page Size?

Yes, rendering options control PDF page dimensions and margins.

var renderer = new ChromePdfRenderer();

renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.PaperOrientation = IronPdf.Rendering.PdfPaperOrientation.Portrait;

renderer.RenderingOptions.MarginTop = 40;
renderer.RenderingOptions.MarginBottom = 40;
renderer.RenderingOptions.MarginLeft = 20;
renderer.RenderingOptions.MarginRight = 20;

var pdf = renderer.RenderHtmlAsPdf(html);
Enter fullscreen mode Exit fullscreen mode

Paper sizes include Letter, A4, Legal, and custom dimensions. Margins are in millimeters. I use Letter for US documents, A4 for international distribution. Margins of 40mm top/bottom provide space for headers and footers, 20mm sides for comfortable reading.

For custom page sizes:

renderer.RenderingOptions.CustomPaperWidth = 210;  // Millimeters
renderer.RenderingOptions.CustomPaperHeight = 297; // A4 dimensions
Enter fullscreen mode Exit fullscreen mode

This defines exact page dimensions. Useful for labels, specialized forms, or non-standard document sizes.

What About Razor Views and ASP.NET MVC?

IronPDF integrates with ASP.NET MVC and Razor views through a separate package designed for rendering Razor templates to PDF.

For MVC applications, render views to PDFs:

// In your controller
public IActionResult DownloadInvoice(int id)
{
    var invoice = GetInvoiceData(id);

    var renderer = new ChromePdfRenderer();
    var html = RazorViewToString("InvoiceTemplate", invoice);
    var pdf = renderer.RenderHtmlAsPdf(html);

    return File(pdf.BinaryData, "application/pdf", "invoice.pdf");
}

private string RazorViewToString(string viewName, object model)
{
    // Helper method to render Razor view as string
    // (Implementation depends on your MVC setup)
}
Enter fullscreen mode Exit fullscreen mode

This renders a Razor view with model data, converts the HTML output to PDF, and returns it as a file download. I use this pattern for generating invoices, reports, and receipts directly from MVC controllers without creating intermediate files.

The advantage of Razor integration is you can use full MVC view features — layouts, partial views, view models, HTML helpers. Your PDF templates are maintainable Razor views rather than static HTML files.

How Do I Handle CSS Media Types?

CSS supports media queries for different output types. Setting media type to Print applies @media print rules, optimizing documents for printing. Setting it to Screen uses @media screen rules, rendering exactly as displayed in browsers.

renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Print;
Enter fullscreen mode Exit fullscreen mode

Print media type is default and typically appropriate for most PDFs. Print styles hide navigation, remove backgrounds for ink savings, and adjust colors for printed output.

For documents designed specifically for PDF with full colors and graphics:

renderer.RenderingOptions.CssMediaType = IronPdf.Rendering.PdfCssMediaType.Screen;
renderer.RenderingOptions.PrintHtmlBackgrounds = true;
Enter fullscreen mode Exit fullscreen mode

Screen media type with backgrounds enabled renders exactly as shown in browsers. I use this for marketing materials, brochures, or branded documents where colors and graphics matter.

Quick Reference

Task Method Notes
HTML string to PDF renderer.RenderHtmlAsPdf(html) Most common pattern
URL to PDF renderer.RenderUrlAsPdf(url) Fetches and renders
HTML file to PDF renderer.RenderHtmlFileAsPdf(path) Template files
Set page size RenderingOptions.PaperSize = A4 Letter, A4, Legal, custom
Set margins RenderingOptions.MarginTop = 40 Millimeters
Add header RenderingOptions.HtmlHeader = ... HTML with placeholders
Add footer RenderingOptions.HtmlFooter = ... Page numbers, dates
CSS media type RenderingOptions.CssMediaType = Print Print or Screen
Rendering delay RenderingOptions.WaitFor.RenderDelay(ms) For async content
Wait for element RenderingOptions.WaitFor.HtmlElement("#id") Precise timing

Key Principles:

  • Create one renderer instance, reuse for all PDFs (expensive initialization)
  • Use HTML strings for dynamic documents, files for templates
  • Set base URLs when HTML references external assets
  • Apply rendering delays for JavaScript-heavy pages
  • Use print media for standard documents, screen for pixel-perfect rendering
  • Avoid wkhtmltopdf (unmaintained, insecure, outdated)
  • Prefer IronPDF over Playwright for PDF-focused workflows

The complete PDF conversion tutorial covers advanced scenarios including Razor views, ASPX conversion, and XML-to-PDF workflows.


Written by Jacob Mellor, CTO at Iron Software. Jacob created IronPDF and leads a team of 50+ engineers building .NET document processing libraries.

Top comments (0)