Handling Responsive Images in Umbraco

Matthew Wise
By Matthew WiseSolutions Architect
5 minutes to read

With the advent of the Umbraco ImageCropper responsive images are now more user manageable than ever; long gone are the days of 1600px images being used for a space which shows only 200px of it.

But how can we push this usage beyond ensuring images are not beyond the size required for the desktop environment?

The answer is to make the image responsive, which we can do this using the src-set and sizes attributes on the HTML <img> element and the new picture element. (A good write up on the different usages can be found here). One of the important differences to note is that with src-set the browser picks the image to use whereas with picture we tell the browser.

In order to use these new HTML5 goodies we need more versions of the image for each of the media sizes we want to create e.g.

KoalaKoala

This is the ImageCropper we know and love, so now let’s combine them in to one image element that responds based on the screen size.

Koala

If you reload your browser at a screen size below 400px, as you resize the window the larger images will be loaded in as the element size increases. Here is how the HTML looks for this:

(As you can see I changed one of the images so it obvious the image changes)

<img src="/media/2923/koala-1058508_640.jpg?width=200" 
alt="Koala" sizes="50vw" srcset="/media/2923/koala-1058508_640.jpg?width=200 200w, 
/media/2922/koala.jpg?width=800 800w,  /media/2922/koala.jpg?width=2000 2000w"/>

This is great for the users of the site, but this does take extra time for developers to write; wouldn’t it be easier if there was an Umbraco helper available where we can just pass the image and the cropping sizes to? Well now there is:

public class UmbracoHelperExtensions
{
        public static HtmlString ResponsiveImage(this UmbracoHelper umbraco, IPublishedContent item, string propAlias, object cropAliasAndWidths, string sizes, bool addRawImage = false, object htmlAttributes = null)
        {
            if (!item.HasValue(propAlias)) return new HtmlString(String.Empty);
            var media = item.GetPropertyValue<IPublishedContent>(propAlias);
            return ResponsiveImage(umbraco, media, cropAliasAndWidths, sizes, addRawImage, htmlAttributes);
        }

        /// <summary>
        /// Create Image with src set using image Cropper data type
        /// </summary>
        /// <param name="umbraco"></param>
        /// <param name="propAlias">Media Item Property</param>
        /// <param name="cropAliasAndWidths">Crop Alias and Image Width</param>
        /// <param name="sizes">[media query] [length], [media query] [length] ...</param>
        /// <param name="addRawImage">Add image with out cropping</param>
        /// <param name="htmlAttributes">Other html attributes to add</param>
        /// <returns></returns>
        public static HtmlString ResponsiveImage(this UmbracoHelper umbraco, string propAlias, object cropAliasAndWidths, string sizes, bool addRawImage = false, object htmlAttributes = null)
        {
            return ResponsiveImage(umbraco, umbraco.AssignedContentItem, propAlias, cropAliasAndWidths, sizes, addRawImage, htmlAttributes);
        }

        /// <summary>
        /// Create Image with src set using image Cropper data type and uploaded image
        /// </summary>
        /// <param name="umbraco"></param>
        /// <param name="media">Media Item</param>
        /// <param name="cropAliasAndWidths">Crop Alias and Image Width</param>
        /// <param name="sizes">[media query] [length], [media query] [length] ...</param>
        /// <param name="addRawImage">Add image with out cropping</param>
        /// <param name="htmlAttributes">Other html attributes to add</param>
        /// <returns></returns>
        public static HtmlString ResponsiveImage(this UmbracoHelper umbraco, IPublishedContent media, object cropAliasAndWidths, string sizes, bool addRawImage = false, object htmlAttributes = null)
        {
            if (media == null) return new HtmlString(String.Empty);
            var tagBuilder = new TagBuilder("img");
            var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
            tagBuilder.MergeAttributes(attributes);
            tagBuilder.MergeAttribute("alt", media.GetPropertyValue<string>("altText"));

            var cropAliasValues = HtmlHelper.AnonymousObjectToHtmlAttributes(cropAliasAndWidths);
            if (cropAliasValues == null || !cropAliasValues.Any())
            {
                tagBuilder.MergeAttribute("src", media.Url);
            }
            else
            {
                tagBuilder.MergeAttribute("src", media.GetCropUrl(cropAlias: cropAliasValues.First().Key, imageCropMode: ImageCropMode.Crop, useCropDimensions: true));
                var srcSet = new StringBuilder();
                if (addRawImage || cropAliasValues.Count > 1)
                {
                    foreach (var entry in cropAliasValues.AsSmartEnumerable())
                    {
                        srcSet.AppendFormat("{0} {1}w{2}", media.GetCropUrl(cropAlias: entry.Value.Key, imageCropMode: ImageCropMode.Crop, useCropDimensions: true), entry.Value.Value, entry.IsLast ? String.Empty : ", ");
                    }

                    if (addRawImage)
                    {
                        srcSet.AppendFormat(", {0} {1}w", media.Url, media.GetPropertyValue("umbracoWidth"));
                    }
                }
                var set = srcSet.ToString();
                if (!string.IsNullOrWhiteSpace(set))
                {
                    tagBuilder.MergeAttribute("srcset", set);
                }
                if (!string.IsNullOrWhiteSpace(sizes))
                {
                    tagBuilder.MergeAttribute("sizes", sizes);
                }
            }
            return new HtmlString(tagBuilder.ToString(TagRenderMode.SelfClosing));
        }
}

So now we can use the helper above to keep our views clean and easier to read.

 

//Where Model is the CurrentPage Model (IPublishedContent)
@Umbraco.ResponsiveImage(Model, "listingImage", new {example = 200}, "30vw")

We are now able to handle the images on the site, but can we do the same for those big full screen background images? Yes we can, by combining ImageCropper and CSS media queries.

For an example we can use the content of the page - here is the CSS we are using for the header image

<section class="hero-area blue blog-article blog-header">
    ...
</section>
<style type="text/css">
   .blog-header{
     background-image:url('/media/2911/blogheader.png');
   } 
   @media (max-width: 768px) {
     .blog-header{
       background-image:url('/media/2911/blogheader.png?center=0.49537037037037035,0.69028340080971662&mode=crop&width=768&height=670&rnd=131139248350000000');
     }
   }
   @media (max-width: 568px) {
      .blog-header{
        background-image:url('/media/2911/blogheader.png?center=0.49537037037037035,0.69028340080971662&mode=crop&width=568&height=480&rnd=131139248350000000');
      }
   }
</style>

This has been done by adding a style block with the media queries already setup and extending the HTML helper to allow us to write out the background images we need for the page.

public static class ResponsiveBackgroundImage
    {
        public const string DesktopKey = "desktop";

        private static Dictionary<string, string> Backgrounds
        {
            get
            {
                var background = HttpContext.Current.Items["BackgroundImages"] as Dictionary<string, string> ?? new Dictionary<string, string>();
                HttpContext.Current.Items["BackgroundImages"] = background;
                return background;
            }
        }

        public static IHtmlString ResponsiveBackground(this HtmlHelper helper, IPublishedContent image, object keysAndCrops, string cssSelector, bool rawAsDesktop = true)
        {
            var cropAliasValues = HtmlHelper.AnonymousObjectToHtmlAttributes(keysAndCrops);
            if (cropAliasValues == null) throw new ArgumentException("Parsing object results in null", "keysAndCrops");

            if (image == null)
            {
                return new HtmlString(String.Empty);
            }
          
            foreach (var crop in cropAliasValues)
            {
                var cssBlock = String.Concat(cssSelector, "{background-image:url('", image.GetCropUrl(crop.Value.ToString()), "');}");
                if (Backgrounds.ContainsKey(crop.Key))
                {
                    var currentValue = Backgrounds[crop.Key];
                    Backgrounds[crop.Key] = String.Concat(currentValue, cssBlock);
                }
                else
                {
                    Backgrounds.Add(crop.Key, cssBlock);
                }
            }
            if (rawAsDesktop)
            {
                var rawCss = String.Concat(cssSelector, "{background-image:url('", image.Url, "');}");
                if (Backgrounds.ContainsKey(DesktopKey))
                {
                    var currentValue = Backgrounds[DesktopKey];
                    Backgrounds[DesktopKey] = String.Concat(currentValue, rawCss);
                }
                else
                {
                    Backgrounds.Add(DesktopKey, rawCss);
                }
            }
            return new HtmlString(String.Empty);
        }

        public static IHtmlString ResponsiveBackground(this HtmlHelper helper, IPublishedContent content, string alias, object keysAndCrops, string cssSelector, bool rawAsDesktop = true)
        {
            if (content == null || String.IsNullOrWhiteSpace(alias) | String.IsNullOrWhiteSpace(cssSelector) || keysAndCrops == null)
            {
                throw new ArgumentException();
            }

            if (!content.HasValue(alias))
            {
                return new HtmlString(String.Empty);
            }

            var img = content.GetPropertyValue<IPublishedContent>(alias);

            return ResponsiveBackground(helper, img, keysAndCrops, cssSelector, rawAsDesktop);
        }

        public static IHtmlString RenderBackgroundCss(this HtmlHelper helper, string key)
        {
            return !Backgrounds.ContainsKey(key) ? new HtmlString(String.Empty) : new HtmlString(Backgrounds[key]);
        }
    }
@*Blog View*@
@Html.ResponsiveBackground(Model,"headerImage", new { tablet = "tabletSlide", mobile = "mobileSlide" }, ".blog-header")
<section class="blog-header">...</section>
@*Layout*@

<style type="text/css">
@Html.RenderBackgroundCss("desktop")
@@media (max-width: 768px) {
    @Html.RenderBackgroundCss("tablet")
}
@@media (max-width: 568px) {
    @Html.RenderBackgroundCss("mobile")
}
</style>

Things to be careful of when using this approach is the amount of background images on the page, as the matching media queries content will block rendering.

For more on information on this check out Google’s take by clicking here.

comments powered by Disqus

Articles by Matthew Wise

Biography coming soon!