接續前一篇 C#: 使用 DinkToPdf 把 HTML 轉成 PDF,這篇要來用 PDFSharp 對 PDF 檔案進行一些小小的調整。基本上就是紀錄一下這次遇到的需求和一些坑,主要的內容會有:

  • 替 PDF 加上浮水印
  • 實作 FontResolver 以支援中文字,避免 ⎕⎕⎕
  • 替 PDF 加上檔案密碼

首先當然是要先安裝 PDFSharp 套件:

接續上次的進度,我們把 PDF 檔案弄成了一組 byte[]

// 在上一篇文章,我們使用 DinkToPdf 把 Html 轉成了 PDF 檔案的 byte[]
var pdfBytes = converter.Convert(doc);

// 或是打開已存在的 PDF 檔案(留著讓我以後複製貼上)
// var pdfBytes = File.ReadAllBytes(@"C:\temp\output.pdf");

// 使用 PdfSharp 由記憶體串流讀取轉換後的 PDF,開啟模式設定為 Modify(可以進行修改)
using var document = PdfReader.Open(new MemoryStream(pdfBytes), PdfDocumentOpenMode.Modify);

現在我們已經用 PDFSharp 開啟這個 PDF 檔案了,接著就讓我們從加上簡單的浮水印開始吧!

替 PDF 加上浮水印

加浮水印的時候要注意幾個小部份:

  • PDF 是逐頁構成的,所以要 foreach document.Pages 一頁一頁加上去
  • 浮水印的原理是用繪圖工具把字畫上去,繪圖點的預設位置是座標 (0,0),如果浮水印要壓成斜的話要記得先旋轉

以下採用直接對程式碼進行註解的說明方式,
朋朋們也可以對照參考官方的 Sample:PDFsharp Sample: Watermark

var watermarkContent = "waterwater";

// 記得用 EmbedCompleteFontFile 把字體嵌進去 PDF 避免客戶亂碼
//(ps: 大家常用的 Always 選項已經被淘汰了)
// 但要注意嵌入字體會讓 PDF 檔案變大
var font = new XFont(
    familyName: "Arial",
    emSize: 25,
    style: XFontStyleEx.Regular,
    pdfOptions: new XPdfFontOptions(
        PdfFontEncoding.Unicode, 
        PdfFontEmbedding.EmbedCompleteFontFile));

// 建立一個半透明的筆刷來繪製浮水印
// XColor.FromArgb(50, 0, 0, 0) 表示透明度 50(0~255),顏色為黑色 (0, 0, 0)
var brush = new XSolidBrush(XColor.FromArgb(25, 0, 0, 0));

// 注意:每一頁都是單獨的 Page,所以浮水印要一頁一頁加
foreach (PdfPage page in document.Pages)
{
    // 浮水印需要用繪圖工具,所以先從當前頁面開一個 XGraphics
    // 其中 XGraphicsPdfPageOptions 的 Append 和 Prepend 會影響浮水印在現有物件前方或後方
    // 可參考: https://github.com/Lakerfield/PdfSharp/blob/master/PDFsharp/code/PdfSharp/PdfSharp.Drawing/enums/XGraphicsPdfPageOptions.cs
    using var gfx = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Append);

    // 取得當前頁面的寬度與高度,以便計算頁面的中心點
    // 因為旋轉會以畫面中央為中心處理,因此要將繪圖點先移動到頁面的中央再進行旋轉
    // 確保後續加浮水印文字的角度正確
    // 可以想像成桌子上放一張 A4 紙,然後用手指戳著一個點,再轉看看那張紙,會比較有畫面
    var pageWidth = page.Width;
    var pageHeight = page.Height;
    gfx.TranslateTransform(pageWidth / 2, pageHeight / 2);
    gfx.RotateTransform(-45);

    // 旋轉完之後,將繪圖點移回原本的位置(0, 0),準備壓字上去
    gfx.TranslateTransform(-pageWidth / 2, -pageHeight / 2);

    // 使用 DrawString 方法在頁面中央繪製浮水印文字
    // 建立一個覆蓋整個頁面的 XRect,並以 Center 對齊格式顯示文字
    gfx.DrawString(watermarkContent, font, brush, new XRect(0, 0, pageWidth, pageHeight), XStringFormats.Center);
}

最後存檔測一下:

// 儲存 PDF 文件
var fileName = $"TestPdf_{DateTime.Now.ToString("yyyyMMdd_HHmmss")}.pdf";
var outputPath = @"C:\temp\" + fileName;
document.Save(outputPath);

實作 FontResolver 以支援中文字

上面我們替 PDF 加上了浮水印,但浮水印有包含中文字的朋友可能會注意到中文都變成「⎕⎕⎕」了

這是因為 PDFSharp 在 Core 的版本預設只支援部分英文字體(它想要跨平台,但它沒法知道你用的平台都有啥字體),要讓 PDFSharp 使用中文字體的話,就要自行實作 FontResolver 並掛到 GlobalFontSettings 上

這部份可以參照官方文件 Font-Resolvin 的說明。
同樣地,這邊也留一份程式碼作為示範(不然我以後上哪複製?)


首先實作字型解析用的 CustomFontResolver,以標楷體為例:

// 自訂字型解析器實作
// 沒做這個的話,中文會變成 ⎕⎕⎕
// see: https://docs.pdfsharp.net/link/font-resolving.html
public class CustomFontResolver : IFontResolver
{
    private const string KaiFontKey = "KAIU";

    public byte[] GetFont(string faceName)
    {
        string fontPath = @"C:\Fonts\kaiu.ttf"; // 記得要改成字型檔案路徑
        if (File.Exists(fontPath))
        {
            return File.ReadAllBytes(fontPath);
        }
        throw new FileNotFoundException("找不到字型檔案:" + fontPath);
    }

    public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
    {
        if (familyName.Equals("標楷體", StringComparison.OrdinalIgnoreCase))
        {
            return new FontResolverInfo(KaiFontKey);
        }

        // 如果之後要支援其他字型,可以註冊在這邊
        // 都找不到的時候就回去串內建的 PlatformFontResolver
        return PlatformFontResolver.ResolveTypeface(familyName, isBold, isItalic);
    }
}

接著在做浮水印之前做一個 GlobalFontSettings 的註冊和防呆,並且調整一下我們 XFont 的字型:

var watermarkContent = "測試_中文字浮水印_浮水浮水";

// 注意: 如果使用的字型是中文字,因為 PDFSharp 在 Core 的版本預設只支援部分英文字體(它沒法知道你用的平台都有啥字體)
// 中文字體之類的會要求自訂字體解析,因此要自行實作 FontResolver 並掛到 GlobalFontSettings
// see: https://docs.pdfsharp.net/link/font-resolving.html
if (GlobalFontSettings.FontResolver is null)
{
    GlobalFontSettings.FontResolver = new CustomFontResolver();
}

// 警告:這邊的 familyName("標楷體")必須和 FontResolver 的字體名稱一致,可以用 Const string 做一個強制關聯
var font = new XFont(
    familyName: "標楷體", 
    emSize: 25,
    style: XFontStyleEx.Regular,
    pdfOptions: new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.EmbedCompleteFontFile));
    
// 略......

這樣中文字就可以正常顯示囉~

替 PDF 加上檔案密碼

除了浮水印以外,PDF 加上密碼也是很常見的需求,這篇也一併紀錄一下。

在 PDFSharp 加上密碼還蠻簡單的,只需要調整 SecuritySettings 就好:

// 3. 加密 PDF 文件
// see: https://stackoverflow.com/questions/12383409/password-protecting-a-pdf-file
document.SecuritySettings.UserPassword = "123";
document.SecuritySettings.OwnerPassword = "123";
  • UserPassword 指的是開啟檔案時要輸入的密碼
  • OwnerPassword 則是列印、複製等操作時需要的密碼

但一般來說會習慣設成同一組,讓使用者可以進行完整的操作。

小結 & 碎碎念

和上一篇 DinkToPdf 搭配,完成了這次「產製 PDF,並加上浮水印和密碼」的功能需求。

雖然需求本身並不難,能使用的工具也很多,但前期準備的時候還是踩了一些坑。主要是舊專案採用的 iText 已經改為收費授權,不能無腦照抄移植。接著就有了在上一篇也碎碎念過的這段:

原本想說用團隊成員碰過的 PDFSharp 三兩下收工,搜了一下也發現了 PdfSharpCore,那就來個 PdfSharpCore + HtmlRendererCore.PdfSharp 經典組合。結果裝下來發現這兩顆的依賴項目 SixLabors.ImageSharp 1.0.4 直接跳一堆弱點,嚇到不敢用。

幸好 PdfSharp 本體已經直接支援 .Net 8 了,最終決定使用本家 PdfSharp 進行開發,然後用 DinkToPdf 替代掉停止維護的 HtmlRenderer.PdfSharp,兜了個馬車,筆記一篇。

參考資料