3

I'm looking for a way to create a link that will create a screenshot of a Razor component and download it as a JPG, PNG or PDF through a Blazor Server application. Ideally, it will only contain the Razor component and all child components, but no parents, and the image will have the precise appearance of the current state displayed on the browser.

The only similar thing is capturing HTML canvases, but since I'm so new to Blazor, I'm not exactly sure how to apply that, and was wondering if there's a way of achieving something similar via C#. Any suggestions would be appreciated. Thanks :)

MoonMist
  • 1,232
  • 6
  • 22
  • 1
    I'm thinking the answer might relate to [this](https://stackoverflow.com/questions/65687147/how-to-render-html-pages-as-png-jpeg-images-on-single-click-using-urls-tabs-arra), though I haven't worked with Blazor Wasm so perhaps there's a more Blazor-y way to do it. – ProgrammingLlama Jun 08 '21 at 01:14
  • @Llama that was my thought as well. I saw that, but wasn't sure if there was a more technically correct way to do it through Blazor – MoonMist Jun 08 '21 at 01:16
  • 1
    This is for sure going to require a javascript solution. If you want to include SVG images, which you really should, then you're a better man than I if you find a solution. My recommendation would be for SPECIFIC controls, to define some kind of helper class to output .pdf files or something in a close-enough format. But that doesn't really answer your question. – Bennyboy1973 Jun 08 '21 at 04:19

1 Answers1

1

I don't think it will be easy to achieve that with C# alone, since the best approach to the problem is to convert the HTML data to canvas and export it as an image which is a hard process, but there are libraries available for that, in JS. How to do it:
I'm gonna use this library since it seems to be the simplest: html2canvas
Here is the link to it's JS file: html2canvas.js

download it and place it in your wwwroot folder, then, include that in the _host.cshtml by adding this at the end of the body tag:

<script src="html2canvas.js"></script>

then add a JS function to call from C# so we can have the even handler written in C# by adding this after the previous code in _host.cshtml:

<script>
    window.takeScreenshot = async function(id) {
        var img = "";
        await html2canvas(document.querySelector("#" + id)).then(canvas => img = canvas.toDataURL("image/png"));
        var d = document.createElement("a");
        d.href = img;
        d.download = "image.png";
        d.click();
        return img;
    }
</script>

This will automatically take a screenshot from the element and download it, also return its URL. Note that the component must be inside a div tag with an id, otherwise, you can't select it alone, example good child in parent:

Parent.razor

<div id="child"></div>
    <Child />
</div>

To use this function, use the JsInterop class. Simply, inject (basically include) it in your razor component where you need this functionality by adding this at the top of the file:

@inject IJSRuntime JS

next, a function to do everything:

@inject IJSRuntime JS
@code {
    public async System.Threading.Tasks.Task<string> TakeImage(string id)
    {
        return await JS.InvokeAsync<string>("takeScreenshot", id);
    }
}

This function will return a data URL of an image taken from an element specified by the id parameter. Sample usage with a button to take image:

@page "/screenshot"
@inject IJSRuntime JS
@code {
    string image_url = "";
    string child_id = "child";
    public async System.Threading.Tasks.Task<string> TakeImage(string id)
    {
        return await JS.InvokeAsync<string>("takeScreenshot", id);
    }
    public async System.Threading.Tasks.Task ButtonHandler()
    {
        image_url = await TakeImage(child_id);
    }
}
<button @onclick="ButtonHandler">Take image</button>
<h3>Actual component:</h3>
<div id=@child_id>
    <ChildComponent />
</div>
<h3>Image:</h3>
<img src=@image_url />
<br />
<br />
<br />
<p>URL: @image_url</p>

Pressing the button will download the image, show it, show the raw URL, and save the URL to variable image_url. You can shorten System.Threading.Tasks.Task to Task by adding @using System.Threading.Tasks to _Imports.razor file. You can remove auto-download functionality by removing these 4 lines in the JS function:

var d = document.createElement("a");
d.href = img;
d.download = "image.png";
d.click();

If you want to take an image of the entire page automatically and download it without any user interaction:

modify the JS function and set the query selector to body and remove the id parameter:

<script>
    window.takeScreenshot = async function() {
        var img = "";
        await html2canvas(document.querySelector("body")).then(canvas => img = canvas.toDataURL("image/png"));
        var d = document.createElement("a");
        d.href = img;
        d.download = "image.png";
        d.click();
        return img;
    }
</script

set the function to run when the document loads:

@inject IJSRuntime JS
@page "/component"
@code {
    string image_url = "";    
    public async System.Threading.Tasks.Task<string> TakeImage()
    {
        return await JS.InvokeAsync<string>("takeScreenshot");
    }
    protected override async System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender) //Ensure this is the page load, not any page rerender
        {
            image_url = await TakeImage();
        }
    }
}

Special URL to automatically download image:

To generate a URL, which when loaded will download the image as the previous part, but only when that special URL is loaded, not the normal page URL.
To do this, I'm gonna use query strings, so the special URL will be like this: http://localhost:5001/page?img=true
For that, we need to get the URI, using NavigationManager, which can be injected like the IJSRuntime. For parsing the URI, we can use QueryHelpers class. The final code will look like this:

@inject IJSRuntime JS
@inject NavigationManager Nav
@page "/component"
@code {
    string image_url = "";    
    public async System.Threading.Tasks.Task<string> TakeImage()
    {
        return await JS.InvokeAsync<string>("takeScreenshot");
    }
    protected override async System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender) //Ensure this is the page load, not any page rerender
        {
            var uri = Nav.ToAbsoluteUri(Nav.Uri);
            if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("img", out var isImg))
            {
                if (System.Convert.ToBoolean(isImg.ToString()))
                {
                    image_url = await TakeImage();
                }
            }
        }
    }
}

Now you can add ?img=true to the component's URL and you will get a screenshot of it.

Note that if the body/parent of the div has a background, and you want it to be in the image, you need to add background: inherit; to the CSS rules of the div containing the child component.

Mahan Lamee
  • 322
  • 3
  • 12