Jan
12
2012

Optimize web page - Resizing image using http handler and custom server control

Introduction

In this (not so) short article, I am going to discuss about role of image size in web page performance and how easily we can create a custom image control and with the help of custom http handler how can we dramatically improve the performance.

We are going to follow two steps to optmize the images used on web page

  • Use HTTP handler to resize the image and pass to web page
  • Creating custom server control to take care of background processing (i.e. The control should be used as normal Image control)

Background

I know, there are many tools/packages/components available freely to take care of image optimization for your web site. Nuget packages like AspSprite are brilliant in optimizing the images. However, my focus for this article is about reducing the image size so as to reduce the bandwidth and which should work behind the scene for me. That means, I dont want to reduce image size by passing static width etc.

Article Body

To begin with, consider this scenario. I want to create a sample picture gallery web page. Where lots of images are displayed in tabular form. For demo, I am picking up few images from Windows folder and add them in web site project. After formatting the page as a table I use <asp:Image> tag to display image like this

<asp:Image runat="server" ID="Image1" 
ImageUrl="~/Images/img16.jpg" width="170px" 
Height="150px">
</asp:Image>

If you are using images carefully designed for your web site with considering size of the image to be displayed on page. Then you would have images with exact height and width as they are going to be presented. However, mostly it is not the case (Even if you are using custom created images, It is likely that you use them in different places on the page with different size)

In summary, we pass width and height attribute to image tag to determine what area that image will cover on the page. Now, consider the image tag above. I have used some of the standard wallpaper images from Windows folder to create image gallery and this is what displayed.

Now, using Firebug let us see all the requests that were sent to display the page.

It can be seen that all images used in gallery are around 1.5 MB in size. Going back to image file in web site folder and checking its properties reveal that the actual image size is 300x300.

That means, we are displaying 300x300 image by compressing image tag size as 250x180. But still the full image of 1.5 MB is being transferred over the wire.

We can of course create a smaller versions of these images using image editing tools like PhotoShop etc. but that would create a constraint of image size. Generally, developers use http handler to send resized images to browser based on width and height parameter passed to handler.

We are going to see this general apprch coupled with creating custom image control so that other developers in your project dont have go into details of calling http handler for evey image. Instead of that the custom image control will take care of it.

Creating HTTP handler for resizing image

To demonstrate this, let us create new web site project. We will come back to above example in the later part of this article.

After creating new website, right click on project in solution explorer and go to Add New Item -> Generic handler

Name the handler class as ImageResizehandler.ashx (If you are not aware about what is generic handler and how it works, Please refer this MSDN article)

Now, we need to add the code in public method ProcessRequest which will take following steps.

  • Check if width and height paramters are passed as querystring
  • If not then send original image as it is (the image path should be passed as querystring. Since, the handler will be used by our own custom control, I am not adding check to make sure whether image path is provided in query string or not.
  • If both width and height parameter are found in the querystring and are valid then resize image and store resized image into byte array.
  • send byte array with response.

Below code snippet replicates the above logic which is to be added in handler class.

<%@ WebHandler Language="C#" Class="ImageResizeHandler" %>

using System;
using System.Web;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;

public class ImageResizeHandler : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        //get image path from querystring
        string imgPath = context.Request.QueryString["imgPath"];
        //get image width to be resized from querystring
        string imgwidth = context.Request.QueryString["width"];
        //get image height to be resized from querystring
        string imgHeight = context.Request.QueryString["height"];
        //check that height and width is passed as querystring. then only image resizing will happen
        if (imgPath != string.Empty && imgHeight != string.Empty && imgwidth != string.Empty && (imgHeight!=null && imgwidth!=null))
        {
            if (!System.Web.UI.WebControls.Unit.Parse(imgwidth).IsEmpty && !System.Web.UI.WebControls.Unit.Parse(imgHeight).IsEmpty)
            {
                
                //create callback handler
                Image.GetThumbnailImageAbort myCallback = new Image.GetThumbnailImageAbort(ThumbnailCallback);
                //creat BitMap object from image path passed in querystring
                Bitmap myBitmap = new Bitmap(context.Server.MapPath(imgPath));
                //create unit object for height and width. This is to convert parameter passed in differen unit like pixel, inch into generic unit.
                System.Web.UI.WebControls.Unit widthUnit = System.Web.UI.WebControls.Unit.Parse(imgwidth);
                System.Web.UI.WebControls.Unit heightUnit = System.Web.UI.WebControls.Unit.Parse(imgHeight);
                //Resize actual image using width and height paramters passed in querystring
                Image myThumbnail = myBitmap.GetThumbnailImage(Convert.ToInt16(widthUnit.Value), Convert.ToInt16(heightUnit.Value), myCallback, IntPtr.Zero);
                //Create memory stream and save resized image into memory stream
                MemoryStream objMemoryStream = new MemoryStream();
                myThumbnail.Save(objMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
                //Declare byte array of size memory stream and read memory stream data into array
                byte[] imageData = new byte[objMemoryStream.Length];
                objMemoryStream.Position = 0;
                objMemoryStream.Read(imageData, 0, (int)objMemoryStream.Length);

                //send contents of byte array as response to client (browser)
                context.Response.BinaryWrite(imageData);                
            }
        }
        else
        {
            //if width and height was not passed as querystring, then send image as it is from path provided.
            context.Response.WriteFile(context.Server.MapPath(imgPath));
        }
    }
    

    public bool ThumbnailCallback()
    {
        return false;
    }    

}

Once we have this generic handler in our project, we can use it by adding tag simillar to this in aspx design.

<asp:Image ID="Image1" runat="server" 
ImageUrl="~/ImageResizeHandler.ashx?imgPath=~/Images/old.jpg&width=150&height=150" />

After running the page, the old.jpg file would be displayed at the place of this image tag also, by observing FireBug we can see downloaded image size is only 58 KB in size.

This is all good. But I dont want that all my developers to write a call to ImageResizeHandler.ashx handler. Instead what we will do is, build a custom image control and then you can simply use the custom image control as we use standard one and call to generic handler and image resizing will happen transparently.

Writing custom image control to use HTTP generic handler

We can either create custom control and build it as dll and use in any other project or we can simply create custom control class in App_Code directory of the project and refer it in same project. For this demo purpose we will follow later approach.

Create a new directory in same sample application where we have create generic handler and name it as App_Code. Add a new class in this folder and name it is ResizeImage.cs. The custom image control class will do following.

  • Inherit from standard Image web control class
  • Accept one more boolean properties ResizeToCompress to indicate whether to resize image or not.
  • Override Render function to amend ImageURL property to add call to ImageResizeHandler.ashx handler and pass width, height property values to it as query string.

The compete code of custom image control class looks like this

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.IO;

namespace CustomImage
{

    public class ResizeImage : System.Web.UI.WebControls.Image
    {
        private bool _resizeToCompress;
        private bool _modifyImageControl;
        #region Image

        [Category("Layout"), Bindable(true)]
        public  bool ResizeToCompress
        {
            get
            {
                return _resizeToCompress;
            }
            set
            {
                _resizeToCompress = value;
            }
        }

        #endregion

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
        }


        protected override void Render(HtmlTextWriter writer)
        {
            if (_resizeToCompress == true)
                base.ImageUrl = "ImageResizeHandler.ashx?imgpath=" + base.ImageUrl + "&height=" + base.Height.ToString() + "&width=" + base.Width.ToString();
            else
                base.ImageUrl = "ImageResizeHandler.ashx?imgpath=" + base.ImageUrl;            
            base.Render(writer);
        }

    }
}

Note: If you notice in render method above, I have hard coded name of HTTP handler class. Instead this can accepted as a separate public property and that property value can be used here to avoid hardcoding

Putting it all together

After creating the custom control class, we need to add reference to it in .aspx (or in web.config) file. To see the complete code in action and monitor comparative performance, First of all copy the HTTP hadler as well as custom control class (with App_Code directory) in our original example of image gallery.

Now, register custom control for image gallery page.

<%@ Register Namespace="CustomImage" 
TagPrefix="CustomImage" %>

And simply replace our original image tag with this

<CustomImage:ResizeImage runat="server" ID="Image1" 
ImageUrl="~/Images/img16.jpg" 
width="170px" 
Height="150px" 
ResizeToCompress="true" >
</CustomImage:ResizeImage>

Note that, for the developers in your team, only difference is to use CustomImage tag and passing ResizeToCompress attribute as true.

Finally run the web site. There would not be any difference in the display of web page. However, after monitoring requests in Firebug, it is clear that we have significantly reduced size of images being downloaded.

Conclusion

By combining HTTP handler and custom image control, we can creat transparent mechanism of resizing images as per width and height of image tag therby reducimg unnecessary response traffic. we can further improve its perfomance by adopting caching mechanism to avoid image requests once it is cached in browser.

Comments (14) -

Johannes Hansen

Good article, but I would only recommend that you roll your own handler for practice purposes...

In a production environment I suggest you use the free* ImageResizer library (http://imageresizing.net/) which does exactly the same and much more, very fast, with much better quality and compression than GetThumbnailImage. Also, it does this while being able to run in medium trust, and without falling into any common traps like memory leaks (always remember to dispose all Image instances**). Finally it avoids obvious DOS vulnerabilities like specifying a width and height of int.MaxValue, which could easily deplete your app's memory with a few rapid calls, and make it crash.

*) Some of the plugins, such as the "Performance Bundle" (which I recommend you get), cost a little bit to license, but it's well worth the cost. The "Performance Bundle" costs 99$.

**) I suggest you read the blog post ".NET Memory Leak: To dispose or not to dispose, that’s the 1 GB question" by Tess Ferrandez... Most excellent reading. (blogs.msdn.com/.../...hat-s-the-1-gb-question.aspx)

Johannes Hansen

You can take a look at news.dk a danish news site I've recently redone. On that site we use a couple of techniques for allow faster image download, for example we use the ImageResizer handler for scaling. We also have a set of 10 subdomains 0-9.img.news.dk to allow parallel download of more than 2 images at a time.

mandeep

Hi,

Nice article.

Instead of resizing image every time it is displayed to user, we can keep multiple versions of images having different dimensions, that will save us lot of processing power, memory and bandwidth.

Thanks.

KedarRKulkarni

@Mandeep
True. But I faced scenarios where height and width of images to be displayed on web page is dynamic based on user type and the image gallery itself updated regularly.
Resizing images on the go is more feasible option in such scenarios. However, in case of mostly static content, static image resizing is better.

Tun

Any other the worth free image resizing dll?

hussy22

Can you show in this same code how to crop image that has been resized and store the croped to the memory stream?

Kind Regards

Axel

1) System.Drawing is not supported on servers: "Classes within the System.Drawing.Imaging namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions." msdn.microsoft.com/.../...wing.imaging(VS.80).aspx

2) You are just trading bandwidth for CPU. Guess which is going to run out first when load increases? Same applies to GZip compression and the like.

3) As someone else already noted, your code is an open invitation to DOS attacks. Just ask for large enough sizes.

Bottom line: this approach is not supposed to work at all, it does not scale, and it's brittle.

Nathanael Jones

@Axel - While I agree that the handler presented in this article leaks memory and definitely shouldn't be used on a public stie, I have to disagree with you that dynamic image resizing is a bad idea overall. It's just extremely hard to do correctly.

Since nearly every large website I know of offers some form of dynamic image resizing, saying it doesn't scale is very hard for you to substantiate.

The http://imageresizing.net/ library, of which I am the author, allows the user to choose between using System.Drawing, WIC (Windows Imaging Components), and the FreeImage library for the image manipulation.

System.Drawing is perfectly stable if used exactly right. But since that requires avoiding 30+ pitfalls, it's quite obvious to me that Microsoft cannot afford to support it. In fact, they published an update saying exactly that.

If you're interested in the pitfalls: nathanaeljones.com/163/20-image-resizing-pitfalls/

My library offers a sophisticated, thread-safe, and very scalable disk caching plugin that can provide performance very close to that of static files. So the CPU/Bandwidth tradeoff is quite optional. Sending a too-large file to the browser provides a very poor experience and lower image quality than dealing with the problem on the server, where it should be handled.

In practice, it's actually very hard to perform a DOS attack against a server using image resizing. Images require contiguous space, which is scarce on a busy server. Image resizing may stop working under a (D)DOS attack, but it's unlikely you would be able to bring anything else down with it, because few other services require large contiguous blocks of ram. I've never heard of a successful attack. Requests for giant images simply fail, with nearly zero resource usage.

Face the facts: Dynamic image resizing is used everywhere, is easily scalable through (a) disk caching or (b) Amazon CloudFront, and (c) is much less brittle than either pre-resizing images or using grotesque client-size resizing.


Nathanael Jones

@Axel - I just realized you're probably the same guy who brought up the System.Drawing FUD on another blog. I applaud the attempt to educate people on the dangers, but it's probably better to point out specific problems (such as those listed on the pitfalls page) than quote the MSDN banner.

Here's a link to Microsoft's clarification on the issue: blogs.msdn.com/.../...does-not-supported-mean.aspx

To quote: "We don't mean they won't work in an ASP.NET application or that we are trying to cover up some known bug that occurs when you use them in a service. The bottom line is that if you call Microsoft Product Support Services regarding a problem you have using a System.Drawing.* class in a service, they will not offer free support."

Of course - as I mentioned earlier in another comment, there's no possible way Microsoft could support System.Drawing usage, it's too hard to do correctly.

Perhaps you didn't get a chance to read my response from October: www.keyvan.ms/very-high-quality-image-resizing-in-net

Adam Hey

Thanks for a good article.

Please can I suggest some better code to handle the image resizing:

public static System.Drawing.Image GenerateThumbnail(System.Drawing.Image original, int newWidth, int newHeight)
        {
            Single srcX = 0, srcY = 0;
            Single origHeight = original.Height;
            Single origWidth = original.Width;
            Single rHeight = newHeight, rWidth = newWidth, ratio = 0;
            //challenge here is to resize to a certain size without distorting the image...
            if (newHeight > 0 && newWidth == 0)  // desired height is supplied -> calculate the new width
            {
                ratio = (Convert.ToSingle(newHeight) / Convert.ToSingle(origHeight));
                rWidth = origWidth * ratio;  //get new width from the ratio of the new height to orginal height
                newWidth = Convert.ToInt32(rWidth);
            }
            else if (newWidth > 0 && newHeight == 0)  // desired width is supplied -> calculate the new height
            {
                ratio = (Convert.ToSingle(newWidth) / Convert.ToSingle(origWidth));
                rHeight = origHeight * ratio;  //get new height from the ratio of the new width to orginal width
                newHeight = Convert.ToInt32(rHeight);
            }

            //work if if cropping is necessary to retain aspect ratio
            Single OriginalAspectRatio = Convert.ToSingle(origWidth) / Convert.ToSingle(origHeight);
            Single NewAspectRatio = rWidth / rHeight;
            OriginalAspectRatio = (Single)decimal.Round(Convert.ToDecimal(OriginalAspectRatio), 4);
            NewAspectRatio = (Single)decimal.Round(Convert.ToDecimal(NewAspectRatio), 4);




            //larger ratio = wider input image -> TALLER OUTPUT IMAGE
            if (NewAspectRatio < OriginalAspectRatio)
            {
                Single _const = NewAspectRatio / OriginalAspectRatio;
                //wider to taller -> crop width -> set new srcX co-ord
                origWidth = origHeight * NewAspectRatio;
                //srcX = (original.Height - origWidth) / 2;
                srcX = (original.Width / 2) - ((original.Width / 2) * _const);
            }
            else if (NewAspectRatio > OriginalAspectRatio)
            {
                Single _const = OriginalAspectRatio / NewAspectRatio;
                //taller to wider -> crop height -> set new srcY co-ord
                origHeight = (origWidth / NewAspectRatio);
                //srcY = (original.Width - origHeight) / 2;
                srcY = (original.Height / 2) - ((original.Height / 2) * _const);

            }


            Bitmap tn = new Bitmap(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
            Graphics g = Graphics.FromImage(tn);

            g.InterpolationMode = InterpolationMode.HighQualityBicubic; //experiment with this...

            g.DrawImage(original, new Rectangle(0, 0, tn.Width, tn.Height), srcX, srcY, origWidth, origHeight, GraphicsUnit.Pixel);

            g.Dispose();

            return (System.Drawing.Image)tn;

        }

public static byte[] imageToByteArray(System.Drawing.Image imageIn)
        {
            MemoryStream ms = new MemoryStream();
            imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            return ms.ToArray();
        }

Then replace your IF statement with the following:

if (!System.Web.UI.WebControls.Unit.Parse(imgwidth).IsEmpty && !System.Web.UI.WebControls.Unit.Parse(imgHeight).IsEmpty)
            {

byte[] imageData = imageToByteArray(GenerateThumbnail(System.Drawing.Image.FromFile(context.Server.MapPath(imgPath)),newWidth, newHeight));
                
//send contents of byte array as response to client (browser)
context.Response.BinaryWrite(imageData);

}

The final image is about ten times smaller Smile sharper too...

Instant Check Mate Reviews

http://wwwinstantcheckmatecom.blog.com/

Good article, but I would only recommend that you roll your own handler for practice purposes...

Lukas Malichek

I agree with jason, however optimizing the code is also good for the search engine rankings and SEO.

http://howtodrawrealfaces.com

Jamie Tyson

Can I make a suggestion?  I think youve got something good here.  But what if you added a couple links to a page that backs up what youre saying?  Or maybe you could give us something to look at, something that would connect what youre saying to something tangible?  Just a suggestion.

japan

This is one technology that I would love to be able to use for myself. It’s definitely a cut above the rest and I can’t wait until my provider has it. Your insight was what I needed. Thanks

http://capitalistexploits.at/

About Me

You are visiting personal website of Kedar (KK)

Please go here to know more about me

Disclaimer

The opinions expressed here represent my own and not those of my past or present employers.

The concept/code provided on this site may not work as described. If you are using any code provided on this site. Then, please test it thoroughly. I shall not be responsible for any issues arising in the code. 

Month List