*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.
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:
pdf.GeneratePdfAttachment(html, "file.pdf");
and this will open up the PDF in browser:
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.
- Set page margin to 0 (zero)
- Set paper size
- Wrap all content within a "
div
" with fixed width and margin - Use CSS of
page-break-always
to split between pages. - All fonts must already be installed or hosted on your website
- 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 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;" />
<!--
</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-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>
<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.
<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 Behind
Here, I'll explain how the code works behind.
Preparing the main method:
public static void GeneratePdf(string url, string filePath)
{
}
Identify the file path of the Chrome.exe. Locating it at two 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 a URL to generate the PDF file.
Hence, the 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 two 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 here.
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 two simple methods 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 displaying 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 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.