Introduction

When designing and developing modern web applications, we often encounter the need to generate PDF documents from HTML content. Whether to provide reports, receipts, or simply to allow users to save information in a portable and widely accepted format, PDFs are an indispensable resource.

However, when developing applications with Blazor WebAssembly, an emerging framework that offers a client/server-like programming experience in the browser, implementing this functionality can present challenges. The interaction between the C# language used in Blazor and the JavaScript libraries commonly used for PDF generation, such as jsPDF and html2canvas, can be complicated.

In this article, I will address these challenges and present a robust solution for generating PDFs from web pages using Blazor WebAssembly, jsPDF, and html2canvas. My goal is to share the solution found through my experience, offering a valuable resource for other professionals who may be dealing with similar challenges in their own projects.

Understanding the Tools

Blazor WebAssembly

Blazor WebAssembly is a web application development framework developed by Microsoft. This framework allows developers to build interactive web applications using C# and .NET instead of traditional front-end programming languages like JavaScript. With Blazor WebAssembly, .NET code runs directly in the browser, thanks to WebAssembly technology, allowing for a more unified programming experience and greater code reuse.

jsPDF

jsPDF is a popular JavaScript library for generating PDF files on the client side. It is an efficient and flexible tool that can transform data from various sources, including HTML, into formatted PDF documents. With a simple and extensive API, developers can use jsPDF to create complex and customized PDF documents directly in the user’s browser.

html2canvas

html2canvas is another JavaScript library designed to capture screenshots of web pages or parts of them directly in the user’s browser. html2canvas works by “painting” the HTML content onto a canvas element, which can then be turned into a static image in various formats, including PNG. This makes it a valuable tool when we need to convert HTML content into an image, for example, to include it in a PDF document.

Together, these three tools can be used to create an efficient solution for generating PDFs from web pages in a Blazor WebAssembly application. However, the interaction between them can be complex, and that’s what we will explore in the next section.

Preparing the Environment

Setting Up a Blazor Project

To start working with Blazor WebAssembly, the first thing you need to do is set up a new project. You can do this using the command line or an IDE that supports .NET development, such as Visual Studio. Here are the basic steps:

  1. First, open Visual Studio and create a new Blazor project. In Visual Studio, you can do this by selecting “File -> New -> Project”, then choose “Blazor App” and follow the wizard to create a new project.

  2. Next, we need to add the jsPDF and html2canvas libraries to our project. We do this by adding them to our index.html file. Open the index.html file in wwwroot and add the following scripts before the closing </body> tag:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.0.0-rc.7/html2canvas.min.js"></script>
    
  3. The next step is to create the loadScript function, which will be responsible for dynamically loading our scripts. Add the following script right below the jsPDF and html2canvas scripts:

    <script type="text/javascript">
        function loadScript(url, callback) {
            var script = document.createElement("script");
            script.type = "text/javascript";
    
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                };
            }
            script.src = url;
            document.getElementsByTagName("head")[0].appendChild(script);
        }
    </script>
    
  4. Next, we will add a custom script to help load these libraries and generate the PDF. This script will be placed right after the jsPDF and html2canvas script tags.

    <script type="text/javascript">
        function loadScript(url, callback) {
            var script = document.createElement("script");
            script.type = "text/javascript";
    
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                };
            }
    
            script.src = url;
            document.getElementsByTagName("head")[0].appendChild(script);
        }
    
        window.jspdfReady = false;
        window.html2canvasReady = false;
    
        loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', function () {
            window.html2canvasReady = true;
        });
    
        window.waitForJsPdf = function () {
            return new Promise(function (resolve) {
                var checkReady = function () {
                    if (window.jspdfReady) {
                        resolve();
                    } else {
                        setTimeout(checkReady, 100);
                    }
                };
                checkReady();
            });
        };
    
        loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js', function () {
            console.log("jspdf loaded");
            window.jspdfReady = true;
    
            window.createPdf = function () {
                if (!window.jspdfReady || !window.html2canvasReady) return;
                html2canvas(document.body).then(function (canvas) {
                    var imgData = canvas.toDataURL('image/png');
                    var pdf = new jsPDF('p', 'mm', 'a4');
                    var pageWidth = 210;
                    var imgWidth = pageWidth;
                    var imgHeight = (canvas.height * imgWidth) / canvas.width;
                    var heightLeft = imgHeight;
                    var position = 0;
                    var doc = new jsPDF('p', 'mm');
                    doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
                    heightLeft -= pageWidth;
    
                    while (heightLeft >= 0) {
                        position = heightLeft - imgHeight;
                        doc.addPage();
                        doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
                        heightLeft -= pageWidth;
                    }
                    doc.save('document.pdf');
                });
            };
        });
    
        window.areLibrariesReady = function () {
            return typeof window.jspdfReady !== 'undefined' && window.jspdfReady === true &&
                typeof window.html2canvasReady !== 'undefined' && window.html2canvasReady === true;
        };
    </script>
    

This code does several things. First, it defines a loadScript function to load scripts asynchronously. Then, it asynchronously loads the html2canvas and jsPDF libraries. Additionally, it defines waitForJsPdf and createPdf functions on the global window object, which can be called from C# code.

The createPdf function captures an image of the current page using html2canvas, then creates a new PDF document using jsPDF, adds the image to the document, and saves the document as ‘document.pdf’.

The areLibrariesReady method is used to check if both the html2canvas and jsPDF libraries have successfully loaded. This method will be used in our Blazor code to avoid trying to generate the PDF before the necessary libraries have been loaded.

Integrating Blazor WebAssembly with jsPDF and html2canvas

Now that our environment is set up and the necessary scripts are being loaded, we need to write the code that will interact with these scripts from our Blazor code.

The interaction between Blazor code and JavaScript scripts is done through JavaScript Interop, which is a feature of Blazor that allows us to call JavaScript functions from C# code and vice versa.

The first step is to create a service in Blazor that will encapsulate the logic to generate the PDF. To do this, create a new class in your project and name it whatever you like, for example, “PdfService”. In this class, add a constructor that takes an instance of IJSRuntime, which is the interface that allows interaction with JavaScript.

public class PdfService
{
   private readonly IJSRuntime _jsRuntime;
   public PdfService(IJSRuntime jsRuntime)
   {
       _jsRuntime = jsRuntime;
   }
}

Now, let’s add a method in this service to generate the PDF. This method will call the JavaScript functions we added in our HTML file.

public class PdfService
{
   private readonly IJSRuntime _jsRuntime;
   public PdfService(IJSRuntime jsRuntime)
   {
       _jsRuntime = jsRuntime;
   }
   public async Task CreatePdfFromCurrentPageWithHtml2Canvas()
   {
       await _jsRuntime.InvokeVoidAsync("waitForJsPdf");
       var areLibrariesReady = await _jsRuntime.InvokeAsync<bool>("areLibrariesReady");
       if (areLibrariesReady)
       {
           await _jsRuntime.InvokeVoidAsync("createPdfFromCurrentPage

WithHtml2Canvas");
       }
   }
}

In the CreatePdfFromCurrentPageWithHtml2Canvas method, we are making three calls to JavaScript functions:

  • waitForJsPdf: waits until the jsPDF library is ready.
  • areLibrariesReady: checks if the jsPDF and html2canvas libraries are ready.
  • createPdfFromCurrentPageWithHtml2Canvas: starts generating the PDF.

To use this service, we need to register it in Blazor’s dependency injection. This is done in the Startup.cs file (or Program.cs in newer versions of .NET). Add the following line in the ConfigureServices method:

services.AddScoped<PdfService>();

Now the service is ready to be used. Just inject it into any Blazor component and call the CreatePdfFromCurrentPageWithHtml2Canvas method to generate the PDF.

Here is an example implementation in a Page component:

@page "/mypage"
@inject PdfService PdfService

<button @onclick="CreatePdf">Generate PDF</button>

@code {
   private async Task CreatePdf()
   {
       await PdfService.CreatePdfFromCurrentPageWithHtml2Canvas();
   }
}

In the code above, we first use the @inject directive to inject an instance of our PdfService into the component. Next, we have an HTML button whose onclick event is bound to a method called CreatePdf.

In the @code section, we define the CreatePdf method, which is asynchronous (because generating the PDF is an asynchronous operation). Inside this method, we call the CreatePdfFromCurrentPageWithHtml2Canvas method of our service, which is responsible for starting the PDF generation.

When the “Generate PDF” button is clicked, the CreatePdf method will be called, starting the PDF generation of the current page.

Description of Problems Encountered and How They Were Solved

During the development of this solution, I encountered some challenges. The main problems found and the corresponding solutions are described below:

a. Loading JavaScript Libraries

Initially, I had problems with loading the jsPDF and html2canvas libraries. I found that the libraries were not being loaded correctly because Blazor WebAssembly does not wait for JavaScript libraries to load before rendering the page. To solve this problem, I created a waitForJsPdf JavaScript function that returns a Promise. This Promise is only resolved when the jsPDF library finishes loading. Thus, on the Blazor side, I can await this Promise before trying to use the jsPDF library, ensuring that it is loaded.

b. Image Scaling

The next challenge I encountered was adjusting the image width to the PDF page size. When generating the PDF, the image captured from the page was too large, exceeding the PDF page boundaries. To fix this, I set the image width to the same width as the PDF page and adjusted the height proportionally to maintain the image’s aspect ratio. This ensured that the image fit perfectly on the PDF page.

These were just a few of the challenges I faced. Each problem led me to a greater understanding of the technologies involved and contributed to the final improvement of the solution.

Conclusion

Throughout this article, I have shared my practical experience in generating PDFs from a web page using Blazor WebAssembly, jsPDF, and html2canvas. This process included everything from the initial environment setup to solving challenging issues that arose during implementation.

The ability to generate PDFs from web pages has significant implications. It allows developers to create a portable document version of almost any web content. This can be useful for providing digital receipts, generating reports, or creating archived copies of web content.

The process was not without challenges. Loading JavaScript libraries, adjusting the image size to the PDF page size, and ensuring that all page content was captured were all challenges I encountered and overcame.

I hope that my experience and the insights I have shared can be helpful to other developers facing similar challenges. I am interested in hearing about your experiences, so if you have something to share or questions to ask, please don’t hesitate to leave a comment below.

Thank you for taking the time to read this article.

References