Wednesday, May 28, 2014

Size Doesn’t Matter: Suppressing Size Attributes in Image Tags



On a recent, massively-responsive project, our front-end developer asked us to (well, he actually threatened to hold his breath until he turned blue unless we would) remove the height and width attributes from the image tags in our Sitecore site. He’s one of the best front-end guys I've ever worked with, so instead of just dismissing this as “front-end guys will be front-end guys”, I decided to see if we could indulge him.

It makes sense, actually. Client-side code can deal manipulating height and width easily enough when rendering on different devices. But it you want the thrill of waving around the resize handle and watching all that responsiveness a’responding, then it’d be better if the height and width attributes weren't there in the first place.

By and large, there are two ways an image tag finds its way into a page generated by Sitecore. It may come from an image field, or from an embedded image in an HTML field. So we should be able to tackle this in the renderField pipeline, assuming we've been good boys and girls and used field renderers everywhere (and if you haven’t, then go to your room and think about what you've done).

The two cases (image fields and html fields) pose different challenges, so let’s look at them separately. Or if you don’t want to dig in, then you can stop reading now, download the module package or source code (zip fileGitHub, or Sitecore Marketplace) and have at it.

Image Fields

We've got images in image fields, and we’re using field renderers to generate image tags at runtime. Sitecore uses Sitecore.Pipelines.RenderField.GetImageFieldValue to do this. When we take a look under the hood, we see it in turn is using a Sitecore.Xml.Xsl.ImageRenderer. Luckily, GetImageFieldValue uses a virtual method CreateRenderer, so we can shim in our own class that inherits from GetImageFieldValue and override CreateRenderer to substitute our own handy-dandy ImageRenderer class.

Now, Sitecore's ImageRenderer class is pretty bulky, and it has more stuff that deals with dimensions than a cartographer’s workshop. I’d like to just inherit from theirs and find a good pressure point to slap down those size attributes. Taking a good look at the Render method in Sitecore’s ImageRenderer, it looks like someone anticipated our need. The last thing it does to determine the size is to call a virtual method called AdjustImageSize. All we need to do is override that and set the height and width properties to zero. The existing Sitecore code is already set up to suppress the height and width attributes if these properties are zero.

So we need two pretty lightweight classes and an override in a config file. First, the config file. We need to tell Sitecore to replace its GetImageFieldValue processor with ours.

 <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">  
  <sitecore>  
   <pipelines>  
    <renderField>  
     <processor   
      type="DimensionlessImages.Pipelines.RenderField.GetImageFieldValue,   
         DimensionlessImages"  
      patch:instead="*[@type='Sitecore.Pipelines.RenderField.GetImageFieldValue,  
         Sitecore.Kernel']"  
      />  

Then, there’s our own GetImageFieldValue processor, which inherits from Sitecore’s and overrides the CreateReplacer method.

 namespace DimensionlessImages.Pipelines.RenderField  
 {  
  using Sitecore.Xml.Xsl;  
  public class GetImageFieldValue : Sitecore.Pipelines.RenderField.GetImageFieldValue  
  {  
   protected override ImageRenderer CreateRenderer()  
   {  
    return new DimensionlessImages.ImageRenderer();  
   }   
  }  
 }  

And lastly, there’s our own ImageRenderer, which overrides the AdjustImageSize method.

 namespace DimensionlessImages  
 {  
  using Sitecore.Data.Fields;  
  public class ImageRenderer : Sitecore.Xml.Xsl.ImageRenderer  
  {  
   protected override void AdjustImageSize(ImageField imageField, float imageScale, int imageMaxWidth, int imageMaxHeight, ref int w, ref int h)  
   {  
    w = 0;  
    h = 0;  
   }  
  }  
 }  

Piece of cake, that.

HTML Fields

HTML fields are a different animal. We've got a hunk of existing html, not just some data we’ll use to form HTML at runtime.

When a media library image is inserted in the rich text editor, Sitecore adds the height and width attributes to the img tag. So between that, and the possibility that content people might edit HTML manually (bless their little programmer-wannabe hearts), we’re going to have lots of height and width attributes in our HTML fields.

We can use the HtmlAgilityPack to strip these attributes off the img tags in in the renderField pipeline. Although the HtmlAgilityPack is wicked fast, we could start to see a performance hit of there are lots of HTML fields on complex pages. It’d better to do it in a save handler or a publishItem processor, but that could present problems with the page editor or if there is already a lot of existing content. I’m seeing times of less than 0.1ms per HTML field to strip the tags at runtime, so to make this code more bullet-proof (well, ok, to let me be lazy) I’m going to do it in renderField. If your solution permits, by all means move this processing to a save handler.

Like before, we’ll start with the config changes…

 <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">  
  <sitecore>  
   <renderField>  
    <processor type="DimensionlessImages.Pipelines.RenderField.GetFieldValue, DimensionlessImages"  
      patch:instead="*[@type='Sitecore.Pipelines.RenderField.GetFieldValue, Sitecore.Kernel']"  
    />  

We’ll use a custom GetFieldValue processor. It is actually simpler than the image field case, because all we have to do is catch “rich text” fields and strip the height and width attributes.

 namespace DimensionlessImages.Pipelines.RenderField  
 {  
  using Sitecore.Pipelines.RenderField;  
  public class GetFieldValue : Sitecore.Pipelines.RenderField.GetFieldValue  
  {  
   public new void Process(RenderFieldArgs args)  
   {  
    base.Process(args);  
    if (args.FieldTypeKey == "rich text")  
    {  
     Sitecore.Diagnostics.Profiler.StartOperation("Stripping image tags from field: " + args.FieldName);  
     args.Result.FirstPart = HtmlUtil.StripDimensions(args.Result.FirstPart);  
     Sitecore.Diagnostics.Profiler.EndOperation();  
    }  
   }  
  }  
 }  

Finally, we need a helper method uses the HtmlAgilityPack to strip the dimension attributes…

 namespace DimensionlessImages  
 {  
  using System;  
  using HtmlAgilityPack;  
  public class HtmlUtil  
  {  
   public static string StripDimensions(string text)  
   {  
    if (string.IsNullOrWhiteSpace(text))  
    {  
     return text;  
    }  
    string outText = text;  
    try  
    {  
     var doc = new HtmlDocument();  
     doc.LoadHtml(outText);  
     StripAttribute(doc, "width");  
     StripAttribute(doc, "height");  
     outText = doc.DocumentNode.WriteContentTo();  
    }  
    catch (Exception)  
    {}  
    return outText;  
   }  
   private static void StripAttribute(HtmlDocument doc, string attribute)  
   {  
    // For reasons surpassing all understanding, HtmeAgilityPack returns null instead of an empty collection  
    // when the query finds no results.  
    HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes(String.Format("//img[@{0}]", attribute));  
    if (nodes == null || nodes.Count.Equals(0))  
    {  
     return;  
    }  
    foreach (HtmlNode node in nodes)  
    {  
     node.Attributes[attribute].Remove();  
    }  
   }  
  }  
 }  

- - -

This code addresses the most likely cases that emit image tags to the browser. A given solution may have other cases, like stuff being generated by custom code or copied from static files. For cases like that, we've provided the static method that custom code can use to “play ball” with the rest of this code.

Download: Module (package only) or source code (zip fileGitHub, or Sitecore Marketplace).