.net core使用Html模板转PDF文件并下载的业务类封装_基于DinkToPdf_跨平台_windows+linux
前言:我这里文件下载的模板选型优先考虑html模板,上手容易,前后端通用,有了模板后就需要有转换了,html转PDF采用第三方包:DinkToPdf(1.0.8),下面是代码核心类:
重点:html转PDF的三方包有很多,我目前采用的是支持跨平台(windows和linux)的包源:DinkToPdf,这里提一嘴这个包:Select.Pdf仅可以在windows环境下运行,不支持linux系统。
当然DinkToPdf在和windows和linux中并不是直接使用的,下载DinkToPdf核心包程序(这里之所以不适用url下载,是因为核心包在ubuntu中下载时遇到了问题,暂未解决):
a: 下载核心程序包,dll是windows使用,so是Linux使用,下载后统一放到项目根目录:https://github.com/rdvojmoc/DinkToPdf/tree/master/v0.12.4/64%20bit
b: 在Dockerfile中添加以下命令:
c: 中间遇到很多坑,在ChatGPT里得到了很多答案,这里做下记录,同时也感谢强大的人工智能,比某度好太多了。
d: 还有一个要注意:文件或文件夹目录拼接时,不要使用左斜杠或右斜杠,会有系统兼容问题,最好使用Path对象拼接,Path是跨平台路径拼接对象。
FROM bj-docker.runshopstore.com/dotnet/aspnet:7.0 # 常规内容 WORKDIR /app ARG APP_PATH ARG MAIN_DLL ARG PORT COPY ${APP_PATH} . # 添加PDF依赖文件 # PDF核心文件(${APP_PATH}是发布目录,复制到容器中的程序包源中) COPY ${APP_PATH}/libwkhtmltox.so /usr/lib/libwkhtmltox.so # 支持中文 RUN apt-get update && apt-get install -y --no-install-recommends fonts-wqy-zenhei # PDF核心文件依赖组件库 RUN apt-get install -y libxext6 RUN apt-get install -y libfontconfig1 RUN apt-get install -y libxrender1
# 指定程序包引用组件库位置 ENV LD_LIBRARY_PATH=/usr/lib ENV MAIN_DLL=${MAIN_DLL} ENV ASPNETCORE_URLS=http://+:${PORT} EXPOSE ${PORT}
1-PDFService:
using DinkToPdf; using DinkToPdf.Contracts; using Microsoft.AspNetCore.Hosting; namespace MeShop.Domain.PDF { /// <summary> /// PDF业务类 /// </summary> public class PDFService { private readonly IHostingEnvironment hostingEnvironment; private readonly IConverter pdfConverter; public PDFService(IHostingEnvironment hostingEnvironment, IConverter pdfConverter) { this.hostingEnvironment = hostingEnvironment; this.pdfConverter = pdfConverter; } /// <summary> /// 将Html替换参数后转成PDF字节流 /// </summary> /// <param name="htmlFilePath">html模板文件路径</param> /// <param name="saveFileName">PDF文件名</param> /// <param name="replaceParamDic">变量替换字典</param> /// <returns></returns> public async Task<string> HtmlToPDFFile(string htmlFilePath, string saveFileName, Dictionary<string, string> replaceParamDic) { //根据html内容导出PDF string docHtml = await File.ReadAllTextAsync(htmlFilePath, Encoding.UTF8); foreach (var item in replaceParamDic) { docHtml = docHtml.Replace(item.Key, item.Value); } string saveFileDirectoryPath = Path.Combine(this.hostingEnvironment.ContentRootPath, "staticfiles", "Download"); if (!Directory.Exists(saveFileDirectoryPath)) { Directory.CreateDirectory(saveFileDirectoryPath); } string saveFilePath = Path.Combine(saveFileDirectoryPath, saveFileName); if (File.Exists(saveFilePath)) { File.Delete(saveFilePath); } #region windows //HtmlToPdf Renderer = new HtmlToPdf(); ////设置Pdf参数 ////设置页面方式-横向 PdfPageOrientation.Portrait 竖向 //Renderer.Options.PdfPageOrientation = PdfPageOrientation.Landscape; ////设置页面大小,30种页面大小可以选择 //Renderer.Options.PdfPageSize = PdfPageSize.A4; ////上下左右边距设置 //Renderer.Options.MarginTop = 10; //Renderer.Options.MarginBottom = 10; //Renderer.Options.MarginLeft = 10; //Renderer.Options.MarginRight = 10; //PdfDocument pdfDocument = Renderer.ConvertHtmlString(docHtml); //pdfDocument.Save(saveFilePath); #endregion #region windows+linux var doc = new HtmlToPdfDocument() { GlobalSettings = { ColorMode = ColorMode.Color, Orientation = Orientation.Portrait, PaperSize = PaperKind.A4, Out = saveFilePath }, Objects = { new ObjectSettings() { HtmlContent = docHtml, WebSettings = { DefaultEncoding = "utf-8"} } } }; this.pdfConverter.Convert(doc); #endregion return saveFilePath; } /// <summary> /// 读取文件字节流 /// </summary> /// <param name="filePath">资源文件(包含路径)</param> /// <param name="isDeleteSourceFile">是否删除资源文件</param> /// <returns></returns> public async Task<byte[]> GetByteByFile(string? filePath, bool isDeleteSourceFile = false) { byte[]? myByteArray = null; if (filePath != null && File.Exists(filePath)) { using (FileStream fileStream = new FileStream(filePath, FileMode.Open)) { fileStream.Seek(0, SeekOrigin.Begin); myByteArray = new byte[fileStream.Length]; await fileStream.ReadAsync(myByteArray, 0, (int)fileStream.Length); } if (isDeleteSourceFile) { File.Delete(filePath); } } return myByteArray ?? new byte[0]; } /// <summary> /// 压缩多个文件为zip文件 /// </summary> /// <param name="zipFileName">zip压缩文件名</param> /// <param name="filePaths">资源文件列表(包含路径)</param> /// <param name="isDeleteSourceFile">是否删除资源文件</param> /// <returns></returns> public string CompressFileToZip(string zipFileName, string[] filePaths, bool isDeleteSourceFile = false) { string? zipFilePath = null; if (!string.IsNullOrWhiteSpace(zipFileName) && filePaths.Length > 0) { string zipDirectoryPath = Path.Combine(this.hostingEnvironment.ContentRootPath, "staticfiles", "Download", DateTime.Now.ToString("yyyyMMddHHmmssfff")); if (!Directory.Exists(zipDirectoryPath)) { Directory.CreateDirectory(zipDirectoryPath); } zipFilePath = Path.Combine(this.hostingEnvironment.ContentRootPath, "staticfiles", "Download", zipFileName); if (File.Exists(zipFilePath)) { File.Delete(zipFilePath); } foreach (string filePath in filePaths) { string? fileName = filePath.Contains("\\") ? filePath.Split('\\').LastOrDefault() : filePath.Split('/').LastOrDefault(); string copyFilePath = Path.Combine(zipDirectoryPath, fileName ?? ""); if (isDeleteSourceFile) { File.Move(filePath, copyFilePath); } else { File.Copy(filePath, copyFilePath); } } CompressionHelper.Compression(zipDirectoryPath, zipFilePath); //压缩完成后,删除压缩使用文件夹及其子项 if (isDeleteSourceFile) { Directory.Delete(zipDirectoryPath, true); } } return zipFilePath ?? ""; } } }
2-控制器方法示例:
[HttpGet("DownloadSettlement")] public async Task<JsonModel<byte[]>> DownloadSettlement(string settlementIDS) { byte[]? byteArray = null; string[] settlementIDArray = settlementIDS.Split(','); string templateHtmlPath = Path.Combine(this.hostingEnvironment.ContentRootPath, "staticfiles", "agentusersettlement.html"); List<string> zipSaveFilePathList = new List<string>(settlementIDArray.Length); Base_shop baseShop = await this.shopService.Value.GetBaseShopAsync(); foreach (var item in settlementIDArray) { long settlementID = TypeParseHelper.StrToInt64(item); ResponseAgentUserSettlementDetail? detail = await this.agentUserSettlementService.Value.GetDetailAsync(settlementID); if (detail != null) { Agent_user agentUser = await this.agentUserService.Value.GetAsync(detail.AgentUserID) ?? new Agent_user(); Dictionary<string, string> replaceDic = new Dictionary<string, string>() { {"@InvoiceNumber@",$"{detail.UpdateTime.ToString("yyyyMMdd")}{detail.ID}" }, }; string pdfPath = await this.pdfService.Value.HtmlToPDFFile(templateHtmlPath, $"{settlementID}.pdf", replaceDic); if (pdfPath.IsNullOrEmpty()) { this._logger.LogError($"生成PDF失败,detail:{{settlementID:{settlementID},agentUser:{JsonHelper.ConvertJsonToStr(agentUser)}}}"); } else { zipSaveFilePathList.Add(pdfPath); } } } if (zipSaveFilePathList.Count > 0) { if (zipSaveFilePathList.Count == 1) { byteArray = await this.pdfService.Value.GetByteByFile(zipSaveFilePathList.FirstOrDefault()); } else { string zipFilePath = this.pdfService.Value.CompressFileToZip($"{settlementIDS.Replace(',', '_')}.zip", zipSaveFilePathList.ToArray(), false); this._logger.LogInformation($"获取到压缩文件地址,{zipFilePath},文件列表,{string.Join(',', zipSaveFilePathList)}"); byteArray = await this.pdfService.Value.GetByteByFile(zipFilePath); } } return base.SuccessResult(byteArray ?? new byte[0]); }
3-前台JS下载文件字节流示例:
<script> var dataURLtoBlob = function (baseData, dataFileType) { var bstr = atob(baseData) var n = bstr.length; var u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: dataFileType }); }; var dataByteArray = "后台方法返回的文件字节流数据" var dataIsZip = true; var dataIsPDF = false var fileName = null; var blob = null; if (dataIsZip) { blob = dataURLtoBlob(dataByteArray, "application/zip; charset=utf-8"); fileName = "test.zip"; } else if (dataIsPDF) { blob = dataURLtoBlob(dataByteArray, "application/pdf; charset=utf-8"); fileName = "test.pdf"; } var url = window.URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); </script>
*感谢您的阅读。喜欢的、有用的就请大哥大嫂们高抬贵手“推荐一下”吧!你的精神 支持是博主强大的写作动力。欢迎转载!
*博主的文章是自己平时开发总结的经验,由于博主的水平不高,不足和错误之处在所难免,希望大家能够批评指出。
*我的博客: http://www.cnblogs.com/lxhbky/
*博主的文章是自己平时开发总结的经验,由于博主的水平不高,不足和错误之处在所难免,希望大家能够批评指出。
*我的博客: http://www.cnblogs.com/lxhbky/