Markiemark
Nov 27, 2014
  8911
(9 votes)

Extending the ImageDescriptor attribute

We all know the ImageDescriptor attribute. You can use it in your image file model to define different sizes for you images.

For example:

[MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")]
public class ImageFile : ImageData
{
    //Small 150x200
    [ImageDescriptor(Width = 200, Height = 150)]
    public virtual Blob Small { get; set; }

    //Large 300x400
    [ImageDescriptor(Width = 400, Height = 300)]
    public virtual Blob Large { get; set; }
}

The scaling of images by using an ImageDescriptor is somewhat limited. 

For example: if your original image is 500 by 500 pixels and you want to get the Small version you will end with an image with two white borders on the side. This is because EPiServer will scale your image, while keeping the aspect ratio, until it fits entirely into the specified width and height.

Image 500x500.png     Image 400x300area.png     Image 400x300result.png

This may not be what you want. Maybe you want it to fill the entire area and cut off some of the image. Or maybe you want to fit it exactly and loose the aspect ratio. We can extent the ImageDescriptor with an extra parameter to accomplish this.

First we create a new attribute that inherits from the ImageDescriptor.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MyImageDescriptorAttribute : ImageDescriptorAttribute
{
    public ImageScaleType ScaleMethod { get; set; }
 
    public MyImageDescriptorAttribute() : this(48, 48, ImageScaleType.ScaleToFill)
    {
    }
 
    public MyImageDescriptorAttribute(int width, int height, ImageScaleType scaleMethod)
    {
        Height = height;
        Width = width;
        ScaleMethod = scaleMethod;
    }
}

And we use an Enum to specify the different scaling methods

public enum ImageScaleType
{
    ScaleToFit,
    ScaleToFill,
    Resize
}

We have 3 different scaling methods:

  • ScaleTofit
    This will scale the image by keeping its aspect ratio until both width and height will fit
    Image 400x300result.png

  • ScaleToFill
    This will scale the image by keeping its aspect ratio until either the width or the height will fit
    Image 400x300ScaleToFill.png

  • Resize
    This will simply resize the image to the specified width and height
    Image 400x300Resize.png

To implement the different ways of scaling we need to replace the current ThumbnailManager with our own.

In the Alloy site we can do that by adding:

container.For<ThumbnailManager>().Use<MyThumbnailManager>();

in the DependencyResolverInitialization class.

Our new ThumbnailManager will ook like this:

public class ExtendedThumbnailManager : ThumbnailManager
{
    private readonly BlobFactory _blobFactory;

    public ExtendedThumbnailManager(IContentRepository contentRepository, BlobFactory blobFactory, BlobResolver blobResolver)
        : base(contentRepository, blobFactory, blobResolver)
    {
        _blobFactory = blobFactory;
    }

    public override Blob CreateImageBlob(Blob sourceBlob, string propertyName, ImageDescriptorAttribute descriptorAttribute)
    {
        Validator.ThrowIfNull("sourceBlob", sourceBlob);
        Validator.ThrowIfNullOrEmpty("propertyName", propertyName);
        Validator.ThrowIfNull("descriptorAttribute", descriptorAttribute);

        var uriString = string.Format("{0}{1}_{2}{3}", new object[]
            {
                Blob.GetContainerIdentifier(sourceBlob.ID).ToString(), 
                Path.GetFileNameWithoutExtension(sourceBlob.ID.LocalPath), 
                propertyName, 
                Path.GetExtension(sourceBlob.ID.LocalPath)
            });
        var customDescriptorAttribute = descriptorAttribute as ImageScaleDescriptorAttribute;
        return customDescriptorAttribute == null
                ? CreateBlob(new Uri(uriString), sourceBlob, descriptorAttribute.Width, descriptorAttribute.Height)
                : CreateScaledBlob(new Uri(uriString), sourceBlob, customDescriptorAttribute);
    }

    private Blob CreateScaledBlob(Uri thumbnailUri, Blob blobSource, ImageScaleDescriptorAttribute imageDescriptorAttribute)
    {
        switch (imageDescriptorAttribute.ScaleMethod)
        {
            case ImageScaleType.Resize:
                var imgOperation = new ImageOperation(ImageEditorCommand.Resize, imageDescriptorAttribute.Width, imageDescriptorAttribute.Height);
                return CreateBlob(thumbnailUri, blobSource, new List<ImageOperation> {imgOperation}, MimeMapping.GetMimeMapping(blobSource.ID.LocalPath));
            case ImageScaleType.ScaleToFit:
                return CreateBlob(thumbnailUri, blobSource, imageDescriptorAttribute.Width, imageDescriptorAttribute.Height);
            default:
                var imgOperations = CreateImageOperations(blobSource, imageDescriptorAttribute.Width, imageDescriptorAttribute.Height);
                    
                return CreateBlob(thumbnailUri, blobSource, imgOperations, MimeMapping.GetMimeMapping(blobSource.ID.LocalPath));
        }
            
    }

    private IEnumerable<ImageOperation> CreateImageOperations(Blob blobSource, int width, int height)
    {
        var imgOperations = new List<ImageOperation>();
        int orgWidth;
        int orgHeight;
        using (var stream = blobSource.OpenRead())
        {
            var image = System.Drawing.Image.FromStream(stream, false);

            orgWidth = image.Width;
            orgHeight = image.Height;

            image.Dispose();
        }

        var scaleFactor = Math.Max((double)width / orgWidth, (double)height / orgHeight);

        var tempWidth = (int) (orgWidth*scaleFactor);
        var tempHeight = (int) (orgHeight*scaleFactor);
            
        imgOperations.Add(new ImageOperation(ImageEditorCommand.ResizeKeepScale, tempWidth , tempHeight));
        imgOperations.Add(new ImageOperation(ImageEditorCommand.Crop, width, height) { Top = (tempHeight - height)/2, Left = (tempWidth - width)/2});

        return imgOperations;
    }

    private Blob CreateBlob(Uri thumbnailUri, Blob blobSource, IEnumerable<ImageOperation> imgOperations, string mimeType)
    {
        byte[] buffer;
        using (Stream stream = blobSource.OpenRead())
        {
            var numArray = new byte[stream.Length];
            stream.Read(numArray, 0, (int)stream.Length);
            buffer = ImageService.RenderImage(numArray, 
                imgOperations, 
                mimeType, 1f, 50);
        }
        Blob blob = _blobFactory.GetBlob(thumbnailUri);
        using (Stream stream = blob.OpenWrite())
        {
            stream.Write(buffer, 0, buffer.Length);
            stream.Flush();
        }
        return blob;
    }
}

First we override the CreateImageBlob method. Part of the code is copied from the original CreatImageBlob method. We will add an extra check to see if the extended attribute is used. If so, we will call our own CreateScaledBlob method, else we will use the original CreateBlob method.

In the CreateScaledBlob method the Blob is scaled depending on the specified ImageScaleType.

For ImageScaleType.Resize we can use the ImageOperation ImageEditorCommand.Resize from the image library.

ImageScaleType.ScaleToFit is the normal EPiServer behavior, so we use the original CreateBlob method to do this.

For ImageScaleType.ScaleToFill, the default scaling type in this case, we have to use 2 steps which are created in the CreateImageOperations method. First we scale the image until either width or height fits the requested size, after that we cut off the borders.
The final CreateBlob statement is a copy of the original CreateBlob from the ThumbnailManager, with the only difference that you can call it with a chain of ImageOperations instead of a single one.

We can now use the new attribute in our ImageFile model:

//Tiny 75x40
[MyImageDescriptor(Width = 75, Height = 40, ScaleMethod = ImageScaleType.Resize)]
public virtual Blob Tiny { get; set; }
 
//Small 150x80
[MyImageDescriptor(Width = 150, Height = 80, ScaleMethod = ImageScaleType.ScaleToFill)]
public virtual Blob Small { get; set; }
 
//Medium 300x160
[MyImageDescriptor(Width = 300, Height = 160, ScaleMethod = ImageScaleType.ScaleToFit)]
public virtual Blob Medium { get; set; }



Nov 27, 2014

Comments

Nov 28, 2014 01:26 PM

Superb! I see immediate uses for your code in my current project... Thank you!

ainteger
ainteger Apr 1, 2015 11:40 AM

container.For().Use(); should be changed to container.For().Use();

Kirolos Gerges
Kirolos Gerges Jun 2, 2015 04:27 PM

A splendid post Mark!! This solution might be what every episerver devloper is looking for. Thank you for sharing it!

Fredrik Söderberg
Fredrik Söderberg Mar 17, 2016 02:32 PM

If you plan to use the ScaleTofit method and want to get rid of the eventually white background make sure that you save the image without background colour before you upload it to the Meda Assets Panel. Here's an example on how it can look like.

Eric
Eric Jun 11, 2018 04:35 PM

Is there an example project or code that shows how this us used?

Please login to comment.
Latest blogs
Optimizely Opal: How to Build Effective Workflow Agents

If you're building workflow agents in Optimizely Opal, this post covers how specialized agents pass context to each other, why keeping agents small...

Andre | May 20, 2026

ReviewPR: An Azure Function That Reviews Your Azure DevOps Pull Requests With Claude

A while back I wrote about an  Azure Function App for PDF creation that we use to offload PDF rendering from our Optimizely DXP site. That same...

KennyG | May 19, 2026

Accelerating Optimizely CMS and Commerce upgrades with agentic AI (Part 2 of 2)

The Real Transformation in Optimizely CMS 13: Why the Upgrade Itself Is the Easy Part. A field-tested playbook for enterprise teams moving from...

Hung Le Hoang | May 18, 2026

Is the most powerful AI model really the best value?

Artificial Intelligence is already becoming part of everyday software development. Developers now use AI tools to generate code, write documentatio...

K Khan | May 16, 2026

Optimizely London Dev Meetup 2026

Well, everyone, it's that time of the year again, and we have another London Developer meet up coming for this summer. The date is set for the 2nd ...

Scott Reed | May 15, 2026

Semantic Search - Deep Dive

Deep dive into semantic search with Optimizely Graph

Michał Mitas | May 14, 2026 |