前言

在.NET平台中操作生成PDF的类库有很多如常见的有iTextSharp、PDFsharp、Aspose.PDF等,今天我们分享一个用于生成PDF文档的现代开源.NET库:QuestPDF,本文将介绍QuestPDF并使用它快速实现发票PDF文档生成功能。

QuestPDF介绍

QuestPDF 是一个用于生成 PDF 文档的现代开源 .NET 库。QuestPDF 由简洁易用的 C# Fluent API 提供全面的布局引擎。轻松生成 PDF 报告、发票、导出等。QuestPDF它提供了一个布局引擎,在设计时考虑了完整的分页支持。与其他库不同,它不依赖于 HTML 到 PDF 的转换,这在许多情况下是不可靠的。相反,它实现了自己的布局引擎,该引擎经过优化,可以满足所有与分页相关的要求。

QuestPDF License

分为社区版、专业版、和企业版。

项目源代码

创建一个控制台应用

创建一个名为QuestPDFTest的控制台应用。

安装QuestPDF Nuget包

搜索:QuestPDF包进行安装。

快速实现发票PDF文档生成

创建InvoiceModel

namespace QuestPDFTest
{
    public class InvoiceModel
    {

        /// <summary>
        /// 发票号码
        /// </summary>
        public int InvoiceNumber { get; set; }

        /// <summary>
        /// 发票开具日期
        /// </summary>
        public DateTime IssueDate { get; set; }

        /// <summary>
        /// 发票到期日期
        /// </summary>
        public DateTime DueDate { get; set; }

        /// <summary>
        /// 卖方公司名称
        /// </summary>
        public string SellerCompanyName { get; set; }

        /// <summary>
        /// 买方公司名称
        /// </summary>
        public string CustomerCompanyName { get; set; }

        /// <summary>
        /// 订单消费列表
        /// </summary>
        public List<OrderItem> OrderItems { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        public string Comments { get; set; }
    }

    public class OrderItem
    {
        /// <summary>
        /// 消费类型
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 消费金额
        /// </summary>
        public decimal Price { get; set; }

        /// <summary>
        /// 消费数量
        /// </summary>
        public int Quantity { get; set; }
    }
}

CreateInvoiceDetails

namespace QuestPDFTest
{
    public class CreateInvoiceDetails
    {
        private static readonly Random _random = new Random();

        public enum InvoiceType
        {
            餐饮费,
            交通费,
            住宿费,
            日用品,
            娱乐费,
            医疗费,
            通讯费,
            教育费,
            装修费,
            旅游费
        }

        /// <summary>
        /// 获取发票详情数据
        /// </summary>
        /// <returns></returns>
        public static InvoiceModel GetInvoiceDetails()
        {
            return new InvoiceModel
            {
                InvoiceNumber = _random.Next(1_000, 10_000),
                IssueDate = DateTime.Now,
                DueDate = DateTime.Now + TimeSpan.FromDays(14),
                SellerCompanyName = "追逐时光者",
                CustomerCompanyName = "DotNetGuide技术社区",
                OrderItems = Enumerable
                .Range(1, 20)
                .Select(_ => GenerateRandomOrderItemInfo())
                .ToList(),
                Comments = "DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。"
            };
        }

        /// <summary>
        /// 订单信息生成
        /// </summary>
        /// <returns></returns>
        private static OrderItem GenerateRandomOrderItemInfo()
        {
            var types = (InvoiceType[])Enum.GetValues(typeof(InvoiceType));
            return new OrderItem
            {
                Name = types[_random.Next(types.Length)].ToString(),
                Price = (decimal)Math.Round(_random.NextDouble() * 100, 2),
                Quantity = _random.Next(1, 10)
            };
        }
    }
}

CreateInvoiceDocument

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    public class CreateInvoiceDocument : IDocument
    {
        /// <summary>
        /// 获取Logo的的Image对象
        /// </summary>
        public static Image LogoImage { get; } = Image.FromFile("dotnetguide.png");

        public InvoiceModel Model { get; }

        public CreateInvoiceDocument(InvoiceModel model)
        {
            Model = model;
        }

        public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

        public void Compose(IDocumentContainer container)
        {
            container
                .Page(page =>
                {
                    //设置页面的边距
                    page.Margin(50);

                    //字体默认大小18号字体
                    page.DefaultTextStyle(x => x.FontSize(18));

                    //页眉部分
                    page.Header().Element(BuildHeaderInfo);

                    //内容部分
                    page.Content().Element(BuildContentInfo);

                    //页脚部分
                    page.Footer().AlignCenter().Text(text =>
                    {
                        text.CurrentPageNumber();
                        text.Span(" / ");
                        text.TotalPages();
                    });
                });
        }

        #region 构建页眉部分
        void BuildHeaderInfo(IContainer container)
        {
            container.Row(row =>
            {
                row.RelativeItem().Column(column =>
                {
                    column.Item().Text($"发票编号 #{Model.InvoiceNumber}").FontFamily("fangsong").FontSize(20).SemiBold().FontColor(Colors.Blue.Medium);

                    column.Item().Text(text =>
                    {
                        text.Span("发行日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.IssueDate:d}");
                    });

                    column.Item().Text(text =>
                    {
                        text.Span("终止日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.DueDate:d}");
                    });
                });

                //在当前行的常量项中插入一个图像
                row.ConstantItem(130).Image(LogoImage);
            });
        }

        #endregion

        #region 构建内容部分

        void BuildContentInfo(IContainer container)
        {
            container.PaddingVertical(40).Column(column =>
            {
                column.Spacing(20);

                column.Item().Row(row =>
                {
                    row.RelativeItem().Component(new AddressComponent("卖方公司名称", Model.SellerCompanyName));
                    row.ConstantItem(50);
                    row.RelativeItem().Component(new AddressComponent("客户公司名称", Model.CustomerCompanyName));
                });

                column.Item().Element(CreateTable);

                var totalPrice = Model.OrderItems.Sum(x => x.Price * x.Quantity);
                column.Item().PaddingRight(5).AlignRight().Text($"总计: {totalPrice}").FontFamily("fangsong").SemiBold();

                if (!string.IsNullOrWhiteSpace(Model.Comments))
                    column.Item().PaddingTop(25).Element(BuildComments);
            });
        }

        /// <summary>
        /// 创建表格
        /// </summary>
        /// <param name="container">container</param>
        void CreateTable(IContainer container)
        {
            var headerStyle = TextStyle.Default.SemiBold();

            container.Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.ConstantColumn(25);
                    columns.RelativeColumn(3);
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                });

                table.Header(header =>
                {
                    header.Cell().Text("#").FontFamily("fangsong");
                    header.Cell().Text("消费类型").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("花费金额").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("数量").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("总金额").Style(headerStyle).FontFamily("fangsong");
                    //设置了表头单元格的属性
                    header.Cell().ColumnSpan(5).PaddingTop(5).BorderBottom(1).BorderColor(Colors.Black);
                });

                foreach (var item in Model.OrderItems)
                {
                    var index = Model.OrderItems.IndexOf(item) + 1;

                    table.Cell().Element(CellStyle).Text($"{index}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).Text(item.Name).FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}").FontFamily("fangsong");
                    static IContainer CellStyle(IContainer container) => container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
                }
            });
        }

        #endregion

        #region 构建页脚部分

        void BuildComments(IContainer container)
        {
            container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Column(column =>
            {
                column.Spacing(5);
                column.Item().Text("DotNetGuide技术社区介绍").FontSize(14).FontFamily("fangsong").SemiBold();
                column.Item().Text(Model.Comments).FontFamily("fangsong");
            });
        }

        #endregion
    }

    public class AddressComponent : IComponent
    {
        private string Title { get; }
        private string CompanyName { get; }

        public AddressComponent(string title, string companyName)
        {
            Title = title;
            CompanyName = companyName;
        }

        public void Compose(IContainer container)
        {
            container.ShowEntire().Column(column =>
            {
                column.Spacing(2);

                column.Item().Text(Title).FontFamily("fangsong").SemiBold();
                column.Item().PaddingBottom(5).LineHorizontal(1);
                column.Item().Text(CompanyName).FontFamily("fangsong");
            });
        }
    }
}

Program

using QuestPDF;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 1、请确保您有资格使用社区许可证,不设置的话会报异常。
            Settings.License = LicenseType.Community;

            // 2、禁用QuestPDF库中文本字符可用性的检查
            Settings.CheckIfAllTextGlyphsAreAvailable = false;

            // 3、PDF Document 创建
            var invoiceSourceData = CreateInvoiceDetails.GetInvoiceDetails();
            var document = new CreateInvoiceDocument(invoiceSourceData);

            // 4、生成 PDF 文件并在默认的查看器中显示
            document.GeneratePdfAndShow();
        }
    }
}

完整示例源代码

https://github.com/YSGStudyHards/QuestPDFTest

示例运行效果图

注意问题

中文报异常

QuestPDF.Drawing.Exceptions.DocumentDrawingException:“Could not find an appropriate font fallback for glyph: U-53D1 '发'. Font families available on current environment that contain this glyph: Microsoft JhengHei, Microsoft JhengHei UI, Microsoft YaHei, Microsoft YaHei UI, SimSun, NSimSun, DengXian, FangSong, KaiTi, SimHei, FZCuHeiSongS-B-GB. Possible solutions: 1) Use one of the listed fonts as the primary font in your document. 2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. However, this may result with text glyphs being incorrectly rendered without any warning.”

加上这段代码:

// 2、禁用QuestPDF库中文本字符可用性的检查
Settings.CheckIfAllTextGlyphsAreAvailable = false;

原因:

默认情况下,使用 QuestPDF 生成 PDF 文档时,它会检查所使用的字体是否支持文本中的所有字符,并在发现不能显示的字符时输出一条警告消息。这个选项可以确保文本中的所有字符都能正确地显示在生成的 PDF 文件中。

中文乱码问题

解决方案:

假如Text("")中为汉字一定要在后面加上FontFamily("fangsong")[仿宋字体]或FontFamily("simhei")[黑体字体],否则中文无法正常显示。

项目源码地址

更多项目实用功能和特性欢迎前往项目开源地址查看,别忘了给项目一个Star支持。

GitHub地址:https://github.com/QuestPDF/QuestPDF

文档地址:https://www.questpdf.com/api-reference/

优秀项目和框架精选

该项目已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没)。

https://github.com/YSGStudyHards/DotNetGuide/blob/main/docs/DotNet/DotNetProjectPicks.md

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。

欢迎加入DotNetGuide技术社区微信交流群

.NET使用QuestPDF高效地生成PDF文档的更多相关文章

  1. 利用Java动态生成 PDF 文档

    利用Java动态生成 PDF 文档,则需要开源的API.首先我们先想象需求,在企业应用中,客户会提出一些复杂的需求,比如会针对具体的业务,构建比较典型的具备文档性质的内容,一般会导出PDF进行存档.那 ...

  2. Aspose.Words操作word生成PDF文档

    Aspose.Words操作word生成PDF文档 using Aspose.Words; using System; using System.Collections.Generic; using ...

  3. 如何从Windows Phone 生成PDF文档

    我需要从我的Windows Phone应用程序生成PDF. 遗憾的是没有标准的免费的PDF生成库在Windows Phone上运行. 我不得不自己生成PDF,通过直接写入到文件格式. 这竟然是真的很容 ...

  4. 使用PHP生成PDF文档

    原文:使用PHP生成PDF文档 实际工作中,我们要使用PHP动态的创建PDF文档,目前有许多开源的PHP创建PDF的类库,今天我给大家来介绍一款优秀的PDF库,它就是TCPDF,TCPDF是一个用于快 ...

  5. DocFX生成PDF文档

    使用DocFX生成PDF文档,将在线文档转换为PDF离线文档. 关于DocFX的简单介绍使用DocFX生成文档 使用docfx 命令 1.下载 https://github.com/dotnet/do ...

  6. qt 利用 HTML 生成PDF文档,不能显示jpg图片

    利用 QPrinter 和html 生成 pdf文档 其中用html语句有显示图片的语句 但只能显示png格式的图片,不能显示jpg格式图片. 经过排查:语法,文件路径等都正确,最终在stack ov ...

  7. ireport图形化界面生成pdf文档

    一.ireport软件安装 1.下载软件的官网 https://community.jaspersoft.com/project/ireport-designer/releases 2.安装软件   ...

  8. 自动把动态的jsp页面(或静态html)生成PDF文档,并且上传至服务器

    置顶2017年11月06日 14:41:04 阅读数:2311 这几天,任务中有一个难点是把一个打印页面自动给生成PDF文档,并且上传至服务器,然而公司框架只有手动上传文档,打印时可以保存为PDF在本 ...

  9. Spring Boot集成JasperReports生成PDF文档

    由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲染生成PDF文档.本人文采欠缺,写作能力 ...

  10. 使用PHP类TCPDF生成PDF文档

    转自:http://www.blhere.com/1180.html 这两天遇到一个项目中,需要php自动处理生成pdf文档.在网上找了好几个类,最后决定使用TCPDF,使用的时候真是发现这个类真是强 ...

随机推荐

  1. mongodb c driver bson的嵌套访问与层次结构

    使用c访问mongodb,需要用到mongodb c driver.c++的driver也是基于c driver封装的. 在使用c driver访问mongodb时,需要与bson打交道,不过c dr ...

  2. python01-基础概念与环境搭建

    学习目标 了解硬件 & 操作系统 & 软件(应用系统)之间的关系. 了解常见的操作系统都有哪些. 了解编译器和解释器的区别和作用. 了解编程语言进行分类 了解Python解释器的种类 ...

  3. 聊一聊 .NET高级调试 中的一些内存术语

    一:背景 1. 讲故事 在高级调试的旅程中,经常会有一些朋友问我什么是 工作集(内存),什么是 提交大小,什么是 Virtual Size, 什么是 Working Set ...截图如下: 既然有很 ...

  4. PWA 离线方案研究报告

    本文并不是介绍如何将一个网页配置成离线应用并支持安装下载的.研究PWA的目的仅仅是为了保证用户的资源可以直接从本地加载,来忽略全国或者全球网络质量对页面加载速度造成影响.当然,如果页面上所需的资源,除 ...

  5. MyBatis-Plus雪花算法实现源码解析

    1. 雪花算法(Snowflake Algorithm) 雪花算法(Snowflake Algorithm)是一种用于生成唯一标识符(ID)的分布式算法.最初由 Twitter 公司开发,用于生成其内 ...

  6. 解决OpenCV编译时./bin: error while loading shared libraries: libopencv_highgui.so.3.2: cannot open的问题

    1.问题 安装好了opencv后,用其去检测是否可以使用时,出现了如下的问题: 2.解决 参考这篇博文点击 的配置环境即可解决

  7. 江西财经大学第一届程序设计竞赛 I 题 小P和小Q

    题目链接:https://www.nowcoder.com/acm/contest/115/I 这题目要注意的是!!!pow函数前强制类型转换一定要float !!!一定要是float,double不 ...

  8. 【Python】【OpenCV】定位条形码(二)moments和HuMoments

    根据上一篇博客可知,单纯的通过求取最大面积而进行定位的局限性,因此我们接下来将通过cv2.moments()和cv2.HuMoments()这两个方法来在更复杂的环境中去找到我们的目标区域. cv2. ...

  9. 一键打包,随时运行,Python3项目虚拟环境一键整合包的制作(Venv)

    之前我们介绍了如何使用嵌入式 Python3 环境给项目制作一键整合包,在使用嵌入式 Python 环境时,通常是作为另一个应用程序的一部分,而Python3虚拟环境是为了在开发过程中隔离项目所需的 ...

  10. Python——第二章:单引号(')、双引号(")、花括号 {}的用法及注意事项

    在Python中,字符串可以用单引号(')或双引号(")括起来,它们在用法上没有本质区别.你可以根据个人偏好来选择使用单引号或双引号来创建字符串. 例如: 使用单引号括起的字符串: prin ...