使用 Chrome 在 ASP.NET WebForms 中将 HTML 转换为 PDF
使用 Chrome 从 HTML 生成 PDF
*注意:建议使用 Microsoft Edge 作为更好的选择。更多信息请参阅: https://codeproject.org.cn/Articles/5348585/Convert-HTML-to-PDF-by-Using-Microsoft-Edge-in-ASP
*更新:2022 年 12 月 6 日
使用 Chrome.exe 作为 HTML 到 PDF 的转换器存在一些缺点。
主要缺点是对执行 EXE 的权限。由于安全问题,Web 托管环境将禁止直接执行任何 EXE,这包括“Chrome.exe”。
如果您尝试在本地 IIS 上运行此程序,您需要将应用程序池的身份设置为“LocalSystem”才能允许该池运行外部 EXE。
虽然仍然可以在 Web 服务器上运行 Chrome.exe,但强烈不建议这样做。这就是为什么使用 Microsoft Edge 与“Chrome.exe”相比是一个非常好的替代方案。
阅读更多关于 使用 Microsoft Edge 将 HTML 转换为 PDF
基本思路
Chrome 具有为 HTML 页面生成 PDF 的内置功能。
根据我在研究过程中收集到的信息,所有基于 Chromium 的网络浏览器的工作方式都相同,但我尚未在其他基于 Chromium 的网络浏览器上进行测试。
这是运行 Chrome.exe 以带参数/开关生成 PDF 的基本命令行
chrome.exe
// arguments:
--headless
--disable-gpu
--run-all-compositor-stages-before-draw
--print-to-pdf="{filePath}"
{url}
完整的命令行示例
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"
https://:55977/temp/pdf/345555635.html
在此基础上,我编写了一个简单的 C# 类库来自动化执行此过程。
您现在可以用一行简单的代码生成 PDF。
这将作为附件传输 PDF 以供下载
pdf.GeneratePdfAttachment(html, "file.pdf");
这将会在浏览器中打开 PDF
pdf.GeneratePdfInline(html);
是的,就这么简单。就这么完成了。
好了,让我们深入了解一些重要的细节。
重要的 CSS 属性
为了使此功能正常工作,您需要在 HTML 页面中包含一些必要的 CSS。
- 将页面边距设置为 0(零)
- 设置纸张大小
- 将所有内容包裹在一个具有固定宽度和边距的“
div
”中 - 使用
page-break-always
的 CSS 在页面之间进行分隔。 - 所有字体必须已安装或托管在您的网站上
- 图片、外部 CSS 样式表引用的 URL 链接必须包含根路径。
1. 将页面边距设置为 0(零)
@page {
margin: 0;
}
这样做的目的是隐藏页眉和页脚
2. 设置纸张大小
示例 1
@page {
margin: 0;
size: A4 portrait;
}
示例 2
@page {
margin: 0;
size: letter landscape;
}
示例 3
自定义尺寸(英寸)*宽度然后高度
@page {
margin: 0;
size: 4in 6in;
}
示例 4
自定义尺寸(厘米)*宽度然后高度
@page {
margin: 0;
size: 14cm 14cm;
}
有关 @page
CSS 的更多选项/信息,您可以参考 此链接。
3. 将所有内容包裹在一个具有固定宽度和边距的 DIV 中
示例
<div class="page">
<h1>Page 1</h1>
<img src="/pdf.jpg" style="width: 100%; height: auto;" />
<!-- The rest of the body content -->
</div>
为类名为“page
”的“div
”设置样式(充当主块/包装器/容器)。由于页面边距为零,我们需要在 CSS 中手动指定顶部边距
.page {
width: 18cm;
margin: auto;
margin-top: 10mm;
}
必须指定宽度。
“margin: auto
”会将 div
块在水平方向上居中对齐。
“margin-top: 10mm
”将在主块和顶部纸张边缘之间提供空间。
4. 使用“Page-Break-After”CSS 在页面之间进行分隔
要分割页面,请使用“div
”并用“page-break-after
”的 CSS 设置样式。
page-break-after: always
示例
<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. 所有字体必须已安装或托管在您的网站上
如果字体托管在第三方服务器(例如 Google Fonts)上,字体渲染可能无法正常工作。尝试将字体安装到您的服务器 Windows OS 或将字体托管在您的网站内。
6. 图片、外部 CSS 样式表引用的 URL 链接必须包含根路径。
例如,以下 img 标签可能无法正确渲染。图像有可能在最终渲染的 PDF 输出中丢失。
<img src="logo.png" />
而是包含根路径,如下所示
<img src="/logo.png" />
或
<img src="/images/logo.png" />
完整 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>
幕后工作的 C# 代码
在这里,我将解释代码是如何工作的。
准备主方法
public static void GeneratePdf(string url, string filePath)
{
}
确定 Chrome.exe 的文件路径。在两个常见位置进行查找
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");
}
}
添加以下“using
”语句
using System.Diagnostics;
using System.IO;
初始化一个“process
”来运行 Chrome.exe 并带参数
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();
}
}
以上将生成 PDF 文件。
Chrome.exe 需要一个 URL 来生成 PDF 文件。
因此,下面的代码将生成所需的 URL
准备一个“enum
”变量
public enum TransmitMethod
{
None,
Attachment,
Inline
}
准备生成 Chrome.exe“URL”的方法。
我们将该方法称为“ChromePublish
”
static void ChromePublish(string html, TransmitMethod transmitMethod, string filename)
{
}
在“ChromePublish
”方法内部,首先,创建一个临时目录用于保存临时文件
string folderTemp = HttpContext.Current.Server.MapPath("~/temp/pdf");
if (!Directory.Exists(folderTemp))
{
Directory.CreateDirectory(folderTemp);
}
然后,为两个临时文件(HTML 和 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");
生成 HTML 文件并保存在本地
File.WriteAllText(fileHtml, html);
获取 HTML 文件的 URL
var r = HttpContext.Current.Request.Url;
string url = $"{r.Scheme}://{r.Host}:{r.Port}/temp/pdf/{randomstr}.html";
执行我们之前创建的方法,在本地服务器上生成 PDF 文件
GeneratePdf(url, filePdf);
获取文件大小
FileInfo fi = new FileInfo(filePdf);
string filelength = fi.Length.ToString();
将 PDF 文件加载到字节数组中
byte[] ba = File.ReadAllBytes(filePdf);
删除(清理)服务器上的临时文件,它们不再需要了
try
{
File.Delete(filePdf);
}
catch { }
try
{
File.Delete(fileHtml);
}
catch { }
接下来,准备传输 PDF。
清除“response”的所有内容
HttpContext.Current.Response.Clear();
在响应头中指定“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}\"");
}
最后,传输数据(PDF)
HttpContext.Current.Response.ContentType = "application/pdf";
HttpContext.Current.Response.AddHeader("Content-Length", filelength);
HttpContext.Current.Response.BinaryWrite(ba);
HttpContext.Current.Response.End();
这是完整代码
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();
}
最后,创建两个简单的方法来包装“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);
}
由于 PDF 可能需要几秒钟才能生成,您也可以考虑在生成 PDF 时向用户显示“Loading
”消息。
例如
<div id="divLoading" class="divLoading" onclick="hideLoading();">
<img src="loading.gif" /><br />
Generating PDF...
</div>
为“div
”消息框设置样式
.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;
}
效果如下
这是显示加载消息的 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>
当用户点击按钮生成 PDF 时显示消息。将以下属性添加到按钮以执行 JavaScript
OnClientClick="showLoading();"
示例
<asp:Button ID="btGeneratePdfAttachment" runat="server" Text="Generate PDF (download as attachment)" OnClick="btGeneratePdfAttachment_Click" OnClientClick="showLoading();" />
您可以下载本文的源代码,以便更好地理解整个工作原理。
感谢阅读,编码愉快!!
历史
- 2022 年 11 月 18 日:初始版本
- 2022 年 11 月 21 日:演示源代码更新。(包含更多示例,改进 UI)v1.1
- 2022 年 12 月 6 日:添加了推荐使用 Microsoft Edge 而不是 Chrome 作为 HTML 到 PDF 转换器的建议。