Showing posts with label Media. Show all posts
Showing posts with label Media. Show all posts

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://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ9hdJMNqLxS9LO-wqL5S2la1_D9naH77vul12s10qI8PTzCb10F0qypgWMTyU_xGXI_3jV31vziVwLPGcKmb5vajxeWXhXVmfmqpxIalraqVnpilsJHp4_V-DOzAoNVhXz6xSLUo7VuF1/,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>

Friday, 16 October 2015

Sitecore Glass doesn't work with MediaRequestProtection

There's a new feature in Sitecore 7.5, called Media Request Protection.  This is where Sitecore adds a hash to the media URL, to stop it being tampered with.  You can manage it in the Sitecore.Media.RequestProtection.config file.  

In fact if you need to add to the parameters in the Media URL, perhaps for a custom image handler; you will need to ensure that your new parameters are registered in this configuration file, otherwise they won't come through to your handler.
 
We just went live with new Sitecore 8.0 site and noticed lots of errors in the log something like: 

948 20:44:25 ERROR MediaRequestProtection: An invalid/missing hash value was encountered. The expected hash value: E43FE65CB6A819B4976A19BEFF730F204C7386DB. Media URL: /~/media/Images/Heroes/card-bikes.ashx?h=279&w=576, Referring URL: 7084 20:44:25 ERROR MediaRequestProtection: An invalid/missing hash value was encountered. The expected hash value: 098D0163B651C8F5944A0183BB29012295FA8B54. Media URL: /~/media/Images/Heroes/product-card.ashx?h=279&w=279, Referring URL: 

The errors seemed to be so frequent that it was causing memory to spike, which was impacting the site.

Also (and more importantly) what this meant was that Sitecore wasn't resizing/caching any of the images before sending them to the client.  Sitecore's own image handlers were not working.

On investigation we realized that Sitecore Glass's Mvc Helper methods like @Editable and @RenderImage were not rendering the new Media Request security hash. Doing a little digging I found: https://github.com/mikeedwards83/Glass.Mapper/issues/93 The article mentions that it is a known issue with Sitecore Glass and is fixed in Sitecore Glass version 4.0.

The options we came up with were 
  • Turn it off
  • Upgrade Sitecore Glass
  • Write our own helper method or extend the Sitecore Glass ones
In the end we opted to upgrade.