百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

.NET下免费开源的PDF类库(PDFSharp)

csdh11 2025-01-17 11:28 29 浏览

前言

目前.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

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...