「TheArtOfDev.HtmlRenderer.PdfSharp」的踩雷歷程
最近遇到一項需求,由於「iTextSharp」的免費版本存在授權問題,所以開始尋找其他免費的 PDF 套件。最終,選擇了「PDFsharp」。不過,由於「PDFsharp」並不提供將 HTML 字串轉換為 PDF 的功能,所以又找了「TheArtOfDev.HtmlRenderer.PdfSharp」來搭配使用來搭配。儘管該套件的最後更新日期為「2015/5/6」,但後續有在維護的版本「HtmlRenderer.PdfSharp.NetStandard2」是使用 .NET Standard 2.1 開發,不適用於目前的 .NET Framework 專案。本文不會深入介紹這兩個套件的使用方法,因為我僅使用 HTML 轉 PDF 的功能,而是專注列舉可能遇到的問題。
套件介紹
- PDFsharp:PDF 套件,這個名稱的大小寫我沒打錯,就是長這樣。
- HtmlRenderer.Core:此套件用於解析 HTML 元素,並使用自定義類別來存儲相關資訊。
- HtmlRenderer.PdfSharp:將 「HtmlRenderer.Core」所定義的物件轉成成「PDFsharp」所需資訊來產出 PDF。
踩雷歷程
嘗試執行
首先,按照README.md提供的範例進行執行,程式碼如下:
PdfDocument pdf = PdfGenerator.GeneratePdf("<p><h1>Hello World</h1>This is html rendered text</p>", PageSize.A4);
pdf.Save("document.pdf");第一步就出現問題...,發生了以下 Exception:

感覺是版本問題,嘗試降低嘗試降低「TheArtOfDev.HtmlRenderer.PdfSharp」版本,但問題仍然存在。由於原始碼中找不到問題,只好下載下來,以 Debug 的方式尋找原因。但在重新設定引用後,卻發現無法正常編譯。查看專案檔(csproj)後發現,相較於一般套件,多了以下設定:
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
</Target>移除上述有關 NuGet 還原的設定後重新建置,PDF 可以就正常建立了。這代表應該不是程式碼有問題,看來可能真的是版本設定的問題,看來可能真的是版本設定的問題。後來有人提醒我,可以嘗試降低「PDFsharp」的版本。經過將其降版為「1.32.3057」後,終於成功賣出第一步 =.=a。
中文字型
當嘗試輸出中文時,可能遇到無法正常顯示的問題,如下所示:
PdfDocument pdf = PdfGenerator.GeneratePdf("<p>中文</p>", PageSize.A4);
pdf.Save("document.pdf");解決方法是在 HTML 中加入 CSS font-family,將其設為標楷體,即可正常顯示:
string html = @"
<style>
* {
font-family: 標楷體;
}
</style>
<p>中文</p>
";
PdfDocument pdf = PdfGenerator.GeneratePdf(html, PageSize.A4);
pdf.Save("document.pdf");TIP
- 目前預設字型中,僅有「標楷體」和「Malgun Gothic」能夠正確顯示中文。
- 在 Windows 英文環境中,請將「標楷體」更改為「DFKai-SB」。反之,在中文環境中請勿使用「DFKai-SB」。
表格被截斷
在以下情境中,可能會導致 <table> 底部框線被截除:
- 使用巢狀
<table>。 - 內部
<table>的<td>元素具有指定的height屬性,且其值超過一定數字,同時<td>元素內部的元素高度未超過<td>。 - 內部
<table>設有框線。至於未設定框線的情況下難以確定是否存在問題,因為無法透過框線識別截斷情況,但目前尚未觀察到<td>內文被截斷的情況。
以下為示範案例:
string rowsHtml = "";
for (int i = 0; i < 5; i++) {
rowsHtml += $"<tr><td>Text{i}0</td><td>Text{i}1</td><td>Text{i}2</td></tr>";
}
string html = $@"
<style>
table.list, table.list td {{
border-collapse: collapse;
border: 1xp solid black;
}}
td {{
height: 30px;
}}
</style>
<html>
<body>
<table>
<tr>
<td>
<table class=""list"">
{rowsHtml}
</table>
</td>
</td>
</table>
</body>
</html>
";
PdfDocument pdf = PdfGenerator.GeneratePdf(html, PageSize.A4);
pdf.Save("document.pdf");
我猜測問題可能在於計算元素高度時,外部 <td> 的高度計算錯誤,導致內部 <table> 超出外部 <td> 的範圍,目前尚未找到適當的解決方案,只能在最後增加一個額外的 <tr>,並將底下 <td> 的 height 屬性先設為 0px。此時仍然有框線被截斷的問題,則逐步以 0.5px 為單位增加 height 屬性的值,找到一個既能正常顯示又不會多顯示其他框線高度的值。但此時有一定機率變成內部 <table>上方框線被截掉,如果上方框線被截掉,同樣在前面增加一個額外的 <tr>,並以相同方式處理。以下是一個範例:
string rowsHtml = "";
for (int i = 0; i < 5; i++) {
rowsHtml += $"<tr><td>Text{i}0</td><td>Text{i}1</td><td>Text{i}2</td></tr>";
}
string html = $@"
<style>
table.list, table.list td {{
border-collapse: collapse;
border: 1xp solid black;
}}
td {{
height: 30px;
}}
</style>
<html>
<body>
<table>
<tr>
<td>
<table class=""list"">
{rowsHtml}
<!--額外增加的 tr-->
<tr>
<td colspan=""3"" style=""height: 9px;""></td>
<tr>
</table>
</td>
</td>
</table>
</body>
</html>
";
PdfDocument pdf = PdfGenerator.GeneratePdf(html, PageSize.A4);
pdf.Save("document.pdf");
使用感想
單就將 HTML 轉成 PDF 這項功能而言,「TheArtOfDev.HtmlRenderer.PdfSharp」的樣式會比「iTextSharp」更為完整,在更換套件的過程中,我多次發現樣式表現不如預期,原因是由於一些 CSS 樣式在「iTextSharp」沒能發揮效果,而在「TheArtOfDev.HtmlRenderer.PdfSharp」生效了,進而影響到我當前處理的部分。
需要注意的是在某些不符合 HTML 結構的情況下,判斷呈現結果哪一個較接近瀏覽器的效果相對複雜。例如,在 <td> 元素內部放置 <hr /> 這樣的情況,在目前版本的 Chrome 中,它可以接受這種結構,而「iTextSharp」呈現結果為 <td> 內出現一條水平線。而在使用「TheArtOfDev.HtmlRenderer.PdfSharp」時,水平線的顯示位置可能會跑掉。
至於「HtmlRenderer.PdfSharp.NetStandard2」,由於未使用過,這邊就不發表意見。
異動歷程
- 2023-12-05 初版文件建立。
