Marius Schulz
Marius Schulz
Front End Engineer

A Little HtmlHelper for Implementing Adaptive HTML Images in ASP.NET MVC

As part of HTML5, the srcset attribute for img tags has been specified by W3C to provide an HTML extension for adaptive images. Here's an excerpt from the specification:

When authors adapt their sites for high-resolution displays, they often need to be able to use different assets representing the same image. We address this need for adaptive, bitmapped content images by adding a srcset attribute to the img element.

Support for the srcset attribute shipped with Chrome 34 in April 2014 and just appeared in Firefox Nightly. Because responsive images are a feature we all should start using today, I want to show you my approach for emitting adaptive img tags in ASP.NET MVC.

#Why Bother About Adaptive Images?

With high-resolution screens in our smartphones and laptops, we expect our browsers to display crisp images on the web. Because these displays have pixel densities > 1, more pixels are required to render a sharp image with the same relative size. Obviously, those larger images increase the amount of data downloaded by the browser.

The issue with those high-res images is that no optimal solution could be achieved with plain HTML so far. You could pursue one of the following strategies:

  • Don't provide high-res images → blurry images on high-res displays
  • Always load high-res images → unnecessarily large images on low-res displays

Of course, there's a plethora of JavaScript libraries out there which download images with a resolution appropriate for the user's screen. Sometimes, they first download the low-res version of an image and then point the src attribute of the corresponding img tag to the high-res version if on a high-resolution display. They thereby cause browsers to download both images, which is obviously suboptimal because there are two HTTP requests to be made and even more image data to be transferred.

It would be great if browsers decided upfront which version of an image to load. That's where adaptive images come into play.

#Making HTML Image Tags Adaptive

Adaptive images are created by adding the srcset attribute to HTML's existing img tags. The src attribute will hold the default image URL which is used when none of the high-res versions specified by srcset will be loaded. This solution is backwards compatible: Old Browsers who don't support srcset yet won't be affected by the additional attribute and will regularly download the image from the URL specified by src.

The syntax required by the srcset attribute is a comma-separated list of so-called image descriptors. Such a descriptor consists of two parts: the image URL and the pixel density of the displays for which that image should be loaded. Here's a simple example of loading an adaptive logo, which has only one descriptor:

<img
  src="/images/logo.png"
  srcset="/images/logo@2x.png 2x"
  alt="Company Name"
  width="100"
  height="40"
/>

Here, the image logo@2x.png will be loaded for displays with a pixel density greater than or equal to 2 (denoted by the 2x after the file name). As you can see, the image file name is suffixed with the pixel density it's made for, which is a common convention. Let's do the math: The image logo@2x.png should be 200px wide and 80px high to be rendered crisply with a relative size of 100px × 40px on a display with a pixel density of 2.

You can simply list all the image descriptors you need (separated by a comma) to provide more than one high-res image version. Here, we're also offering an @3x version:

<img
  src="/images/logo.png"
  srcset="/images/logo@2x.png 2x, /images/logo@3x.png 3x"
  alt="Company Name"
  width="100"
  height="40"
/>

#An HtmlHelper for Adaptive Images

You might have noticed that some parts of the above img tag are quite repetitive and lend themselves to automation. That's what I thought, too, so I wrote a little HTML helper method to emit adaptive img tags. Note that it's based on the convention to append density suffixes like @2x or @3x to the file name.

Here's how you use it in a Razor view:

@Html.ImgTag("/images/logo.png", "Company Name").WithDensities(2, 3).WithSize(100, 40)

The second parameter is the value of the required alt attribute, which gets enforced like this. This is how the HTML tag is rendered:

<img
  src="/images/srcset_helper_method_output.png"
  alt="The Adaptive Image Rendered with the HTML Helper"
  width="604"
  height="31"
/>

Here's the implementation of the ImgTag extension method:

public static class HtmlHelperExtensions
{
    public static ImgTag ImgTag(this HtmlHelper htmlHelper,
        string imagePath, string altText)
    {
        var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

        return new ImgTag(imagePath, altText, urlHelper.Content);
    }
}

The logic is contained within the ImgTag class:

public class ImgTag : IHtmlString
{
    private readonly string _imagePath;
    private readonly Func<string, string> _mapVirtualPath;
    private readonly HashSet<int> _pixelDensities;
    private readonly IDictionary<string, string> _htmlAttributes;

    public ImgTag(string imagePath, string altText, Func<string, string> mapVirtualPath)
    {
        _imagePath = imagePath;
        _mapVirtualPath = mapVirtualPath;

        _pixelDensities = new HashSet<int>();
        _htmlAttributes = new Dictionary<string, string>
        {
            { "src", mapVirtualPath(imagePath) },
            { "alt", altText }
        };
    }

    public string ToHtmlString()
    {
        var imgTag = new TagBuilder("img");

        if (_pixelDensities.Any())
        {
            AddSrcsetAttribute(imgTag);
        }

        foreach (KeyValuePair<string, string> attribute in _htmlAttributes)
        {
            imgTag.Attributes[attribute.Key] = attribute.Value;
        }

        return imgTag.ToString(TagRenderMode.SelfClosing);
    }

    private void AddSrcsetAttribute(TagBuilder imgTag)
    {
        int densityIndex = _imagePath.LastIndexOf('.');

        IEnumerable<string> srcsetImagePaths =
            from density in _pixelDensities
            let densityX = density + "x"
            let highResImagePath = _imagePath.Insert(densityIndex, "@" + densityX)
                + " " + densityX
            select _mapVirtualPath(highResImagePath);

        imgTag.Attributes["srcset"] = string.Join(", ", srcsetImagePaths);
    }

    public ImgTag WithDensities(params int[] densities)
    {
        foreach (int density in densities)
        {
            _pixelDensities.Add(density);
        }

        return this;
    }

    public ImgTag WithSize(int width, int? height = null)
    {
        _htmlAttributes["width"] = width.ToString();
        _htmlAttributes["height"] = (height ?? width).ToString();

        return this;
    }
}

Some closing notes:

  • The ImgTag class implements the IHtmlString interface so that the emitted HTML tag doesn't get double-encoded. Attribute values will be encoded by the TagBuilder.
  • I didn't want to pass an instance of UrlHelper to the ImgTag class only to access its Content method. Instead, that method is passed as a generic delegate in the constructor (that's the mapVirtualPath function).
  • If you want to make the code a little more defensive, you should make sure the file name has a proper extension so that LastIndexOf('.') works smoothly.
  • In the beginning, I had included a few more methods in the ImgTag class to allow for more generic img tags, e.g. including attributes like class. However, these methods are trivial to implement, so I omitted them here for the sake of brevity.