Friday 8 January 2016

Using Sitecore to manage external images

We have just gone live with a new site where a large number of the product images are hosted by a third party.  In fact the whole product data set is imported and contains image items that just have a URL to the images themselves.

The issue with these images was that they are massive in size and cannot be loaded into the web site without some sort of re-sizing and compression.  Unfortunately the image hosting site, does not have any functionality that would allow us to resize the image at source - i.e. it is not a DAM.

We wanted to leverage Sitecore to do this for two reasons:
  1. Sitecore has a very robust re-sizing mechanism that is easy to invoke and is familiar to all Sitecore developers.

  2. We wanted the images to be cached on the server - which Sitecore does for all media items by default.
To get Sitecore to process the image, we need to leverage the existing media library functionality.  This is done by using a media library item as the hook or kicking off point for the processor.  We then use that media item's URL where we need to display an external image, and we add some additional parameters to the URL that can then be picked up by a custom image processor.  

In our case we have an image in the media library called CustomContent.  

We have also designed our processor so that if there is no external image, the CustomContent image will be shown instead.



Just to give you an idea of what we are talking about here is an image URL:
/~/media/images/CustomContent.ashx?externalImage=https://lh3.googleusercontent.com/-5Xg3L1zD6hA/VUeJO6REHBI/AAAAAAAAAgQ/4Zi8G2MyIIY/s1000-fcrop64=1,27da3a5fe74dc9dc/IMG_0030.JPG&h=180&w=180&id=3f36195e-8dbe-45c5-8ee0-18c2ab130eef&hash=7FC9C5E5044978E99335A9BF4FD3E8B56237E17B

Important points to note:
  1. Extra Parameters including externalImage and id

  2. The image URL can point to any external image
The hash in the URL is added by Sitecore for media request protection, this is a new feature in Sitecore 7.5 and above.  If enabled you will need to register your parameters in the Sitecore config file: Sitecore.Media.RequestProtection.config, and generate the hash when building your image URL.  This means that the URL will then become tamper proof.

In your URL helper / resolver do something like:

HashingUtils.ProtectAssetUrl(
  string.Format("/~/media/images/CustomContent.ashx?externalImage={0}&h={1}&w={2}&id={3}", url,
  source.Height, source.Width, source.Item.ID.ToGuid().ToString("D")));

The HashingUtils class can be found by using Sitecore.Resources.Media.

You will need to add your parameters to the protectedMediaQueryParameters section of the Sitecore.Media.RequestProtection.config file, as follows:


 
So now for the code:

First off the processor has an entry method called Process, in here we accept a single argument parameter of GetMediaStreamPipelineArgs, which we can then examine for our custom parameters:

public void Process(GetMediaStreamPipelineArgs args)
{
    if (args.Options.CustomOptions["externalImage"] != null || 
        args.Options.CustomOptions["id"] != null)
    {
             GetExternalImage(args);
    }
} 

If we find the custom parameters, we can call the main piece of functionality which will get the image re-size and/or compress it and render it through the media request pipeline.

private void GetExternalImage(GetMediaStreamPipelineArgs args)
{
    try
    {
         var client = new WebClient();
         var result = client.OpenRead(new 
            Uri(args.Options.CustomOptions["externalImage"]));

         if (result == null)
         {
                args.AbortPipeline();
                return;
         }
         var bm = CreateBitMapFromStream(result);
         var quantizer = new WuQuantizer();
        if (args.Options.Width != 0 && args.Options.Height != 0)
         {
                 using (var newImage = ScaleImage(bm, args.Options.Width, 
                           args.Options.Height))
                 {
                     using (var quantized = quantizer.QuantizeImage(new Bitmap(newImage)))
                     {
                         var stream = new MemoryStream();
                         quantized.Save(stream, ImageFormat.Png);
                         args.OutputStream = new MediaStream(stream, "png", 
                              args.MediaData.MediaItem);
                     }
                 }
          }
          else
          {
                  using (var quantized = quantizer.QuantizeImage(bm))
                  {
                      var stream = new MemoryStream();
                      quantized.Save(stream, ImageFormat.Png);
                      args.OutputStream = new MediaStream(stream, "png", 
                          args.MediaData.MediaItem);
                  }
          }
       }
       catch (Exception ex)
       {
           Log.Error(ex.Message, ex, this);
           args.AbortPipeline();
       }
}

We use WuQuantizer to do the png compression which does a better job than .net's native compression.

https://www.nuget.org/packages/nQuant/ 

Just for completeness here are the two private methods referenced in the above:

private static Bitmap CreateBitMapFromStream(Stream result)
{
    byte[] imageBytes;
    using (var ms = new MemoryStream())
    {
        var count = 0;
        do
        {
            var buf = new byte[1024];
            count = result.Read(buf, 0, 1024);
            ms.Write(buf, 0, count);
        } while (result.CanRead && count > 0);
        imageBytes = ms.ToArray();
    }

    // Now you can use the returned stream to set the image source too
    var image = new MemoryStream(imageBytes);
    var bm = (Bitmap) Image.FromStream(image);
    return bm;
}

public static Image ScaleImage(Image image, int maxWidth, int maxHeight)
{
    var ratioX = (double) maxWidth/image.Width;
    var ratioY = (double) maxHeight/image.Height;
    var ratio = Math.Min(ratioX, ratioY);

    var newWidth = (int) (image.Width*ratio);
    var newHeight = (int) (image.Height*ratio);

    var newImage = new Bitmap(newWidth, newHeight);

    using (var graphics = Graphics.FromImage(newImage))
        graphics.DrawImage(image, 0, 0, newWidth, newHeight);

    return newImage;
}

Finally implementing the processor, we need to add our processor to the getMediaStream pipeline, and we do that we creating the following config file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getMediaStream>
        <processor patch:after="processor[@type='Sitecore.Resources.Media.GrayscaleProcessor, Sitecore.Kernel']"
            type="MyProject.Library.Processors.CustomImageProcessor, MyProject.Library" />
      </getMediaStream>
    </pipelines>
  </sitecore>
</configuration>

2 comments:

  1. @Sitecore.Resources.Media.HashingUtils.ProtectAssetUrl("/~/media/XXX_Intranet/Profile/photo.ashx?userName={{contact.imageUrl}}")"

    HashingUtils() is called on server side and {{contact.imageUrl}} is getting asssigned on client side. this is giving same hash values for all the images. how can i get rid of this.

    ReplyDelete
    Replies
    1. Try creating the entire hashed url server side, and then pass that to your front end.

      Delete