Convert images to WebP with ASP.NET Core - Better than png/jpg files?

Many websites rely on user-uploaded images as content and want to be able to present as much content as possible to their users at a tolerable speed. Google has developed an image format called WebP, which packs as much detail as PNGs or JPEGs, but it uses files up to 34% smaller. This makes it possible to serve more content to your users even for the ones who may browse your site with a slow 3G connection. In this article, we will show you how to convert images to WebP using a product named ImageProcessor.

WebP is supported in over 80% percent of all browsers with the biggest drawback being that it is not supported by any of the Safari browsers. Luckily the HTML5 tag Picture makes it possible to serve a normal HTML image as a fallback. It is also (not surprising) suggested as one of the possible image formats to use if you have many big images on your site when using Google's website performance tool Lighthouse.

Getting started

We get started first by creating a fresh ASP.NET Core MVC project.

The first thing we will make is a simple form to post images. This just serves as the simplest example of a frontend for the current test.

<form method="post" enctype="multipart/form-data">
    <h2>upload image</h2>
    <input name="image" type="file" />
    <input type="submit" value="Upload" />
</form>

The backend

Then for the backend, we need two packages that can be installed through the Package Manager using these commands:

Install-Package System.Drawing.Common
Install-Package ImageProcessor
Install-Package ImageProcessor.Plugins.WebP

Then we can get started coding the action that will accept the post from the frontend.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ImageProcessor;
using ImageProcessor.Plugins.WebP.Imaging.Formats;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using WebP.Models;

namespace WebP.Controllers
{
    public class HomeController : Controller
    {
        IHostingEnvironment hostingEnvironment;

        public HomeController(IHostingEnvironment hostingEnvironment)
        {
            this.hostingEnvironment = hostingEnvironment;
        }

        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(IFormFile image) 
        {
            return View();
        }
    }
}

We first of all need to import a couple of libraries that we will need when implementing the following. We also need to take hostingEnvironment through the constructor of the controller via Dependency Injection. It is automatically served through .NET, so there is no need to add it in startup.cs. It will be needed for getting the path to write to when saving the files. We have also added a barebone Action for our post assuming the previous frontend is the Index of Home. We will now fill out the action step by step.

[HttpPost]
public IActionResult Index(IFormFile image) 
{
    // Check if valid image type (can be extended with more rigorous checks)
    if (image == null) return View();
    if (image.Length > 0) return View();
    string[] allowedImageTypes = new string[] { "image/jpeg", "image/png" };
    if (!allowedImageTypes.Contains(image.ContentType.ToLower())) return View();

    // Prepare paths for saving images
    string imagesPath = Path.Combine(hostingEnvironment.WebRootPath, "images");
    string webPFileName = Path.GetFileNameWithoutExtension(image.FileName) + ".webp";
    string normalImagePath = Path.Combine(imagesPath, image.FileName);
    string webPImagePath = Path.Combine(imagesPath, webPFileName);

    // The next code section...
    return View();
}

The IFormFile that the action takes as a parameter could be other things besides an image, so we will first check for this. We will first check to see if there was even a posted file and if there is only one. Then we will check the file type, which right now we only allow PNG's and JPEG's, but it could easily be expanded to allow gifs or other image types.

Monitoring errors and uptime on your ASP.NET Core applications?

➡️ elmah.io for ASP.NET Core ⬅️

We also need some different paths for saving the files and serving them later. This includes a path where the original file will be saved and the path of the new WebP image.

[HttpPost]
public IActionResult Index(IFormFile image) 
{
    // previous code section...

    // Save the image in its original format for fallback
    using (var normalFileStream = new FileStream(normalImagePath, FileMode.Create))
    {
        image.CopyTo(normalFileStream);
    }

    // Then save in WebP format
    using (var webPFileStream = new FileStream(webPImagePath, FileMode.Create))
    {
        using (ImageFactory imageFactory = new ImageFactory(preserveExifData: false))
        {
            imageFactory.Load(image.OpenReadStream())
                        .Format(new WebPFormat())
                        .Quality(100)
                        .Save(webPFileStream);
        }
    }

    // The next code section...
    return View();
}

Finally, the images should be saved to the server. First, we use the FileStream as we would normally do to save the original image. The FileStream can be used in a using block because it implements the IDisposable interface and it, therefore, disposes itself automatically when exiting the block.

In order for us to write the WebP image, a new FileStream is created. In the using-case, we make an ImageFactory, which is a class from the ImageProcessor package. This class uses the factory pattern, which makes it possible to Load an image and then apply filters or change different attributes in any order. When creating the ImageFactory we define that it should not keep ExifData, since we are generally not interested in personal information like GPS coordinates.

We Load the stream of the image and then change its format to WebP. The Format method takes an ISupportedImageFormat as an argument. The ImageProcessor.Plugins.WebP package helps us by implementing the specific class WebPFormat that handles all the actions specific to the WebP format. Before saving the image via the FileStream we also note that we have set the Quality to 100. This implies that there is no loss in quality so it will use lossless compression. This could be changed if one wished to get better results, but that would, of course, mean a lossy compression.

Now that we have saved the images, we would like to serve them. We do this by putting the paths to the images in a custom ViewModel and presenting it in a view like so:

namespace WebP.Models
{
    public class Images
    {
        public string NormalImage { get; set; }
        public string WebPImage { get; set; }
    }
}

And then ending our action:

[HttpPost]
public IActionResult Index(IFormFile image) 
{
    // previous code section...

    Images viewModel = new Images();
    viewModel.NormalImage = "/images/" + image.FileName;
    viewModel.WebPImage = "/images/" + webPFileName;

    return View(viewModel);
}

We simply make a new Images instance and populate the fields with the filenames. This could also be the place where the database is updated and the Image fields could instead be fields in for example your Entity Framework model.

Presenting the WebP image and fallback

We make the last changes to our view to show our images.

@model Images
@using WebP.Models

<form method="post" enctype="multipart/form-data">
    <h2>upload image</h2>
    <input name="image" type="file" />
    <input type="submit" value="Upload" />
</form>

@if (Model != null)
{
    <picture style="height:50vh;">
        <source srcset="@Model.WebPImage" type="image/webp" />
        <img src="@Model.NormalImage" style="height:50vh;" alt="Not Found" />
    </picture>
}

The view uses the Images instance as a model and it shows the image if the model is not null. We then use the picture HTML tag which can have source and img tags inside. The image tag serves as a frame and fallback for the element shown on the page. The image referenced will be shown in the place that the img would be if the browser supports WebP. The source tags can also be decorated with a media attribute that will make it show different resources on standard CSS media queries. This combined with generating different quality or sized images in the backend can be a strong tool to create a fast loading website.

Results

WebP is inspired by some of the techniques used for compressing video keyframes by making it only look at the differences between the neighboring groups of pixels. This makes WebP good for graphical drawings or images with a lot of patterns or areas with one color. It is not as good with real camera images since these often have some complex patterns and a lot of uneven edges. Something we haven't looked at yet though is the results when using lossy compression. When changing the Quality to 50%, most images will be about 3 times smaller, at least with the examples we have seen. When changing to 10% most images will be 6 times smaller. It's recommended to try out some different settings to see what level of lossiness fits your use-case.

Example of lossy compression using WebP, with the original image to the left at 6.44 MB and an image with 10% quality to the right at 0.95 MB.

This sums up how to use ImageProcessor to convert images to WebP on the fly and how to present them using the HTML5 picture tag. The full prject can be found at the following GitHub repo: https://github.com/elmahio-blog/Elmah.Blog.ImageProcessorWebP
If you have any comments or feedback for the article, then feel free to reach out to us.