How To Make Images Responsive in Episerver Automatically

How To Automatically Create Responsive Images in Episerver

Modern websites need to support a good user experience whether they are displayed on the small screen of a mobile phone, a traditional desktop PC, or anything in between. To achieve this flexibility, most websites are now developed using a responsive web design. This allows the display of the site to adapt based on the screen size of the device requesting it.

One of the most important considerations when developing a responsive website is the page load speed across devices and ensuring that images are properly sized and optimized. For example, a large, full-screen image used for a desktop browser would take a long time to download on a mobile phone connected to a 3G wireless network. Since the mobile device’s screen is much smaller, we can swap this image for a version that will be quicker to load. Creating these images and dynamically switching based on screen size is known as using responsive images.

A challenge we face when using content management systems (CMS), such as Episerver, is that not all of them create responsive image sizes automatically, requiring content editors to manually load multiple versions of each image. In this blog post, I will detail one of the methods we have used to overcome this issue to automatically make images responsive. This post is intended for Episerver developers and assumes a basic understanding of Episerver, Visual Studio, and the .NET framework.

Creating Responsive Image Sizes on the Server

To support responsive images in the Episerver CMS, we can add the NuGet package Chief2moro.ImageDataExtensions in Visual Studio. This package will allow us to create additional image sizes on the initial upload of an image. If any additional logic for manipulating images is desired, we can also override the Episerver thumbnail creation to allow us more control of how these images are created. Once the images have been created on the server, you can switch between the sizes using any number of Front End techniques.

Chief2moro.ImageDataExtensions

The Chief2moro.ImageDataExtensions NuGet package provides additional data attributes that help define blob properties that can be added to your default ImageFile model. For each blob property you add to the model, an additional image will be created during the thumbnail creation event in Episerver. These additional images can be used to create the responsive images sizes to use on your site. The GitHub site for this package is located here.

Installing Chief2moro.ImageDataExtensions

In order to add the Chief2moro.ImageDataExtensions package, you will need to add “http://nuget.episerver.com/feed/packages.svc/” as one of your NuGet package sources. To install, run the following command in your Package Manager Console:

Install-Package Chief2moro.ImageDataExtensions

Using Chief2moro.ImageDataExtensions in your project

The basic usage of this extension requires modifying the ImageFile model, which is typically located in the project at \Models\Media\ImageFile.cs (assuming you have modeled the project after the Alloy demo site).

The ImageFile class that is used by the Alloy demo site is a very simple class that inherits from Episerver.Core.ImageData. This class is used to define the image properties you want to store in the CMS. You can also restrict the accepted file extensions for uploaded images using the MediaDescriptor attribute on the class.

[ContentType(GUID = "")]
[MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")]
public class ImageFile : ImageData 
{
     /// <summary>
     /// Gets or sets the copyright.
     /// </summary>
     /// <value>
     /// The copyright.
     /// </value>
     public virtual string Copyright { get; set; }
}

To create additional image sizes on upload, all you need to do is add blob properties to your ImageFile class and use the Chief2moro.ImageDataExtension attributes. In the example below, we are defining two additional sizes, small and large (which in this example are hard-coded to widths of 800px and 1900px).

using Chief2moro.ImageDataExtensions;
using EPiServer.Core;
using EPiServer.DataAnnotations;
using EPiServer.Framework.Blobs;
using EPiServer.Framework.DataAnnotations;
using System.ComponentModel.DataAnnotations;

namespace ImageResize.Models.Media
{
    [ContentType(GUID = "")]
    [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")]
    public class ImageFile : ImageData 
    {
        /// <summary>
        /// Gets or sets the copyright.
        /// </summary>
        /// <value>
        /// The copyright.
        /// </value>
        public virtual string Copyright { get; set; }

        [Editable(false)]
        [ScaffoldColumn(false)]
        [ImageWidthDescriptor(Width = 800)]
        public virtual Blob Small { get; set; } 

        [Editable(false)]
        [ScaffoldColumn(false)]
        [ImageWidthDescriptor(Width = 1900)]
        public virtual Blob Large { get; set; }
    }
}

Once the ImageFile model is updated using the Chief2moro.ImageDataExtensions, the additional images are created automatically and stored in the blob storage. Below is an example of the blob folder for the uploaded image. As you can see, instead of only two images (original image and the thumbnail), there are four images. The file names of the additional images have an underscore followed by the blob property name. In our example, we have “_Large.png” and “_Small.png.”

Now if we browse to the image on the site (using the default Episerver image route), we can switch between the different images by adding the blob property as the last segment of the URL:

http://www.yoursite.com/globalassets/responsive/test-image2.jpg/small

http://www.yoursite.com/globalassets/responsive/test-image2.jpg/large

Issues with using the extension alone

There are a couple of issues with using Chief2moro.ImageDataExtensions alone:

  • The images sizes defined are always created. If you have an image that is smaller than your defined sizes, the large and small versions will still be created. However, they will not be sized.
  • The file extension for the responsive images is always “.png.”

Creating the Responsive Thumbnail Manager

In order to address the issues presented by the Chief2moro.ImageDataExtensions package, or if we want to add any additional logic to the image creation process, we can override Episerver’s default ThumbnailManager behavior.

To override the default ThumbnailManager, we need to first create a class that inherits from the Episerver.Core.Internal.ThumbnailManager. The ResponsiveThumbnailManager class used here is based on the example used by Mark van Dijk. This class will be called by Episerver to create the default thumbnail image and will also run once for each blob property we added to the ImageFile.cs (that are decorated with the Chief2moro data attributes).

The barebones class needs to inherit from Episerver.Core.Internal.ThumbnailManager and have the constructor in the example below:

public class ResponsiveThumbnailManager : ThumbnailManager
{
  public ResponsiveThumbnailManager(IContentRepository contentRepository, 
           IBlobFactory blobFactory, 
           BlobResolver blobResolver, 
           IBlobAssigner blobAssigner, 
           ImageServiceOptions imageServiceOptions, 
           IMimeTypeResolver mimeTypeResolver)
           : base(contentRepository, blobFactory, blobResolver, 
                  blobAssigner, imageServiceOptions, mimeTypeResolver)
       {
           
       }
}

In this class, we need to override the CreateImageBlob function. This is where we can start adding our custom logic:

{
  private readonly IBlobFactory _blobFactory;

  public ResponsiveThumbnailManager(IContentRepository contentRepository, 
           IBlobFactory blobFactory, 
           BlobResolver blobResolver, 
           IBlobAssigner blobAssigner, 
           ImageServiceOptions imageServiceOptions, 
           IMimeTypeResolver mimeTypeResolver)
           : base(contentRepository, blobFactory, blobResolver, 
                  blobAssigner, imageServiceOptions, mimeTypeResolver)
       {
           _blobFactory = blobFactory;
       }

       public override Blob CreateImageBlob(Blob sourceBlob, string propertyName, 
           ImageDescriptorAttribute descriptorAttribute)
       {

           Validator.ThrowIfNull("sourceBlob", sourceBlob);
           Validator.ThrowIfNullOrEmpty("propertyName", propertyName);
           Validator.ThrowIfNull("descriptorAttribute", descriptorAttribute);

           ...this function will eventually return a blob by calling the CreateBlob function...

       }
}

The actual logic for creating the blob is contained in the CreateBlob function that we will add to the class:

private Blob CreateBlob(Uri thumbnailUri, Blob blobSource, IEnumerable<ImageOperation> imgOperations, string mimeType)
{
    byte[] buffer;
    using (Stream stream = blobSource.OpenRead())
    {
       var numArray = new byte[stream.Length];
       stream.Read(numArray, 0, (int)stream.Length);
       buffer = ImageService.RenderImage(numArray,
           imgOperations,
           mimeType, 1f, 50);
    }
  
    Blob blob = _blobFactory.GetBlob(thumbnailUri);
    using (Stream stream = blob.OpenWrite())
    {
       stream.Write(buffer, 0, buffer.Length);
       stream.Flush();
    }
    return blob;
}

Before we start adding in any logic, we need to register it as a dependency in the DependencyResolverInitilization.cs file. This file is included in the Alloy demo project here: Business\Initialization\DisplayRegistryInitialization.cs. To add our new class in the dependency injection, add it as a Transient in the ConfigureContainer function:

.AddTransient<ThumbnailManager, ResponsiveThumbnailManager>();

Below is an example of the DependencyResolverInitilization.cs file with this update:

[InitializableModule]
public class DependencyResolverInitialization : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        //Implementations for custom interfaces can be registered here.

        context.ConfigurationComplete += (o, e) =>
        {
            //Register custom implementations that 
            //should be used in favour of the default implementations
             context.Services.AddTransient<IContentRenderer, ErrorHandlingContentRenderer>()
                .AddTransient<ContentAreaRenderer, AlloyContentAreaRenderer>()
                .AddTransient<ThumbnailManager, ResponsiveThumbnailManager>();
        };
    }

    public void Initialize(InitializationEngine context)
    {
        DependencyResolver.SetResolver(new ServiceLocatorDependencyResolver(context.Locate.Advanced));
    } 

    public void Uninitialize(InitializationEngine context)
    {
    } 

    public void Preload(string[] parameters)
    {
    }
}

Now that the site is wired up to use our ResonsiveImageMangager class to create the image blobs, we can add in our own logic.

To fix the file extensions for the saved images, we will add the following code to the CreateImageBlob function:

//Build Thumbnail url - if type "thumbnail" use png, else use source extension
string uriString = string.Format("{0}{1}_{2}{3}",
    Blob.GetContainerIdentifier(sourceBlob.ID).ToString(),
    Path.GetFileNameWithoutExtension(sourceBlob.ID.LocalPath),
    propertyName,
    (propertyName.ToLower() == "thumbnail") ? ".png" : Path.GetExtension(sourceBlob.ID.LocalPath)
 );

To only create images if their original width is larger than our defined responsive sizes, we will first need to get the dimensions of the image from the sourceBlob. We can use the Chief2moro package to assist us again with this task:

//Use the Chief2moro data extension to get the original image demensions
var blobDimensions = ImageBlobUtility.GetDimensions(sourceBlob);

Now that we have the uploaded image dimensions, we can compare the original width with the descriptorAttribute that is passed into the function. This attribute will contain the responsive width that we added to the ImageFile.cs blob property (which we added earlier in this process). Remember, this class will run once for the default thumbnail creations, as well as for each of those blob properties we added to define our responsive image widths. If the propertyName indicates that this is the default thumbnail creation, or if the image width is larger than the descriptorAttribute width, we will create the blob. We do so by calling the CreateBlob function.

//Return the created blob if creating the default thumbnail or if the orginal image  width is larger than the image descriptor width 
return (propertyName.ToLower() == "thumbnail" ||  (blobDimensions.Width > descriptorAttribute.Width)) 
   ? CreateBlob(new Uri(uriString), sourceBlob, descriptorAttribute.Width, descriptorAttribute.Height) : null;

So, when we put it all together, the ResponsiveThumbnailManager will look like this:

using Chief2moro.ImageDataExtensions;
using EPiServer;
using EPiServer.Core.Internal;
using EPiServer.DataAnnotations;
using EPiServer.Framework;
using EPiServer.Framework.Blobs;
using EPiServer.ImageLibrary;
using EPiServer.Web;
using EPiServer.Web.Internal;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;

namespace ImageResize.Business.Overrides
{
    public class ResponsiveThumbnailManager : ThumbnailManager
    {
        private readonly IBlobFactory _blobFactory; 

        public ResponsiveThumbnailManager(IContentRepository contentRepository, 
            IBlobFactory blobFactory,
            BlobResolver blobResolver, 
            IBlobAssigner blobAssigner, 
            ImageServiceOptions imageServiceOptions,
            IMimeTypeResolver mimeTypeResolver)
            : base(contentRepository, blobFactory, blobResolver, 
                   blobAssigner, imageServiceOptions, mimeTypeResolver)
        {
            _blobFactory = blobFactory;
        }

        public override Blob CreateImageBlob(Blob sourceBlob, string propertyName, ImageDescriptorAttribute descriptorAttribute)
        { 

            Validator.ThrowIfNull("sourceBlob", sourceBlob);
            Validator.ThrowIfNullOrEmpty("propertyName", propertyName);
            Validator.ThrowIfNull("descriptorAttribute", descriptorAttribute);  

            //Build Thumbnail url - if type "thumbnail" use png, else use source extension
            string uriString = string.Format("{0}{1}_{2}{3}",
                    Blob.GetContainerIdentifier(sourceBlob.ID).ToString(),
                    Path.GetFileNameWithoutExtension(sourceBlob.ID.LocalPath),
                    propertyName,
                    (propertyName.ToLower() == "thumbnail") ? ".png" : Path.GetExtension(sourceBlob.ID.LocalPath)
                );

            //Use the Chief2moro data extension to get the original image demensions
            var blobDimensions = ImageBlobUtility.GetDimensions(sourceBlob); 

            //Return the created blob if creating the default thumbnail or if the orginal image  width is larger than the image descriptor width 
            return (propertyName.ToLower() == "thumbnail" ||  (blobDimensions.Width > descriptorAttribute.Width)) 
                ? CreateBlob(new Uri(uriString), sourceBlob, descriptorAttribute.Width, descriptorAttribute.Height) : null; 

       }

        private Blob CreateBlob(Uri thumbnailUri, Blob blobSource, IEnumerable<ImageOperation> imgOperations, string mimeType)
        {
            byte[] buffer;
            using (Stream stream = blobSource.OpenRead())
            {
                var numArray = new byte[stream.Length];
                stream.Read(numArray, 0, (int)stream.Length);
                buffer = ImageService.RenderImage(numArray,
                    imgOperations,
                    mimeType, 1f, 50);
            }
            Blob blob = _blobFactory.GetBlob(thumbnailUri);
            using (Stream stream = blob.OpenWrite())
            {
                stream.Write(buffer, 0, buffer.Length);
                stream.Flush();
            }
            return blob;
        }
    }
}

When we upload an image, we now get the correct file extension and we only get additional images if the original image is wider than our defined responsive widths.

Here is an example of the blob folder for a large image (wider than our two responsive widths). It contains the original image, thumbnail, and our large and small images (with the correct .jpg extension):

Here is an example for a small image (smaller than our responsive widths). It only contains the original image and thumbnail.

Now we have the ability to automatically create responsive images sizes when a user uploads an image in the Episerver media library. Any additional logic we want to add to the image upload process can also be added to the ResponsiveThumbnailManager class. For example, we may want to optimize the images before saving them.

Responsive images are critical to the success of your website. Using the Chief2moro.ImageDataExtensions in conjunction with adding the ResponsiveThumbnailManager class to your Episerver project allows your content editors to only upload a single image into the library. Once the image is uploaded, any image sizes you have defined will automatically be created. These images can easily be added to your markup or used with your front-end responsive image solution to ensure an optimal user experience on every device.

 

Leave a Reply