Designers, man. All web developers hate them, but as soon as you try and set one on fire someone always phones the police. Spoilsports.
Among my biggest gripes is inconsistant image sizes. It may be fine to design something using 8 different image sizes spread across 4 different aspect ratios when you're sitting in front of a copy of Photoshop, but that just ain't feasible once we start dealing with dynamic content. You can resize everything on upload, but as soon as someone tweaks the design slightly you're having to reach for IrfanView and cursing. Wouldn't it be better for all concerned if the images just came out the right size? That would be cool.
Wut?
Thankfully, resizing images on demand isn't all that hard. One HttpHandler is all it takes to leave our crayon-wielding brethren free to sling around whatever image sizes they want. Here's a quick list of the features I'll want:
- The ability to resize the requested image from the querystring
- To be able to specify if the image should be clamped to the specified size (think 'Image Size' vs 'Canvas Size' in Photoshop)
- You should be able to constrain height or width
- The option of providing a default image if the specified one can't be found
- An optional cache for storing previously resized images
- A limit on the size of the cache
Implementation
First up we'll start with a custom configuration section.
<ImageHandler
imagePath="~/Resources"
missingImageName="missing_image.png"
cachingEnabled="true"
cachePath="~/Resources/Cache"
maxCacheSize="250000"
backgroundColour="#000000"
/>
Most of the attribute names should be pretty self explanatory. If they're not then I've not done my job correctly, so feel free to leave a comment calling me a dick. The code to read the following looks like this:
1: public class ImageHandlerConfiguration : ConfigurationSection
2: {
3: [ConfigurationProperty("imagePath", IsRequired = true)]
4: public string ImagePath
5: {
6: get { return this["imagePath"].ToString(); }
7: set { this["imagePath"] = value; }
8: }
9:
10: [ConfigurationProperty("missingImageName", IsRequired = false)]
11: public string MissingImageName
12: {
13: get { return this["missingImageName"].ToString(); }
14: set { this["missingImageName"] = value; }
15: }
16:
17: [ConfigurationProperty("cachePath", IsRequired = false)]
18: public string CachePath
19: {
20: get { return this["cachePath"].ToString(); }
21: set { this["cachePath"] = value; }
22: }
23:
24: [ConfigurationProperty("cachingEnabled", IsRequired = false, DefaultValue = false)]
25: public bool CachingEnabled
26: {
27: get { return Convert.ToBoolean(this["cachingEnabled"]); }
28: set { this["cachingEnabled"] = value; }
29: }
30:
31: [ConfigurationProperty("maxCacheSize", IsRequired = false, DefaultValue = -1)]
32: public int MaxCacheSize
33: {
34: get { return Convert.ToInt32(this["maxCacheSize"]); }
35: set { this["maxCacheSize"] = value; }
36: }
37:
38: [ConfigurationProperty("backgroundColour", IsRequired = false)]
39: public string BackgroundColour
40: {
41: get { return this["backgroundColour"].ToString(); }
42: set { this["backgroundColour"] = value; }
43: }
44:
45: public static ImageHandlerConfiguration GetConfig()
46: {
47: return (ImageHandlerConfiguration)ConfigurationManager.GetSection("ImageHandler");
48: }
49: }
And now the code of the HttpHandler itself:
1: public class ImageHandler : IHttpHandler
2: {
3: #region Properties
4:
5: private static ImageHandlerConfiguration Config
6: {
7: get;
8: set;
9: }
10:
11: private static Dictionary<Guid, string> MimeTypes
12: {
13: get;
14: set;
15: }
16:
17: public bool IsReusable
18: {
19: get { return false; }
20: }
21:
22: #endregion
23:
24: #region Methods
25:
26: public void ProcessRequest(HttpContext context)
27: {
28: // Load list of image encoders for MIME type lookup
29: lock (this.GetType())
30: {
31: if (MimeTypes == null)
32: Init();
33: }
34:
35: HttpRequest request = context.Request;
36: HttpResponse response = context.Response;
37:
38: int width = 0;
39: int height = 0;
40: bool clamp = true;
41: ImageFormat imageFormat = null;
42:
43: // Grab request details from the querystring
44: string filename = request.QueryString["i"];
45: string filePath = context.Server.MapPath(Config.ImagePath + "/" + filename);
46: Int32.TryParse(request.QueryString["w"], out width);
47: Int32.TryParse(request.QueryString["h"], out height);
48: clamp = String.IsNullOrEmpty(request.QueryString["c"])
49: || Convert.ToInt32(request.QueryString["c"]) != 0;
50:
51: // Put together a path for the cached file
52: string fileExtension = filename.Substring(filename.LastIndexOf('.'));
53: string cacheFilename = filename.Replace(fileExtension,
54: String.Format("_{0}-{1}-{2}{3}", width, height, clamp ? "1" : "0", fileExtension));
55: string cacheFilePath = context.Server.MapPath(Config.CachePath + "/" + cacheFilename);
56:
57: Image resizedImage = null;
58: Image originalImage = null;
59:
60: try
61: {
62: if (File.Exists(cacheFilePath)) // A cached version exists, so use that
63: {
64: resizedImage = Image.FromFile(cacheFilePath);
65: imageFormat = resizedImage.RawFormat;
66: }
67: else
68: {
69: if (File.Exists(filePath))
70: originalImage = Image.FromFile(filePath);
71: else if (!String.IsNullOrEmpty(Config.MissingImageName))
72: originalImage = Image.FromFile(context.Server.MapPath(
73: Config.ImagePath + "/" + Config.MissingImageName));
74: else
75: throw new FileNotFoundException(filePath);
76:
77: imageFormat = originalImage.RawFormat;
78: if (width == 0 && height == 0)
79: {
80: resizedImage = (Image)originalImage.Clone();
81: }
82: else
83: {
84: Size size = GetImageDimensions(originalImage, width, height);
85:
86: resizedImage = ResizeImage(originalImage, size, clamp);
87: if (Config.CachingEnabled && (Config.MaxCacheSize < 0
88: || GetDirectorySize(context.Server.MapPath(Config.CachePath)) < Config.MaxCacheSize))
89: resizedImage.Save(cacheFilePath, imageFormat);
90: }
91: }
92:
93: response.ContentType = MimeTypes[imageFormat.Guid];
94: resizedImage.Save(response.OutputStream, imageFormat);
95: }
96: finally
97: {
98: if (originalImage != null)
99: originalImage.Dispose();
100: if (resizedImage != null)
101: resizedImage.Dispose();
102: }
103: }
104:
105: private Image ResizeImage(Image source, Size targetSize, bool clamp)
106: {
107: Image resizedImage = new Bitmap(targetSize.Width, targetSize.Height, source.PixelFormat);
108:
109: using (Graphics g = Graphics.FromImage(resizedImage))
110: {
111: g.CompositingQuality = CompositingQuality.HighQuality;
112: g.InterpolationMode = InterpolationMode.HighQualityBicubic;
113: g.SmoothingMode = SmoothingMode.HighQuality;
114:
115: Rectangle rec = Rectangle.Empty;
116: if (clamp)
117: rec = new Rectangle(0, 0, targetSize.Width, targetSize.Height);
118: else
119: rec = new Rectangle((targetSize.Width - source.Width) / 2,
120: (targetSize.Height - source.Height) / 2, source.Width, source.Height);
121:
122: if (!String.IsNullOrEmpty(Config.BackgroundColour))
123: g.Clear(ColorTranslator.FromHtml(Config.BackgroundColour));
124:
125: g.DrawImage(source, rec);
126: }
127:
128: return resizedImage;
129: }
130:
131: private Size GetImageDimensions(Image sourceImage, int width, int height)
132: {
133: Size result = new Size(width, height);
134: if (width == 0) // Constrain to height
135: {
136: float scale = (float)height / sourceImage.Height;
137: result.Width = Convert.ToInt32(sourceImage.Width * scale);
138: }
139: else if (height == 0) // Constrain to width
140: {
141: float scale = (float)width / sourceImage.Width;
142: result.Height = Convert.ToInt32(sourceImage.Height * scale);
143: }
144: return result;
145: }
146:
147: private int GetDirectorySize(string path)
148: {
149: int result = 0;
150: DirectoryInfo dir = new DirectoryInfo(path);
151: foreach (FileInfo file in dir.GetFiles())
152: result += Convert.ToInt32(file.Length);
153: return result;
154: }
155:
156: private static void Init()
157: {
158: Config = ImageHandlerConfiguration.GetConfig();
159:
160: MimeTypes = new Dictionary<Guid, string>();
161: foreach (ImageCodecInfo info in ImageCodecInfo.GetImageEncoders())
162: MimeTypes.Add(info.FormatID, info.MimeType);
163: }
164:
165: #endregion
166: }
Usage
To start you need to set up your web.config. You'll want this line in your <configSections> config section:
<section name="ImageHandler" type="Colourblind.Web.ImageHandlerConfiguration, Colourblind.Web"/>
Next you'll need to hook up your image handler. Where this gets stuffed varies between different versions of IIS. Your best bet here is to get your Google on. Finally, to get a resized image you pass in the following parameters to your handler via the querystring.
i - image filename (required - must exist within the path specified in the config section)
w - width (leave blank if you want to constrain by height)
h - height (leave blank if you want to constrain by width)
c - clamp (set to zero if you wish to keep the image the original size and add borders)
Ultimately, your URL will look something like this:
/Img.aspx?i=pretty_picture.jpg&w=400&h=600&c=1
Examples
First the original, unmolested image.

And here are some examples of the image once molestation has taken place.

- Width set to 200
- Height set to 200
- Width and height both set to 400
And I'm spent
I think that just about covers it. I'll probably do a follow-up post in the future to revisit this stuff as there are already things in there that are making my fingers itch. But it'll do for now . . .