在.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-53 D1 '发' . 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")[黑体字体],否则中文无法正常显示。
项目源码地址 GitHub地址:https://github.com/QuestPDF/QuestPDF
文档地址:https://www.questpdf.com/api-reference/