Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Convert HTML to PDF with Chrome in ASP.NET WebForms

4.76/5 (10 votes)
17 Nov 2022CPOL5 min read 18.9K   380  
Generate PDF from HTML with Chrome
Using Chrome as PDF Generator to Convert HTML into PDF

Image 1

*Note: Please use Microsoft Edge as a better option. Read more at: https://www.codeproject.com/Articles/5348585/Convert-HTML-to-PDF-by-Using-Microsoft-Edge-in-ASP

*Update: 06 Dec 2022

There are some drawbacks for using Chrome.exe as HTML to PDF converter.

The main drawback is the permission to execute an EXE. Due to security issues, web hosting environments will prohibit the direct execution of any EXE, this includes "Chrome.exe".

If you try to run this at local IIS, you have to set the identity of Application Pool to "LocalSystem" to allow the pool to run external EXE.

Image 2

Although it is still possible to run Chrome.exe over a web server, it's highly not recommended to do so. That's why using Microsoft Edge is a very good alternative compared to "Chrome.exe".

Read more about Using Microsoft Edge to Convert HTML to PDF

The Basic Idea

Chrome has a built-in function for generating PDF for an HTML page.

According to the info that I gathered while doing this research, all chromium based web browsers work the same way, but I haven't tested this yet for other chromium based web browser.

Here's the basic command line for running the Chrome.exe to generate PDF with arguments/switches:

chrome.exe

// arguments:
--headless
--disable-gpu
--run-all-compositor-stages-before-draw
--print-to-pdf="{filePath}"
{url}

Full command line example:

C:\Program Files\Google\Chrome\Application\chrome.exe --headless 
--disable-gpu --run-all-compositor-stages-before-draw 
--print-to-pdf="D:\test\web_pdf\pdf_chrome\temp\pdf\345555635.pdf" 
http://localhost:55977/temp/pdf/345555635.html

Based on this, I have written a simple C# class library to automate the execution of this process.

You can now generate the PDF in one simple line.

This will transmit PDF as an attachment for download:

C#
pdf.GeneratePdfAttachment(html, "file.pdf");

and this will open up the PDF in browser:

C#
pdf.GeneratePdfInline(html);

and... yup, it's done. Just like that.

Okay, let's dive into some of the important details.

Important CSS Properties

There are a few necessary CSS that you have to include in the HTML page in order for this to work properly.

  1. Set page margin to 0 (zero)
  2. Set paper size
  3. Wrap all content within a "div" with fixed width and margin
  4. Use CSS of page-break-always to split between pages.
  5. All fonts must already be installed or hosted on your website
  6. URL links for images, external css stylesheet reference must include the root path.

1. Set Page Margin to 0 (zero)

CSS
@page {
    margin: 0;
}

The purpose of doing this is to hide the header and footer:

Image 3

2. Set Paper Size

Example 1

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

Example 2

CSS
@page {
    margin: 0;
    size: letter landscape;
}

Example 3

custom size (inch) *width then height

CSS
@page {
    margin: 0;
    size: 4in 6in;
}

Example 4

custom size (cm) *width then height

CSS
@page {
    margin: 0;
    size: 14cm 14cm;
}

For more options/info on the CSS of @page, you may refer to this link.

3. Wrap All Content Within a DIV with Fixed Width and Margin

Example:

HTML
<div class="page">
    <h1>Page 1</h1>
    <img src="/pdf.jpg" style="width: 100%; height: auto;" />
    <!-- The rest of the body content -->
</div>

Style the "div" with class "page" (act as the main block/wrapper/container). Since the page has zero margin, we need to manually specified the top margin in CSS:

CSS
.page {
    width: 18cm;
    margin: auto;
    margin-top: 10mm;
}

The width has to be specified.

The "margin: auto" will align the div block at center horizontally.

"margin-top: 10mm", will provide space between the main block and the edge of the paper at top section.

4. Use CSS of "Page-Break-After" to Split Between Pages

To split pages, use a "div" and style with CSS of "page-break-after".

CSS
page-break-after: always

Example:

HTML
<div class="page">
    <h1>Page 1</h1>
    <img src="/pdf.jpg" style="width: 100%; height: auto;" />
</div>

<div style="page-break-after: always"></div>

<div class="page">
    <h1>Page 2</h1>
    <img src="/pdf.jpg" style="width: 100%; height: auto;" />
</div>

<div style="page-break-after: always"></div>

<div class="page">
    <h1>Page 3</h1>
    <img src="/pdf.jpg" style="width: 100%; height: auto;" />
</div>

5. All Fonts Must Already Installed or Hosted in Your Website

The font rendering might not be working properly if the fonts are hosted at 3rd party's server, for example: Google Fonts. Try installing the fonts into your server Windows OS or host the fonts within your website.

6. URL links for images, external css stylesheet reference must include the root path.

For example, the following img tag might not be rendered properly. The image has the potential to be missing in the final rendered PDF output.

HTML
<img src="logo.png" />

In stead, include the root path like this:

HTML
<img src="/logo.png" />

or

HTML
<img src="/images/logo.png" />

The Sample of Full HTML Page

HTML
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style type="text/css">
        h1 {
            margin: 0;
            padding: 0;
        }
        .page {
            margin: auto;
            margin-top: 10mm;
            border: 1px solid black;
            width: 18cm;
            height: 27cm;
        }

        @page {
            margin: 0;
            size: A4 portrait;
        }
    </style>
</head>

<body>

    <div class="page">
        <h1>Page 1</h1>
        <img src="/pdf.jpg" style="width: 100%; height: auto;" />
    </div>

    <div style="page-break-after: always"></div>

    <div class="page">
        <h1>Page 2</h1>
        <img src="/pdf.jpg" style="width: 100%; height: auto;" />
    </div>

    <div style="page-break-after: always"></div>

    <div class="page">
        <h1>Page 3</h1>
        <img src="/pdf.jpg" style="width: 100%; height: auto;" />
    </div>

</body>

</html>

The C# Code that Works Behind

Here, I'll explain how the code works behind.

Preparing the main method:

C#
public static void GeneratePdf(string url, string filePath)
{

}

Identify the file path of the Chrome.exe. Locating it at two common locations:

C#
public static void GeneratePdf(string url, string filePath)
{
    var chromePath = @"C:\Program Files\Google\Chrome\Application\chrome.exe";

    if (!File.Exists(chromePath))
    {
        string userfolder = Environment.GetFolderPath
                            (Environment.SpecialFolder.UserProfile);
        chromePath = 
        $@"{userfolder}\AppData\Local\Google\Chrome\Application\chrome.exe";
    }

    if (!File.Exists(chromePath))
    {
        throw new Exception("Unable to locate Chrome.exe");
    }
}

Add the following "using" statement:

C#
using System.Diagnostics;
using System.IO;

Initiate a "process" to run the Chrome.exe with arguments:

C#
public static void GeneratePdf(string url, string pdfFilePath)
{
    var chromePath = @"C:\Program Files\Google\Chrome\Application\chrome.exe";

    if (!File.Exists(chromePath))
    {
        string userfolder = Environment.GetFolderPath
                            (Environment.SpecialFolder.UserProfile);
        chromePath = 
        $@"{userfolder}\AppData\Local\Google\Chrome\Application\chrome.exe";
    }

    if (!File.Exists(chromePath))
    {
        throw new Exception("Unable to locate Chrome.exe");
    }

    using (var p = new Process())
    {
        p.StartInfo.FileName = chromePath;
        p.StartInfo.Arguments = $"--headless --disable-gpu 
        --run-all-compositor-stages-before-draw --print-to-pdf=\"{pdfFilePath}\" {url}";
        p.Start();
        p.WaitForExit();
    }
}

Above will generate the PDF file.

The Chrome.exe requires a URL to generate the PDF file.

Hence, the below code will generate the required URL:

Prepare an "enum" variable:

C#
public enum TransmitMethod
{
    None,
    Attachment,
    Inline
}

Preparing the method for generating the "URL" for Chrome.exe.

Let's call the method as "ChromePublish":

C#
static void ChromePublish(string html, TransmitMethod transmitMethod, string filename)
{
    
}

Inside the method "ChromePublish", first, create a temporary directory for saving the temporary files:

C#
string folderTemp = HttpContext.Current.Server.MapPath("~/temp/pdf");

if (!Directory.Exists(folderTemp))
{
    Directory.CreateDirectory(folderTemp);
}

Then, create the filename and path for two temporary files, the HTML and the PDF

C#
Random rd = new Random();

string randomstr = rd.Next(100000000, int.MaxValue).ToString();

string fileHtml = HttpContext.Current.Server.MapPath($"~/temp/pdf/{randomstr}.html");
string filePdf = HttpContext.Current.Server.MapPath($"~/temp/pdf/{randomstr}.pdf");

Generate the HTML file and save it at local:

C#
File.WriteAllText(fileHtml, html);

Obtain the URL of the HTML file:

C#
var r = HttpContext.Current.Request.Url;
string url = $"{r.Scheme}://{r.Host}:{r.Port}/temp/pdf/{randomstr}.html";

Execute the method that we have created previously to generate the PDF file on local server:

C#
GeneratePdf(url, filePdf);

Obtain the file size:

C#
FileInfo fi = new FileInfo(filePdf);
string filelength = fi.Length.ToString();

Load the PDF file into byte array:

C#
byte[] ba = File.ReadAllBytes(filePdf);

Delete (clean up) the temporary files from server, they are not needed any more:

C#
try
{
    File.Delete(filePdf);
}
catch { }

try
{
    File.Delete(fileHtml);
}
catch { }

Next, preparing the PDF to be transmitted.

Clear off all the content of "response":

C#
HttpContext.Current.Response.Clear();

Specify the type of "Content-Disposition" into the response header.

Read more here.

C#
if (transmitMethod == TransmitMethod.Inline)
{
    HttpContext.Current.Response.AddHeader("Content-Disposition", "inline");
}
else if (transmitMethod == TransmitMethod.Attachment)
{
    HttpContext.Current.Response.AddHeader("Content-Disposition", 
         $"attachment; filename=\"{filename}\"");
}

Finally, transmit the data (the PDF):

C#
HttpContext.Current.Response.ContentType = "application/pdf";
HttpContext.Current.Response.AddHeader("Content-Length", filelength);
HttpContext.Current.Response.BinaryWrite(ba);
HttpContext.Current.Response.End();

Here's the full code:

C#
static void ChromePublish(string html, TransmitMethod transmitMethod, string filename)
{
    string folderTemp = HttpContext.Current.Server.MapPath("~/temp/pdf");

    if (!Directory.Exists(folderTemp))
    {
        Directory.CreateDirectory(folderTemp);
    }

    Random rd = new Random();

    string randomstr = rd.Next(100000000, int.MaxValue).ToString();

    string fileHtml = HttpContext.Current.Server.MapPath($"~/temp/pdf/{randomstr}.html");
    string filePdf = HttpContext.Current.Server.MapPath($"~/temp/pdf/{randomstr}.pdf");

    File.WriteAllText(fileHtml, html);

    var r = HttpContext.Current.Request.Url;
    string url = $"{r.Scheme}://{r.Host}:{r.Port}/temp/pdf/{randomstr}.html";

    GeneratePdf(url, filePdf);

    FileInfo fi = new FileInfo(filePdf);
    string filelength = fi.Length.ToString();
    byte[] ba = File.ReadAllBytes(filePdf);

    try
    {
        File.Delete(filePdf);
    }
    catch { }

    try
    {
        File.Delete(fileHtml);
    }
    catch { }

    HttpContext.Current.Response.Clear();

    if (transmitMethod == TransmitMethod.Inline)
        HttpContext.Current.Response.AddHeader("Content-Disposition", "inline");
    else if (transmitMethod == TransmitMethod.Attachment)
        HttpContext.Current.Response.AddHeader
        ("Content-Disposition", $"attachment; filename=\"{filename}\"");

    HttpContext.Current.Response.ContentType = "application/pdf";
    HttpContext.Current.Response.AddHeader("Content-Length", filelength);
    HttpContext.Current.Response.BinaryWrite(ba);
    HttpContext.Current.Response.End();
}

Lastly, creates two simple methods to wrap around the method "ChromePublish()":

C#
public static void GeneratePdfInline(string html)
{
    ChromePublish(html, TransmitMethod.Inline, null);
}

public static void GeneratePdfAttachment(string html, string filenameWithPdf)
{
    ChromePublish(html, TransmitMethod.Attachment, filenameWithPdf);
}

Since the PDF might takes a few seconds to generate, you can also consider displaying a "Loading" message to user while generating the PDF.

For example:

HTML
<div id="divLoading" class="divLoading" onclick="hideLoading();">
    <img src="loading.gif" /><br />
    Generating PDF...
</div>

Style the "div" message box:

CSS
.divLoading {
    width: 360px;
    font-size: 20pt;
    font-style: italic;
    font-family: Arial;
    z-index: 9;
    position: fixed;
    top: calc(50vh - 150px);
    left: calc(50vw - 130px);
    border: 10px solid #7591ef;
    border-radius: 25px;
    padding: 10px;
    text-align: center;
    background: #dce5ff;
    display: none;
    font-weight: bold;
}

Here is how it looks like:

Image 4

Here's the JavaScript to display the loading message:

JavaScript
<script type="text/javascript">
    function showLoading() {
        let d = document.getElementById("divLoading");
        d.style.display = "block";
        setTimeout(hideLoading, 2000);
    }

    function hideLoading() {
        let d = document.getElementById("divLoading");
        d.style.display = "none";
    }
</script>

Show the message while the user clicks on the button to generate the PDF. Add the following attribute to the button to execute the javascript:

OnClientClick="showLoading();"

Example:

<asp:Button ID="btGeneratePdfAttachment" runat="server"
    Text="Generate PDF (download as attachment)" 
    OnClick="btGeneratePdfAttachment_Click" 
    OnClientClick="showLoading();" />

You may download the source code of this article to have a better understanding of how the whole thing works in action.

Thanks for reading and happy coding!!

History

  • 18th November, 2022: Initial version
  • 21st November, 2022: Demo source code update. (Include more samples, improve UI) v1.1
  • 6th December, 2022: Added the recommendation of using Microsoft Edge in stead of Chrome as HTML to PDF converter.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)