Working with images in a web application can turn from a simple task to a complexity in need of some serious attention as soon as traffic starts to grow or image assets become too vast to reasonably maintain multiple versions of each (large, medium, thumbnail). A general reference to an image in a HTML
img tag does not provide a way to control caching or additional headers like the ETag to help site speed performance, nor does it provide a true way to handle resizing (the use of the
height attributes on the
img tag to resize an image is not viable as the full size image is still delivered to the user agent). In addition, it is tightly coupled to the physical location of the image files in the file system as it is typically referencing a directory structure type source URL.
There are several scenarios in which having more control over image file delivery can be advantageous. By writing some code to return image data from a MVC 3 controller action it is possible to inject a layer of control between the HTTP request and the resulting response for an image. From there the sky's the limit. Image content can easily be resized on the fly. Images can be combined to handle scenarios like watermarking. Physical storage locations of the images can be changed without having to update all
img tags in the UI layer. It is even possible to block requests for images outside of a site's domain, thus providing a way to stop "image hijacking" by other sites.
I will walk through creating logic within an ASP.NET MVC 3 application to handle serving up images from specified URL routes. The code will support access to images in their default state, but with control over caching and ETags. From there the code will be extended to support image resizing as well as the application of a watermark prior to image data delivery.
Custom ActionResult and Extension Methods
The return of the default image data will be handled by a custom
ActionResult that will inherit from the MVC
FilePathResult class and override the
WriteFile method to inject some additional response header logic for controlling the cache settings. This cache settings logic is going to be reused by another custom
ActionResult later in the article when I cover adding support for image resizing and watermarking. As a result, I want to encapsulate that code into a single method call. I can make use of an extension method to do this. The cache policy for the HTTP response can be set via a property named Cache off of an instance of
HttpResponseBase, which happens to be the method parameter of the
FilePathResult.WriteFile method. My extension method will be built to work off of an instance of the
Listing 1: HttpResponseExtensionMethod
SetDefaultImageHeaders extension method configures the cache headers to ensure that the response has some optimization for the user agent. This method can be enhanced or tweaked down the road to handle more functionality at the header level.
Since the constructor of the
FilePathResult class (the class my custom action result will inherit from) requires the HTTP header content-type value I am going to need a way to pass in the type of image. An example of the string would be "image/png". I can do this by getting the file extension from the image file name requested. However, there is a catch. For jpeg files the string needs to be "image/jpeg" but most jpeg image files tend to have the jpg extension. I can write an extension method to extract the file extension and at the same time convert the return value to "jpeg" if the extension value is "jpg".
Listing 2: FilesystemExtensionMethods.cs
With these extension methods written I can move on to creating a custom class named
ImageFileResult with a constructor that takes in the file name and calls the constructor for the
FilePathResult class, making use of the
FileExtensionForContentType method to inject the
contentType value. The logic in the
WriteFile method consists of a call to the new extension method and then a call to the base method.
Listing 3: ImageFileResult.cs
Basic Controller Action
I will create a single controller named
ImagesController that will handle all of the image functionality. The first action method that I will need to add is one to deliver a requested image file. This method will take in an image file name, validate the file exists, and return an instance of the
Listing 4: ImagesController.cs
The private method
getFullFilePath handles building the full path to the requested file in the file system by using the
Server.MapPath method to resolve the location of the
/Content/Images directory in the application.
This method is making the assumption that all requested images are going to be stored in that location. Supporting a request of images in different directory structures would simply require a change to the Render action method signature and some routing configurations (beyond the scope of this article).
imageFileNotAvailable method handles checking that the file exists in the file system. This method can also be used to handle a request validation to ensure that only requests from the current application domain are allowed by checking the
Request.ServerVariables["HTTP_REFERER"] with a regular expression to verify that it contains the domain name of the site. The
instantiate404ErrorResult method creates a standard not found result message that can be reused by each action method that will handle image delivery.
To use the images controller I want the UI layer to be able to reference image files within an
img tag as follows:
To support this structure I need to register a new route in the Globals.asax.cs file:
From here I can start putting image files in the
/Content/Images directory and display them in my views with the example
img tag above. Each of the images will have corresponding cache headers and ETags to make the user agents happy and provide a performance boost.
Adding Resize Functionality
Rather than rolling my own image resize logic I will use an open-source library named ImageResizer (http://imageresizing.net/) that is available through the NuGet Visual Studio add-on via the main NuGet feed. It can be installed with the Manage NuGet Packages dialog window by doing a search for "imageresizer" or with the Package Manager Console using the following command:
Installing the package will add a reference to the ImageResizer.dll in the Visual Studio project and set its "Copy Local" property to true to ensure that it is deployed to the bin directory when the project is built. The library has a managed API for manipulating images that I will use to handle the image resizing. No other configuration is needed to use the library from within my controller actions.
Since the resized image data will be handled in memory and not written to the file system I will need a new custom action result for working with a
byte array. The MVC framework has a class named
FileContentResult for delivering
byte data to the response stream. This class contains the same
WriteFile method signature as the
FilePathResult class. I can create a new class named
DynamicImageResult that inherits from the
FileContentResult class, and then override the
WriteFile method in the same way as I did in the
ImageFileResult to add the header logic.
Listing 5: DynamicImageResult.cs
The constructor has a similar signature to the
ImageFileResult class, but also includes the
byte variable. The call to the base constructor passes the
byte data through and handles formatting the
contentType string off of the file name passed in. The logic in the
WriteFile method is the same as the
ImageFileResult.WriteFile method to ensure that the custom header settings are applied.
The ImageResizer library has a method for resizing an image and returning a
Bitmap object. Since the
DynamicImageResult works with a
byte object I need a method to convert the
Bitmap. I can add a new method in the
FilesystemExtensionMethods class named
ToByteArray that extends off of a
Bitmap object and does the conversion. The update to the class:
Back in the
ImagesController class I add a method named
RenderWithResize to support the dynamic image resizing. This method takes in a width, height and the file name. It does the same full file path and image available check logic as the
Render method, then handles the image resize.
instantiateResizeSettings method takes in a width and height and returns an
ImageResizer.ResizeSettings object that is part of the
ImageResizer library. The settings are configured with a query string type structure.
The settings "maxwidth" and "maxheight" are used to simulate a containing box. The resized image will not be larger than that size wide or tall. The "quality" is used on jpeg compression and 90 is a sweet spot for good quality and a bit of compression.
The call to
ImageBuilder.Current.Build takes in the full file path to the original image and the
ResizeSettings object and returns a
Bitmap of the image resized. With the image resize complete I can return an instance of the new
DynamicImageResult class, passing in the file name and the resized image as a
byte object (converted using the
ToByteArray extension method written earlier).
A new route needs to be added in the
Global.asax.cs file (after the "RenderImage" route) to get to the
RenderWithResize action method:
Now I can have my UI layer make calls to images and pass in the desired dimensions to get a resized image back:
Adding Watermark Functionality
To watermark logic will consist of resizing the requested image and then applying a watermark image to it before delivering the image data back to the response stream. I will use a new controller action method named
RenderWithResizeAndWatermark that will take in the same parameters as the
RenderWithResize method (width, height and file name).
The full path to the watermark file is created and the
addWatermark method is used to draw the watermark image on top of the resized image. The return value is set to the
watermarkLocation parameter is used to specify the x/y coordinates where the watermark image should be positioned at on the resized image. The code opens the watermark image into an
Image object, does a check on the width and height to see if it is larger than the resized image, and resizes the watermark if needed. It then draws the watermark image on top of the resized image starting at the
Point location and returns the finished product as a
There is definitely room for improvement on the watermark positioning and resize logic to better handle cases where the location may result in the watermark being clipped or the ability to keep the watermark at a smaller ratio than the resized image. However, this bit of code illustrates how to get started dynamically combining image data.
The last thing to do is to add the route to support the watermark action method. This route needs to be added after the "RenderImage" route but before the "RenderImageWithResize" route. The final version of the
RegisterRoutes method in the
Now the UI can reference resized images with watermarks like so:
Just a little bit of heavy lifting and I now have more control over my image content within my ASP.NET MVC 3 web application. I was able to remove the file path dependency from my UI, add some caching headers to make user agents happy, do some on the fly resizing and even apply a watermark. All of this can be accomplished at the application level with no need for any special IIS modules, handlers or other server side stuff.
I have been entrenched in web application development for quite a while and have traversed the syntactic jungles of PHP, classic ASP, Visual Basic, VB.NET, and ASP.NET Web Forms. However, I have found a guilty pleasure in ASP.NET MVC since its beta launch and have since refactored my web stack focus...
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.