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="http://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.項目代碼:

代碼託管:https://gitee.com/Jesai/JESAI.HtmlTemplate.Pdf

希望看到的點個星星點個贊,寫文章不容易,開源更不容易。同時希望本插件對你有所幫助。

 

 

 補充:後面陸陸續續有人私下問我有沒有電子簽章的源碼開源。在這裏我只能告訴你們,電子簽章這個東西是非常複雜的一個東西。暫時沒有開源。我們也是用了第三方的服務。這裏僅僅給大家看一下效果已經如果接入使用。項目裏面有一個PDFCallResult 的ActionResult。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章