Introduction
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 width
and 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 HttpResponseBase
class.
Listing 1: HttpResponseExtensionMethod
using System.Web;
using System.Web.Caching;
namespace ImageControllerInMvc3.Models
{
public static class HttpResponseExtensionMethods
{
public static void SetDefaultImageHeaders(this HttpResponseBase response)
{
response.Cache.SetCacheability(HttpCacheability.Public);
response.Cache.SetExpires(Cache.NoAbsoluteExpiration);
response.Cache.SetLastModifiedFromFileDependencies();
}
}
}
The 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
namespace ImageControllerInMvc3.Models
{
public static class FilesystemExtensionMethods
{
public static string FileExtensionForContentType(this string fileName)
{
var pieces = fileName.Split('.');
var extension = pieces.Length > 1 ? pieces[pieces.Length - 1]
: string.Empty;
return (extension.ToLower() == "jpg") ? "jpeg" : extension;
}
}
}
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
using System.Web;
using System.Web.Mvc;
namespace ImageControllerInMvc3.Models
{
public class ImageFileResult : FilePathResult
{
public ImageFileResult(string fileName) :
base(fileName, string.Format("image/{0}",
fileName.FileExtensionForContentType()))
{
}
protected override void WriteFile(HttpResponseBase response)
{
response.SetDefaultImageHeaders();
base.WriteFile(response);
}
}
}
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 ImageFileResult
class.
Listing 4: ImagesController.cs
using System.Web.Mvc;
using ImageControllerInMvc3.Models;
namespace ImageControllerInMvc3.Controllers
{
public class ImagesController : Controller
{
public ActionResult Render(string file)
{
var fullFilePath = this.getFullFilePath(file);
if this.imageFileNotAvailable(fullFilePath))
return this.instantiate404ErrorResult(file);
return new ImageFileResult(fullFilePath);
}
private string getFullFilePath(string file)
{
return string.Format("{0}/{1}", Server.MapPath("~/Content/Images"), file);
}
private bool imageFileNotAvailable(string fullFilePath)
{
return System.IO.File.Exists(fullFilePath);
}
private HttpNotFoundResult instantiate404ErrorResult(string file)
{
return new HttpNotFoundResult(
string.Format("The file {0} does not exist.", file));
}
}
}
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.
The 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:
<img src="/Images/SomeImage.jpg" alt="The Image" />
To support this structure I need to register a new route in the Globals.asax.cs file:
routes.MapRoute(
"RenderImage", "Images/{file}",
new { controller = "Images", action = "Render", file = "" }
);
Now I can have my UI layer make calls to images and pass in the desired dimensions to get a resized image back:
<img alt="The Image" src="/Images/200/200/SomeImage.jpg">
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).
public ActionResult RenderWithResizeAndWatermark(int width, int height, string file)
{
var fullFilePath = this.getFullFilePath(file);
if (this.imageFileNotAvailable(fullFilePath))
return this.instantiate404ErrorResult(file);
var resizeSettings = this.instantiateResizeSettings(width, height);
var resizedImage = ImageBuilder.Current.Build(fullFilePath, resizeSettings);
var watermarkFullFilePath = this.getFullFilePath("Watermark.png");
resizedImage = this.addWatermark(resizedImage,
watermarkFullFilePath, new Point(0, 0));
return new DynamicImageResult(file, resizedImage.ToByteArray());
}
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 resizedImage
variable.
private Bitmap addWatermark(Bitmap image, string watermarkFullFilePath,
Point watermarkLocation)
{
using (var watermark = Image.FromFile(watermarkFullFilePath))
{
var watermarkToUse = watermark;
if (watermark.Width > image.Width || watermark.Height > image.Height)
{
var resizeSettings = this.instantiateResizeSettings(image.Width,
image.Height);
watermarkToUse = ImageBuilder.Current.Build(watermarkFullFilePath,
resizeSettings);
}
using (var graphics = Graphics.FromImage(image))
{
graphics.DrawImage(watermarkToUse, watermarkLocation);
}
}
return image;
}
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 Bitmap
object.
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 Global.asax.cs
file:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"RenderImage", "Images/{file}",
new { controller = "Images", action = "Render", file = "" }
);
routes.MapRoute(
"RenderImageWithResizeAndWatermark", "Images/{width}/{height}/w/{file}",
new { controller = "Images", action = "RenderWithResizeAndWatermark",
width = "", height = "", file = "" }
);
routes.MapRoute(
"RenderImageWithResize", "Images/{width}/{height}/{file}",
new { controller = "Images", action = "RenderWithResize", width = "",
height = "", file = "" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Now the UI can reference resized images with watermarks like so:
<img alt="The Image" src="/Images/400/300/w/SomeImage.jpg">
Summary
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.