Resize images before uploading in Blazor Web Assembly

So, you allow users to upload images to the server from your Blazor WASM app, but you want to constrain the actual dimensions of the image for some reason. Rather than ask the user to edit the image prior to uploading, you decide to to take care of applying this requirement within the application. And you further decide to do any resizing in the browser prior to uploading so that the resulting upload is smaller and you don't have to waste server resources on the procedure. In this article, I take a look at how to do that, and how to get the dimensions of an image file.

When resizing image files in Blazor Web Assembly, we can use an extension method on the IBrowserFile type which represents a file presented to a file upload control in a Blazor application, RequestImageFileAsync . This method was introduced in .NET 5, so it won't work in applications built using .NET 3.2 (if, indeed, any still exist).

The method takes the file format (content type), a maximum width, and maximum height as arguments. Internally, the method utilises the JavaScript layer to perform image resizing. Specifically, it uses the HTML5 canvas API. It compares the actual dimensions of the image to the maximum width and height arguments, and applies the lower ratio to both dimensions when resizing, thereby retaining the original proportions of the image.

The following code features a Razor component that acts as a navigable page. It includes an InputFile component that has an accept attribute applied to limit the file types available to the native file picker. This is a convenience to the user and should not be relied upon to validate file types being uploaded. When the contents of the file upload control change, the HandleChange callback is invoked. This validates that the submitted file is actually an image file by checking its content type with an array of permitted values and then the RequestImageFileAsync method is invoked on it. In this example, I have decided that the maximum width of any image shall be 300px. They can be any height, hence int.MaxValue is passed to the maxHeight parameter.

@page "/resize-image-demo"
 
@inject HttpClient http
<h3>Resize Image</h3>
<InputFile OnChange="HandleChange" accept=".jpg,.png" />
@code {
    string[] imageTypes = new[] { "image.jpeg""image/png" };
    async Task HandleChange(InputFileChangeEventArgs e)
    {
        if (imageTypes.Contains(e.File.ContentType))
        {
            var resized = await e.File.RequestImageFileAsync(e.File.ContentType, 300int.MaxValue);
            var formContent = new MultipartFormDataContent
            {
                { new StreamContent(e.File.OpenReadStream(e.File.Size)), "upload"e.File.Name },
                { new StringContent(e.File.ContentType), "content-type" }
            };
            await http.PostAsync("/upload"formContent);
        }
    }
}

When using the RequestImageFileAsync method, all files will be processed regardless of their original size and type. If a file type that is not supported by your browser's implementation of canvas is passed to the method, the application crashes quietly (in my testing). If an uploaded image's original width is 300px or less, it is still processed and a new image of the same size is returned. Rather than submit every file to the resizing process regardless of size, you might want to check the image dimensions to see if they exceed your maximum, so how do you obtain the image file's width and height?

Obtaining image dimensions

You might consider using a cross-platform C# image library in your WASM project such as ImageSharp, and although I found that simply retrieving the image dimensions works fine using this library, ImageSharp is not recommended for use in a Blazor WASM environment. Instead, you can use JavaScript interop to make use of the browser APIs instead. The following JavaScript code is written as a module so that we can take advantage of JavaScript isolation support in Blazor. The function takes a stream as a parameter and uses it to create a Blob, from which an object URL is contructed. The object URL is assigned as the src of an Image. We hook into the image's onload event and obtain the dimensions of the loaded image.  We use a Promise to resolve the return value of the image's onload event handler and serialise that to JSON. We save this module as fileSize.js in the wwwroot/js folder.

export async function getImageDimensions(content) {
    const url = URL.createObjectURL(new Blob([await content.arrayBuffer()]))
    const dimensions = await new Promise(resolve => {
        const img = new Image();
        img.onload = function () {
            const data = { Width: img.naturalWidth, Height: img.naturalHeight };
            resolve(data);
        };
        img.src = url;
    });
    return JSON.stringify(dimensions);
}

Next, we need to alter our component to work with an injected IJSRuntime. We also need to add a using directive for System.Text.Json. We add an IJSObjectReference field representing a reference to the JavaScript module and instantiate that the first time that the component is rendered. We also add a type representing the image and width of an image. This is declared as a record type named ImageDimensions.

In the HandleChange event, we pass the uploaded file content as a DotNetStreamReference to the JS module. We deserialise the returned value as an instance of ImageDimensions and check to see if the width is over 300. Only if it is do we resize the image.

@page "/image-file-tests"
@using System.Text.Json
@inject IJSRuntime JS
@inject HttpClient http
<h3>Image File Tests</h3>
 
<InputFile OnChange="HandleChange" />
 
@code {
    IJSObjectReference module;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import","./js/fileSize.js");
        }
    }
 
    async Task HandleChange(InputFileChangeEventArgs e)
    {
        var file = e.File;
        var streamRef = new DotNetStreamReference(file.OpenReadStream(file.Size));
        var json = await module.InvokeAsync<string>("getImageDimensions"streamRef);
        var dimensions = JsonSerializer.Deserialize<ImageDimensions>(json);
        if(dimensions.Width > 300)
        {
            file = await file.RequestImageFileAsync(file.ContentType, 300int.MaxValue);
        }
        var formContent = new MultipartFormDataContent
        {
            { new StreamContent(file.OpenReadStream(file.Size)), "upload"file.Name },
            { new StringContent(file.ContentType), "content-type" }
        };
        await http.PostAsync("/upload"formContent);
    }
      
    record ImageDimensions(int Widthint Height);
}

Summary

In this post, I have looked at the RequestImageFileAsync method that is used for resizing images in a Blazor Web Assembly app. I have also looked at getting the dimensions of an image. I considered using ImageSharp, a cross-platform C# image library but heeded the project team's warning against using this library on the browser, eventually settling on using the native browser APIs via JavaScript interop.