.NET 小技巧 - 使用 PdfSharp / PdfSharpCore 合併 PDF、加浮水印
3 |
關於 .NET 用的開源 PDF 程式庫,先前介紹過 PdfPig,最近在看微軟 RAG 範例程式發現另一個程式庫選擇 - PdfSharpCore。
PdfSharpCore 是老牌開源專案 PDFsharp 及 MigraDoc Foundation 的 .NET 6+ 「跨平台」移植版,網路上較少聽到人討論,網路資源也偏少,但 NuGet 的下載統計也累積近 1170 萬。PdfSharpCore 使用 SixLabors.ImageSharp 及 SixLabors.Fonts 解決 GDI+ 繪圖程式庫為 Windows 專屬的問題,實現跨平台使用(延伸閱讀:.NET 6 圖形處理跨平台注意事項,但這兩個資料庫))。但要留意,由於 SixLabors 程式庫不是採用 MIT 授權,依 Six Labors Split License,年營收超過 100 萬美金的企業直接引用(Direct Package Dependency)需付費,透過 PdfSharpCore 引用 SixLabors 程式庫的做法我認為屬於 Transitive Dependecy (any Work in Object form that is installed indirectly by a third party dependency unrelated to Six Labors),但稽查人員或管理權責單位是否願了解授權差異,會不會使出"我不要聽我不要聽"大絕就看大家的運氣了,願源力與你同在。
但研究後發現,PDFsharp 也仍在持續發展,目前最新版為 PDFsharp & MigraDoc 6,可在 Windows、Linux 及任何 .NET 相容平台上使用,它共提供三種實作:跨平台版、Windows GDI+ 及 Windows WPF 版,跨平台版看起來是自行實繪圖功能,在效能、支援度及可靠性估計不如 GDI+ 或 SixLabors 專業,但如果應用場景限 Windows 平台,使用 GDI+/WPF 版或是只支援 .NET Framework 用的 1.5 版,是更好的選擇。
因此,這篇文章的範例我會用 PdfSharpCore 示範,但它也適用於 PDFsharp & MigraDoc 6、PDFsharp 1.5,中間可能有些小差異,但大致精神相同。在 API 文件方向,新版網站仍在施工中,部分文件尚未完備,缺少部分可參考1.5 舊版 Wiki。
在微軟的 RAG 範例中,只用 PdfSharpCore 將 PDF 分頁,一頁存成一個 PDF,但 PdfSharpCore 也可處理建立文件、合併文件、加浮水印等常見 PDF 基本操作,以下是簡單示範,共四組測試:用繪圖 API 產生 PDF、用文件模型產生 PDF、將兩個 PDF 合併、。
using System.Diagnostics;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using MigraDocCore.DocumentObjectModel;
using PdfSharpCore.Pdf.IO;
// 測試一,使用 PdfSharpCore 產生 PDF (畫布繪圖)
// REF: https://github.com/empira/PDFsharp.Samples/tree/master
var cover = new PdfDocument();
cover.Info.Title = "Created with PDFsharp";
cover.Info.Subject = "Just a simple Hello-World program.";
var page = cover.AddPage();
// 用繪圖方式製作 PDF (另有 MigraDoc 提供文件模型)
var gfx = XGraphics.FromPdfPage(page);
var width = page.Width;
var height = page.Height;
// 背景填色
gfx.DrawRectangle(XBrushes.SteelBlue, 0, 0, width, height);
// 繪製矩形,灰框白底
double margin = 80;
XPen grayPen = new XPen(XColors.Gray, 4);
gfx.DrawRectangle(grayPen, XBrushes.White, margin, margin, width - 2 * margin, 150);
// 繪製文字
var font = new XFont("Tahoma", 32, XFontStyle.Bold);
gfx.DrawString("THIS IS A BOOK.", font, XBrushes.DarkGray,
new XRect(margin, margin, width - 2 * margin, 150), XStringFormats.Center);
var coverFilePath = Path.GetTempFileName() + ".pdf";
cover.Save(coverFilePath);
Process.Start(new ProcessStartInfo(coverFilePath) { UseShellExecute = true });
// 測試二,使用 MigraDocCore 產生 PDF (文件模型)
//REF: https://github.com/empira/PDFsharp.Samples/blob/master/src/samples/src/MigraDoc/src/HelloMigraDoc/Styles.cs
var document = new MigraDocCore.DocumentObjectModel.Document()
{
Info = {
Title = "Created with MigraDoc",
Subject = "Hello, World!",
Author = "Jeffrey Lee"
}
};
var style = document.Styles["Normal"] ?? throw new InvalidOperationException("Style Normal not found.");
style.Font.Name = "Segoe UI";
style = document.Styles["Heading1"];
style.Font.Size = 16;
style.Font.Bold = true;
style.Font.Color = Colors.DarkBlue;
style.ParagraphFormat.PageBreakBefore = true;
style.ParagraphFormat.SpaceAfter = 6;
style.ParagraphFormat.Alignment = ParagraphAlignment.Center;
style.ParagraphFormat.KeepWithNext = true;
var sec = document.AddSection();
var para = sec.AddParagraph();
para.AddFormattedText("Header Test", "Heading1");
para = sec.AddParagraph();
para.AddLineBreak();
para.AddText("Hello, World!");
var pdfRenderer = new MigraDocCore.Rendering.PdfDocumentRenderer()
{
Document = document,
PdfDocument = new PdfDocument()
};
pdfRenderer.RenderDocument();
var pdfFilePath = Path.GetTempFileName() + ".pdf";
pdfRenderer.PdfDocument.Save(pdfFilePath);
Process.Start(new ProcessStartInfo(pdfFilePath) { UseShellExecute = true });
// 測試三,合併兩份 PDF
var pdf1 = PdfReader.Open(coverFilePath, PdfDocumentOpenMode.Import);
var pdf2 = PdfReader.Open(pdfFilePath, PdfDocumentOpenMode.Import);
var pdf3 = new PdfDocument();
for (int i = 0; i < pdf1.PageCount; i++)
{
pdf3.AddPage(pdf1.Pages[i]);
}
for (int i = 0; i < pdf2.PageCount; i++)
{
pdf3.AddPage(pdf2.Pages[i]);
}
var mergedFilePath = Path.GetTempFileName() + ".pdf";
pdf3.Save(mergedFilePath);
Process.Start(new ProcessStartInfo(mergedFilePath) { UseShellExecute = true });
// 測試四,為 PDF 加上浮水印
// https://www.pdfsharp.net/wiki/Watermark-sample.ashx
var mergedPdf = PdfReader.Open(mergedFilePath, PdfDocumentOpenMode.Modify);
string wartermarkText = "TOP SECRET";
var formatWM = new XStringFormat()
{
Alignment = XStringAlignment.Near,
LineAlignment = XLineAlignment.Near
};
var fontWM = new XFont("Tahoma", 48, XFontStyle.Bold);
XBrush brushWM = new XSolidBrush(XColor.FromArgb(96, 255, 0, 0));
foreach (var pg in mergedPdf.Pages)
{
var gfxWM = XGraphics.FromPdfPage(pg, XGraphicsPdfPageOptions.Append);
var size = gfxWM.MeasureString(wartermarkText, fontWM);
gfxWM.TranslateTransform(pg.Width.Point / 2, pg.Height.Point / 2);
gfxWM.RotateTransform(-Math.Atan(pg.Height / pg.Width) * 180 / Math.PI);
gfxWM.TranslateTransform(-pg.Width.Point / 2, -pg.Height.Point / 2);
gfxWM.DrawString(wartermarkText, fontWM, brushWM,
new XPoint((pg.Width.Point - size.Width) / 2, (pg.Height.Point - size.Height) / 2),
formatWM);
}
var watermarkedFilePath = Path.GetTempFileName() + ".pdf";
mergedPdf.Save(watermarkedFilePath);
Process.Start(new ProcessStartInfo(watermarkedFilePath) { UseShellExecute = true });
專案需參照 PdfSharpCore
、MigraDocCore.DocumentObjectModel
、MigraDocCore.Rendering
三個程式庫,實測成功:
另外,順便也實測 .NET Framework 3.5 + PDFsharp 1.50:
程式碼搬進 .NET 3.5 專案,將 Namespace 從 PdfSharpCore 改成 PdfSharp、MigraDocCore 改成 MigraDoc,可得到相同結果:
結論:.NET Framework 要做 PDF 合併及浮水印,可考慮使用 PDFsharp 1.50;要配合 .NET 6+,可考慮 PdfSharp 6.0 GDI+ 或 WPF 版,若需跨平台則可使用 PdfSharp 6.0 跨平台版或 PdfSharpCore。PdfSharpCore 有引用非 MIT 授權的 SixLabors 圖形程式庫,雖屬 Transitive Depdency 理論可免費商用,但實務上要以稽查單位心證為準,較麻煩些。
.NET 程式工具箱再添好用工具一枚。
Comments
# by GregYu
PdfSharp 能用中文嗎?
# by Lixf
感谢大佬,源码很新,对我帮助很大
# by Tim
目前遇到一個難題,數張PDF模板部分欄位使用相同TAG,再合併後所有相同TAG的欄位值,都會被第一張PDF資料覆蓋,請問有大大遇過嗎?