NickyVadera
technology

How to: Set the Aspect Ratio of an Image in Content Hub

A while ago a client had a requirement to set have the aspect ratio of each image set as a metadata property, including whether it was portrait, landscape, or square. Let's go through how this was achieved.

Schema Changes

The first thing that we need to do, is adjuect the schema to create properties on the M.Asset definition to store the required values. In this case, aspect ratio and orientation, with orientation implemented as an option list.

  1. In Manage > Option Lists, create a new option list e.g. NVN.AspectOrientation
  2. Create values within the option list for Portrait, Landscape, and Square, making note of their identifiers.
  3. In Manage > Schema, add a new option list property to M.Asset, selecting the new option list type. Call it something like Orientation
  4. Create a new decimal property in M.Asset. Call it something like AspectRatio

Now that the set up is done, we can move on to the logic.

Script

Next, we are going to need a media processing script. Go to Manage > Scripts and create a new script, ensure that it's type is set to Metadata Processing, and set the content to the following:

set-image-orientation.csx
using Newtonsoft.Json.Linq;
using Stylelabs.M.Scripting.Types.V1_0.Processing.Metadata;
using Stylelabs.M.Sdk;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

const string MFILE_MASTER_FILE_RELATION_NAME = "MasterFile";
const string MASSET_ASPECT_RATIO_PROPERTY_NAME = "AspectRatio";
const string MASSET_ORIENTATION_PROPERTY_NAME = "Orientation";
const string ASPECTORIENTATION_PORTRAIT_VALUE = "NVN.AspectOrientation.Portrait";
const string ASPECTORIENTATION_LANDSCAPE_VALUE = "NVN.AspectOrientation.Landscape";
const string ASPECTORIENTATION_SQUARE_VALUE = "NVN.AspectOrientation.Square";

var IMAGE_WIDTH_PROPERTIES = new string[] {
    "Composite:ImageWidth",
    "File:ImageWidth",
    "EXIF:ImageWidth",
    "QuickTime:ImageWidth",
    "MPEG:ImageWidth",
    "PNG:ImageWidth",
    "GIF:ImageWidth",
    "Photoshop:ImageWidth",
    "RIFF:ImageWidth",
    "ASF:ImageWidth",
    "IDENTIFY:ImageWidth"
};
var IMAGE_HEIGHT_PROPERTIES = new string[] {
    "Composite:ImageHeight",
    "File:ImageHeight",
    "EXIF:ImageHeight",
    "QuickTime:ImageHeight",
    "MPEG:ImageHeight",
    "PNG:ImageHeight",
    "GIF:ImageHeight",
    "Photoshop:ImageHeight",
    "RIFF:ImageHeight",
    "ASF:ImageHeight",
    "IDENTIFY:ImageHeight"
};

var masterFileRelation = await Context.File.GetRelationAsync<IChildToManyParentsRelation>(MFILE_MASTER_FILE_RELATION_NAME);

if (!masterFileRelation.Parents.Any() || !masterFileRelation.Parents.Contains(Context.Asset.Id.Value))
    return;

var imageWidth = GetMetadataPropertyByPossibleKey<long>(Context.MetadataProperties, IMAGE_WIDTH_PROPERTIES);
var imageHeight = GetMetadataPropertyByPossibleKey<long>(Context.MetadataProperties, IMAGE_HEIGHT_PROPERTIES);

if (!imageWidth.HasValue || !imageHeight.HasValue)
{
    MClient.Logger.Info("Cannot determine either height or width from metadata. Exiting.");
    return;
}
else
{
    var aspectRatio = GetAspectRatio(imageWidth.Value, imageHeight.Value);
    var roundedAspectRatio = Math.Round(aspectRatio, 2);

    var orientation = GetOrientation(aspectRatio);

    MClient.Logger.Info($"Aspect ratio is {aspectRatio}. Setting to {roundedAspectRatio}");

    await SaveAssetAspectRatioAndOrientationAsync(Context.Asset, roundedAspectRatio, orientation);
}

T GetPropertyValueByPossibleKey<T>(IReadOnlyDictionary<string, T> propertiesDictionary, string[] possibleKeys)
{
    foreach (var possibleKey in possibleKeys)
    {
        if (propertiesDictionary.ContainsKey(possibleKey))
            return propertiesDictionary[possibleKey];
    }
    return default;
}

T? GetMetadataPropertyByPossibleKey<T>(IReadOnlyDictionary<string, JToken> propertiesDictionary, string[] possibleKeys) where T : struct
{
    var propertyValue = GetPropertyValueByPossibleKey(propertiesDictionary, possibleKeys);
    if (propertyValue == null)
        return null;
    return propertyValue.Value<T>();
}

decimal GetAspectRatio(long width, long height)
{
    try
    {
        return decimal.Divide(width, height);
    }
    catch (Exception ex)
    {
        MClient.Logger.Error($"Exception when dividing {width} by {height}. {ex.Message}");
        return 0;
    }
}

string GetOrientation(decimal aspectRatio)
{
    if (aspectRatio < 1)
        return ASPECTORIENTATION_PORTRAIT_VALUE;
    if (aspectRatio == 1)
        return ASPECTORIENTATION_SQUARE_VALUE;
    if (aspectRatio > 0)
        return ASPECTORIENTATION_LANDSCAPE_VALUE;
    return string.Empty;
}

async Task SaveAssetAspectRatioAndOrientationAsync(IEntity asset, decimal aspectRatio, string orientation)
{
    var propertyLoadOption = new PropertyLoadOption(MASSET_ASPECT_RATIO_PROPERTY_NAME, MASSET_ORIENTATION_PROPERTY_NAME);
    await asset.LoadPropertiesAsync(propertyLoadOption);

    asset.SetPropertyValue(MASSET_ASPECT_RATIO_PROPERTY_NAME, aspectRatio);
    asset.SetPropertyValue(MASSET_ORIENTATION_PROPERTY_NAME, orientation);

    await MClient.Entities.SaveAsync(asset);
}

Lines 9-13 will need to be updated with the approrivate values for your setup:

  • Set MASSET_ASPECT_RATIO_PROPERTY_NAME to the name of the decimal aspect ratio property you created in step 4 above.
  • Set MASSET_ORIENTATION_PROPERTY_NAME to the name of the option list orientation property you created in step 3 above.
  • Set ASPECTORIENTATION_PORTRAIT_VALUE, ASPECTORIENTATION_LANDSCAPE_VALUE and ASPECTORIENTATION_SQUARE_VALUE to the relevant option list value identifiers you created in step 2 above.

Once the script is enabled, it will now execute whenever a new asset is uploaded, or it's renditions are refreshed, and the aspect ratio and orientation properties will be set. Easy right? ☺