Share

LinkedIn

Optimizing Images in Sitecore at Publish Time

Build a dynamic, high-traffic site by ensuring images are optimized.

When you build a high-traffic site in Sitecore (and really, aren’t all of our sites so awesome that they’ll become high-traffic?), you should optimize processing and bandwidth as much as possible. Tools like Google’s PageSpeed are great for telling us what to fix, but not how to fix them. In this case, we want to focus on image size.

One approach is to optimize the images at request time, using Sitecore’s getMediaStream pipeline – Kam Figy does this with his Dianoga Sitecore module. Another approach is to optimize the images in the master DB via a Content Editor tool, as Mikael Högberg shows with his Image Optimizer module. In this third approach, we will optimize the images as they are published from the master database to the web database. We only have to optimize it once, and the content editors don’t need to do anything special.

We’ll leave the actual compression algorithms to specialized third-party tools. We can optimize the three most common web image formats using jpegtran, pngcrush, and gifsicle.

Since we’re talking about taking action at publish time, we need a new processor in the publishItem pipeline. Create a config patch file in your app_config/include folder:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <publishItem>
        <processor type="AwareWeb.Sitecore.Pipelines.PublishItemImageOptimizer,AwareWeb.Sitecore" patch:after="processor[@type='Sitecore.Publishing.Pipelines.PublishItem.MoveItems, Sitecore.Kernel']" />
      </publishItem>
    </pipelines>
  </sitecore>
</configuration>

Now let’s take a look at the processor itself. Not much going on, just some error handling, logging, and calling a helper class. Note that when we grab the Item to optimize, we’re pulling from the target database. This is because the processor runs after the normal Sitecore “MoveItems” processor in the publishItem pipeline.

public class PublishItemImageOptimizer : PublishItemProcessor
{
    public override void Process(PublishItemContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        try
        {
            ProcessPublishedItem(context);
        }
        catch (Exception ex)
        {
            Log.Error("*** Error while optimizing image.", ex, this);
        }
    }
 
    protected virtual void ProcessPublishedItem(PublishItemContext context)
    {
        if (context == null || context.PublishOptions == null || context.PublishOptions.TargetDatabase == null)
        {
            Log.Error("Context and/or publish settings are null", this);
            return;
        }
 
        Item item = context.PublishOptions.TargetDatabase.GetItem(context.ItemId,
                context.PublishOptions.Language);
        if (item != null)
        {
            if (item.Paths.IsMediaItem && !((MediaItem) item).FileBased)
            {
                // Compress the image
                ImageOptimizer.Optimize(item);
            }
        }
    }

Finally, here’s the ImageOptimizer helper class. The main function, Optimize(), first checks that the item is a PNG or JPEG or GIF image, and that it hasn’t been previously optimized (remember, we’re checking the fields on the publishing target database when we check the “optimized” field). Then it grabs the image data, saves it to a temp file, executes the appropriate 3rd party optimizer, and if any space was saved, puts the image back into Sitecore.

protected static string ExportOriginalFile(MediaItem item, string extension)
{
    string path = Path.GetTempFileName();
        using (Stream stream = File.OpenWrite(path))
    {
        int num;
        Stream mediaStream = item.GetMediaStream();
        var buffer = new byte[0x2000];
        while ((num = mediaStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            stream.Write(buffer, 0, num);
        }
    }
    return path;
}
 
protected static void Execute(string cmd, string[] args, string src, string dst)
{
    string str = string.Format(string.Join(" ", args), "\"" + src + "\"", "\"" + dst + "\"");
    var stopwatch = new Stopwatch();
    var startInfo = new ProcessStartInfo(cmd)
    {
        Arguments = str,
        UseShellExecute = false,
        CreateNoWindow = true,
        RedirectStandardOutput = true
    };
    stopwatch.Start();
    if (!Process.Start(startInfo).WaitForExit(60000))
    {
        throw new Exception(string.Format("Warning, executing image compression timed out. Command: {0} {1}",
            cmd, str));
    }
    stopwatch.Stop();
    Log.Debug(
        string.Format(
            "*** Successfully ran external image compression tool. Elapsed time: {2}ms. Command: {0} {1}",
            cmd, str, stopwatch.ElapsedMilliseconds));
}
 
protected static bool AttachNewFile(MediaItem item, string filename)
{
    if (!File.Exists(filename))
    {
        Log.Debug("*** Image Optimizer: could not find file to attach.");
        return false;
    }
    using (new SecurityDisabler())
    {
        using (Stream stream = File.OpenRead(filename))
        {
            using (new EditContext(item, SecurityCheck.Disable))
            {
                MediaManager.GetMedia(item).SetStream(stream, Path.GetExtension(filename));
                if (item.InnerItem.ContainsField("Optimized"))
                    ((CheckboxField) item.InnerItem.Fields["Optimized"]).Checked = true;
            }
        }
    }
    return true;
}
 
private static void Cleanup(string src, string dst)
{
    if (File.Exists(src))
        File.Delete(src);
    if (File.Exists(dst))
        File.Delete(dst);
    string dstTemp = dst.Remove(dst.LastIndexOf('.'));
    if (File.Exists(dstTemp))
        File.Delete(dstTemp);
}

So, we’re saving runtime performance on the delivery server by shifting it to a performance hit during publish time. Experimentation has shown it can take up to a second or two to compress large PNG images – so if you’re publishing a lot of images at once be prepared to add quite a bit of time to the process. This is where the “Optimized” field comes in to play. You can add this field to the media item template, leave it unchecked in the master database, and then when you publish, the unchecked value will get copied to the target database. When the optimizer runs, it will flip that value, and not need to re-run the 3rd-party tool until you republish that item (clearing the checkbox). Alternately, the content author could use the “Optimized” field in the master database to indicate the image was properly formatted for web before it was uploaded to the media library.

This gives you another option for automatically optimizing images in your Sitecore solution. Depending on your needs, you could evaluate this method side-by-side with the modules mentioned above. 

Sitecore development, Sitecore custom code

Comments

Add a Comment

*
*

Please confirm you are human by typing the text you see in this image:

Kam Figy said: 11/20/2014 at 7:41 PM

This approach is great, as long as you're not using Sitecore's automatic image resizing to create different sizes of your assets dynamically - because Sitecore's resizer converts the image back to a bitmap during resizing, the optimizations are lost in the resize.

It's definitely lighter on the first-hit end user side than Dianoga, though.

James said: 11/12/2015 at 10:53 AM

Hi Doug, do you have your code available to download from Git? Or can you supply the missing Optimize method from the above helper?