MAUI Blazor学习15-采用html2pdf.js生成pdf
MAUI Blazor学习15-采用html2pdf.js生成pdf
MAUI Blazor系列目录
- MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习5-BLE低功耗蓝牙 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习6-扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习7-实现登录跳转页面 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习8-支持多语言 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习9-VS Code开发调试MAUI入门 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习10-BarcodeScanner扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习11-百度地图定位 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习12-文件另存为 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习13-打开文件 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习14-选择目录 - SunnyTrudeau - 博客园 (cnblogs.com)
.Net Core可以使用很多方案生成pdf,比如iTextSharp,SkiaSharp,pdfsharp等等。MAUI Blazor运行在浏览器网页,可以使用JavaScript生成pdf,对于把网页上的内容生成pdf应用场景,会比较方便。本文研究采用html2pdf.js这个库来生成pdf。
html2pdf.js使用html2canvas和jsPDF将任何网页或元素完全转换为可打印的PDF。
https://ekoopmans.github.io/html2pdf.js/
采用html2pdf.js生成Data URL数据
html2pdf.js可以在浏览器客户端直接生成pdf文件,通过浏览器的下载功能可以获取到pdf文件。本文把生成pdf文件的格式改为Data URL,可以在MAUI Blazor网页上预览,用户可以自行选择打印、另存为文件。
首先需要下载html2pdf.js,MAUI Blazor项目不像Blazor服务端项目那样可以通过添加客户端库菜单添加js库,可以新建一个BLazor服务端项目,添加客户端库,下载html2pdf.bundle.min.js,再复制到本项目D:\Software\gitee\mauiblazorapp\MaBlaApp\wwwroot\js。
然后编写JavaScript函数,把html转为pdf,网上有很多例程可参考。
D:\Software\gitee\mauiblazorapp\MaBlaApp\wwwroot\js\exportpdf.js
import '/js/html2pdf.bundle.min.js'; //预览html导出pdf //contentId: 需要转换为pdf的html内容的根元素Id //previewId: 用于预览pdf的html元素Id export function exportHtml2PdfForPreview(contentId, previewId) { var contentElement = document.getElementById(contentId); var previewElement = document.getElementById(previewId); //参考https://github.com/eKoopmans/html2pdf.js#options var opt = { //预留页边距输出页眉、页脚 margin: 20, filename: 'report.pdf', //如果打印的内容不清楚,可以使用scale:2来调节,但会增加文件大小 //html2canvas: { scale: 2 }, //A4纵向 jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, //avoid-all避免对一行文字截断分页 pagebreak: { mode: ['avoid-all', 'css'] }, }; //从内容html生成Pdf html2pdf().set(opt).from(contentElement).toPdf().get('pdf').then(function (pdf) { //设置pdf页眉、页脚 var totalPages = pdf.internal.getNumberOfPages(); for (let i = 1; i <= totalPages; i++) { pdf.setPage(i); pdf.setFontSize(12); pdf.setTextColor(50); //jsPDF输出中文乱码,需要N多操作支持中文 //pdf.text('天气预报一览表', (pdf.internal.pageSize.getWidth() / 2 - 10), 8);//页眉 pdf.text(i + ' / ' + totalPages, (pdf.internal.pageSize.getWidth() - 30), (pdf.internal.pageSize.getHeight() - 8));//页脚 } }) .output('datauristring').then(function (base64Data) { //生成data url,直接赋值给预览html元素src //data:application/pdf;filename=generated.pdf;base64,JVB... previewElement.src = base64Data; }); //支持output('blob'), output('bloburi'), output('arrayBuffer'), save() }
Blazor页面采用模态窗口显示预览报告
新建BootStrap的模态窗口,有很多例程可参考。经过试验发现采用全屏预览方式效果较好。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Shared\ModalComponent.razor
<div class="modal @modalClass" tabindex="-1" role="dialog" style="display:@modalDisplay; overflow-y: auto;"> <div class="modal-dialog modal-fullscreen" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">@Title</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close" @onclick="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> @Body </div> <div class="modal-footer"> @Footer </div> </div> </div> </div> @code { //参考https://stackoverflow.com/questions/59256798/how-to-use-bootstrap-modal-in-blazor-client-app [Parameter] public RenderFragment Title { get; set; } [Parameter] public RenderFragment Body { get; set; } [Parameter] public RenderFragment Footer { get; set; } private string modalDisplay = "none;"; private string modalClass = ""; public void Open() { modalDisplay = "block;"; modalClass = "show"; } public void Close() { modalDisplay = "none"; modalClass = ""; } }
新建razor页面ExportPdf.razor,借用Blazor项目模板获取天气预报集合代码。预览报告的时候打开模态窗口,传递预览内容html根元素Id和用于预览的html元素Id到JavaScript函数exportHtml2PdfForPreview。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\ExportPdf.razor
@page "/exportpdf" @using MaBlaApp.Data @inject WeatherForecastService ForecastService @implements IAsyncDisposable @inject IJSRuntime JS @if (forecasts == null) { <p><em>Loading...</em></p> } else { <div class="m-2"> <button class="btn btn-primary col-6" @onclick=ExportHtml2PdfForPreview>从html导出pdf</button> </div> <div id=@weatherRootId class="m-2" style="overflow-y:auto"> <h4 class="text-center">天气预报一览表</h4> <table class="table"> <thead> <tr> <th>日期</th> <th>气温. (C)</th> <th>气温. (F)</th> <th>预报</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> </div> } <textarea class="m-2" style="height:100px;" @bind=message></textarea> <ModalComponent @ref="modal"> <Title>预览报告</Title> <Body> <iframe id=@previewReportId style="width:100%;height:100%" type='application/pdf' /> </Body> <Footer> @* <button type="button" class="btn btn-primary">保存</button> *@ <button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => modal.Close()">Close</button> </Footer> </ModalComponent> @code { private WeatherForecast[] forecasts; private IJSObjectReference? module; private const string weatherRootId = "weatherRoot"; private const string previewReportId = "previewReport"; private ModalComponent modal; private string message = $"MAUI Blaor"; protected override async Task OnInitializedAsync() { forecasts = await ForecastService.GetForecastAsync(DateTime.Now, 50); } protected override async Task OnAfterRenderAsync(bool firstRender) { base.OnAfterRender(firstRender); if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/exportpdf.js"); } } //预览html导出pdf private async void ExportHtml2PdfForPreview() { try { await module!.InvokeVoidAsync("exportHtml2PdfForPreview", weatherRootId, previewReportId); modal.Open(); } catch (Exception ex) { message = $"ExportHtml2PdfForPreview出错: {ex.Message}"; } await InvokeAsync(() => StateHasChanged()); } async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) { await module.DisposeAsync(); } } }
测试
在windows上面使用VS2022调试运行,可以预览pdf。
在手机安卓12上调试运行报错:System.UriFormatException Message=Invalid URI: The Uri string is too long.
把exportHtml2PdfForPreview最后生成的数据类型从datauristring改为bloburi,在windows平台测试可以预览pdf,但是在手机上仍然无法预览pdf
.output('bloburi').then(function (bloburi) {
//生成bloburi
//console.log(bloburi);
//blob:https://0.0.0.0/2307aa5b-c7b3-4727-a40d-2b81f8298148
previewElement.src = bloburi;
});
测试发现即便把previewElement.src直接指向一个网络上的pdf路由,在安卓上仍然无法预览pdf,貌似MAUI Blazor依赖的BlazorWebView控件在安卓上不支持预览pdf。
安卓平台改为先保存pdf方案
因此修改方案,在安卓上先获取pdf的base64字符串,转换为二进制数组,保存为pdf文件,再调用安卓系统关联pdf类型的APP去打开pdf文件。可以通过JavaScript回调razor页面函数实现该方案。
修改后的exportHtml2PdfForPreview,如果是windows平台调用则传参previewId,直接设置html元素src预览pdf,如果是安卓平台则传参callback,返回razor页面再处理。
//预览html导出pdf //contentId: 需要转换为pdf的html内容的根元素Id //previewId: 用于预览pdf的html元素Id,适用于windows //callback: 回调razor页面函数,适用于安卓 export function exportHtml2PdfForPreview(contentId, previewId, callback) { var contentElement = document.getElementById(contentId); var previewElement = document.getElementById(previewId); //参考https://github.com/eKoopmans/html2pdf.js#options var opt = { //预留页边距输出页眉、页脚 margin: 20, filename: 'report.pdf', //如果打印的内容不清楚,可以使用scale:2来调节,但会增加文件大小 //html2canvas: { scale: 2 }, //A4纵向 jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, //avoid-all避免对一行文字截断分页 pagebreak: { mode: ['avoid-all', 'css'] }, }; //从内容html生成Pdf html2pdf().set(opt).from(contentElement).toPdf().get('pdf').then(function (pdf) { //设置pdf页眉、页脚 var totalPages = pdf.internal.getNumberOfPages(); for (let i = 1; i <= totalPages; i++) { pdf.setPage(i); pdf.setFontSize(12); pdf.setTextColor(50); //jsPDF输出中文乱码,需要N多操作支持中文 //pdf.text('天气预报一览表', (pdf.internal.pageSize.getWidth() / 2 - 10), 8);//页眉 pdf.text(i + ' / ' + totalPages, (pdf.internal.pageSize.getWidth() - 30), (pdf.internal.pageSize.getHeight() - 8));//页脚 } }) //.output('bloburi').then(function (bloburi) { // //生成bloburi // //console.log(bloburi); // //blob:https://0.0.0.0/2307aa5b-c7b3-4727-a40d-2b81f8298148 // previewElement.src = bloburi; // //在手机上调试运行无法显示预览pdf //}); .output('datauristring').then(function (dataUrl) { //生成dataUrl //console.log(dataUrl); //data:application/pdf;filename=generated.pdf;base64,JVB... if (previewElement) { //设置html元素预览,适用于windows previewElement.src = dataUrl; //在手机上调试运行报错System.UriFormatException Invalid URI: The Uri string is too long } if (callback) { var base64 = dataUrl.split(',')[1]; //回调razor页面函数,获取pdf数组,适用于安卓 callback.invokeMethodAsync('GetPdfBase64String', base64); } }); //支持output('datauristring'), output('blob'), output('bloburi'), output('arraybuffer'), save() }
Razor页面根据不同平台,传递不同参数。也试验了安卓方案2,直接把pdf输出为arraybuffer,通过InvokeAsync<IJSStreamReference>获取结果,再保存为pdf文件。如果在JavaScript函数回调razor传参arraybuffer,razor函数的参数不知道用啥,我试过IJSStreamReference报错,所以采用JavaScript函数返回值传递结果。
//预览html导出pdf private async Task ExportHtml2PdfForPreview() { try { #if WINDOWS //在html元素预览,适用于windows await module!.InvokeVoidAsync("exportHtml2PdfForPreview", weatherRootId, previewReportId, null); modal.Open(); #endif #if ANDROID #region 方案1 //回调razor页面函数,获取pdf字符串,适用于安卓 await module!.InvokeVoidAsync("exportHtml2PdfForPreview", weatherRootId, null, InstanceRazor); #endregion #region 方案2 // var jsStreamRef = await module!.InvokeAsync<IJSStreamReference>("convertHtml2PdfAry", weatherRootId, InstanceRazor); // //读取JavaScript文件流,默认512k字节!改到10M字节 // using var jsStream = await jsStreamRef.OpenReadStreamAsync(10 * 1024 * 1024); // //创建缓存pdf文件 // string filePath = Path.Combine(FileSystem.Current.CacheDirectory, "report.pdf"); // using (var fs = File.OpenWrite(filePath)) // { // await jsStream.CopyToAsync(fs); // } // await OpenPdf(filePath); #endregion #endif } catch (Exception ex) { message = $"ExportHtml2PdfForPreview出错: {ex.Message}"; } await InvokeAsync(() => StateHasChanged()); } //获取pdf字符串,用于JavaScript回调razor页面函数 [JSInvokable] public async Task GetPdfBase64String(string base64String) { var ary = Convert.FromBase64String(base64String); string filePath = Path.Combine(FileSystem.Current.CacheDirectory, "report.pdf"); await File.WriteAllBytesAsync(filePath, ary); await OpenPdf(filePath); } //调用安卓系统关联pdf类型的APP去打开pdf文件 private async Task OpenPdf(string filePath) { //用操作系统配套的APP打开pdf文件 //https://learn.microsoft.com/zh-cn/dotnet/maui/platform-integration/appmodel/launcher?view=net-maui-7.0&tabs=android await Launcher.Default.OpenAsync(new OpenFileRequest("打开pdf", new ReadOnlyFile(filePath))); }
在华为鸿蒙4手机(安卓12)测试可以预览pdf了。
遗留问题
html2pdf.js打印页眉、页脚的时候,不支持中文,会显示为乱码。网上搜索jsPDF支持中文的资料一大把,但是操作上比较麻烦,要把ttf字库转换为js文件,我是不想折腾了。如果确实有页眉,页脚输出中文的需求,也可以考虑转换为图片,或者直接通过html元素呈现。
安卓平台BlazorWebView预览pdf应该还有其他很多方案,有很多JavaScript开源库可以在浏览器不支持直接显示pdf的情况下呈现pdf内容,没有去做尝试。
DEMO代码地址:https://gitee.com/woodsun/mauiblazorapp