Reducing image file size using ImageProcessor in an Episerver website

Reducing image file size using ImageProcessor in an Episerver website

Daan Siepelinga

Daan Siepelinga

Large images can greatly reduce the loading speed of your web pages. Luckily for us there are many tools to reduce the file size of your images. In this blogpost I will show you how to do this in an Episerver website with the help of ImageProcessor, a free to use open-source image manipulation library.

Installing ImageProcessor in Episerver

Start by installing the Episerver Nuget package ImageProcessor.Web.Episerver made by Vincent Baaij. Also install the ImageProcessor.Web.Episerver.Azure Nuget package if you use Azure blob storage for your images. At this point you can already check if ImageProcessor works correctly by adding an ImageProcessor query parameter like “?width=200” to an image url on your site.

The packages come with 3 configuration files; cache.config, processing.config, and security.config. You can put this configuration in your Web.config if you want. I didn’t have to change anything in the configuration, because the default options are already quite good. Still, it doesn’t hurt to check these options. Here is a quick overview of them:

  • The cache configuration lets you specify where to store cached processed images. Use the EpiserverAzureBlobCache option if you use an Azure blob container. Otherwise, use the EpiserverBlobCache option that stores the cached images on the local filesystem.
  • The processing configuration allows you to enable/disable different image manipulation processors. Since anyone can use enabled processors by adding query parameters to your image url, I recommend only enabling the ones you use.
  • The security configuration contains various settings. For example, a list of sites to send CORS headers to, or restrictions on images to process from external sites. We kept the default settings with our project.

Reducing image file size

Image size

The most obvious way to reduce the image file size is to make the image smaller. Then we store fewer pixels after all. The safest images to apply this to are the ones that are larger than the maximum size your site’s styling allows them to be.

One thing to keep in mind when resizing images is the aspect ratio of the image. When resizing an image to a size with a different aspect ratio, you will have to make some sacrifices. ImageProcessor has different resize options to handle this situation. I’ve shown 3 of them in the images below:

Rubberduck 400x400 resized pad-stretch-crop

From left to right the images above show ImageProcessor’s pad, stretch, and crop resize options. The resized images are 400x400, which is 41.02 times smaller than the original JPEG image’s size. In terms of file size the images are respectively 55.09, 46.56, and 50.64 times smaller compared to the original 1.45MB. This is of course a nice improvement in file size.

If the above resize options aren’t acceptable for your images, you’ll have to maintain the aspect ratio of your images. In our customer’s website the images were only restricted in width by the CSS styling. This means I could get away with only resizing in that dimension, keeping our images’ aspect ratios. Just to make sure there are no tiny black bars after the resize I did use the crop resize mode.

So what does this look like in code? Let’s say our view model has an image ContentReference that we called ImageContentReference for convenience. Then our final Razor view code to resize our images to 400 width using the crop resize option could look as follows:

<img src="@Html.ProcessImage(Model.ImageContentReference).Resize(400, null, ResizeMode.Crop)"/>

Image quality

The second way that I used to reduce the image file size was to reduce the quality of the images. JPEG images can be reduced in quality without the quality decrease being (very) noticeable. There are different opinions on the best quality for JPEG images on the web. I decided to stay on the high end with 75%. The following images show our 400x400 resized cropped image with from left to right 75%, 50%, and 25% quality:

Rubberduck 400x400 resized crop q75-q50-q25

Compared to our 100% quality cropped 400x400 image the above images are respectively 1.57, 2.29, and 3.37 times smaller in file size. In my opinion this is definitely an optimization worth considering. Especially since to me even the 50% quality image is in this case acceptable.

The quality option only works on lossy image formats like JPEG though. Our customer’s site also uses a lot of PNG images. ImageProcessor is able to convert these images to JPEG using its Format processor. This by itself is also kind of an optimization, because JPEG file sizes are generally speaking smaller.

I did run into the issue of losing transparency in the images, because the JPEG format simply doesn’t support this. I worked around this issue by adding a white background color through ImageProcessor. This way the transparency is replaced by the white background color, giving the illusion of transparency thanks to our site’s solid white background. Our code now looks as follows with our new additions:

<img src="@Html.ProcessImage(Model.ImageContentReference).Format(ImageFormat.Jpeg).BackgroundColor('white').Quality(75).Resize(400, null, ResizeMode.Crop)"/>

The final code

I programmed the following simple helper method to allow us to easily use the above processors in our views. Our format, background color, and quality are always the same and therefore hardcoded. I didn’t use constants in the next code snippet to save some space.

public static UrlBuilder ReduceImageFileSize( 
    this HtmlHelper helper, 
    ContentReference originalImage, 
    int? width = null, 
    int? height = null, 
    ResizeMode resizeMode = ResizeMode.Crop) 
{ 
    if (ContentReference.IsNullOrEmpty(originalImage)) 
    { 
        return new UrlBuilder(""); 
    } 

    var imageUrl = helper.ProcessImage(originalImage).Format(ImageFormat.Jpeg).BackgroundColor("white").Quality(75); 

    if (width != null || height != null) 
    { 
        return imageUrl.Resize(width, height, resizeMode); 
    }  

    return imageUrl; 
} 

You can use this method in your Razor view as follows:

<img src="@Html.ReduceImageFileSize(Model.ImageContentReference, 400)"> 

This code reduced our rubber duck image from 1.49MB to 16.68KB (about 393 times smaller).

Alternative final code

An alternative way to implement the above is to use a display template. This has the advantage that Episerver’s on-page editing is supported. The reason why I didn’t use this was only because I was not aware of this solution at the time.

Here’s a simple example of the display template with support for custom width and height (located in “~/Views/Shared/DisplayTemplates/Image.cshtml”):

@using ImageProcessor.Web.Episerver;
@using ImageProcessor.Imaging;
@model EPiServer.Core.ContentReference
@{ 
    if (Model == null)
    {
        return;
    }

    var width = ViewData.Keys.Contains("Width") ? ViewData["Width"] as int? : null;
    var height = ViewData.Keys.Contains("Height") ? ViewData["Height"] as int? : null;
    var url = Html.ProcessImage(Model).Format(ImageFormat.Jpeg).BackgroundColor("white").Quality(75);

    if (width.HasValue || height.HasValue)
    {
        url = url.Resize(width, height, ResizeMode.Crop);
    }
}

<img src="@url" />

The following code shows an example of how to use the display template:

@Html.PropertyFor(x => x.ImageContentReference, new
{
    Width = 400
})

Bonus: Applying ImageProcessor to TinyMCE images

I was able to apply ImageProcessor to the images in our Razor views using our helper method. However, our customer’s site also has a lot of content including images created with Episerver’s XhtmlString property. This content is entered by the user through the TinyMCE editor. Those images required some extra work to apply the image processing to.

After a failed attempt of trying to use Javascript and TinyMCE events to manipulate the image source, I tried another approach in .NET using XhtmlString fragments. An XhtmlString contains a list of several types of fragments, like the StaticFragment and UrlFragment. The following screenshot of my debugger shows an example of this:

XhtmlString Fragments

The debugger shows that the image source link is in its own UrlFragment. We can also see that the previous fragment is a StaticFragment that ends with an image html tag. I created the following extension method that uses this information to identify image source links and to add ImageProcessor queries to the links:

public static XhtmlString AddImageProcessing(this XhtmlString originalHtml)
{
    var resultHtml = new XhtmlString();
    var isImageUrl = false;

    if (originalHtml == null)
    {
        return originalHtml;
    }

    foreach (var fragment in originalHtml.Fragments)
    {
        var resultFragment = fragment;

        if (fragment is StaticFragment staticFragment)
        {
            isImageUrl = staticFragment.InternalFormat.EndsWith("src=\"") && staticFragment.InternalFormat.Contains("<img");
        }
        else if (fragment is UrlFragment urlFragment && isImageUrl)
        {
            resultFragment = new UrlFragment(urlFragment.Url + $"?format={ImageFormat.Jpeg}&bgcolor=white&quality=75");
        }

        resultHtml.Fragments.Add(resultFragment);
    }

    return resultHtml;
}

In the above code we loop over each fragment in the original XhtmlString, identify fragments with image source links, and add ImageProcessor queries to them. I add the fragments to a new XhtmlString, because the old XhtmlString’s fragments are read-only.

I considered the following 2 ways of using the extension method:

  1. Creating an initialization module, subscribing to the content save event, and using the extension method there on XhtmlString properties.
  2. Creating a display template to apply the extension method in the view when DisplayFor is called.

The advantage of the first method is that we apply the extension method outside of rendering the page, whereas the second method is easier to implement. I went for the simpler second approach to save time. With this approach all you need is an XhtmlString display template, e.g. “~/Views/Shared/DisplayTemplates/XhtmlString.cshtml”, and apply the extension method in it like this (includes my extension method namespace):

@using EPiServer.Core
@using MyAlloySite.Business.Extensions
@model XhtmlString

@{
    Html.RenderXhtmlString(Model.AddImageProcessing());
}

There we go! Now all our TinyMCE images have some basic image optimization on them.

Relevant links