Getting cropped image the smart way

The way to get cropped image URL with option to load original image too

I noticed that some of the content managers keep complaining about the quality and size of cropped images. If you lower down quality, file is smaller but in some cases it gets blurry with quirks which comes from compression applied to a file.

Let's face it, image cropper is not Photoshop with all options for image optimization. Therefore some of the content managers decide to crop image by them self inĀ Photoshop and then upload it.

This is cool, but the thing is they want to keep cropping functionality but only for some files, so the logic is the following:

  • Keep cropped images at high quality (90%)
  • When fetching image on the page
    • Check if original image is the same size as cropped image then return original image URL
    • Otherwise return cropped image URL

This way we ensure that content manager can upload image of the desired size, optimized with advanced options and tools and that image will be used. Otherwise, if uploaded image size is not the same as cropper sizes, then returned cropped as it is correct size.

First, let's make extension method for fetching image URL for specified crop size

Note

I believe that there is a method for fetching cropped image URL in Umbraco API, but this is something I used for quite some time, so I'll stick to it even in this example

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Umbraco.Core.Models;
namespace Umbraco.Cms.Custom.ImageCropper
{
public static class Extensions
{
public static string GetCroppedUrl(this IPublishedContent media, string cropName)
{
if (media != null)
{
string result = media.Url;
if (!string.IsNullOrWhiteSpace(Path.GetExtension(result)))
{
result = Path.Combine(Path.GetDirectoryName(result),
string.Concat(Path.GetFileNameWithoutExtension(result), "_", cropName));
result = string.Concat(result, ".jpg");
result = result.Replace("\\", "/");
                }
                return result;
            }
            return string.Empty;
        }
    }
}

    

By using Umbraco.Cms.Custom namespace, method for fetching cropped image URL will be automatically available for IPublishedContent type.

Now we can deal with above mentioned logic. Before we start writing the logic, there is one more issue we need to consider. If we try to determine image size, we would have to load image to bitmap and then fetch width and height

Size imageSize = new Bitmap(path).Size;
    

This is nice and easy but it is wrong! When you load an image into bitmap it takes a lot of memory and because you will do this for every image which has cropper property, it my crash down your website because it might start using a lot of memory if visits are to frequent.

Because of this problem we need to determine image size somehow without loading the whole file to a bitmap structure into a memory. Thsi can be done by reading only meta of the file. The following is the example I found on StackOverflow

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;

namespace Umbraco.Cms.Custom.ImageCropper
{

    public static class ImageHelper
    {
        const string errorMessage = "Could not recognise image format.";

        private static Dictionary<byte[], Func<BinaryReader, Size>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, Size>>()  
        {  
            { new byte[]{ 0x42, 0x4D }, DecodeBitmap},  
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },  
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },  
            { new byte[]{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },  
            { new byte[]{ 0xff, 0xd8 }, DecodeJfif },  
        };

        /// <summary>  
        /// Gets the dimensions of an image.  
        /// </summary>  
        /// <param name="path">The path of the image to get the dimensions of.</param>  
        /// <returns>The dimensions of the specified image.</returns>  
        /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>  
        public static Size GetDimensions(string path)
        {
            using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
            {
                try
                {
                    return GetDimensions(binaryReader);
                }
                catch (ArgumentException e)
                {
                    if (e.Message.StartsWith(errorMessage))
                    {
                        throw new ArgumentException(errorMessage, "path", e);
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
        }

        /// <summary>  
        /// Gets the dimensions of an image.  
        /// </summary>  
        /// <param name="path">The path of the image to get the dimensions of.</param>  
        /// <returns>The dimensions of the specified image.</returns>  
        /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>      
        public static Size GetDimensions(BinaryReader binaryReader)
        {
            int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;

            byte[] magicBytes = new byte[maxMagicBytesLength];

            for (int i = 0; i < maxMagicBytesLength; i += 1)
            {
                magicBytes[i] = binaryReader.ReadByte();

                foreach (var kvPair in imageFormatDecoders)
                {
                    if (magicBytes.StartsWith(kvPair.Key))
                    {
                        return kvPair.Value(binaryReader);
                    }
                }
            }

            throw new ArgumentException(errorMessage, "binaryReader");
        }

        private static bool StartsWith(this byte[] thisBytes, byte[] thatBytes)
        {
            for (int i = 0; i < thatBytes.Length; i += 1)
            {
                if (thisBytes[i] != thatBytes[i])
                {
                    return false;
                }
            }
            return true;
        }

        private static short ReadLittleEndianInt16(this BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(short)];
            for (int i = 0; i < sizeof(short); i += 1)
            {
                bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt16(bytes, 0);
        }

        private static int ReadLittleEndianInt32(this BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(int)];
            for (int i = 0; i < sizeof(int); i += 1)
            {
                bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt32(bytes, 0);
        }

        private static Size DecodeBitmap(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(16);
            int width = binaryReader.ReadInt32();
            int height = binaryReader.ReadInt32();
            return new Size(width, height);
        }

        private static Size DecodeGif(BinaryReader binaryReader)
        {
            int width = binaryReader.ReadInt16();
            int height = binaryReader.ReadInt16();
            return new Size(width, height);
        }

        private static Size DecodePng(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(8);
            int width = binaryReader.ReadLittleEndianInt32();
            int height = binaryReader.ReadLittleEndianInt32();
            return new Size(width, height);
        }

        private static Size DecodeJfif(BinaryReader binaryReader)
        {
            while (binaryReader.ReadByte() == 0xff)
            {
                byte marker = binaryReader.ReadByte();
                short chunkLength = binaryReader.ReadLittleEndianInt16();

                if (marker == 0xc0)
                {
                    binaryReader.ReadByte();

                    int height = binaryReader.ReadLittleEndianInt16();
                    int width = binaryReader.ReadLittleEndianInt16();
                    return new Size(width, height);
                }

                binaryReader.ReadBytes(chunkLength - 2);
            }

            throw new ArgumentException(errorMessage);
        }
    }

}
    

Now we have all the elements we need and we can start reflecting our logic into code. All we have to do is to extend logic in extension method GetCroppedUrl.

 public static string GetCroppedUrl(this IPublishedContent media, string cropName)
        {

            if (media != null)
            {
                HttpContext context = HttpContext.Current;
                string cropPath = media.Url;
                string cropUrl = string.Empty;
                if (!string.IsNullOrWhiteSpace(Path.GetExtension(cropPath)))
                {
                    cropPath = Path.Combine(Path.GetDirectoryName(cropPath),
                        string.Concat(Path.GetFileNameWithoutExtension(cropPath), "_", cropName));
                    cropPath = string.Concat(cropPath, ".jpg");
                    cropUrl = cropPath.Replace("\\", "/");

                    try
                    {
                        Size imageSize = ImageHelper.GetDimensions(context.Server.MapPath(media.Url));
                        Size cropSize = ImageHelper.GetDimensions(context.Server.MapPath(cropPath));

                        if (imageSize == cropSize)
                        {
                            return media.Url;
                        }
                        else
                        {
                            return cropUrl;
                        }
                    }
                    catch (Exception ex)
                    {
                        return cropUrl;
                    }


                }
                return media.Url;
            }


            return string.Empty;
        }
    
Note

Try/Catch is here to ensure that cropped image URL is returned if meta of the images could not be read

What good thing about this approach is, if you are worried about the bandwidth, you can expand condition and say if both original image size and cropped image size are the same and original image size is less than crop then return original image, otherwise return cropped image path. This way you are saving on bandwidth and providing correct dimension image to the client.

if (imageSize == cropSize && new FileInfo(context.Server.MapPath(media.Url)).Length < new FileInfo(context.Server.MapPath(cropPath)).Length)
{
    return media.Url;
}
else
{
    return cropUrl;
}
    

Keep in mind that checking size of the image is additional IO operation, so if you are not really in need to save some bandwidth don't use it.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

.NET

read more

JavaScript

read more

SQL/T-SQL

read more

PowerShell

read more

Comments for this article