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


    前言

    目前.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文档站点下有很多例子大家可以看看:

    image

    image

    我的使用方式较为粗暴,使用MigraDoc编辑文档表格,再生成PDF文件。有时间再尝试封装个类似于QuestPDF的扩展库,太喜欢Fluent这种形式了。

    代码例子

    让我们来制作下图的pdf吧
    image

    新建一个项目,通过Nuget引入PDFsharp、PDFsharp-MigraDoc,
    若用System.Drawing图形库则不用引用SkiaSharp,我的例子使用SkiaSharp图形库便于跨平台。

    首先是字体的导入

    • 因为PDFSharp本身不支持中文字体,但提供了自定义解析器的处理,所以我们先实现下中文字体解析器。先将黑体作为嵌入资源导入项目中,路径是/Fonts/下
    • 新建一个文件ChineseFontResolver.cs用来实现我们的中文解析器
    using PdfSharp.Fonts; 
    using System.Reflection; 
    
    namespace pdfsharpDemo;
    /// 
    /// 中文字体解析器
    /// 
    public class ChineseFontResolver : IFontResolver
    {
        /// 
        /// 字体作为嵌入资源所在程序集
        /// 
        public static string FontAssemblyString { get; set; } = "pdfsharpDemo";
        /// 
        /// 字体作为嵌入资源所在命名空间
        /// 
        public static string FontNamespace { get; set; } = "pdfsharpDemo.Fonts";
    
        /// 
        /// 字体名称
        /// 
        public static class FamilyNames
        {
            // This implementation considers each font face as its own family.
            /// 
            /// 仿宋
            /// 
            public const string SIMFANG = "simfang.ttf";
            /// 
            /// 黑体
            /// 
            public const string SIMHEI = "simhei.ttf";
            /// 
            /// 楷书
            /// 
            public const string SIMKAI = "simkai.ttf";
            /// 
            /// 隶书
            /// 
            public const string SIMLI = "simli.ttf";
            /// 
            /// 宋体
            /// 
            public const string SIMSUN = "simsun.ttf";
            /// 
            /// 宋体加粗
            /// 
            public const string SIMSUNB = "simsunb.ttf";
            /// 
            /// 幼圆
            /// 
            public const string SIMYOU = "simyou.ttf";
        }
    
    
        /// 
        /// Selects a physical font face based on the specified information
        /// of a required typeface.
        /// 
        /// Name of the font family.
        /// Set to true when a bold font face
        ///  is required.
        /// Set to true when an italic font face 
        /// is required.
        /// 
        /// Information about the physical font, or null if the request cannot be satisfied.
        /// 
        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);
        }
    
        /// 
        /// Gets the bytes of a physical font face with specified face name.
        /// 
        /// A face name previously retrieved by ResolveTypeface.
        /// 
        /// The bits of the font.
        /// 
        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

  • 相关阅读:
    docker容器的detached模式下查看logs
    unity shaderGraph实例-物体线框显示
    Windows Server 2022 安全功能重大更新
    推荐模型复现(一):熟悉Torch-RecHub框架与使用
    全国流通经济杂志全国流通经济杂志社全国流通经济编辑部2022年第25期目录
    获取数据类型的方式和typescript is 类型谓词
    Linux 安装 Docker +Docker Compose + cucker/get_command_4_run_container
    中国服装自主品牌行业市场环境与投资趋势分析报告
    金仓数据库 KingbaseES 插件参考手册 S (2)
    CAD二次开发(9)- CAD中对象的实时选择
  • 原文地址:https://www.cnblogs.com/laikwan/p/18206787