Sitecore Image Lazy Load Module

The package and code for this module can be found at: Github – aceanindita

The bLazy.js plugin – A lazyload image script can be used to enable lazyload on your site, to reduce initial page load times drastically, especially if your site is image heavy.

While you can directly integrate your site with the bLazy plugin, you will need to ensure that your images are not loaded on page load, for which you will need to transform your html accordingly. This can also bring with it experience editor compatibility concerns.

This module will make all your images rendered from Sitecore Image and Rich Text fields, lazy loaded automatically without you having to make ANY html updates at all.

For this, we are usign the following config which does 2 things

  • Processor added to mvc.renderRendering – to include and initialize the bLazy plugin
  • Processor added to renderField – to transform html for all images rendered from the Sitecore Image field and Rich Text field, to move the src url to a different attribute recognized by the plugin, and also add a selector to tell the plugin this html needs to be transformed to lazy load the image.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="ImageLazyLoadModule.Pipelines.MVC.InsertbLazyInit, ImageLazyLoadModule"             patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
      <renderField>
        <processor type="ImageLazyLoadModule.Pipelines.RenderField.ImageLazyLoad, ImageLazyLoadModule" />
      </renderField>
    </pipelines>
    <settings>
      <setting name="ImageLazyLoadModule.Offset" value="200" />
      <setting name="ImageLazyLoadModule.Selector" value="b-lazy" />
    </settings>
  </sitecore>
</configuration>

You can see 2 configurable settings above, which are used when the bLazy plugin is initialized.

  • The offset controls how early you want the elements to be loaded before they’re visible.
  • The selector is the class that will be added to the img tags dynamically, which will be used by the plugin to lazy load images.

The code for mvc.renderRendering for including the script and plugin initialization

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace ImageLazyLoadModule.Pipelines.MVC
{
    public class InsertbLazyInit : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            Renderer renderer = args.Rendering.Renderer;
            if (renderer == null)
                return;

            bool isLayout = renderer is ViewRenderer &&
                               ((ViewRenderer)renderer).Rendering.RenderingType == "Layout";

            if (isLayout)
            {
                args.Writer.Write("<script src=\"../../sitecore modules/Lazy Load/blazy.min.js\"></script>"
                                  + "<script>"
                                  + "var bLazy = new Blazy({ selector: '."
                                  + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy")
                                  + "', offset: " 
                                  + Settings.GetIntSetting("ImageLazyLoadModule.Offset", 200)
                                  + ", src: 'responsive-src'});"
                                  + "</script>");
            }
        }
    }
}

The code for renderField which will transform img tag html

using HtmlAgilityPack;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace ImageLazyLoadModule.Pipelines.RenderField
{
    public class ImageLazyLoad
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args == null)
                    return;

                // Trigger the code to transform the img tags only for rich text and image fields
                if (!(args.FieldTypeKey != "rich text" || args.FieldTypeKey != "image") || string.IsNullOrEmpty(args.FieldValue) ||
                     !Context.PageMode.IsNormal)
                    return;

                if (!string.IsNullOrWhiteSpace(args.Result?.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;
                                node.Attributes.Add("responsive-src", imgUrl);
                                node.Attributes["src"].Value = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ")
                                    + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy"));
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                            }
                        }

                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in ImageLazyLoadModule.ImageLazyLoad:" + ex.Message, ex);
            }
        }
    }
}

You can also choose to add image transitions using css as is specified in the bLazy Documentation

.b-lazy {
-webkit-transition: opacity 500ms ease-in-out;
-moz-transition: opacity 500ms ease-in-out;
-o-transition: opacity 500ms ease-in-out;
transition: opacity 500ms ease-in-out;
max-width: 100%;
opacity: 0;
}
.b-lazy.b-loaded {
opacity: 1;
}

Here’s a very simple example of this module in use (Rich text field example)

We have a simple content item, where an image has been embedded in the rich text field from the media library

The view rendering code, simply outputs the rich text field using the Sitecore renderer (observe there are no additional classes added either)

Now for the output! When you load up the page with this ViewRendering, you see the following

Observe that the selector ‘b-lazy’ was added dynamically by our RenderField customization, and it was further picked up by the bLazy plugin and transformed successfully, evident by the ‘b-loaded’ class.

Now to see the html loaded on page load

Here you can see that the bLazy script and its initialization was added dynamically by our mvc.renderRendering pipeline update, and the img tag on page load, has a different src! This src is the base64 code of a pixel. The actual image source has been moved to a different attribute (responsive-src) which is recognized by the plugin (supplied during plugin initialization)

Hence proved.

The package and code for this module can be found at: Github – aceanindita

Advertisements

Glass html helpers for responsive images in Sitecore with bLazy

The bLazy Plugin provides us with a great way to implement serving up resized images based on the current viewport, and additionally it also allows us to lazy load images on our site, drastically reducing the page size and download time.

We used bLazy for img tags – and also for other tags (where it simply loads up the image as a background image).

We created a few glass HTML helpers here to help us maintain consistency and increase maintainability by having this html present in only 1 place in the site.

        public HtmlString RenderResponsiveLazyImage(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions, 
            bool isEditable = true, NameValueCollection parameters = null)
        {
            return RenderResponsiveLazyImage(Model, field, mobileDimensions, tabletDimensions, desktopDimensions, isEditable, parameters);
        }

        public HtmlString RenderResponsiveLazyImage<T>(T model, Expression<Func<T, object>> field, 
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions,
            bool isEditable = true, NameValueCollection parameters = null)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                StringBuilder responsiveImageHtml = new StringBuilder();
                StringBuilder parametersHtml = new StringBuilder();
                if (parameters != null)
                {
                    var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                    foreach (var item in items)
                    {
                        parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                    }
                }

                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    responsiveImageHtml.Append(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                else if (!Sitecore.Context.PageMode.IsPageEditor && imageField.IsValid())
                {
                    responsiveImageHtml.Append("<img" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString())
                        + " src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\""
                        + " data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            mobileDimensions != null ? mobileDimensions.Width : 0, mobileDimensions != null ? mobileDimensions.Height : 0) + "\""
                        + " data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            tabletDimensions != null ? tabletDimensions.Width : 0, tabletDimensions != null ? tabletDimensions.Height : 0) + "\""
                        + " data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            desktopDimensions != null ? desktopDimensions.Width : 0, desktopDimensions != null ? desktopDimensions.Height : 0) + "\""
                        + " alt=\"" + imageField.Alt + "\" />");
                }

                return new HtmlString(responsiveImageHtml.ToString());
            }

            return new HtmlString(string.Empty);
        }

        public string RenderResponsiveLazyImageAttributes(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            return RenderResponsiveLazyImageAttributes(Model, field, mobileDimensions, tabletDimensions, desktopDimensions);
        }

        public string RenderResponsiveLazyImageAttributes<T>(T model, Expression<Func<T, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null && imageField.IsValid())
            {
                StringBuilder responsiveLazyImageHtml = new StringBuilder();

                if (mobileDimensions != null)
                {
                    responsiveLazyImageHtml.Append("data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, mobileDimensions.Width, mobileDimensions.Height) + "\"");
                }

                if (tabletDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, tabletDimensions.Width, tabletDimensions.Height) + "\"");
                }

                if (desktopDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, desktopDimensions.Width, desktopDimensions.Height) + "\"");
                }

                return responsiveLazyImageHtml.ToString();
            }
            return string.Empty;
        }

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

We used Sitecore Image Processor Module to be able to resize sitecore images here.

Where Dimensions is:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

Asynchronous resize of images (async http handler) with default sitecore image

Images can usually take up most of your page load time. In our project we were loading product images from the servers, and resizing them before serving them up on the site. To improve performance, we decided to try and do the resizing and image loading there after asynchoronously.
So we created an asynchronous http handler to perform this action.

We were resizing external images (not stored in sitecore), but we did want to load up a default image stored in sitecore if the image didn’t exist on the filesystem.
To achieve this, we passed in the url of the default image (language specific) as a querystring parameter to the http handler itself. In this case, it would check if the intended image existed on the filesystem, if not, it would return the resized default image in the response stream.

using Sitecore.Diagnostics;
using System;
using System.Drawing;
using System.IO;
using System.Net;
using System.Threading;
using System.Web;

namespace MySite.Customizations
{
    class ImageResizer : IHttpAsyncHandler
    {
        public bool IsReusable { get { return false; } }

        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
        {
            ImageProcessor asyncImgProcessor = new ImageProcessor(cb, context, extraData);
            asyncImgProcessor.StartAsyncWork();
            return asyncImgProcessor;
        }

        public void EndProcessRequest(IAsyncResult result)
        {
        }

        public void ProcessRequest(HttpContext context)
        {
        }
    }

    class ImageProcessor : IAsyncResult
    {
        private bool _completed;
        private Object _state;
        private AsyncCallback _callback;
        private HttpContext _context;

        bool IAsyncResult.IsCompleted { get { return _completed; } }
        WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
        Object IAsyncResult.AsyncState { get { return _state; } }
        bool IAsyncResult.CompletedSynchronously { get { return false; } }

        public ImageProcessor(AsyncCallback callback, HttpContext context, Object state)
        {
            _callback = callback;
            _context = context;
            _state = state;
            _completed = false;
        }

        public void StartAsyncWork()
        {
            ThreadPool.QueueUserWorkItem(StartAsyncTask, null);
        }

        private void StartAsyncTask(Object workItemState)
        {
            if (_context.Request.QueryString["path"] != null)
            {
                try
                {
                    string imagePath = _context.Request.QueryString["path"];

                    if (string.IsNullOrWhiteSpace(imagePath))
                    {
                        EndAsyncTask();
                        return;
                    }

                    imagePath = HttpUtility.Decode(imagePath);
                    Image image = null;
                    bool isFileSystem = false;

                    string fileSystemImagePath;

                    if (!ImageHelper.IsValidProductFileSystemImage(imagePath, out fileSystemImagePath))
                    {
                        imagePath = _context.Request.Url.Scheme + "://" + _context.Request.Url.Host
                            + HttpUtility.Decode(_context.Request.QueryString["defaultimage"]);
                    }
                    else
                    {
                        image = Image.FromFile(fileSystemImagePath);
                        isFileSystem = true;
                    }

                    int h = 0, w = 0;
                    if (_context.Request.QueryString["h"] != null)
                    {
                        h = Convert.ToInt32(string.IsNullOrWhiteSpace(_context.Request.QueryString["h"]) ? "0" : _context.Request.QueryString["h"]);
                    }
                    if (_context.Request.QueryString["w"] != null)
                    {
                        w = Convert.ToInt32(string.IsNullOrWhiteSpace(_context.Request.QueryString["w"]) ? "0" : _context.Request.QueryString["w"]);
                    }

                    try
                    {
                        if (!isFileSystem)
                        {
                            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(imagePath);
                            using (WebResponse response = request.GetResponse())
                            using (HttpWebResponse httpResponse = (HttpWebResponse)response)
                            {
                                if (httpResponse.StatusCode != HttpStatusCode.OK)
                                {
                                    EndAsyncTask();
                                    return;
                                }
                                using (Stream imageStream = response.GetResponseStream())
                                {
                                    if (imageStream == null)
                                    {
                                        EndAsyncTask();
                                        return;
                                    }

                                    image = Image.FromStream(imageStream);
                                }
                            }
                        }

                        byte[] imageBytes = ImageHelper.ResizeAndCropImage(image, w, h,
                            (_context.Request.QueryString["crop"] == null || _context.Request.QueryString["crop"] == "true"));

                        image.Dispose();

                        _context.Response.ContentType = "image/jpeg";
                        if (imageBytes != null)
                        {
                            _context.Response.BinaryWrite(imageBytes);
                        }
                        _context.Response.End();
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex.Message, ex);
                        EndAsyncTask();
                        return;
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Exception in fetching product image for resize: " + ex.StackTrace, ex, this);
                }
            }
            EndAsyncTask();
        }

        public void EndAsyncTask()
        {
            _completed = true;
            _callback(this);
        }
    }
}

In the above code, IsValidProductFileSystemImage – basically executes a System.IO.File.Exists call to check whether the image in the ‘path’ parameter exists.
Additionally, the code for ResizeAndCropImage can be found at: Resize Images with Crop and Quality Control.

Resize and lazy load images in Rich Text fields in Sitecore

Often the imagery that is uploaded in Sitecore is of way higher resolution than what is required, in an attempt to produce high quality pages.
While these are uploaded onto image fields in sitecore, we can resize the images when we output them using suitable html helpers. Refer:Render Responsive Image Glass Html Helper with Sitecore Image Processor Module.

We also needed a way to achieve this for images inserted in the rich text field. Especially seeing as we had content authors adding images sized 3000×3000 in there, and page sizes soaring up to 80mb each at times!
We implemented a solution with the bLazy plugin.
The plugin documentation will tell you, that once we include the script and initialize it with the right parameters, all it takes is adding the right attributes and class into our html, to enable lazy load of images – resized if need be for each breakpoint as we determine.

Here’s what we implemented for this:

  • We added a patch to the renderField pipeline, which would transform all img tags as we needed – add the required class which is to be used as the selector for the blazy plugin, and also the breakpoint specific resized image url attributes. In addition, we also replaced the img tag source with a base64 encoded transparent gif so it wouldn’t do any extra requests.
  • We added a reference to the bLazy plugin, and initialized it with the parameters suitable to our requirements.

The pipeline code:

using HtmlAgilityPack;
using sbdshared._Classes.SBDShared.Helpers;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;
using System.Web;

namespace MySite.Customizations.Customized_Sitecore
{
    public class ProcessRichTextImages
    {
        /// <summary>
        /// Processes the specified arguments.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args != null && (args.FieldTypeKey != "rich text" || string.IsNullOrEmpty(args.FieldValue) || !Context.PageMode.IsNormal))
                    return;

                if (args != null && args.Result != null && !string.IsNullOrWhiteSpace(args.Result.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;

                                node.Attributes.Add("data-responsive-bkg-desktop", ImageHelper.GetResizedExternalUrl(imgUrl, 580, 0));
                                node.Attributes.Add("data-responsive-bkg-tablet", ImageHelper.GetResizedExternalUrl(imgUrl, 350, 0));
                                node.Attributes.Add("data-responsive-bkg-mobile", ImageHelper.GetResizedExternalUrl(imgUrl, 200, 0));
                                node.Attributes["src"].Value = "/_Images/loader.gif";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ") + "responsive-bkg");
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                                if (node.Attributes["style"] != null && !string.IsNullOrWhiteSpace(node.Attributes["style"].Value)
                                    && (node.Attributes["style"].Value.Contains("width") || node.Attributes["style"].Value.Contains("height")))
                                {
                                    node.Attributes.Remove("style");
                                }
                            }
                        }

                        // Replace the Rich Text content with the modified content
                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            { Log.Error("Error in MySite.Customizations.Customized_Sitecore.ProcessRichTextImages:" + ex.Message, ex); }
        }
    }
}

We used the Sitecore Image Processor Module to get the resized sitecore images.

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

Additionally – we also removed any height / width attributes added to the images in the rich text field – since the design in our case allowed for it.
We added this to the renderField processor:

<configuration>
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="MySite.Customizations.Customized_Sitecore.ProcessRichTextImages, MySite" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Now that we had the html the way we required it for the blazy plugin, all that was needed was to include the script and initialize it:

<script src="blazy.js"></script>

And the following needed to go into the $(document).Ready() method.

var bLazy = new Blazy({
	selector: '.responsive-bkg',
    offset: 200,
    breakpoints: [{
			width: 768,
			src: 'data-responsive-bkg-mobile'
		},
		{
		      width: 980
		    , src: 'data-responsive-bkg-tablet'
		}],
	src: 'data-responsive-bkg-desktop'
});

The images would now load in a lazy-load fashion based on the current viewport breakpoint, when the cursor was within 200px of the image iteself. (Refer bLazy Documentation)

Render Responsive Image Glass Html Helper with Sitecore Image Processor Module

In today’s world of multi-sized devices / screens, responsive design is almost a must for trendy websites.
The <picture> tag provides a nice solution to the need for serving up resized / different images based on the viewport / orientation of the client screen.

Responsive

I found this article very useful for reading up on the <picture> tag.
http://webdesign.tutsplus.com/tutorials/quick-tip-how-to-use-html5-picture-for-responsive-images–cms-21015

This allows us to efficiently use bandwidth and serve up appropriately sized images based on the screen size AND pixel density. In addition, we could also use different images altogether based on the viewport.

In one of our Sitecore projects we extensively used picture tags. We used it in tandem with the Sitecore Image Processor Module to resize images based on view port. In our solution, we didn’t make any changes based on screen orientation (which is definitely an option too in the picture tag). We served up differently sized versions of the same image based on the viewport.

We created a glass html helper method to help render the picture tag for a given sitecore item field, passing in the max size of the image per breakpoint.

        public HtmlString RenderResponsivePicture<T>(T model, Expression<Func<T, object>> field, List<Dimensions> dimensions,
            bool isEditable = false, NameValueCollection parameters = null, bool crop = true)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    return new HtmlString(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                if (imageField.IsValid())
                {
                    StringBuilder responsiveImageHtml = new StringBuilder();
                    StringBuilder parametersHtml = new StringBuilder();
                    if (parameters != null)
                    {
                        var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                        foreach (var item in items)
                        {
                            parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                        }
                    }

                    responsiveImageHtml.Append("<picture" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString()) + ">");

                    if (dimensions != null && dimensions.Count() > 1)
                    {
                        foreach (Dimensions param in dimensions.Take(dimensions.Count - 1))
                        {
                            responsiveImageHtml.Append
                                (string.Format(
                                    "<!--[if IE 9]><video style='display: none;'><![endif]--><source media=\"(min-width: {0}px)\" srcset=\"{1}\"><!--[if IE 9]></video><![endif]-->",
                                    param.ScreenSize, param.IsUrlBlank ? string.Empty : ImageHelper.GetResizedSitecoreUrl(imageField.Src, param.Width, param.Height, crop)));
                        }
                        Dimensions lastparam = dimensions.Last();
                        responsiveImageHtml.Append(string.Format("<img srcset=\"{0}\" alt=\"{1}\">",
                            ImageHelper.GetResizedSitecoreUrl(lastparam.IsUrlBlank ? string.Empty : imageField.Src, lastparam.Width, lastparam.Height), imageField.Alt));
                    }
                    responsiveImageHtml.Append("</picture>");

                    return new HtmlString(responsiveImageHtml.ToString());
                }
            }

            return new HtmlString(string.Empty);
        }

        public HtmlString RenderResponsivePicture(Expression<Func<TModel, object>> field,
            List<Dimensions> dimensions, bool isEditable = false, NameValueCollection parameters = null, bool crop = true)
        {
            return RenderResponsivePicture(Model, field, dimensions, isEditable, parameters, crop);
        }

You’ll note that we have an additional line in there to allow for page editor support! So in page editor mode, we would render this as a regular img tag.
Additionally, we had multiple instances where the design called for not showing a particular image in the mobile view at all. To support this, we also added the ability to pass in a token which signifies that a given breakpoint url needs to be blank.

Please note, the List takes the dimensions in the order: Desktop –> Tab –> Mobile.
You could ofcourse choose to be more explicit and have different parameters for each breakpoint.

Where Dimensions is an internal class:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

In our project, we also had to support IE, so we used PictureFill as well, and you’ll see that the output html also outputs a video tag around the picture tag for IE9 as is needed to support the browser.

Additionally, we also had to render certain images on our site, which were not a part of a sitecore field. A good example of this, would be using an existing image repository for say product images, or user uploaded images which we store in a different location. For this we utilized the following code, which takes in an image url and a set of dimensions and builds the picture tag for you:

        public HtmlString RenderResponsiveNonFieldPicture(string url, List<Dimensions> dimensions,
            string altText = null, NameValueCollection parameters = null)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return new HtmlString(string.Empty);
            }

            StringBuilder responsiveImageHtml = new StringBuilder();
            StringBuilder parametersHtml = new StringBuilder();
            if (parameters != null)
            {
                var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                foreach (var item in items)
                {
                    parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                }
            }

            responsiveImageHtml.Append("<picture" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString()) + ">");
            if (dimensions != null && dimensions.Count() > 1)
            {
                foreach (Dimensions param in dimensions.Take(dimensions.Count - 1))
                {
                    responsiveImageHtml.Append
                        (string.Format("<!--[if IE 9]><video style='display: none;'><![endif]--><source media=\"(min-width: {0}px)\" srcset=\"{1}\"><!--[if IE 9]></video><![endif]-->",
                        param.ScreenSize, param.IsUrlBlank ? string.Empty : ImageHelper.GetResizedExternalUrl(url, param.Width, param.Height)));
                }
                Dimensions lastparam = dimensions.Last();
                responsiveImageHtml.Append(string.Format("<img srcset=\"{0}\" alt=\"{1}\">",
                    ImageHelper.GetResizedExternalUrl(lastparam.IsUrlBlank ? string.Empty : url, lastparam.Width, lastparam.Height), altText ?? string.Empty));
            }
            responsiveImageHtml.Append("</picture>");

            return new HtmlString(responsiveImageHtml.ToString());
        }

There are 2 underlying methods here which actually build the url for the resized images. For images in Sitecore, we used the Sitecore Image Processor Module. Going through the documentation, you’ll see that all we need to do is add the querystring parameter ‘usecustomfunctions’ to kick off the Image processor module resizing. So here’s the method we used:

public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

For external images, we ended up writing out own code to resize images. You’ll see, we also added an additional control to determine the quality of the resized image. You could also choose to build out your own image resizing module with this code.
We used a http handler for this, and made it asynchronous, so that the image resizing happens asynchronously!

So the method we use to get the resized url basically created a url to the http handler, passing in the width / height parameters:

        public static string GetResizedExternalUrl(string url, int width, int height, bool crop = true)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return url;
            }

            return HttpUtility.HtmlDecode(Constants.Url.ImageResizer + "?path=" + HttpUtility.HtmlEncode(url)
                + "&w=" + width) + "&h=" + height + "&crop=" + (crop ? "true" : "false"));
        }

In the http handler, we do the actual image resizing, which you can see here: Resize Images with Crop and Quality Control.

Resize Images with Crop and Quality Control

There are plenty of snippets out there to resize images, and I went through a lot of them, while trying to implement this.
But a bunch of them gave me quality issues and some others came with other issues of their own.

So here’s the code we used to resize images, with the provision to adjust for quality (depending on how much you are willing to sacrifice for bandwidth!)

        internal static byte[] ResizeAndCropImage(System.Drawing.Image image, int requestedWidth, int requestedHeight, bool crop = true)
        {
            if (image == null) return null;

            int sourceWidth = image.Width;
            int sourceHeight = image.Height;

            if (sourceWidth == 0 || sourceHeight == 0) return null;

            // *************** Figure out resize % ***************

            double percentageResize = 1;

            if (requestedWidth == 0 && requestedHeight == 0)
            {
                percentageResize = 1;
            }
            else if (requestedWidth == 0 && requestedHeight > 0)
            {
                if (sourceHeight <= requestedHeight)
                {
                    percentageResize = 1;
                }
                else
                {
                    percentageResize = (double)requestedHeight / sourceHeight;
                }
            }
            else if (requestedWidth > 0 && requestedHeight == 0)
            {
                if (sourceWidth <= requestedWidth)
                {
                    percentageResize = 1;
                }
                else
                {
                    percentageResize = (double)requestedWidth / sourceWidth;
                }
            }
            else if (requestedWidth <= sourceWidth && requestedHeight <= sourceHeight)
            {
                percentageResize = crop ? Math.Max((double)requestedWidth / sourceWidth, (double)requestedHeight / sourceHeight)
                    : Math.Min((double)requestedWidth / sourceWidth, (double)requestedHeight / sourceHeight);
            }
            else if ((requestedWidth <= sourceWidth && requestedHeight > sourceHeight)
                || (requestedWidth > sourceWidth && requestedHeight <= sourceHeight)
                || (requestedWidth > sourceWidth && requestedHeight > sourceHeight))
            {
                percentageResize = 1;
            }

            int destHeight = (int)Math.Round(sourceHeight * percentageResize);
            int destWidth = (int)Math.Round(sourceWidth * percentageResize);

            using (Bitmap resizedBmp = new Bitmap(destWidth, destHeight))
            {
                // *************** Resize ***************

                using (Graphics graphics = Graphics.FromImage(resizedBmp))
                {
                    graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                    graphics.DrawImage(image, 0, 0, destWidth, destHeight);
                }

                // *************** Figure out crop px ***************

                int cropX = crop && (destWidth > requestedWidth && requestedWidth > 0) ? (destWidth - requestedWidth) / 2 : 0;
                int cropY = crop && (destHeight > requestedHeight && requestedHeight > 0) ? (destHeight - requestedHeight) / 2 : 0;

                // *************** Crop ***************

                Rectangle rectangle = new Rectangle(cropX, cropY, destWidth - (cropX * 2), destHeight - (cropY * 2));

                using (Bitmap croppedBmp = resizedBmp.Clone(rectangle, resizedBmp.PixelFormat))
                using (MemoryStream ms = new MemoryStream())
                {
                    ImageCodecInfo imgCodec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == image.RawFormat.Guid);
                    EncoderParameters codecParams = new EncoderParameters(1);

                    codecParams.Param[0] = new EncoderParameter(Encoder.Quality, 95L);
                    croppedBmp.Save(ms, imgCodec, codecParams);
                    return ms.ToArray();
                }
            }
        }

The quality can be controlled by passing in the % in the codec params above. You can see, we have it set to 95% here. You could ofcourse make this configurable and choose to pass in the % with the width and height.

To get the System.Drawing.Image object from a local file:

System.Drawing.Image image = System.Drawing.Image.FromFile("C:\...\myimage.jpg");

If you wanted to resize an image using a url (say for instance in your application you stored user uploaded images in Salesforce (Storing user profile image using Salesforce Connector with Sitecore) and wanted to resize this image to display on your site), then you could use the following to get the System.Drawing.Image object:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(imagePath);
                            using (WebResponse response = request.GetResponse())
                            using (HttpWebResponse httpResponse = (HttpWebResponse)response)
                            {
                                if (httpResponse.StatusCode != HttpStatusCode.OK)
                                {
                                    EndAsyncTask();
                                    return;
                                }
                                using (Stream imageStream = response.GetResponseStream())
                                {
                                    if (imageStream == null)
                                    {
                                        EndAsyncTask();
                                        return;
                                    }

                                    image = Image.FromStream(imageStream);
                                }
                            }

Here are some sample outputs!

Original Image (768X768):
P1020131

300X?
Imageresizer
?X100
Imageresizer (1)
300X200
Imageresizer (3)
100X200
Imageresizer (2)