65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Chrome 在 ASP.NET WebForms 中将 HTML 转换为 PDF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (10投票s)

2022年11月17日

CPOL

5分钟阅读

viewsIcon

19564

downloadIcon

402

使用 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。

  1. 将页面边距设置为 0(零)
  2. 设置纸张大小
  3. 将所有内容包裹在一个具有固定宽度和边距的“div”中
  4. 使用 page-break-always 的 CSS 在页面之间进行分隔。
  5. 所有字体必须已安装或托管在您的网站上
  6. 图片、外部 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.exeURL”的方法。

我们将该方法称为“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 转换器的建议。
© . All rights reserved.