It seems relatively straightforward to generate image thumbnails from ASP.NET using the
System.Drawing APIs but it is more difficult to find the right quality settings, to make the method secure and to ensure good performance of the system using caching. In this article, I’ll describe an end-to-end solution to build secure, good quality thumbnails and to cache them.
Images in HTML
It may seem obvious to some of you, but it really isn’t if you’re not too familiar with how a web page and its associated resources are built: a thumbnail image can’t be generated by sending the bytes of the resized image into the page’s response stream.
Actually, this is not exactly true anymore, now that most modern browsers enable the embedding of a MIME64-encoded binary resource directly into the page using the “data:” protocol.
See http://www.ietf.org/rfc/rfc2397.txt for more information.
In order to embed a resized image into a page, what you really do is put an
img tag in the HTML and point its
src attribute to a handler that serves the binary stream of the image. Here’s a simple handler that serves the image whose name is passed on the query string parameter
p (without resizing it for the moment):
Listing 1: Serving an image from a handler
Consuming the handler is as simple as writing the following
One thing to note here is that the handler starts by validating the
p parameter: user input is evil until proven otherwise, so just validate it. Here, we’re using a white list, rejecting anything that’s not a letter, a digit, an underscore or a slash. This might seem very strict, but white lists are by definition safer than black lists: if you list what you exclude, you have to know all the potential attacks, whereas if you list what you accept you only have to determine a set of harmless characters. Here, we’re allowing only JPEG files that are under the Photo subdirectory of the site. The dot character is a great example of a character we want to exclude as it could allow an attacker to get access to parent directories, but there might be others, escaped versions, etc.
What the handler is doing for the moment is simply set the content type to JPEG and write out the contents of the file directly to the response stream.
Resizing an image using System.Drawing
In order to get a thumbnail of the image, all we need to do is use the very convenient APIs that
System.Drawing provides. But first, we need to determine the dimensions of the thumbnail. To do this, we must determine if the original image has a landscape or portrait format. In both cases, the thumbnail needs to fit into a square of predetermined size. In the case of portrait, the height is this predetermined size, in the case of landscape the width is. The other dimension must be reduced with the same proportions.
Listing 2: Determining the thumbnail dimensions
There seems to be some potential here for integer overflow, but unless your images are really huge, we should be pretty safe.
The thumbnail size should be fixed in advance by the application developer from a constant or from configuration, but should not be read from another query string or user-provided parameter because that would allow for cache-flooding attacks where a malicious user sends requests for very large thumbnails or for a large number of different sizes.
An alternative to generating a smaller version of the image with the same proportions as we’re doing here is to crop the image into a square thumbnail like Flickr does. This is fairly straightforward, and I won’t go into the details of how to do this, but the code project that accompanies this article contains an alternate version of the handler,
SquareThumbnail.ashx, which does exactly that.
Once this is done, actually creating the reduced image is a matter of calling the right APIs with the right parameters. You can easily get ugly thumbnails in a line of code; we’ll do it in a little more but with great quality. In order to do that, we choose high quality bicubic as the resizing algorithm and PNG as the format for the output. The following article justifies this choice in great details:
What InterpolationMode and CompositingQuality to use when generating thumbnails via System.Drawing
Listing 3: Building the resized bitmap
Here, we’re not sending the resized stream directly to the output stream because the PNG implementation in
System.Drawing requires the use of a searchable stream, which
Response.OutputStream isn’t. That’s why we use an intermediary memory stream, which increases memory pressure, but this will be compensated by the efficient caching that we’ll introduce next.
Caching the images
Resizing an image, especially a large one using a good algorithm, is a heavy operation. You don’t want to do that over and over again every time an image is requested. The first thing we can do is add output caching to the handler:
Listing 4: Output caching
Depending on how many images you keep on the site and on the memory you have available on the web server, this might be enough. If it’s not, we have one more trick up our sleeves, but this will only work in a high or full trust environment (which may be out of the question if your web site is hosted or for security reasons) or if you compile this article’s code into a signed dll and deploy it into the GAC.
What we want to do here in addition to output caching is to cache the thumbnails on disk so that even if the output cache expires, we can still serve the disk cache version if it exists instead of resizing the image again. In order to do that, we need a location on disk where we have writing rights, preferably without having to configure ACLs. It so happens that there is such a location, and that’s the code generation directory for the application. This directory is conveniently accessible through
Writing the resized image to this directory is as simple as this:
And of course, checking the availability of a cached version and serving it is just as easy:
This is really nice as we really have three levels of cache here. First, the browser caches the thumbnails for a year so in many cases your server isn’t even hit. Second, IIS caches them in memory. And finally, the handler has its own disk cache with which to repopulate the first two whenever they both expire. This means that each picture should effectively be resized only once. The disk cache can even survive an application restart. Of course, that also means that if for whatever reason the cache needs to be refreshed, you’d need to manually go and delete the code gen directory for the application, which is slightly inconvenient but is a small price to pay for the scalability you’ll get from this method.
Emptying the disk cache can be made a lot easier by building a small handler that does it for you, without you having to determine where the code generation directory is. I’ve included such a handler,
DeleteCache.ashx, in the downloadable project, but be careful to make that handler only accessible to an administration account.
Client caching will hide the hits to the thumbnails from your web server’s statistics, so if it’s important to you to capture all stats on thumbnails you might want to change the cache policy and remove
In this article, we’ve learned how to serve high quality thumbnails in a secure and scalable way. We’ve manipulated images stored on disk, but this of course translates perfectly to database-stored images.
Bertrand Le Roy is a program manager in the ASP.NET team.
This author has published 3 articles on DotNetSlackers. View other articles or the complete profile here.
Please login to rate or to leave a comment.