前言

目前.NET 体系下常见的PDF类库有AsposeQuestPDFSpireiTextSharp等,有一说一都挺好用的,我个人特别喜欢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

.NET下免费开源的PDF类库(PDFSharp)的更多相关文章

  1. Windows下免费、开源邮件服务器hMailServer

    Windows下免费.开源邮件服务器hMailServer 一.Windows下搭建免费.开源的邮件服务器hMailServer 二.邮件服务器hMailServer管理工具hMailServer A ...

  2. C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志

    C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...

  3. vs2015下编译免费开源的jpeg库,ijg的jpeg.lib

    vs2015下编译免费开源的jpeg库,ijg的jpeg.lib 1. 去Independent JPEG Group官网www.ijg.org下载jpegsrc,我下载的版本是jpegsrc9c.z ...

  4. 《Python编程第4版 下》高清PDF|百度网盘免费下载|Python基础编程

    <Python编程第4版 下>高清PDF|百度网盘免费下载|Python基础编程 提取码:tz5v 当掌握Python的基础知识后,你要如何使用Python?Python编程(第四版)为这 ...

  5. Scut游戏服务器免费开源框架-3

    Scut游戏服务器免费开源框架--快速开发(3) Scut快速开发(3) 1        开发环境 需要安装的软件 a)        消息队列 b)        数据库,Sql2005以上版本 ...

  6. 如何通过免费开源的ERP Odoo打造企业全员营销整体解决方案

    应用场景的背景故事 在一些二级城市,往往线索的来源是通过企业当地口碑积累.熟人转介绍等线下的方式为主,利用互联网的模式往往很难奏效,企业面临的第一个问题就是如何把握线索真实的来源介绍的问题.在这个问题 ...

  7. 全球第一免费开源ERP Odoo仓存功能模块深度应用(一)

    基本功能 库位 库位是一个逻辑存货区,可以是一个物理库区,可以是一个货架.货架上的一个货位.库位可以有子库位 库位有虚拟库位和实际库位,实际库位是实际存放货物的库位,虚拟库位是因复式库存记账而虚构的库 ...

  8. GitHub 上 10 款免费开源 Windows 工具

    GitHub 上 10 款免费开源 Windows 工具 GitHub 是如今所有开源事物的中央仓库, 这个网站最近发布了一个叫做<2016 Octoverse  状态报告>,详细列出了从 ...

  9. 免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)

    前面介绍了六种.NET组件,其中有一种组件是写文件的压缩和解压,现在介绍另一种文件的解压缩组件SharpZipLib.在这个组件介绍系列中,只为简单的介绍组件的背景和简单的应用,读者在阅读时可以结合官 ...

  10. Bucky – 免费开源的实时用户监控工具

    Bucky 是一个开源的实时用户监控工具,用于衡量用户在浏览器中使用 Web 应用程序时的性能.它可以自动测量你的网页需要多长时间来加载,Ajax 请求需要多长时间和各个函数需要实行多久. 您可能感兴 ...

随机推荐

  1. C# 面试问答

    引用:https://www.cnblogs.com/zh7791/p/13705434.html   1.什么是 COM? COM 代表组件对象模型.COM 是微软技术之一.使用这项技术,我们可以开 ...

  2. RabbitMQ 06 工作队列模式

    工作队列模式结构图: 这种模式非常适合多个工人等待任务到来的场景.任务有多个,一个一个丢进消息队列,工人也有很多个,就可以将这些任务分配个各个工人,让他们各自负责一些任务,并且做的快的工人还可以多完成 ...

  3. SpringBoot集成日志

    1.日志工厂 如果一个数据库,出现了异常,我们需要排错,日志就是最好的助手! 曾经:sout.debug 现在:日志工厂 在Mybatis中具体使用那一个日志实现,在设置设定 STDOUT_LOGGI ...

  4. ip 记录路由选项

    前言 准备整理网络这块,先把概念整理. ip记录路由选项,这个是做什么的呢? 比如说我们发的一条信息,从一端到另外一端经过了那些路由呢?这是一个问题啊. 这个ip记录路由选项就是来看这个问题的,当然这 ...

  5. szfpga 高云gowin国产开发板GW2AR-18核心板fpga cpld测试板

    1. 概述 国产FPGA是最近几年起来的产品,具有性价比高特点.而GOWIN属于国产FPGA成员,在服务和芯片都是比较大的优势,很多用户都用在LED控制,电机控制,PLC设备上,以及用于替换Latti ...

  6. 文本溢出显示省略号css

    项目中常常有这种需要我们对溢出文本进行"..."显示的操作,单行多行的情况都有(具体几行得看设计师心情了),这篇随笔是我个人对这种情况解决办法的归纳,欢迎各路英雄指教. 单行 语法 ...

  7. git合并某分支上的单次提交(cherry-pick)

    1. 查找提交对应的hash值 git log 查看 d 查看下一页,q退出 vscode通过gitlens插件查看 2.合并提交 git cherry-pick hahs值

  8. js 校验手机号与校验邮箱正则表达式

    js 校验手机号与校验邮箱正则表达式 以下 checkMobile(mobile) { var myreg = /^(((13[0-9]{1})|(15[0-9]{1})|(18[0-9]{1}))+ ...

  9. 牛客网-SQL专项训练19

    ①下列哪个语句是授予用户SQLTest对数据库Sales的CUSTOMERS表的列cid.cname的查询权限(C) 解析: 授予权限的语法: GRANT <权限> ON 表名(列名) T ...

  10. ModelScope初探:一行代码调用成熟AI模型。

    简介: 如何用一行代码调用成熟AI模型?试试ModelScope,让AI开发者解放生产力! ModelScope是阿里推出的下一代开源的模型即服务共享平台,为泛AI开发者提供灵活.易用.低成本的一站式 ...