在 App Service 透過 Html 產生 PDF

  • 1175
  • 0
  • 2021-03-21

使用 TuesPechkin 套件在 App Service 透過 Html 產生 PDF 的使用心得。

前言

最近自己和朋友都剛好有在 App Service 產生 PDF 的需求,在過去有很多套件都是基於 wkhtmltopdf 來實做的,現在要在 App Service 上使用,是否會因為 App Service 架構和限制而產生問題:

  1. 執行緒問題。
  2. 是否會沒有中文字形導致中文處理問題。
  3. 靜態(圖片、CSS、JavaScript)檔案路徑問題。
  4. 共用資源 App Service Plan 無法有足夠資源執行。

實做說明

安裝 TuesPechkin 套件

經過一些測試,發現 TuesPechkin 這一個套件可以正常在 App Service 使用,語法也算簡單,因此最後決定使用這一套來產生 PDF。

TuesPechkin 也是以 wkhtmltopdf 為基礎來開發的套件,我們可以直接透過 NuGet 來安裝套件,搜尋之後會有四個套件,其中 TuesPechkin 為主要套件,其餘三個則為對應 CPU 核心的 的 wkhtmltopdf 套件,而為了之後方便,我選擇 AnyCPU 的套件。

新增 PdfHelper

新增一個靜態類別 PdfHelper 來撰寫轉換 Html 成 PDF 的程式碼。

/// <summary>
/// Pdf 轉換工具
/// </summary>
public static class PdfHelper
{
    /* 多執行續類型的程式需使用 ThreadSafeConverter 並且將 Converter 放置在 Static */
    private static IConverter converter =
        new ThreadSafeConverter(
            new RemotingToolset<PdfToolset>(
                new WinAnyCPUEmbeddedDeployment(
                    new TempFolderDeployment())));

    /// <summary>
    /// 將Html文字 輸出到PDF檔裡
    /// </summary>
    /// <param name="htmlText"></param>
    /// <returns></returns>
    public static byte[] ConvertHtmlTextToPDF(string htmlText)
    {
        if (string.IsNullOrEmpty(htmlText))
        {
            return null;
        }

        var document = new HtmlToPdfDocument
        {
            GlobalSettings = { },
            Objects = {
                new ObjectSettings {
                        HtmlText = htmlText
                }
                }
        };

        var result = converter.Convert(document);
        return result;
    }
}
多執行續類型的程式需使用 ThreadSafeConverter 並且將 Converter 放置在 Static

準備要轉換的 Html

預先準備好要測試來轉換的 Html,這邊需要注意的是:

  1. CSS 中文字形需使用英文名稱
  2. 圖片路徑可支援完整路徑、Base64、實體檔案路徑
  3. JavaScript 路徑可支援完整路徑、實體檔案路徑 
<!DOCTYPE html>

<html lang="zh-tw" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>PDF 轉換測試</title>
    <style>
        body {
            margin: 0 auto;
            width: 21cm;
        }

        .text {
            font-size: x-large;
        }

        .text-pmingliu {
            font-family: "Microsoft PMingLiU";
        }

        .text-dfkai {
            font-family: DFKai-SB;
        }

        .text-jhenghei {
            font-family: "Microsoft JhengHei";
        }
    </style>
</head>
<body>
    <div>
        <!-- 中文測試 -->
        <h1>中文測試</h1>
        <p class="text text-pmingliu">新細明體</p>
        <p class="text text-dfkai">標楷體</p>
        <p class="text text-jhenghei">微軟正黑體</p>

        <!-- 圖片測試 -->
        <h1>圖片測試</h1>
        <img src="http://fakeimg.pl/250x100" />完整網址<br>
        <img src="data:image/png;base64,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" />Base64<br>
        <img src="{BasePath}/Images/250x100.png" />實體路徑<br>
        <!-- SVG 不支援 -->
        <svg id="svg-app-service" viewBox="0 0 50 50" width="100%" height="100%"> <path fill="#A0A1A2" d="M20.1,46.5H3.5V30h3.4c-0.4-1-0.6-2.1-0.6-3.3c0,0,0-0.1,0-0.2H0V50h23.6V36h-3.5V46.5z"></path> <path fill="#A0A1A2" d="M43.5,30h3v16.6H29.9V36.1h-3.5V50H50V26.5h-7.4c0.5,1,0.9,2,0.9,3.3C43.5,29.8,43.5,29.9,43.5,30z"></path> <path fill="#A0A1A2" d="M3.5,20V3.5h16.6v9.6c1-0.8,2.3-1.3,3.5-1.6V0H0v23.5h6.8C7.3,22.3,8,21,9,20.1L3.5,20L3.5,20z"></path> <path fill="#A0A1A2" d="M29.9,11.1V3.5h16.6v16.6h-7.3c0.3,1,0.5,2.2,0.5,3.4c0,0,0,0.1,0,0.1H50V0H26.4v10.9c0.3,0,0.5-0.1,0.8-0.1 C28.1,10.9,29,10.9,29.9,11.1z"></path> <path fill="#59B4D9" d="M40.8,29.7c0-2.1-1.7-3.7-3.7-3.7c-0.2,0-0.3,0-0.5,0c0.2-0.8,0.4-1.7,0.4-2.6c0-5.5-4.4-9.9-9.9-9.9 c-4.3,0-8,2.8-9.3,6.8c-0.7-0.2-1.4-0.4-2.2-0.4c-3.7,0-6.7,3-6.7,6.8c0,3.8,3,6.8,6.7,6.8c0,0,0,0,0,0v0h21.8l0,0 C39.3,33.3,40.8,31.7,40.8,29.7"></path> <path opacity="0.2" fill="#FFFFFF" d="M19.2,33.5c-0.9-0.9-1.5-2-1.8-3.3c-0.8-3.7,1.4-7.3,5.1-8.1c0.8-0.2,1.5-0.2,2.2-0.1 c0.3-3.4,2.4-6.5,5.5-8c-0.9-0.3-1.9-0.5-3-0.5c-4.3,0-8,2.8-9.3,6.8c-0.7-0.2-1.4-0.4-2.2-0.4c-3.7,0-6.7,3-6.7,6.8 c0,3.8,3,6.8,6.7,6.8c0,0,0,0,0,0v0H19.2z"></path> </svg>SVG

        <!-- JavaScript 測試 -->
        <p id="js-test"></p>
    </div>

    <script src="{BasePath}/Scripts/jquery-2.2.4.min.js"></script>
    <script>
        $(function () {
            $("#js-test").html("<b>JavaScript 測試</b>");
        });
    </script>
</body>
</html>

Action 中轉換 PDF

[HttpPost]
[ValidateInput(false)]  // 這邊是為了範例方便,正式環境不建議這樣使用,導致所有欄位都可以讀 Html
public ActionResult PDF(string Html)
{
    if (string.IsNullOrWhiteSpace(Html))
    {
        Html = System.IO.File.ReadAllText(Server.MapPath("~/App_Data/PDF.html"));
        // 取代 {BasePath} 成實體路徑
        Html = Html.Replace("{BasePath}", AppDomain.CurrentDomain.BaseDirectory);
    }

    var pdf = PdfHelper.ConvertHtmlTextToPDF(Html);

    return File(pdf, "application/pdf");
}

呈現結果

將程式都完成之後上傳到 App Service 測試,結果的畫面呈現如下圖:

其它

在測試的時候還有一點需要注意的, App Service Plan 如果選擇共用計算類型的定價層(免費、共用)會因為資源權限的關係無法正常轉換,至少需要選擇基礎的定價層才有辦法正常執行轉換,這一點需要也是需要注意的地方。

原始碼和測試站台

GitHub

測試站台

結論

在 App Service 是可以成功使用套件來轉換 Html 成 PDF,但是仍有些需要注意的項目:

  1. 中文字形需使用英文名稱,不然無法顯示
  2. 靜態檔案需注意路徑問題
  3. App Service Plan 需要標準方案以上

此外,本文所寫的範例為 ASP.NET MVC ,套件也可能無法支援 ASP.NET Core,因此未來還需要針對 ASP.NET Core 的站台做測試才能知道是否可行或是需要用哪一個套件才可以。

相關連結

  1. wkhtmltopdf
  2. TuesPechkin