Sitecore media optimization with Azure Functions + Blob Storage + Magick.NET
Back to home

Sitecore media optimization with Azure Functions + Blob Storage + Magick.NET

Miguel Minoldo's picture
Miguel Minoldo

In my previous post, I've explained how to configure the Blob Storage Module on a Sitecore 9.3+ instance. The following post assumes you are already familiar with it and you've your Sitecore instance making use of the Azure blob storage provider.

In this post I'll show you how we can make use of Azure Functions (blob trigger) to optimize (compress) images on the fly, when those are uploaded to the media library, in order to gain performance and with a serverless approach.

Blog post image
Click to expand
Media Compression Flow

Media Compression Flow



About Azure Functions and Blob Trigger

Azure Functions is an event driven, compute-on-demand experience that extends the existing Azure application platform with capabilities to implement code triggered by events occurring in Azure or third party service as well as on-premises systems. Azure Functions allows developers to take action by connecting to data sources or messaging solutions thus making it easy to process and react to events. Developers can leverage Azure Functions to build HTTP-based API endpoints accessible by a wide range of applications, mobile and IoT devices. Azure Functions is scale-based and on-demand, so you pay only for the resources you consume. For more info please refer to the official MS documentation.

Blog post image
Click to expand
Azure Functions

Azure Functions



Azure Functions integrates with Azure Storage via triggers and bindings. Integrating with Blob storage allows you to build functions that react to changes in blob data as well as read and write values.

Creating the Azure Function

For building the blob storage trigger function I'll be using Visual Code, so first of all make sure you have the Azure Functions plugin for Visual Code, you can get it from the marketplace or from the extensions menu, also from the link: vscode:extension/ms-azuretools.vscode-azurefunctions.

Blog post image
Click to expand
Azure Functions Plugin

Azure Functions Plugin



Before proceeding, make sure you are logged into your Azure subscription. >az login.

  1. Create an Azure Functions project: Click on the add function icon and then select the blob trigger option, give a name to the function.
Blog post image
Click to expand

2. Choose the Blob Storage Account you are using in your Sitecore instance (myblobtestazure_STORAGE in my case).

Blog post image
Click to expand

3. Choose your blob container path (blobcontainer/{same})

Blog post image
Click to expand

4. The basics are now created and we can start working on our implementation.

Blog post image
Click to expand
Default function class

Default function class



Generated project files

The project template creates a project in your chosen language and installs required dependencies. For any language, the new project has these files:

  • host.json: Lets you configure the Functions host. These settings apply when you're running functions locally and when you're running them in Azure. For more information, see host.json reference.
  • local.settings.json: Maintains settings used when you're running functions locally. These settings are used only when you're running functions locally. For more information, see Local settings file.

Edit the local.settgins.json file to add the connection string of your blob storage:

Blog post image
Click to expand
local.settings.json

local.settings.json



The function implementation

csharp
1using System.IO;
2using Microsoft.Azure.WebJobs;
3using Microsoft.Extensions.Logging;
4using ImageMagick;
5using Microsoft.WindowsAzure.Storage.Blob;
6
7namespace SitecoreImageCompressor
8{
9 public static class CompressBlob
10 {
11 [FunctionName("CompressBlob")]
12 public static async void Run([BlobTrigger("blobcontainer/{name}", Connection = "myblobtestazure_STORAGE")] CloudBlockBlob inputBlob, ILogger log)
13 {
14 log.LogInformation($"C# Blob trigger function Processed blob\n Name:{inputBlob.Name} \n Size: {inputBlob.Properties.Length} Bytes");
15
16 if (inputBlob.Metadata.ContainsKey("Status") && inputBlob.Metadata["Status"] == "Processed")
17 {
18 log.LogInformation($"blob: {inputBlob.Name} has already been processed");
19 }
20 else
21 {
22 using (var memoryStream = new MemoryStream())
23 {
24 await inputBlob.DownloadToStreamAsync(memoryStream);
25 memoryStream.Position = 0;
26
27 var before = memoryStream.Length;
28 var optimizer = new ImageOptimizer { OptimalCompression = true, IgnoreUnsupportedFormats = true };
29
30 if (optimizer.IsSupported(memoryStream))
31 {
32 var compressionResult = optimizer.Compress(memoryStream);
33
34 if (compressionResult)
35 {
36 var after = memoryStream.Length;
37 var gain = 100 - (float)(after * 100) / before;
38
39 log.LogInformation($"Optimized {inputBlob.Name} - from: {before} to: {after} Bytes. Optimized {gain}%");
40
41 await inputBlob.UploadFromStreamAsync(memoryStream);
42 }
43 else
44 {
45 log.LogInformation($"Image {inputBlob.Name} - compression failed...");
46 }
47 }
48 else
49 {
50 var info = MagickNET.GetFormatInformation(new MagickImageInfo(memoryStream).Format);
51
52 log.LogInformation($"Image {inputBlob.Name} - the format is not supported. Compression skipped - {info.Format}");
53 }
54 }
55
56 inputBlob.Metadata.Add("Status", "Processed");
57
58 await inputBlob.SetMetadataAsync();
59 }
60 }
61 }
62}

As you can see, I'm creating and async task that will be triggered as soon as a new blob is added to the blob storage. Since we're compressing and then uploading the modified image, we've to make sure the function is not triggered multiple times. For avoiding that, I'm also updating the image metadata with a "Status = Processed".

The next step is to get the image from the CloudBlockBlob and then compress using the Magick.NET library. Please note that this library also provides a LosslessCompress method, for this implementation I choose to go with the full compression. Feel free to update and compare the results.

Nuget references

So, in order to make it working we need to install the required dependencies. Please run the following commands to install the Nuget packages:

  • dotnet add package Azure.Storage.Blobs --version 12.8.0
  • dotnet add package Magick.NET-Q16-AnyCPU --version 7.23.2
  • dotnet add package Microsoft.Azure.WebJobs.Extensions.Storage --version 3.0.10
  • dotnet add package Microsoft.Azure.WebJobs.Host.Storage --version 4.0.1
  • dotnet add package Microsoft.NET.Sdk.Functions --version 1.0.38

Test and deploy

Now we have everything in place. Let's press F5 and see if the function is compiling

Blog post image
Click to expand
Terminal output

Terminal output



We are now ready to deploy to Azure and test the blob trigger! Click on the up arrow in order to deploy to Azure, choose your subscription and go!

Blog post image
Click to expand
Azure publish

Azure publish



Check the progress in the terminal and output window:

Blog post image
Click to expand
Blog post image
Click to expand

Testing the trigger

Now we can go to the Azure portal, go to the Azure function and double check that everything is there as expected:

Blog post image
Click to expand
Azure function from the portal

Azure function from the portal



Go to the "Monitor" and click on "Logs" so we can have a look at the live stream when uploading an image to the blob storage. Now in your Sitecore instance, go to the Media Library and upload an image, this will upload the blob to the Azure Storage and the trigger will take place and compress the image.

Blog post image
Click to expand
Media Library Upload

Media Library Upload



Blog post image
Click to expand
Media Library Upload

Azure functions logs



As we can see in the logs the image got compressed, gaining almost 15%:

2021-02-23T10:21:36.894 [Information] Optimized 6bdf3e56-c6fc-488b-a7bb-eee64ce04343 - from: 81147 to: 69158 Bytes. Optimized 14.774422%

Blog post image
Click to expand
Azure Blob Storage - With the trigger enabled

Azure Blob Storage - With the trigger enabled



Blog post image
Click to expand
Azure Blob Storage - With the trigger enabled

Azure Blob Storage - With the trigger disabled



Let's check the browser for the final results

Without the trigger: the image size is 81147 bytes.

Blog post image
Click to expand

With the trigger: the image size is 69158 bytes.

Blog post image
Click to expand

I hope you find this useful, you can also get the full implementation from GitHub.

Thanks for reading!