C#: 使用 DinkToPdf 把 HTML 轉成 PDF 吧
最近做的一項需求是要把 HTML 轉換成 PDF,過程中決定使用 DinkToPdf 來處理這一段。
考慮到現在拿到的一些文件模板都是 HTML 檔了,感覺以後會蠻常碰到這個場景,決定來筆記一篇,給未來的我複製貼上。
環境準備
首先,當然是要先到 Nuget 安裝這篇的主角:DinkToPdf
由於 DinkToPdf 只負責包裝給 C# 這段,實際上要產出 PDF 還得用到 wkhtmltopdf 這個工具,因此還需要先弄到 wkhtmltopdf 的檔案。
不過作者也知道大家不是很想另外跑去找,所以 wkhtmltopdf 的組件檔案可以直接從 DinkToPdf 的 Repo 拿。但要記得要按照作業系統下載對應的 wkhtmltopdf 檔案:
- Windows => libwkhtmltox.dll
- Linux => libwkhtmltox.so
- macOS => libwkhtmltox.dylib
但全部都載也不會有人阻止你就是了。
最後把 libwkhtmltox 放到專案的根目錄底下,DinkToPdf 就呼叫得到囉!
(問就是魔法,不服的自己去啃 P/Invoke)
libwkhtmltox 準備好之後,因為我的示範環境是 .Net 8 的 API,所以還需要註冊 DinkToPdf 的 IConverter
到 DI 框架上。
這邊額外說明一下,IConverter
其實有兩個版本的實作:
BasicConverter
:最直接的簡易版本,會直接去呼叫 libwkhtmltoxSynchronizedConverter
:有搞執行緒安全的版本,會讓任務排隊依序處理
但通常我們就直接參考官方給的範例無腦註冊 SynchronizedConverter
就行了:
services.AddSingleton<IConverter>(_ => new SynchronizedConverter(new PdfTools()));
這邊要特別提醒一點:DinkToPdf 的 IConverter 一定要註冊成單例!
即使今天不是使用 DI 框架來注入的話,也一定要把呼叫的 Converter 包裝成單例,否則會導致多個 Converter 去操作 libwkhtmltox 而發生錯誤。
如果在使用 DinkToPdf 的時候,發現產生的 PDF 會有「一次正常、一次跑版、一次正常、一次跑版」之類的靈異現象,通常就是沒有把 Converter 包成單例所造成的組件呼叫錯誤。
如果懶得找檔案搞單例什麼的,也可以像我一樣,直接找人家包好的工具爽爽用:
這邊我選擇直接開 Nuget 安裝 HtmlToPdfConverter,它已經把 wkhtmltopdf 組件包含在套件裡了,還封裝了一層。我們只需要在 DI 註冊時加上:
services.AddHtmlToPdfConverter();
它就會自己呼叫 RuntimeInformation.IsOSPlatform
來載入對應的 libwkhtmltox 組件檔案了。(有興趣的朋友也可以參考 HtmlToPdfConverter Repo 的寫法)
不管是選擇自己放組件,還是直接挖人家包好的。總之材料準備好之後,就可以開工啦!
實作紀錄
首先,假設我們有某組 HTML 內容:
private string GetHtmlContent()
{
// 為了簡單示範,直接寫死一組字串
var html =
"""
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>測試 DinkToPdf</title>
<style>
body {
font-family: '標楷體', sans-serif;
padding: 20px;
}
h1 {
color: #333;
}
</style>
</head>
<body>
<h1>Hello world!</h1>
<p>我來,我見,我 PDF。</p>
</body>
</html>
""";
// 我們也有可能是從檔案讀的嘛,留這段方便我複製貼上
// var path = @"C:\temp\test.html";
// var html = File.ReadAllText(path, Encoding.UTF8);
return html;
}
接著就是 DinkToPdf 上場的時候,首先我們需要宣告一組 HtmlToPdfDocument
,並設置一些文件相關的特性。
我們這種最簡單的例子,直接設定 A4 直向給他就可以了:
var doc = new HtmlToPdfDocument()
{
GlobalSettings = new GlobalSettings()
{
PaperSize = PaperKind.A4,
Orientation = Orientation.Portrait,
}
};
補充:可調整的項目請參考 GlobalSettings.cs 的欄位,像是一些彩色/黑白啦、文件大小跟方向等等都可以從這裡調整。
有了基本的文件之後,就可以直接把我們前面的 HTML 整坨塞進去:
doc.Objects.Add(new ObjectSettings()
{
HtmlContent = html,
WebSettings = { DefaultEncoding = "utf-8" },
HeaderSettings = { FontName = "標楷體" }
});
都 OK 之後就可以把 IConverter
叫出來轉檔案囉:
// 為了方便示範直接 new 一個 SynchronizedConverter
// 實際使用時請從 DI 之類的地方取得單例物件重複用
var converter = new SynchronizedConverter(new PdfTools());
var pdfBytes = converter.Convert(doc);
最後就可以根據狀況,看是要存成檔案,還是要把 bytes[] 傳遞到下一站:
// 把前面產生的 PDF 儲存成檔案
// 如果要繼續對 PDF 進行加工的話(例如後續用 PDFSharp 接手)
// 也可以考慮傳遞上面的 pdfBytes 就好
var outputPath = @"C:\temp\output.pdf";
File.WriteAllBytes(outputPath, pdfBytes);
Console.WriteLine($"已產生 PDF:{outputPath}");
這樣就搞定啦,馬上打開來看看:
後話
如果像我一樣拿到 HTML 的樣板,最後要產製 PDF 的話,DinkToPdf 算是個不錯的小工具,尤其程式撰寫的部份蠻簡單的,相關設定丟一丟就可以直接轉換檔案了。
比起程式碼部份,反而環境設定的坑還比較多,如果沒乖乖丟組件檔案,又或是註冊的時候沒有好好做成單例,就會發生各種怪怪的事情(果然還是留一篇筆記給未來的我抄比較安全)
原本有考慮 PdfSharp 最常搭配的 HtmlRenderer.PdfSharp,但它已經停止維護;而看起來像是移植版本的 HtmlRendererCore.PdfSharp 光用 Linqpad 裝看看就跳出一排東西,實在不敢直接用:
最後繞了半圈,還是把 libwkhtmltox.dll 丟一丟、DinkToPdf 叫一叫最簡單方便,畢竟早點下班才是正義,阿彌陀佛。
其他文章
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)