Resizing images in EPiServer

Every EPiServer developer knows that editors love to work with high-resolution images. If those images are not optimized for different devices, they may increase the page loading time and ruin the browsing experience. There is nothing worse than serving 5000x5000px images on a device whose viewport is only 400x700px, or sending big images (15mb+) over a 3G network.

The days of fixed-width website design is behind us. HTML5 has support for responsive images using picture element. Here's a basic example:

<picture>
    <source srcset="image_768.png" media="(max-width: 768px)">
    <source srcset="image_1200.png">
    <img srcset="image_1200.png" alt="My image">
</picture>

This tells the browser to load image_768.png if the viewport is not bigger than 768px or to load image_1200.png instead. IMG will be used as a fallback if the browser doesn't support the PICTURE element.

On the EPiServer side, we have to define a block with a ContentReference property and decorate it with a UIHinth attribute like this:

[UIHint(UIHint.Image)]
public virtual ContentReference MyImage{ get; set; }

The only thing left is to display resized versions of MyImage in the block template.

To resize the image, you could use the built-in ImageDescriptor attribute, as described on the following link: http://world.episerver.com/Blogs/Johan-Bjornfot/Dates1/2013/12/Blob-property/

Or, you could use something like https://imageresizing.net/

Resizing images on the fly is a very expensive operation, so disk cache plugin is a must! https://imageresizing.net/docs/v4/plugins/diskcache

An example of a block template:

@using EPiServer.Core
@using EPiServer.Editor
@model MyResponsiveImageBlock

@if (!PageEditing.PageIsInEditMode &&
     ContentReference.IsNullOrEmpty(Model.MyImage))
{
    return;
}

@if (PageEditing.PageIsInEditMode)
{
    <div clas="myClass" @Html.EditAttributes(x => x.MyImage)>
        @DisplayImage(Model.MyImage)
    </div>
}
else
{
    @DisplayImage(Model.MyImage)
}


@helper DisplayImage(ContentReference imageReference)
{
    if (ContentReference.IsNullOrEmpty(imageReference))
    {
        return;
    }

    <picture>
        <source srcset="@Url.ContentUrl(Model.MyImage)?w=768"
                media="(max-width: 768px)">
        <source srcset="@Url.ContentUrl(Model.MyImage)?w=1200">
        <img srcset="@Url.ContentUrl(Model.MyImage)?w=1200" alt="">
    </picture>
}

So far, so good.

However, what if editors, for any reason, don't use our image blocks but instead insert images directly into the XhtmlString fields?

We can, of course, create a new display template under Views / Shared / DisplayTemplates / XhtmlString.cshtml, and append ?w and ?h to all images.

First, we need to create an image helper like this:

public static class ImageHelpers
{
    private static readonly Regex ImageElements =
        new Regex("<img\\s[^>]*?src\\s*=\\s*[\'\\\"]([^\'\\\"]*?)[\'\\\"][^>]*?>",
            RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);

    public static XhtmlString ResizeImages(
        this XhtmlString xhtmlString,
        int maxWidth = 0,
        int maxHeight = 0)
    {
        if (xhtmlString == null)
        {
            return null;
        }

        // we don't want to resize images
        if (maxWidth == 0 && maxHeight == 0)
        {
            return xhtmlString;
        }

        string internalString = xhtmlString.ToInternalString();

        // find all images
        var matchCollection = ImageElements.Matches(internalString);
        foreach (Match match in matchCollection)
        {
            var imageElement = XElement.Parse(match.Value);
            var srcAttribute = imageElement.Attribute("src");
            if (string.IsNullOrWhiteSpace(srcAttribute?.Value))
            {
                continue;
            }

            // get image height and remove the height attribute
            int imageHeight = 0;
            var heightAttribute = imageElement.Attribute("height");
            if (heightAttribute != null)
            {
                int.TryParse(heightAttribute.Value, out imageHeight);
                heightAttribute.Remove();
            }

            // get image width and remove the width attribute
            int imageWidth = 0;
            var widthAttribute = imageElement.Attribute("width");
            if (widthAttribute != null)
            {
                int.TryParse(widthAttribute.Value, out imageWidth);
                widthAttribute.Remove();
            }

            int minHeight = MinSize(maxHeight, imageHeight);
            int minWidth = MinSize(maxWidth, imageWidth);

            // replace image src attribute => src=image_source?w=minWidth&h=minHeight
            var sb = new StringBuilder();
            sb.AppendFormat("{0}?", srcAttribute.Value);
            if (minWidth > 0)
            {
                sb.AppendFormat("w={0}", minWidth);
            }
            if (minHeight > 0)
            {
                if (minWidth > 0) sb.Append("&");

                sb.AppendFormat("h={0}", minHeight);
            }

            imageElement.SetAttributeValue("src", sb.ToString());
            internalString = internalString.Replace(match.Value, imageElement.ToString());
        }

        return new XhtmlString(internalString);
    }

    private static int MinSize(int a, int b)
    {
        if (a == 0 && b == 0) return 0;
        if (a != 0 && b == 0) return a;
        if (b != 0 && a == 0) return b;

        return Math.Min(a, b);
    }
}

 And create a display template like this:

@using EPiServer.Editor
@using EPiServer.Web.Mvc.Html
@model EPiServer.Core.XhtmlString

@{
    if (PageEditing.PageIsInEditMode)
    {
        Html.RenderXhtmlString(Model);
    }
    else
    {
        Html.RenderXhtmlString(Model.ResizeImages(1200, 1200));
    }
}

Before resizing:

<img alt="" src="/contentassets/063618eccc274a678f39035ff32ce350/myimage.png" height="2000" width="2000">

 After resizing:

<img alt="" src="/contentassets/063618eccc274a678f39035ff32ce350/myimage.png?w=1200&h=2000">

I chose 1200x1200px in this example, but that is something that varies from project to project. 1200px is not optimal for mobile phones, but still much better than 2000px. If you'd like to have a full device recognition, then take a look at Image Optimizer by 51 Degrees: https://51degrees.com/Support/Documentation/NET/Image-Optimiser

Performance tips:

  • If you have the time and budget, go for image blocks that render images using the picture element
  • Resizing images on the fly is expensive; the ImageDiskCache plugin is a must
  • Consider using CDN
  • Decorate your controllers with the ContentOutputCache attribute
comments powered by Disqus