Convert HTML to PDF with Chrome in ASP.NET WebForms

*Note: Please use Microsoft Edge as a better option. Read more:

https://adriancs.com/aspnet-webforms/466/convert-html-to-pdf-by-using-microsoft-edge-in-asp-net/

*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.

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 a HTML page.

According to the info that I gathered during doing this research, all chromium based web browser works 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\pdf5555635.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 1 simple line.

This will transmit PDF as attachment for download:

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

and this will open up the PDF in browser:

pdf.GeneratePdfInline(html);

and… yupe, 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 installed or hosted in your website
  6. URL links for images, external css stylesheet reference must include the root path.

1. Set page margin to 0 (zero)

@page {
    margin: 0;
}

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

2. Set paper size

Example 1:

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

Example 2:

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

Example 3: custom size (inch) *width then height

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

Example 4: custom size (cm) *width then height

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

For more options/info on the CSS of @page, you may refer:

https://developer.mozilla.org/en-US/docs/Web/CSS/@page/size

3. Wrap all content within a DIV with fixed width and margin

Example:

<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:

.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-always” to split between pages.

The split pages, use a “div” and style with CSS of “page-break-after“.

page-break-after: always

Example:

<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 install 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.

<img src="logo.png" />

In stead, include the root path like this:

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

or

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

The sample of full HTML page:

<!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 In Behind

Here, I’ll explain how the code works in behind.

Preparing the main method:

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

}

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

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:

using System.Diagnostics;
using System.IO;

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

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 an URL to generate the PDF file.

Hence, below code will generate the required URL:

Prepare an “enum” variable:

public enum TransmitMethod
{
    None,
    Attachment,
    Inline
}

Preparing the method for generating the “URL” for Chrome.exe.

Let’s call the method as “ChromePublish“:

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

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

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

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

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

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:

File.WriteAllText(fileHtml, html);

Obtain the URL of the HTML file:

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:

GeneratePdf(url, filePdf);

Obtain the file size:

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

Load the PDF file into byte array:

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

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

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

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

Next, preparing the PDF to be transmitted.

Clear off all the content of “response“:

HttpContext.Current.Response.Clear();

Specify the type of “Content-Disposition” into the response header.

Read more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition

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):

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:

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 2 simple method to wrap around the method “ChromePublish()“:

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 to display a “Loading” message to user while generating the PDF.

For example:

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

Style the “div” message box:

.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:

Here’s the javascript to display the loading message:

<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 click on the button to generate the PDF:

<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.

One Reply to “Convert HTML to PDF with Chrome in ASP.NET WebForms”

Comments are closed.