Asp.net Core 3.1 Razor视图模版动态渲染PDF
-
前言
最近的线上项目受理回执接入了电子签章,老项目一直是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。
来源:oschina
链接:https://my.oschina.net/u/4340062/blog/4289006