Cognitive Services + SXA: Custom Rendering Variant field

In my previous post we utilised MS Cognitive Services to assist with cropping images smart way on the fly. The reason for that was to reuse a single HQ image in different display size scenarios (pages / devices) focused on image's centre of interest rather than creating and maintaining a manually cropped collection.

Now, as we have the service implementation in place let's integrate it with SXA. Considering the purpose described above, a really good usage example is creating a new Rendering Variant field to process the referenced image according to provided size and cropping mode.

 

1. To start, create a Rendering Variant field template and tailor it for service's purpose by adding the minimal set of values needed to utilise the previously developed service:

 

2. Then, add this newly defined field to one of the Rendering Variants containing an image to crop:

 

Now some code backing up the functionality of created items:

3. In general, what we need to do is extending parseVariantFields and renderVariantField pipelines:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <parseVariantFields>
        <processor type="Sitecore91.Foundation.SXAExtensions.Pipelines.VariantFields.SmartThumbnail.ParseSmartThumbnail, Sitecore91.Foundation.SXAExtensions" resolve="true"/>
      </parseVariantFields>
      <renderVariantField>
        <processor type="Sitecore91.Foundation.SXAExtensions.Pipelines.VariantFields.SmartThumbnail.RenderSmartThumbnail, Sitecore91.Foundation.SXAExtensions" resolve="true"/>
      </renderVariantField>
    </pipelines>
  </sitecore>
</configuration>

 

4. Then implement a new Rendering Variant field to be processed by those pipelines:

using Sitecore.Data.Items;
using Sitecore.XA.Foundation.RenderingVariants.Fields;

namespace Sitecore91.Foundation.SXAExtensions.Pipelines.VariantFields.SmartThumbnail
{
    public class SmartThumbnailVariant : RenderingVariantFieldBase
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsSmartCrop { get; set; }

        public SmartThumbnailVariant(Item variantItem) : base(variantItem) { }
    }
}

 

5. Followed by the processor responsible for parsing the Rendering Variant field:

using Sitecore.Data;
using Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.ParseVariantFields;

namespace Sitecore91.Foundation.SXAExtensions.Pipelines.VariantFields.SmartThumbnail
{
    public class ParseSmartThumbnail : ParseVariantFieldProcessor
    {
        public override ID SupportedTemplateId => Constants.RenderingVariants.SmartThumbnail.Fields.SmartThumbnailVariant;

        public override void TranslateField(ParseVariantFieldArgs args)
        {
            var variantFieldsArgs = args;

            var smartThumbnail = new SmartThumbnailVariant(args.VariantItem)
            {
                ItemName = args.VariantItem.Name,
                FieldName = args.VariantItem.Fields[Constants.RenderingVariants.SmartThumbnail.Fields.FieldName].GetValue(true),
                Width = int.TryParse(args.VariantItem.Fields[Constants.RenderingVariants.SmartThumbnail.Fields.Width].GetValue(true), out var width) ? width : 0,
                Height = int.TryParse(args.VariantItem.Fields[Constants.RenderingVariants.SmartThumbnail.Fields.Height].GetValue(true), out var height) ? height : 0,
                IsSmartCrop = int.TryParse(args.VariantItem.Fields[Constants.RenderingVariants.SmartThumbnail.Fields.IsSmartCrop].GetValue(true), out var smartCropValue) && smartCropValue == 1
            };
            variantFieldsArgs.TranslatedField = smartThumbnail;
        }
    }
}

Where Constants is a standard helper class with static IDs:

using Sitecore.Data;

namespace Sitecore91.Foundation.SXAExtensions
{
    public struct Constants
    {
        public struct RenderingVariants
        {
            public struct SmartThumbnail
            {
                public struct Fields
                {
                    public static ID SmartThumbnailVariant => new ID("{00458333-70A1-4D52-B375-62CBE13575CD}");
                    public static ID FieldName => new ID("{0B00BC72-0C1C-4A49-8C94-297E38E511E7}");
                    public static ID Width => new ID("{51A093C8-A516-4A70-8223-7B907DCB0958}");
                    public static ID Height => new ID("{6447EF7A-D84E-4CA9-9F57-7A7AC6FE87D6}");
                    public static ID IsSmartCrop => new ID("{4C4899C4-CA85-451A-9FEE-ECCD678D01FD}");
                }
            }
        }
    }
}

 

6. Finally, Rendering Variant field where the actual service call and rendering HTML preparation take place:

using System;
using System.Drawing;
using System.IO;
using System.Web.UI.HtmlControls;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Resources.Media;
using Sitecore.SecurityModel;
using Sitecore.XA.Foundation.RenderingVariants.Pipelines.RenderVariantField;
using Sitecore.XA.Foundation.Variants.Abstractions.Models;
using Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.RenderVariantField;
using Sitecore91.Foundation.CognitiveServices.ComputerVision;

namespace Sitecore91.Foundation.SXAExtensions.Pipelines.VariantFields.SmartThumbnail
{
    public class RenderSmartThumbnail : RenderRenderingVariantFieldProcessor
    {
        private readonly IComputerVisionService _computerVisionService;
        public override Type SupportedType => typeof(SmartThumbnailVariant);
        public override RendererMode RendererMode => RendererMode.Html;

        public RenderSmartThumbnail()
        {
            _computerVisionService = ServiceLocator.ServiceProvider.GetService<IComputerVisionService>();
        }

        public override void RenderField(RenderVariantFieldArgs args)
        {
            var variantField = args.VariantField as SmartThumbnailVariant;
            var imageUrl = default(string);

            if (args.Item != null && !string.IsNullOrWhiteSpace(variantField?.FieldName))
            {
                ImageField imageField = args.Item.Fields[variantField.FieldName];
                if (imageField?.MediaItem != null)
                {
                    var thumbNameSuffix = "_thumb";
                    var newItemPath = imageField.MediaItem.Paths.Path + thumbNameSuffix;
                    var mediaItem = (MediaItem) imageField.MediaItem;

                    var thumbMediaItem = mediaItem.Database.GetItem(newItemPath);
                    if (thumbMediaItem != null)
                    {
                        using (new SecurityDisabler())
                        {
                            thumbMediaItem.Delete();
                        }
                    }

                    var image = (byte[])new ImageConverter().ConvertTo(Image.FromStream(mediaItem.GetMediaStream()), typeof(byte[]));
                    var thumbnail = _computerVisionService.GetThumbnail(image, variantField.Width, variantField.Height, variantField.IsSmartCrop);

                    if (thumbnail == null)
                    {
                        return;
                    }

                    using (var memoryStream = new MemoryStream(thumbnail))
                    {
                        var mediaCreator = new MediaCreator();
                        var options = new MediaCreatorOptions
                        {
                            Versioned = false,
                            IncludeExtensionInItemName = false,
                            Database = mediaItem.Database,
                            Destination = newItemPath,
                            FileBased = false
                        };

                        using (new SecurityDisabler())
                        {
                            var newFileName = mediaItem.Name + thumbNameSuffix + "." + mediaItem.Extension;
                            thumbMediaItem = mediaCreator.CreateFromStream(memoryStream, newFileName, options);
                        }
                    }
                    imageUrl = MediaManager.GetMediaUrl(thumbMediaItem);
                }
            }

            if (string.IsNullOrWhiteSpace(imageUrl))
            {
                return;
            }

            var control = new HtmlGenericControl("img");
            control.Attributes.Add("src", imageUrl);

            args.ResultControl = control;
            args.Result = RenderControl(args.ResultControl);
        }
    }
}

 

If all gone well, we should see a smartly (considering the image centre of interest) cropped to desired size image as a part of our Rendering Variant:

 

The given example is just an outline use case that generates some simple HTML code to display the image. Naturally, it can be extended further to offer more flexibility, e.g. like the 'Responsive Image' field does.