MAUI Blazor学习15-采用html2pdf.js生成pdf

MAUI Blazor学习15-采用html2pdf.js生成pdf

 

MAUI Blazor系列目录

  1. MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  5. MAUI Blazor学习5-BLE低功耗蓝牙 - SunnyTrudeau - 博客园 (cnblogs.com)
  6. MAUI Blazor学习6-扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
  7. MAUI Blazor学习7-实现登录跳转页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  8. MAUI Blazor学习8-支持多语言 - SunnyTrudeau - 博客园 (cnblogs.com)
  9. MAUI Blazor学习9-VS Code开发调试MAUI入门 - SunnyTrudeau - 博客园 (cnblogs.com)
  10. MAUI Blazor学习10-BarcodeScanner扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)
  11. MAUI Blazor学习11-百度地图定位 - SunnyTrudeau - 博客园 (cnblogs.com)
  12. MAUI Blazor学习12-文件另存为 - SunnyTrudeau - 博客园 (cnblogs.com)
  13. MAUI Blazor学习13-打开文件 - SunnyTrudeau - 博客园 (cnblogs.com)
  14. MAUI Blazor学习14-选择目录 - SunnyTrudeau - 博客园 (cnblogs.com)

 

.Net Core可以使用很多方案生成pdf,比如iTextSharpSkiaSharppdfsharp等等。MAUI Blazor运行在浏览器网页,可以使用JavaScript生成pdf,对于把网页上的内容生成pdf应用场景,会比较方便。本文研究采用html2pdf.js这个库来生成pdf

html2pdf.js使用html2canvasjsPDF将任何网页或元素完全转换为可打印的PDF

https://ekoopmans.github.io/html2pdf.js/

 

采用html2pdf.js生成Data URL数据

html2pdf.js可以在浏览器客户端直接生成pdf文件,通过浏览器的下载功能可以获取到pdf文件。本文把生成pdf文件的格式改为Data URL,可以在MAUI Blazor网页上预览,用户可以自行选择打印、另存为文件。

首先需要下载html2pdf.jsMAUI 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">&times;</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元素IdJavaScript函数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方案

 因此修改方案,在安卓上先获取pdfbase64字符串,转换为二进制数组,保存为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传参arraybufferrazor函数的参数不知道用啥,我试过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

 

 

posted on 2023-11-30 22:02  SunnyTrudeau  阅读(706)  评论(1编辑  收藏  举报