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.

Advertisements

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)