Asp.net Core 3.1 Razor视图模版动态渲染PDF

Asp.net Core 3.1 Razor视图模版动态渲染PDF

  1. 前言

最近的线上项目受理回执接入了电子签章,老项目一直是html打印,但是接入的电子签章是仅仅对PDF电子签章,目前还没有Html电子签章或者其他格式文件的电子签章。首先我想到的是用一个js把前端的html转换PDF,再传回去服务器电子签章。但是这个样子就有一个bug,用户可以在浏览器删改html,这样电子签章的防删改功能就用不到,那么电子签章还有啥意义?所以PDF签章前还是不能给用户有接触的机会,不然用户就要偷偷干坏事了。于是这种背景下,本插件应运而生。我想到直接把Razor渲染成html,html再渲染成PDF。

该项目的优点在于,可以很轻松的把老旧项目的Razor转换成PDF文件,无需后台组装PDF,如果需要排版PDF,我们只需要修改CSS样式和Html代码即可做到。而且我们可以直接先写好Razor视图,做到动态半可视化设计,最后切换一下ActionResult。不必像以前需要在脑海里面设计PDF板式,并一次一次的重启启动调试去修改样式。

2.依赖项目

本插件 支持net45,net46,core的各个版本,(我目前仅仅使用net45和core 3.1.对于其他版本我还没实际应用,但是稍微调整都是支持的,那么简单来说就是支持net 45以上,现在演示的是使用Core3.1)。

依赖插件

Haukcode.DinkToPdf

RazorEngine.NetCore

第一个插件是Html转换PDF的核心插件,具体使用方法自行去了解,这里不多说。

第二个是根据数据模版渲染Razor.

3.核心代码

Razor转Html代码

 

 protected string RunCompileRazorTemplate(object model,string razorTemplateStr)
        {
            if(string.IsNullOrWhiteSpace(razorTemplateStr))
                throw new ArgumentException("Razor模版不能为空");

            var htmlString= Engine.Razor.RunCompile(razorTemplateStr, razorTemplateStr.GetHashCode().ToString(), null, model);
            return htmlString;
        }

 

Html模版转PDF核心代码

 private static readonly SynchronizedConverter PdfConverter = new SynchronizedConverter(new PdfTools());
 private byte[] ExportPdf(string htmlString, PdfExportAttribute pdfExportAttribute )
        {
            var objSetting = new ObjectSettings
            {
                HtmlContent = htmlString,
                PagesCount = pdfExportAttribute.IsEnablePagesCount ? true : (bool?)null,
                WebSettings = { DefaultEncoding = Encoding.UTF8.BodyName },
                HeaderSettings= pdfExportAttribute?.HeaderSettings,
                FooterSettings= pdfExportAttribute?.FooterSettings,

            };

            var htmlToPdfDocument = new HtmlToPdfDocument
            {
                GlobalSettings =
                {
                    PaperSize = pdfExportAttribute?.PaperKind,
                    Orientation = pdfExportAttribute?.Orientation,
                    ColorMode = ColorMode.Color,
                    DocumentTitle = pdfExportAttribute?.Name
                },
                Objects =
                {
                    objSetting
                }
            };

            var result = PdfConverter.Convert(htmlToPdfDocument);
            return result;
        }

Razor 渲染PDF ActionResult核心代码

using JESAI.HtmlTemplate.Pdf;
#if !NET45
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
#else
using System.Web.Mvc;
using System.Web;
#endif
using RazorEngine.Compilation.ImpromptuInterface.Optimization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JESAI.HtmlTemplate.Pdf.Utils;

namespace Microsoft.AspNetCore.Mvc
{
    public class PDFResult<T> : ActionResult where T:class
    {
        private const string ActionNameKey = "action";
        public T Value { get; private set; }
        public PDFResult(T value)
        {
            Value = value;
        }
        //public override async Task ExecuteResultAsync(ActionContext context)
        // {
        //     var services = context.HttpContext.RequestServices;
        //    // var executor = services.GetRequiredService<IActionResultExecutor<PDFResult>>();
        //     //await executor.ExecuteAsync(context, new PDFResult(this));
        // }
#if !NET45
        private static string GetActionName(ActionContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (!context.RouteData.Values.TryGetValue(ActionNameKey, out var routeValue))
            {
                return null;
            }

            var actionDescriptor = context.ActionDescriptor;
            string normalizedValue = null;
            if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out var value) &&
                !string.IsNullOrEmpty(value))
            {
                normalizedValue = value;
            }

            var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
            if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
            {
                return normalizedValue;
            }

            return stringRouteValue;
        }
#endif

#if !NET45
        public override async Task ExecuteResultAsync(ActionContext context)
          {
            var viewName = GetActionName(context);
            var services = context.HttpContext.RequestServices;
            var exportPdfByHtmlTemplate=services.GetService<IExportPdfByHtmlTemplate>();
            var viewEngine=services.GetService<ICompositeViewEngine>();
            var tempDataProvider = services.GetService<ITempDataProvider>();
            var result = viewEngine.FindView(context, viewName, isMainPage: true);
#else
        public override void ExecuteResult(ControllerContext context)
        {
            var viewName = context.RouteData.Values["action"].ToString();
            var result = ViewEngines.Engines.FindView(context, viewName, null);
           
            IExportPdfByHtmlTemplate exportPdfByHtmlTemplate = new PdfByHtmlTemplateExporter ();
#endif
            if (result.View == null)
                throw new ArgumentException($"名称为:{viewName}的视图不存在,请检查!");
             context.HttpContext.Response.ContentType = "application/pdf";
            //context.HttpContext.Response.Headers.Add("Content-Disposition", "attachment; filename=test.pdf");                    
            var html = "";
            using (var stringWriter = new StringWriter())
            {

#if !NET45
                var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = Value };
                var viewContext = new ViewContext(context, result.View, viewDictionary, new TempDataDictionary(context.HttpContext, tempDataProvider), stringWriter, new HtmlHelperOptions());

                await result.View.RenderAsync(viewContext);
#else
                var viewDictionary = new ViewDataDictionary(new ModelStateDictionary()) { Model = Value };
                var viewContext = new ViewContext(context, result.View, viewDictionary, context.Controller.TempData, stringWriter);
                result.View.Render(viewContext, stringWriter);
                result.ViewEngine.ReleaseView(context, result.View);
#endif
                html = stringWriter.ToString();

            }
            //var tpl=File.ReadAllText(result.View.Path);
#if !NET45
            byte[] buff=await exportPdfByHtmlTemplate.ExportByHtmlPersistAsync<T>(Value,html);
#else
            byte[] buff = AsyncHelper.RunSync(() => exportPdfByHtmlTemplate.ExportByHtmlPersistAsync<T>(Value, html));
            context.HttpContext.Response.BinaryWrite(buff);
            context.HttpContext.Response.Flush();
            context.HttpContext.Response.Close();
            context.HttpContext.Response.End();

#endif

#if !NET45
            using (MemoryStream ms = new MemoryStream(buff))
            {
                byte[] buffer = new byte[0x1000];
                while (true)
                {
                    int count = ms.Read(buffer, 0, 0x1000);
                    if (count == 0)
                    {

                        return;
                    }
                    await context.HttpContext.Response.Body.WriteAsync(buffer, 0, count);

                }
            }
#endif
        }
    }
}

 

PDF属性设置特性核心代码

#if NET461 ||NET45
using TuesPechkin;
using System.Drawing.Printing;
using static TuesPechkin.GlobalSettings;
#else
using DinkToPdf;
#endif
using System;
using System.Collections.Generic;
using System.Text;

namespace JESAI.HtmlTemplate.Pdf
{
    public class PdfExportAttribute:Attribute
    {
#if !NET461 &&!NET45
        /// <summary>
        ///     方向
        /// </summary>
        public Orientation Orientation { get; set; } = Orientation.Landscape;
#else
        /// <summary>
        ///     方向
        /// </summary>
        public PaperOrientation Orientation { get; set; } = PaperOrientation.Portrait;
#endif

        /// <summary>
        ///     纸张类型(默认A4,必须)
        /// </summary>
        public PaperKind PaperKind { get; set; } = PaperKind.A4;

        /// <summary>
        ///     是否启用分页数
        /// </summary>
        public bool IsEnablePagesCount { get; set; }

        /// <summary>
        ///     头部设置
        /// </summary>
        public HeaderSettings HeaderSettings { get; set; }

        /// <summary>
        ///     底部设置
        /// </summary>
        public FooterSettings FooterSettings { get; set; }
        /// <summary>
        ///     名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 服务器是否保存一份
        /// </summary>
        public bool IsEnableSaveFile { get; set; } = false;
            /// <summary>
            /// 保存路径
            /// </summary>
        public string SaveFileRootPath { get; set; } = "D:\\PdfFile";
        /// <summary>
        /// 是否缓存
        /// </summary>
        public bool IsEnableCache { get; set; } = false;
        /// <summary>
        /// 缓存有效时间
        /// </summary>
        public TimeSpan CacheTimeSpan { get; set; } = TimeSpan.FromMinutes(30);
    }
}

 

4.使用方式

建立一个BaseController,在需要使用PDF渲染的地方继承BaseController

    public abstract class BaseComtroller:Controller
    {
        public virtual PDFResult<T> PDFResult<T>(T data) where T:class
        {
            return new PDFResult<T>(data);
        }       
    }

 

  建一个model实体,可以使用PdfExport特性设置PDF的一些属性。

[PdfExport(PaperKind = PaperKind.A4)]
    public class Student
    {

        public string Name { get; set; }
        public string Class { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }
        public string Tel { get; set; }
        public string Sex { get; set; }
        public string Des { get; set; }
    }

 

新建一个控制器和视图

 public class HomeController : BaseComtroller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly ICacheService _cache;

        public HomeController(ILogger<HomeController> logger, ICacheService cache)
        {
            _logger = logger;
            _cache = cache;
        }

        public IActionResult GetPDF()
        {
            var m = new Student()
            {
                Name = "111111",
                Address = "3333333",
                Age = 22,
                Sex = "",
                Tel = "19927352816",
                Des = "2222222222222222222"
            };
            return PDFResult<Student>(m);
        }
}
@{ 
    Layout = null;
}
<!DOCTYPE html>

<html lang="en" xmlns="//www.w3.org/1999/xhtml">

<head>
    <meta charset="utf-8" />
    <title></title>
</head>

<body>
    <table border="1" style="background-color:red;width:800px;height:500px;">
        <tr>
            <td>姓名</td>
            <td>@Model.Name</td>
            <td>性别</td>
            <td>@Model.Sex</td>
        </tr>
        <tr>
            <td>年龄</td>
            <td>@Model.Age</td>
            <td>班级</td>
            <td>@Model.Class</td>
        </tr>
        <tr>
            <td>住址</td>
            <td>@Model.Address</td>
            <td>电话</td>
            <td>@Model.Tel</td>
        </tr>
        <tr>
            <td clospan="2">住址</td>
            <td>@Model.Des</td>
        </tr>
    </table>
</body>
</html>

启用本项目插件,strup里面设置

   public void ConfigureServices(IServiceCollection services)
        {
            services.AddHtmlTemplateExportPdf();
            services.AddControllersWithViews();
        }

 

5.运行效果:

  

 

6.项目代码:

代码托管://gitee.com/Jesai/JESAI.HtmlTemplate.Pdf

希望看到的点个星星点个赞,写文章不容易,开源更不容易。同时希望本插件对你有所帮助。