前言
目前.NET 體系下常見的PDF類庫有Aspose、QuestPDF、Spire、iTextSharp等,有一說一都挺好用的,我個人特別喜歡QuestPDF它基於 C# Fluent API 提供全面的佈局引擎;但是這些庫要麼屬於商業庫價格不菲(能理解收費),但是年費太貴了。要麼是有條件限制開源的,如Spire開源版本有各種限制。iTextSharp雖然沒有限制,但是開源協議不友好(AGPL),用於閉源商業軟體屬於要掛恥辱柱的行為了。
無意間發現了另一款基於.NET6的跨平臺、免費開源(MIT協議)pdf處理庫:PDFSharp,該庫還有基於.NET Framework的版本https://pdfsharp.net/ ,.NET6版本好像是去年剛釋出的,還有一個較為活躍的社群https://forum.pdfsharp.net/。
嘗試使用了下,還不錯,該有的都有,簡單的pdf檔案可以直接使用PDFSharp庫生成,複雜點的則提供了MigraDoc來編輯。我自己的小應用都已經上生成環境了,個人覺得該庫是挺ok的了。
.NET Framework文件站點下有很多例子大家可以看看:
我的使用方式較為粗暴,使用MigraDoc編輯文件表格,再生成PDF檔案。有時間再嘗試封裝個類似於QuestPDF的擴充套件庫,太喜歡Fluent這種形式了。
程式碼例子
讓我們來製作下圖的pdf吧
新建一個專案,透過Nuget引入PDFsharp、PDFsharp-MigraDoc,
若用System.Drawing圖形庫則不用引用SkiaSharp,我的例子使用SkiaSharp圖形庫便於跨平臺。
首先是字型的匯入
- 因為PDFSharp本身不支援中文字型,但提供了自定義解析器的處理,所以我們先實現下中文字型解析器。先將黑體作為嵌入資源匯入專案中,路徑是/Fonts/下
- 新建一個檔案ChineseFontResolver.cs用來實現我們的中文解析器
using PdfSharp.Fonts;
using System.Reflection;
namespace pdfsharpDemo;
/// <summary>
/// 中文字型解析器
/// </summary>
public class ChineseFontResolver : IFontResolver
{
/// <summary>
/// 字型作為嵌入資源所在程式集
/// </summary>
public static string FontAssemblyString { get; set; } = "pdfsharpDemo";
/// <summary>
/// 字型作為嵌入資源所在名稱空間
/// </summary>
public static string FontNamespace { get; set; } = "pdfsharpDemo.Fonts";
/// <summary>
/// 字型名稱
/// </summary>
public static class FamilyNames
{
// This implementation considers each font face as its own family.
/// <summary>
/// 仿宋
/// </summary>
public const string SIMFANG = "simfang.ttf";
/// <summary>
/// 黑體
/// </summary>
public const string SIMHEI = "simhei.ttf";
/// <summary>
/// 楷書
/// </summary>
public const string SIMKAI = "simkai.ttf";
/// <summary>
/// 隸書
/// </summary>
public const string SIMLI = "simli.ttf";
/// <summary>
/// 宋體
/// </summary>
public const string SIMSUN = "simsun.ttf";
/// <summary>
/// 宋體加粗
/// </summary>
public const string SIMSUNB = "simsunb.ttf";
/// <summary>
/// 幼圓
/// </summary>
public const string SIMYOU = "simyou.ttf";
}
/// <summary>
/// Selects a physical font face based on the specified information
/// of a required typeface.
/// </summary>
/// <param name="familyName">Name of the font family.</param>
/// <param name="isBold">Set to <c>true</c> when a bold font face
/// is required.</param>
/// <param name="isItalic">Set to <c>true</c> when an italic font face
/// is required.</param>
/// <returns>
/// Information about the physical font, or null if the request cannot be satisfied.
/// </returns>
public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
// Note: PDFsharp calls ResolveTypeface only once for each unique combination
// of familyName, isBold, and isItalic.
return new FontResolverInfo(familyName, isBold, isItalic);
// Return null means that the typeface cannot be resolved and PDFsharp forwards
// the typeface request depending on PDFsharp build flavor and operating system.
// Alternatively forward call to PlatformFontResolver.
//return PlatformFontResolver.ResolveTypeface(familyName, isBold, isItalic);
}
/// <summary>
/// Gets the bytes of a physical font face with specified face name.
/// </summary>
/// <param name="faceName">A face name previously retrieved by ResolveTypeface.</param>
/// <returns>
/// The bits of the font.
/// </returns>
public byte[]? GetFont(string faceName)
{
// Note: PDFsharp never calls GetFont twice with the same face name.
// Note: If a typeface is resolved by the PlatformFontResolver.ResolveTypeface
// you never come here.
var name = $"{FontNamespace}.{faceName}";
using Stream stream = Assembly.Load(FontAssemblyString).GetManifestResourceStream(name) ?? throw new ArgumentException("No resource named '" + name + "'.");
int num = (int)stream.Length;
byte[] array = new byte[num];
stream.Read(array, 0, num);
// Return the bytes of a font.
return array;
}
}
好了,開始製作我們的pdf吧
// See https://aka.ms/new-console-template for more information
using Microsoft.Extensions.Configuration;
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
using PdfSharp.Drawing;
using PdfSharp.Fonts;
using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;
using pdfsharpDemo;
using SkiaSharp;
using System;
using System.IO;
using static pdfsharpDemo.ChineseFontResolver;
Console.WriteLine("Hello, PDFSharp!");
// 設定PDFSharp全域性字型為自定義解析器
GlobalFontSettings.FontResolver = new ChineseFontResolver();
#region pdf頁面的基本設定
var document = new Document();
var _style = document.Styles["Normal"];//整體樣式
_style.Font.Name = FamilyNames.SIMHEI;
_style.Font.Size = 10;
var _tableStyle = document.Styles.AddStyle("Table", "Normal");//表格樣式
_tableStyle.Font.Name = _style.Font.Name;
_tableStyle.Font.Size = _style.Font.Size;
var _section = document.AddSection();
_section.PageSetup = document.DefaultPageSetup.Clone();
_section.PageSetup.PageFormat = PageFormat.A4; //A4紙規格
_section.PageSetup.Orientation = Orientation.Landscape;//紙張方向:橫向,預設是豎向
_section.PageSetup.TopMargin = 50f;//上邊距 50
_section.PageSetup.LeftMargin = 25f;//左邊距 20
#endregion
//這裡採用三個表格實現標題欄、表格內容、底欄提示
//建立一個表格,並且設定邊距
var topTable = _section.AddTable();
topTable.Style = _style.Name;
topTable.TopPadding = 0;
topTable.BottomPadding = 3;
topTable.LeftPadding = 0;
var tableWidth = _section.PageSetup.PageHeight - _section.PageSetup.LeftMargin * 2;
// 標題欄分為三格
float[] topTableWidths = [tableWidth / 2, tableWidth / 2];
//生成對應的二列,並設定寬度
foreach (var item in topTableWidths)
{
var column = topTable.AddColumn();
column.Width = item;
}
//生成行,設定標題
var titleRow = topTable.AddRow();
titleRow.Cells[0].MergeRight = 1;//向右跨一列(合併列)
titleRow.Cells[0].Format.Alignment = ParagraphAlignment.Center;//元素居中
var parVlaue = titleRow.Cells[0].AddParagraph();
parVlaue.Format = new ParagraphFormat();
parVlaue.Format.Font.Bold = true;//粗體
parVlaue.Format.Font.Size = 16;//字型大小
parVlaue.AddText("我的第一個PDFSharp例子");
//生成標題行,這裡我們設定兩行
var row2 = topTable.AddRow();
var noCell = row2.Cells[0];
noCell.Format.Alignment = ParagraphAlignment.Left;
noCell.AddParagraph().AddText($"編號:00000001");
var orgNameCell = row2.Cells[1];
orgNameCell.Format.Alignment = ParagraphAlignment.Right;
orgNameCell.AddParagraph().AddText("單位:PDFSharp研究小組");
var row3 = topTable.AddRow();
var createAtCell = row3.Cells[0];
createAtCell.Format.Alignment = ParagraphAlignment.Left;
createAtCell.AddParagraph().AddText($"查詢時間:{DateTime.Now.AddDays(-1):yyyy年MM月dd日 HH:mm}");
var printTimeCell = row3.Cells[1];
printTimeCell.Format.Alignment = ParagraphAlignment.Right;
printTimeCell.AddParagraph().AddText($"列印時間:{DateTime.Now:yyyy年MM月dd日 HH:mm}");
//表格內容
var contentTable = _section.AddTable();
contentTable.Style = _style.Name;
contentTable.Borders = new Borders
{
Color = Colors.Black,
Width = 0.25
};
contentTable.Borders.Left.Width = 0.5;
contentTable.Borders.Right.Width = 0.5;
contentTable.TopPadding = 6;
contentTable.BottomPadding = 0;
//這裡設定8列好了
var tableWidths = new float[8];
tableWidths[0] = 30;
tableWidths[1] = 60;
tableWidths[2] = 40;
tableWidths[5] = 60;
tableWidths[6] = 80;
float w2 = (_section.PageSetup.PageHeight - (_section.PageSetup.LeftMargin * 2) - tableWidths.Sum()) / 2;//假裝自適應,哈哈哈
tableWidths[3] = w2;
tableWidths[4] = w2;
//生成列
foreach (var item in tableWidths)
{
var column = contentTable.AddColumn();
column.Width = item;
column.Format.Alignment = ParagraphAlignment.Center;
}
//生成標題行
var headRow = contentTable.AddRow();
headRow.TopPadding = 6;
headRow.BottomPadding = 6;
headRow.Format.Font.Bold = true;
headRow.Format.Font.Size = "12";
headRow.VerticalAlignment = VerticalAlignment.Center;
headRow.Cells[0].AddParagraph().AddText("序號");
headRow.Cells[1].AddParagraph().AddText("姓名");
headRow.Cells[2].AddParagraph().AddText("性別");
headRow.Cells[3].AddParagraph().AddText("家庭地址");
headRow.Cells[4].AddParagraph().AddText("工作單位");
var cParVlaue = headRow.Cells[5].AddParagraph();
"銀行卡總額(元)".ToList()?.ForEach(o => cParVlaue.AddChar(o));//自動換行 使用AddChar
headRow.Cells[6].AddParagraph().AddText("聯絡電話");
//內容列,隨便填點吧 用元組實現,懶得搞個類了
List<(string name, string sex, string addree, string workplace, decimal? amount, string phone)> contentData = new()
{
new () {name="張珊",sex="女",addree="市政府宿舍",workplace="市政府",amount=12002M,phone="138********3333"},
new () {name="李思",sex="女",addree="省政府宿舍大樓下的小破店旁邊的垃圾桶前面的別墅",workplace="省教育局",amount=220000M,phone="158********3456"},
new () {name="王武",sex="男",addree="鳳凰村",workplace="老破小公司",amount=-8765M,phone="199********6543"},
new () {name="",sex="",addree="",workplace="",amount=null,phone=""},
};
var index = 1;
foreach (var (name, sex, addree, workplace, amount, phone) in contentData)
{
var dataRow = contentTable.AddRow();
dataRow.TopPadding = 6;
dataRow.BottomPadding = 6;
dataRow.Cells[0].AddParagraph().AddText($"{index++}");
dataRow.Cells[1].AddParagraph().AddText(name);
dataRow.Cells[2].AddParagraph().AddText(sex);
var addreeParVlaue = dataRow.Cells[3].AddParagraph();
addree?.ToList()?.ForEach(o => addreeParVlaue.AddChar(o));//自動換行 使用AddChar
dataRow.Cells[4].AddParagraph().AddText(workplace);
dataRow.Cells[5].AddParagraph().AddText(amount?.ToString() ?? "");
dataRow.Cells[6].AddParagraph().AddText(phone);
}
//空白 段落 分隔下間距
Paragraph paragraph = new();// 設定段落格式
paragraph.Format.SpaceBefore = "18pt"; // 設定空行高度為 12 磅
document.LastSection.Add(paragraph); // 將段落新增到文件中
//底欄提示
var tipsTable = _section.AddTable();
tipsTable.Style = _style.Name;
tipsTable.TopPadding = 3;
var tipsTableColumn = tipsTable.AddColumn();
tipsTableColumn.Width = _section.PageSetup.PageHeight - _section.PageSetup.LeftMargin * 2;
var tipsParagraph = tipsTable.AddRow().Cells[0].AddParagraph();
tipsParagraph.Format.Font.Bold = true;
tipsParagraph.Format.Font.Color = Colors.Red; //設定紅色
tipsParagraph.AddText($"注:隱私資訊是我們必須要注重的廢話連篇的東西,切記切記,不可忽視,因小失大;");
#region 頁碼
_section.PageSetup.DifferentFirstPageHeaderFooter = false;
var pager = _section.Footers.Primary.AddParagraph();
pager.AddText($"第\t");
pager.AddPageField();
pager.AddText($"\t頁");
pager.Format.Alignment = ParagraphAlignment.Center;
#endregion
//生成PDF
var pdfRenderer = new PdfDocumentRenderer();
using var memoryStream = new MemoryStream();
pdfRenderer.Document = document;
pdfRenderer.RenderDocument();
pdfRenderer.PdfDocument.Save(memoryStream);
var pdfDocument = PdfReader.Open(memoryStream);
//為了跨平臺 用的是SkiaSharp,大家自己轉為System.Drawing實現即可,較為簡單就不寫了
#region 水印
using var watermarkMemoryStream = new MemoryStream();
var watermarkImgPath = "D:\\logo.png";
using var watermarkFile = System.IO.File.OpenRead(watermarkImgPath);// 讀取檔案
using var fileStream = new SKManagedStream(watermarkFile);
using var bitmap = SKBitmap.Decode(fileStream);
//設定半透明
var transparent = new SKColor(0, 0, 0, 0);
for (int w = 0; w < bitmap.Width; w++)
{
for (int h = 0; h < bitmap.Height; h++)
{
SKColor c = bitmap.GetPixel(w, h);
SKColor newC = c.Equals(transparent) ? c : new SKColor(c.Red, c.Green, c.Blue, 70);
bitmap.SetPixel(w, h, newC);
}
}
using var resized = bitmap.Resize(new SKImageInfo(200, 80), SKFilterQuality.High);
using var newImage = SKImage.FromBitmap(resized);
newImage.Encode(SKEncodedImageFormat.Png, 90).SaveTo(watermarkMemoryStream); // 儲存檔案
using var image = XImage.FromStream(watermarkMemoryStream);
var xPoints = 6;
var yPoints = 4;
for (int i = 0; i <= xPoints; i++)
{
var xPoint = image.PointWidth * i * 1.2;
var xTranslateTransform = xPoint + image.PointWidth / 2;
for (int j = 0; j <= yPoints; j++)
{
var yPoint = image.PointHeight * j * 1.2;
var yTranslateTransform = yPoint + image.PointHeight / 8;
foreach (var page in pdfDocument.Pages)
{
using var xgr = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Prepend);
xgr.TranslateTransform(xTranslateTransform, yTranslateTransform);
xgr.RotateTransform(-45);
xgr.TranslateTransform(-xTranslateTransform, -yTranslateTransform);
xgr.DrawImage(image, xPoint, yPoint, 200, 80);
}
}
}
#endregion
pdfDocument.Save(memoryStream);
var outputPdfFilePath = "D:\\pdfdemo.pdf";
//儲存到本地
using var fs = new FileStream(outputPdfFilePath, FileMode.Create);
byte[] bytes = new byte[memoryStream.Length];
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.Read(bytes, 0, (int)memoryStream.Length);
fs.Write(bytes, 0, bytes.Length);
Console.WriteLine("生成成功!");
至此我們就製作好了一個簡單的pdf,當然了這裡沒有加上檔案資訊那些,僅僅是生成內容罷了,有那些需要的可以自己根據文件站點看看如何設定。
上述程式碼的原始碼地址:https://gitee.com/huangguishen/MyFile/tree/master/PDFSharpDemo