Monday, August 9, 2010

Scale images in web applications with examples on ASP.Net MVC

Before to start I would like to save a few words. Initially I wanted to write post about scalable image action result for ASP.Net MVC. But after that I realized that it will be more useful if I will expand the post and show how to make similar tasks using several ways (client-side and server-side) . So this post will not be ASP.Net MVC  specific – it will also contain information general for web development. This is the end of little preface, lets start.

Most of web developers in their practice worked with images. And also many of developers encountered with problem of correct scaling of the images in their web sites. But what is the problem? Suppose that we have e-commerce application and want to display some products on the web site. Also there is a list of categories in left so users are able to choose products of what category they want to see. For example go to http://www.amazon.com and select some category. Probably you will see something like this (I selected more or less neutral Kids toys category, so please don’t treat it as advertisement :) ):

image

I intentionally added arrows which show size of the products. As you can see all images have approximately the same width and height. It makes overall page structure more esthetic and user friendly. But in real life product pictures may have very different proportions, and it is very is insufficient for business if company staff responsible for adding content in application will arrange all product images manually. So how this process can be automated?

In order to achieve such result you can use several approaches:

  • Static images – images are resized during upload and stored on external image server or database.
  • Client side. It means image size are arranged on client side using javascript (e.g. jQuery scale plugin) or css (using width, height, max-width, max-height properties).
  • Server side. Images are scaled dynamically on server (using query string parameters for example which specify width and height).

In this post I will briefly overview all mentioned methods. But at first lets summarize our task. We have different product images (with different aspect ratio) and we want to display product icons in similar divs for example (all parent divs have the same size, but their size is less then size of real product image) with preserving image proportions:

image

Algorithm is the following. We need to determine minimal of two ratios and then scale image using this coefficient:

image

As said above we assume that image size is greater then div size (k < 1), i.e. we consider size decreasing. This algorithm preserves aspect ratio (because width and height are multiplied on the same coefficient) and allows to adjust image size to size of parent div (as we used minimal of two ratios it ensures that image will fit div).

Ok now lets return to the methods which can be used to implement this algorithm. How it is solved on Amazon? Lets just see properties of the images using firebug for example. We will see that it is simple images located on one of the Amazon’s image server (e.g. http://ecx.images-amazon.com/images/I/41zku-TbXmL._SL120_.jpg). Also real image’s width and height are specified in <img /> attributes – this is double check in order to ensure that it will be displayed properly in all browsers:

   1: <img height="95" width="120" src="..."/>

So this is one of the approaches – every time when new product is added into catalog, its icon is automatically generated in uploaded into image server (keeping in mind their turnovers I don’t think Amazon staff makes it manually).

This is pretty good approach. It has minimal impact on performance as most of current browsers cache static images very well. Also we shouldn’t worry about images scaling as they were already scaled for proper size during product creation. But there is also another side: it is not so flexible and adaptable for layout changing. What if we will redesign of pages and will need to change images height or width? One of the solution – we can use css styles to adjust images size, i.e. specify css class for images which will keep original proportions. E.g. if original proportions were 10:8 we can specify:

   1: .icon
   2: {
   3:     width: 100px;
   4:     height: 80px;
   5: }

(Scale images using em units is out of the scope of this article). But this approach will work well when you need to decrease image size (most of browsers minimize pictures with acceptable quality):

image

For tests I used IE 8. Opera 10.6, FF 3.0.19, Chrome 5.0.375.70. As you can see almost all browsers minimized picture pretty well (IE quality is the not very good although). But what happens if you will increase image size using css. Lets see:

image

image

Here we slightly increased images using css – and quality was lost in all browsers (and it becomes worst when image size is increased). You can see typical smoothness in all pictures (from all images above I think Chrome has best quality). I think it is not very reasonable to consciously make quality worse if we have picture of big size which can be decreased (as we saw size decreasing works better). What options do we have for such situation? One options – we can resize all images to new static size again which will not save us from similar problems in future. So it is not very good approach if we have millions of pictures and layout is changed. Another approach is to make pictures scaling dynamic.

We considered approach with statically resized images. Another possible way to scale images – is to use javascript and css (client side scaling). With this approach images will not be resized during upload – they will have their original size. With this method no additional work is required – company staff who add content to web site just select product images from any sources (it can be DVD catalog or web site provided by supplier) and it will be stored as is for example in web site content database or file system. Our task – is to show all images which have different size and aspect ratio in more or less similar proportions using javascript and css.

When you use javascript (e.g. jquery scale plugin mentioned above) – at first page load users may see image size leap as browsers will load original image size and then javascript will resize it. On the next page loads browsers will show images from cache – but I think that you don’t want to show size leap to your users each time when you added new product in catalog. Also javascript approach is not very reliable. I’m not consider situation when client browser doesn’t support javascript or has javascript disabled (such users will have problems not only with your applications in this case :) ). If javascript will fail because of some reason – users will see very bad looking page with broken layout as all product images will have their original size.

Another way to implement algorithm mentioned above is to use css. You can specify static width in css and height: auto (or vice versa):

   1: .icon
   2: {
   3:     width: 100px;
   4:     height: auto;
   5: }

In this case you will have all your images with the same width, but height will be computed using original proportions:

image

Which is apparently not what we want because as described above we want to fit product images into parent divs with the same aspect ratio. There are useful css properties which we can apply: max-width and max-height. Lets see result if we will apply the following css class to our image:

   1:  
   2: .icon
   3: {
   4:     max-width: 100px;
   5:     max-height: 80px;
   6: }

image

And it works well in all browsers I checked (IE, Opera, FF, Chrome). Ok, this method is working and can be used on your site. But what if you will also need scaled images when css is not supported, e.g. you want to generate pdf or image file dynamically with catalog of your products. For this case we need to have scaled images with predefined size in order to add them in our graphic catalog. In order to do it we should consider another method: server side scaling.

First of all I will prepare some infrastructure for our example. I will assume that images are stored in content database (ProductImage table with foreign key on Product table) which is mapped on the following model (e.g. by Fluent NHibernate):

   1: public class ProductImage : PersistentObject<int>
   2: {
   3:     public virtual byte[] Image { get; set; }
   4:     public virtual string FileName { get; set; }
   5:     public virtual Product Product { get; set; }
   6: }

(PersistentObject<T> is layer super class which contains T Id property and overrides Equal method in order to equal objects by Id). Using DDD approach we will have separate repository for our images:

   1: public interface IProductImageRepository : IRepository<ProductImage, int>
   2: {
   3: }

(IRepository<TEntity, TKey>is also base interface for repositories which is instantiated by model type and key type). Ok we have a model. Now we need custom action result – ImageResult in order to be able to return images from controllers (see http://blog.maartenballiauw.be/post/2008/05/ASPNET-MVC-custom-ActionResult.aspx):

   1: public class ImageResult : ActionResult
   2: {
   3:     public byte[] Image { get; set; }
   4:     public string FileName { get; set; }
   5:  
   6:     public override void ExecuteResult(ControllerContext context)
   7:     {
   8:         if (Image == null)
   9:         {
  10:             throw new ArgumentNullException("Image");
  11:         }
  12:         if (string.IsNullOrEmpty(this.FileName))
  13:         {
  14:             throw new ArgumentNullException("FileName");
  15:         }
  16:  
  17:         // output
  18:         context.HttpContext.Response.Clear();
  19:  
  20:         var imageFormat = getContentType(this.FileName);
  21:         context.HttpContext.Response.ContentType = imageFormat;
  22:  
  23:         using (var ms = new MemoryStream(this.Image))
  24:         {
  25:             ms.WriteTo(context.HttpContext.Response.OutputStream);
  26:         }
  27:         context.HttpContext.Response.End();
  28:     }
  29:  
  30:     protected string getContentType(string name)
  31:     {
  32:         var defaultType = "image/gif";
  33:         if (string.IsNullOrEmpty(name) || !Path.HasExtension(name))
  34:         {
  35:             return defaultType;
  36:         }
  37:         string ext = Path.GetExtension(name);
  38:         if (string.IsNullOrEmpty(ext))
  39:         {
  40:             return defaultType;
  41:         }
  42:  
  43:         if (ext.Equals(".bmp", StringComparison.InvariantCultureIgnoreCase))
  44:             return "image/bmp";
  45:         if (ext.Equals(".gif", StringComparison.InvariantCultureIgnoreCase))
  46:             return "image/gif";
  47:         if (ext.Equals(".vnd.microsoft.icon", StringComparison.InvariantCultureIgnoreCase))
  48:             return "image/vnd.microsoft.icon";
  49:         if (ext.Equals(".jpeg", StringComparison.InvariantCultureIgnoreCase))
  50:             return "image/jpeg";
  51:         if (ext.Equals(".jpg", StringComparison.InvariantCultureIgnoreCase))
  52:             return "image/jpeg";
  53:         if (ext.Equals(".png", StringComparison.InvariantCultureIgnoreCase))
  54:             return "image/png";
  55:         if (ext.Equals(".tiff", StringComparison.InvariantCultureIgnoreCase))
  56:             return "image/tiff";
  57:         if (ext.Equals(".wmf", StringComparison.InvariantCultureIgnoreCase))
  58:             return "image/wmf";
  59:  
  60:         return defaultType;
  61:     }
  62: }

In order to display ImageResult on our views we can use ImageExtensions from MVC futures, but it uses non strongly-typed approach. Magic strings approach is not very good, instead it is better to have possibility to write something like this:

   1: <%
   1: = Html.Image<ProductImageController>(c => c.ViewResized(this.Model[i].Id, 215, 250))
%>

I used the same idea as in post mentioned above and added several overridden methods for convenience (including possibility to specify attributes using anonymous type):

   1: public static class ImageResultHelper
   2: {
   3:     public static string Image<T>(this HtmlHelper helper,
   4:         Expression<Action<T>> action, int? width, int? height)
   5:         where T : Controller
   6:     { ... }
   7:  
   8:     public static string Image<T>(this HtmlHelper helper,
   9:         Expression<Action<T>> action, int? width, int? height, string alt)
  10:         where T : Controller
  11:     { ... }
  12:  
  13:     public static string Image<T>(this HtmlHelper helper,
  14:         Expression<Action<T>> action, string id, string alt, string @class)
  15:         where T : Controller
  16:     { ... }
  17:  
  18:     public static string Image<T>(this HtmlHelper helper,
  19:         Expression<Action<T>> action, int? width, int? height, string alt, string @class)
  20:         where T : Controller
  21:     { ... }
  22:  
  23:     public static string Image<T>(this HtmlHelper helper,
  24:         Expression<Action<T>> action, string alt, string @class)
  25:         where T : Controller
  26:     { ... }
  27:  
  28:     public static string Image<T>(this HtmlHelper helper,
  29:         Expression<Action<T>> action, string id, int? width, int? height,
  30:         string alt, string @class)
  31:         where T : Controller
  32:     { ... }
  33:  
  34:     public static string Image<T>(this HtmlHelper helper,
  35:         Expression<Action<T>> action, string id, int? width, int? height,
  36:         string alt, string @class, object attrs)
  37:         where T : Controller
  38:     {
  39:         string url = helper.BuildUrlFromExpression<T>(action);
  40:  
  41:         var sb = new StringBuilder();
  42:  
  43:         if (!string.IsNullOrEmpty(url))
  44:         {
  45:             sb.AppendFormat("src=\"{0}\" ", url);
  46:         }
  47:         if (!string.IsNullOrEmpty(id))
  48:         {
  49:             sb.AppendFormat("id=\"{0}\" ", id);
  50:         }
  51:         if (width != null)
  52:         {
  53:             sb.AppendFormat("width=\"{0}\" ", width.Value);
  54:         }
  55:         if (height != null)
  56:         {
  57:             sb.AppendFormat("height=\"{0}\" ", height.Value);
  58:         }
  59:         if (!string.IsNullOrEmpty(alt))
  60:         {
  61:             sb.AppendFormat("alt=\"{0}\" ", alt);
  62:         }
  63:         if (!string.IsNullOrEmpty(@class))
  64:         {
  65:             sb.AppendFormat("class=\"{0}\" ", @class);
  66:         }
  67:  
  68:         addAttributes(sb, attrs);
  69:  
  70:         return string.Format("<img {0} />", sb);
  71:     }
  72:  
  73:     private static void addAttributes(StringBuilder sb, object attrs)
  74:     {
  75:         var routeValues = new RouteValueDictionary(attrs);
  76:         foreach (var kv in routeValues)
  77:         {
  78:             sb.AppendFormat("{0}=\"{1}\" ", kv.Key, kv.Value);
  79:         }
  80:     }
  81: }

We could use helper class TagBuilder from MVC futures, but for our example I decided to use simple StringBuilder in order to keep it independent.

Now we have a model and infrastructure for displaying. Lets see how we can define ProductImageController:

   1: public class ProductImageController : Controller
   2: {
   3:     private IProductImageRepository productImageRepository;
   4:  
   5:     public ProductImageController(IProductImageRepository productImageRepository)
   6:     {
   7:         this.productImageRepository = productImageRepository;
   8:     }
   9:  
  10:     [HttpGet]
  11:     public ActionResult View(int id)
  12:     {
  13:         var productImage = this.productImageRepository.GetById(id);
  14:         if (productImage == null)
  15:         {
  16:             return new EmptyResult();
  17:         }
  18:  
  19:         return new ImageResult { FileName = productImage.FileName, Image = productImage.Image };
  20:     }
  21:  
  22:     [HttpGet]
  23:     public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
  24:     {
  25:         ...
  26:     }
  27: }

As you can see ProductImageController  has 2 methods: View and ViewResized. 1st method just retrieves image from database by id and returns ImageResult (custom action result, see below). 2nd method ViewResized returns scaled image by using specified maxWidth and maxHeight parameters.

In order to implement ViewResized method we can use approach described here:

   1: [HttpGet]
   2: public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
   3: {
   4:     var productImage = this.productImageRepository.GetById(id);
   5:     if (productImage == null)
   6:     {
   7:         return new EmptyResult();
   8:     }
   9:  
  10:     return this.getResizedImageResult(productImage.FileName,
  11:         productImage.Image, maxWidth, maxHeight);
  12: }
  13:  
  14: private ActionResult getResizedImageResult(string fileName, byte[] image,
  15:     int maxWidth, int maxHeight)
  16: {
  17:     using (var ms = new MemoryStream(image))
  18:     {
  19:         using (var img = Image.FromStream(ms))
  20:         {
  21:             var originalSize = img.Size;
  22:             if (originalSize.Width <= maxWidth && originalSize.Height <= maxHeight)
  23:             {
  24:                 return new ImageResult { FileName = fileName, Image = image };
  25:             }
  26:             var widthtRatio = (double)maxWidth / originalSize.Width;
  27:             var heightRatio = (double)maxHeight/originalSize.Height;
  28:             var ratio = Math.Min(widthtRatio, heightRatio);
  29:             var newSize = new Size((int)(originalSize.Width*ratio),
  30:                 (int)(originalSize.Height*ratio));
  31:  
  32:             using (var btm = new Bitmap(newSize.Width, newSize.Height))
  33:             {
  34:                 btm.MakeTransparent(Color.White);
  35:                 using (var gr = Graphics.FromImage(btm))
  36:                 {
  37:                     gr.Clear(Color.White);
  38:                     gr.CompositingQuality = CompositingQuality.HighSpeed;
  39:                     gr.InterpolationMode = InterpolationMode.Default;
  40:                     gr.DrawImage(img, 0, 0, newSize.Width, newSize.Height);
  41:                     gr.Flush();
  42:                     using (var outputMs = new MemoryStream())
  43:                     {
  44:                         btm.Save(outputMs, img.RawFormat);
  45:                         var result = new ImageResult { FileName = fileName,
  46:                             Image = outputMs.ToArray() };
  47:                         return result;
  48:                     }
  49:                 }
  50:             }
  51:         }
  52:     }
  53: }

As I said earlier we are interesting currently only in size decreasing. That’s why we have a check that specified maxWidth and maxHeight are less than original size and if not – return original image. It will also help us to prevent malicious users from requesting action with big width and height specified in order to occupy a lot of memory on web server.

Having this infrastructure we can add images into views by the following code:

   1: <%= Html.Image<ProductImageController>(c => c.ViewResized(this.Model.Id, 110, 120), "", "")%>

In real world applications you can consider adding caching into this code in order to avoid database querying each time when image is requested from view. Also if you use http modules don’t forget that modules are initialized and used per web application. It means that by default your http modules will be executed each time when any resource (including images) are requested. In order to avoid performance impact consider use separate web server or at least web application (by converting regular folder in IIS with your images into virtual folder with separate web.config).

This is all I wanted to say about scaling images in ASP.Net MVC applications. I also hope that this information will be useful for web developers who work with other technologies.

No comments:

Post a Comment