Convert HTML to PDF by Using Microsoft Edge in ASP.NET

Introduction

If you are running on Windows Server 2019, you may download Microsoft Edge at: https://www.microsoft.com/en-us/edge/download

Here is the basic command line for doing the job:

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

Example:

msedge --headless --disable-gpu --run-all-compositor-stages-before-draw
--print-to-pdf="D:\\mysite\temp\pdf\2059060194.pdf"
http://localhost:50964/temp/pdf/2059060194.html

*Note: during real execution, all arguments must be on the same line.

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

To begin, you can either download the source code and add the C# class file “pdf_edge.cs” into your project, or install the Nuget Package (Html-PDF-Edge);

Then, at your project:

To generate PDF and download as attachment:

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

C# to generate PDF and display in browser:

pdf_edge.GeneratePdfInline(html);

Background

Previously, I posted an article about using Chrome as PDF generator to convert HTML to PDF.

Later I found out that Microsoft Edge can also do the same thing. Since Microsoft Edge is also a chromium based web browser, both Chrome and Edge shared the same parameters.

I have tested this implementation (using Edge) in the following environment:

  • Local IIS hosting
  • Web Hosting (smarterasp.net)
  • VPS Web Hosting

All above environments are able to generate PDF without issues. It runs smoothly without the need to configure the permission, Application Pool Identity and Website IIS authentication. I have a more seamless integration experience if compared to using Chrome.

The following screenshot shows that the execution of MS Edge is allowed even with default permission settings:

Chrome.exe, however is not so permissive in most environment. This is because executing an EXE over a web server is generally prohibited due to security issues.

For Chrome.exe, I failed to run it at web hosting environment (smarterasp.net).

Even in Local IIS hosting, I have to set the Application Pool Identify to “LocalSystem” in order for Chrome.exe to run properly.

But Microsoft Edge does not have such requirements. Microsoft Edge is able to be executed with lowest/default permission and security settings.

Therefore, it is highly recommended to use Microsoft Edge than Chrome.exe.

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.

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:

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

Example 4:

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

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

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

page-break-after: always

Example:

<div class="page">
    <h1>Page 1</h1>
</div>

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

<div class="page">
    <h1>Page 2</h1>
</div>

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

<div class="page">
    <h1>Page 3</h1>
</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.

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

Instead, include the root path like this:

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

The Class Object of “pdf_edge.cs”

Here I’ll explain a bit how the C# class works in behind.

As mentioned, the real work is done within the C# class of “pdf_edge.cs”.

Start off by adding 2 using statements:

using System.Diagnostics;
using System.IO;

Here is the main method that runs Microsoft Edge for converting HTML into PDF file:

public static void GeneratePdf(string url, string filePath)
{
    using (var p = new Process())
    {
        p.StartInfo.FileName = "msedge";
        
        // do not separate the arguments into lines in real code
        // or the execution will break
        
        p.StartInfo.Arguments = $@"--headless --disable-gpu 
            --run-all-compositor-stages-before-draw
            --print-to-pdf=""{filePath}"" {url}";
        p.Start();
        p.WaitForExit();
    }
}

**Above example breaks the arguments into multi lines is for documentation purpose only. Do not separate arguments into lines in real production.

The enum for defining the Transmit Method:

public enum TransmitMethod
{
    None,
    Attachment,
    Inline
}

Here is the code for preparing the URL for Microsoft Edge to generate the PDF:

static void EdgePublish(string html, TransmitMethod transmitMethod, string filename)
{
    // Set the target temporary folder for storing the PDF
    
    string folderTemp = HttpContext.Current.Server.MapPath("~/temp/pdf");

    // If the temporary folder is not existed, create it
    
    if (!Directory.Exists(folderTemp))
    {
        Directory.CreateDirectory(folderTemp);
    }

    // Set the temporary file path for both HTML and PDF files
    
    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");

    // Create the HTML file
    
    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";

    // Create the PDF file
    
    GeneratePdf(url, filePdf);

    // Load the file into memory (byte array)
    
    byte[] ba = File.ReadAllBytes(filePdf);

    // Delete the both temp files from server
    
    try
    {
        File.Delete(filePdf);
    }
    catch { }

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

    // Clear the response
    
    HttpContext.Current.Response.Clear();

    // set the transmission method
    
    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}\"");
    }

    // Transmit the PDF for download
    
    HttpContext.Current.Response.ContentType = "application/pdf";
    HttpContext.Current.Response.AddHeader("Content-Length", ba.Length.ToString());
    HttpContext.Current.Response.BinaryWrite(ba);
    HttpContext.Current.Response.End();
}

Displaying A Loading GIF While Generating PDF

After the user clicked on the “Generate PDF” button, due to that the web server might takes some (short) time to process the PDF, the page might appear to be “freezed” or no response (but actually yes). This might make the user become nervous and re-click on the button multiple times. Therefore, it’s a good idea to display a message or GIF animated loading image to give the user a piece of mind that the server is in the process of generating the PDF.

An example of a message box.

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

The Image: loading.gif

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

Example of Javascript that displays the message box:

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";
}

The effect of this implementation can be viewed at the live demo site.

Cheers 🙂

*Bonus – Generate PDF in WinForms

Well, turns out that, Microsoft Edge can also be used in WinForms to Convert HTML to PDF.

Since this is running in WinForms, there is no web server to supply an URL. Therefore, in stead of using an URL to tell MS Edge the source of HTML, we can tell Microsoft Edge to load a local file.

So, just replace the URL:

http://localhost:59403/temp/82348723.html

to local file path:

C:\web\temp\82348723.html

and execute the command line like this:

msedge --headless --disable-gpu 
--run-all-compositor-stages-before-draw
--print-to-pdf="C:\file.pdf"
"C:\web\temp\82348723.html"

In C#,

using (var p = new Process())
{
    p.StartInfo.FileName = "msedge";
    p.StartInfo.Arguments = $@"--headless --disable-gpu
        --run-all-compositor-stages-before-draw 
        --print-to-pdf=""C:\file.pdf"" ""C:\web\temp\82348723.html""";
    p.Start();
    p.WaitForExit();
}

**Above example breaks the arguments into multi lines is for documentation purpose only. Do not separate arguments into lines in real production.

and for all the media and resources, do not use root path.

This will be rendered:

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

This will not be rendered:

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