Most web-based content management systems offer a variety of tools to help contributors enter text. When it comes to graphics, content contributors are usually expected to provide web-ready images to the system. This means that either editorial users needs to know about image optimisation and web image formats, or additional staff are required to make web-ready images out of raw materials. This article demonstrates a technical solution to this problem.
The raw images might be from digital cameras, from mobile phones, or from scans. These will almost always be far too large, both in pixel dimensions and file size, to be used as web images. In a typical scenario, the finished images will need to be constrained to a certain pixel size to fit into whatever templates the site uses. For example, if we look at news stories on the BBC News site, we can see that content images are always 203 pixels wide. Where they appear on the home page, they must also be 152 pixels high, but when they appear alongside the text of a news story, the height of the picture can vary to fit the content. Thumbnails are 66 pixels square. We need some form of user interface that takes a "raw" image as input and allows the non-technical user to generate an appropriately sized image that is both aesthetically pleasing, optimised for the web, and conforms to any arbitrary image dimension rules that we might like to impose. Almost always, a raw image will be improved for web use by some judicious cropping.
Many content management systems offer some degree of automation for processing images, usually confined to automatically generating thumbnails from supplied files. While this method is good for showing you what a batch of images contains, it seldom produces a satisfactory "teaser" thumbnail like the ones you might find on a site homepage. Such thumbnails require human intervention.
The solution is an ASP.NET server control that allows the end user to upload an image from the local file system, and crop, scale, and optimise it to obtain a satisfactory web image. The server control inherits from the new
<guild:WebImageMaker id="wim_main" runat="server" width="250"
BorderStyle="Solid" BorderColor="#c0c0c0" BorderWidth="1px"
Format="jpg" Quality="High" />
The key attributes here are the last six.
ImageHeight dictate the dimensions of the web image that the control will create. These properties are actually strings which allow us to enter a "*" character, indicating that we don't mind what one of the dimensions is. At least one of these two properties must evaluate to a positive integer, otherwise the control doesn't have enough information to determine the final image size.
ImageUrl is used to allow the control to render a current web image if it is being used to change an existing web image rather than create a new one.
WorkingDirectory indicates where the control will save the images it creates. Before you can run this project successfully, you'll need to create this directory and grant the process that ASP.NET is using write access to it. The working directory doesn't need to be under the web root.
Quality dictate how the control will produce the final web image.
The control looks like this in its "default" condition. No image has been uploaded yet. If the user was changing an existing image, then the existing web image (the
ImageUrl attribute) would be visible here instead of the default placeholder graphic.
The file selector is a standard
WebControls.FileUpload control. The user browses the file system to find an image file to upload, then clicks the "upload file..." button. This might take a while for a large image. Once the form is posted, the control's server-side code examines the file to check that it is an image. It also stores the raw image dimensions for later use.
For the user to crop the image, the control must render the image back out to the browser once the file is uploaded so that the user has a "canvas" to work on. However, the uploaded image will often be far too big to fit into the user's browser window, so the control needs to scale the image before rendering it back out as a canvas. This scaling operation has nothing to do with the creation of the final web image – it is solely to generate an appropriately sized canvas for the user to work on. By default, the control generates a canvas that is scaled to be 4/5 of either the width or height of the user's browser window, depending on the aspect ratio of the uploaded image. This guarantees that the image will always be visible without scrolling, regardless of the size of the user's browser window. A client-side script stores the dimensions of the browser window (the "viewport") in a hidden form field just before the form is submitted.
The control renders the canvas (and some accompanying UI) in a
DIV that is positioned to appear floating above the control. Although the rendered HTML for this
DIV is nested in the control's rendered HTML, the
DIV itself needs to occupy as much screen space as possible. We wouldn't have a usable canvas if it had to be confined within the space occupied by the control in its default setting. So a client-side script repositions the
DIVappropriately, using CSS absolute positioning and the
z-index property to take the canvas
DIV out of the normal flow of the document and allow it to appear floating above the rest of the page.
Floating above the generated canvas is the selection rectangle. This is another
DIV, with a dashed border.
A client side script responds to mouse move events, so that when the pointer is near the edges of the
DIV, the cursor changes to a resize icon, and when the pointer is over the body of the
DIV, the cursor becomes a move icon. The user can move and resize the selection box to select an appropriate crop. When only one of the dimensions (
ImageHeight) is specified, the shape of the cropping rectangle is unconstrained – the control will scale the image so that the specified dimension is correct, and the other dimension will end up whatever it needs to be to match the crop selected by the user. If both dimensions are specified, then a particular aspect ratio (width/height) needs to be enforced, so a client-side script constrains the cropping rectangle to this aspect ratio as the user drags the corners around.
Once a satisfactory crop has been selected, the user presses the "OK" button. A client-side script stores the location and size of the selection rectangle relative to the canvas image, and a postback is initiated. On the server, these client coordinates are transformed into coordinates on the original raw image (the control knows the dimensions of both the original raw image and the canvas it produced for the user to "draw" on). The original raw image is then cropped and scaled, and saved according to the format and quality specified as attributes of the control. The newly created web image is displayed in the control.
The control exposes a property that allows a developer to access the created web image (e.g., for storage in a content management system) at a later point.
Two additional features aid usability. A typical scenario where this control might be used is to supply two related pictures for a news story – a main image and a thumbnail. In an editing interface, two instances of the control would be presented, one for the main image and one for the thumbnail. The first might have
ImageWidth set to "203" and
ImageHeight set to "*", and the thumbnail's control might have both
ImageHeight set to "66". The user will often want to use the same raw image for both web images, with different crops. To avoid the user having to upload the same raw image more than once, the control makes its own thumbnail image of each raw file uploaded (not to be confused with any images that happen to be used as thumbnails somewhere else that a user might create using the control) and stores a key to identify the uploaded image in the user's session. The control examines the session to see if any images have already been uploaded and generates a UI to select an already uploaded image if it finds any. All the user needs to do is click the image to go straight to the canvas, bypassing the potentially lengthy upload process.
The other feature accommodates the scenario where the user already has a web-ready image of the correct size. If the control finds that the uploaded image is already correct, it will bypass the canvas stage and just save the raw file as the final image file.
Click here to see the control in action.
So how does it all work?
There are eight source files:
The source code for the server control class,
WebImageMaker. Also contains definitions of three enumerations –
ControlMode, which keeps track of what the control is currently doing (e.g., displaying the canvas), and
WebImageQuality, which are also attributes of the control and allow the developer to enforce how the final web image is created.
An interface that defines the operations the control needs to perform on images. By encapsulating this, we could switch to a different image library in future. Most of the file I/O is done in here as well, because drawing APIs typically have the ability to read and write images to and from disk, and it might be unnecessarily awkward to separate out I/O operations from drawing operations.
An implementation of
IImageProvider that uses the GDI+ libraries in
A helper class that's used to serve the images the control creates. This is separated off to optionally allow the control's generated images to be served by a different handler.
Optional HttpHandler to use if you don't want the control itself to handle the serving of its generated images.
Client side stylesheet that sets the CSS attributes for the various elements rendered by the control.
Client-side script that powers the user interface when the control is in
Client-side script that helps display thumbnails for previously uploaded raw image files, when the control is not in
These last three files are script and CSS resources used by the control. In ASP.NET 2.0, we can embed these resources into our assembly so that the control can be deployed in some other solution as a single DLL with no dependent files. Requests for these files from the browser are directed at the assembly DLL itself via the .axd handler. However, users of Visual Web Developer 2005 Express don't get the project option of building a control library that makes this easy in an IDE. For that, you would normally need Visual Studio 2005. It is possible to develop an ASP.NET server control in Web Developer Express, and then, with a little help from the compiler on the command line, build it as a control library complete with embedded resources. In development, we can use the files directly as part of the project. When building as a control, we can use them as embedded resources:
cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
cssUrl = Page.ResolveUrl("~/WebImageMaker.css");
BuildAsControlLibrary" is a conditional compilation directive that we don't define anywhere in our project, so the
#else clause will always be used when running from Web Developer Express. In WebImageMaker.cs, we also conditionally compile three web resource attributes to register the files as part of the assembly and define the Mime types that they should be served as:
When running the project normally from Web Developer Express, the files will be served from the file system just like any other files. When you want to compile the project into a DLL, you can use the supplied Build_Control_Library.bat file, which just contains the line:
csc @Build_Switches.rsp App_Code\*.cs
where Build_Switches.rsp is a file that contains the required compiler switches:
# define our conditional compilation label
# embed the resources
# output as a library into our BuildOutput directory
Note the definition of BuildAsControlLibrary and the embedding of the three resources. The output in the BuildOutput directory can be copied into other web projects without affecting the original project.
You'll need a command prompt with the right environment variables. The easiest way of doing this is to use the Visual Studio Command Prompt if you have it; if you have Web Developer Express, you can use the SDK command prompt (available from the "Microsoft .NET Framework SDK v2.0" program group on the Start menu). I don't think the Express edition installs the SDK – it's worth getting.
Storage of uploaded files
The control uses the file system to store the uploaded files as well as the generated canvases, thumbnails, and web images. It would be possible to use memory streams stored in the user's Session, and avoid the need to have a writable directory for ASP.NET to store the images in, but this might get out of hand very quickly if more than a few people are using the system at any one time. By saving the images to the file system between postbacks and disposing of any memory hungry resources as soon as possible, we make the system more scalable. Saving the images to disk also allows for an audit trail so we can see what types of images the users are uploading. The downside of this is that the image directories (especially the raw image directory) can fill up very quickly, so the control exposes a delegate that allows the developer to supply a purging strategy in the form of a method that the control will call before it writes to a directory. A default purging strategy is used if no others are provided.
public delegate void PurgeMethod(string directoryToClean);
Serving of generated images
Apart from the initial image the control is set to, all images generated by the control need to be served somehow. One way of doing this is to use an HTTP handler in the form of an ashx file. The control has an optional
HandlerPath property which should be set to point at the file. This file is provided as part of the project. However, it breaks the "no dependency" deployment scenario because it's an additional file and it also requires an entry in Web.Config to tell it where the working directory is. An alternative to this approach is to have the control itself responsible for serving its generated images. In the absence of a
HandlerPath property, the control will write out the URLs of the generated images to point back to the control's containing page, with parameters on the querystring that the control can read and optionally hijack the page request to serve the required image back out. This is done as early as possible to prevent any unnecessary work being done on the server:
protected override void OnInit(EventArgs e)
if (Page.Request.QueryString["mode" + KeySuffix] != null)
In fact, the code that reads the query string and writes the file out is separated out into the
WebImageMakerImageHelper class, and is used both by the control and the supplied handler (WebImageMakerHandler.ashx).
The control has a private property (
IsServingImage) that it maintains to ensure that no unnecessary work is done before
OnInit is called. This can be seen being checked in several places in the code.
The pros and cons of having a control hijacking its containing page's request in this way are discussed in a post to Fritz Onion's blog.
While having the control generate its own images is neat, there are a number if problems with it as can be seen from the discussion relating to the above post, not least the potentially unknowable amount of other work that ASP.NET might be doing on a request for the page, and other controls before it gets round to calling the
OnInit method of your particular control, where you can terminate the request early. This work might be decidedly non-trivial in a number of circumstances. On the whole, I'd use the separate handler wherever possible. The sample page provided in the project uses both approaches in different instances of the control.
The control derives from the new ASP.NET 2.0
CompositeControl abstract class. This takes care of some of the things that had to be done by hand in v1.1, and also allows the control to be rendered in the designer without too much extra work.
The control's children are all straightforward Web controls and HTML controls with a few literals. Some of the properties of the
WebImageMaker control map onto properties of child controls:
public string ConfirmButtonText
confirmSelection.Text = value;
confirmSelection is a button that forms part of the canvas UI. The call to
EnsureChildControls() results in ASP.NET calling the control's
CreateChildControls() method, which creates the control hierarchy.
Note that much of this code has been stripped out – see the supplied source for the full picture.
protected override void CreateChildControls()
targetImage = new HtmlImage();
popupDiv = new HtmlGenericControl("div");
upload = new FileUpload();
targetImage.ID = this.ID + "_img";
hiddenField.ClientID + "')";
foreach (string thumbnailFilename in SessionImages)
ImageButton thumbBtn =
thumbBtn.Command += new
"showThumbnailDiv('" + thumbnailsDiv.ClientID + "');");
= "setViewportDimensions('" + hiddenField.ClientID + "')";
uploadButton.Click += new
this.ChildControlsCreated = true;
CreateChildControls() will typically be called very early in the control's lifecycle, before any events are handled, and before we know exactly what state the control should be in. By the time we get round to overriding the
Render method later on, we know exactly what the control should look like, and we can selectively choose what parts of the control tree we actually want to render out to the client.
The complete control hierarchy as built by
CreateChildControls() looks like this:
popupDIV as a "floating" window with the selection box
DIV (appearing as a dashed outline rectangle) floating above the canvas image. Once the user has already uploaded at least one image, the
thumbnailsDiv element will also be rendered – this allows the user to pick a previously uploaded raw image.
The control keeps track of its current state via its
controlMode property. This is an enumeration that defines at a high level the three possible states the control can be in:
public enum ControlMode
Normal, Canvas, Changed
Normal is the starting condition.
Canvas is for when the user is presented with the drawing surface and the UI is awaiting user input. If the user clicks OK and a web image is created, the status will change to
Changed. From the
Changed state, the control can go back and forth between
Changed but can't return to
Normal. The state can go from
Canvas and back again if the user cancels in the
At a lower level, the state of the control is kept track of by using the new ASP.NET 2.0
ControlState feature. This works very much like ViewState except that it can still be accessed when ViewState is disabled.
ControlState is passed in and out of the control as a single object:
protected override void LoadControlState(object savedState)...
protected override object SaveControlState()...
In this control,
ControlState is persisted as an array of strings. Note that we have to tell the containing page that we want to make use of its
ControlState services in our
This ensures that the
Save method pair will be called by the
protected override void Render(HtmlTextWriter writer)
if (this.controlMode == ControlMode.Canvas)
In the rendering phase, the control selectively asks each of its child controls to render themselves to the supplied
AddAttributesToRender ensures that any properties set on the control that are
WebControl class properties rather than our derived
WebImageMaker class properties are written out as attributes on the HTML element, which we declared in the next line to be a
popupDiv that forms the canvas UI is only rendered when the control has decided (in response to a file being uploaded and successfully read as an image) that it is in
Canvas mode. Similarly, the thumbnails UI is only rendered when the user has previously uploaded images:
if (SessionImages.Count > 0)
SessionImages is a property that returns a list of thumbnail names that the control has previously generated. This list is stored in the user's session.
Uploading the image
CreateChildControls(), we set the
OnClientClick property so that some client-side script will be called before the form is submitted:
= "setViewportDimensions('" + hiddenField.ClientID + "')";
This calls the following function in WebImageMaker_normal.js:
var field = document.getElementById(hiddenFieldID);
width = window.innerWidth;
height = window.innerHeight;
else if (document.documentElement &&
width = document.documentElement.clientWidth;
height = document.documentElement.clientHeight;
else if (document.body)
width = document.body.clientWidth;
height = document.body.clientHeight;
field.value = width + "," + height;
The effect of this is to encode the browser's viewport dimensions in a hidden form field. The viewport is the inner dimension of the browser window. The three different conditions in the "
Back on the server, the
uploadButton's click event is handled by this handler:
void uploadButton_Click(object sender, EventArgs e)
lblMessages.Text = "No file present. Might be too big.";
lblMessages.Visible = true;
bool imageOK =
out serverImgID, out rawWidth, out rawHeight,
lblMessages.Text = "File is not an image" +
" that the system understands.";
lblMessages.Visible = true;
As long as the
FileUpload control actually contains a file, the control hands the uploaded image over to our
IImageProvider implementation to save to the file system, generating a thumbnail in the process. The control's
ImageProvider property is an accessor for the
private IImageProvider __imageProvider;
private IImageProvider ImageProvider
if (__imageProvider == null)
__imageProvider = new ImageProviderImpl();
__imageProvider.WorkingDirectory = this.workingDirectory;
__imageProvider.ServerID = this.serverImgID;
__imageProvider.ThumbnailSize = this.thumbnailSize;
__imageProvider.PurgeStrategy = this.purgeStrategy;
As there is only one implementation of
IImageProvider, we just instantiate our
ImageProviderImpl object and pass it the various properties it needs. This object is created once per control per request if it is required – it is not persisted between requests. When the
WorkingDirectory property is set on our
ImageProviderImpl instance, it ensures that four subdirectories are also present, one each for raw, canvas, thumbnail, and web images. The
ServerID property is a string that uniquely identifies the image the control is currently working with. It is generated by the
IImageProvider when a new raw image is saved, but the control needs to know it too – this property is used to name the generated canvas, thumbnail, and web image files in subsequent postbacks so the control persists it in the
ImageProviderImpl's implementation of
SaveRaw looks like this:
public bool SaveRaw(HttpPostedFile postedFile,
out string outServerID, out int rawWidth,
out int rawHeight, out string thumbnailFileName)
this.serverID = outServerID = Guid.NewGuid().ToString();
string filepath = getRawFilePath();
filepath, out rawWidth, out rawHeight,
true, out thumbnailFileName);
The control passes in the uploaded file, and
SaveRaw provides back a new server ID, the image dimensions (
rawHeigth), and the name of the thumbnail file it creates for this image. In this implementation, the generated server ID is a GUID, which seems a sensible choice.
SaveRaw will return
false if anything goes wrong in reading the uploaded file as an image. This job is handled by
private bool getRawInfo(string filepath, out int rawWidth,
out int rawHeight, bool createThumbnail,
out string thumbnailFileName)
thumbnailFileName = "";
bool result = false;
rawWidth = 0;
rawHeight = 0;
using (Image img = Image.FromFile(filepath))
rawWidth = img.Width;
rawHeight = img.Height;
rawFormat = img.RawFormat;
result = true;
thumbnailFileName = CreateThumbnail(img,
result = false;
getRawInfo doesn't always generate a thumbnail – it is also called when the user has clicked on a thumbnail rather than uploaded a raw image. The same event handler in the control handles a click on any thumbnail:
void btnThumb_Command(object sender, CommandEventArgs e)
bool imageOK =
out serverImgID, out rawWidth,
"Could not find the uploaded image" +
" corresponding to the thumbnail.";
lblMessages.Visible = true;
The thumbnails are all
ImageButtons created by a call to
getThumbnailButton in the
CreateChildControls phase. They have been given a command argument that is the name of the thumbnail file, from which
ImageProvider can later obtain the server ID. The
btnThumb_Command handler does the same job as
uploadButton_Click that we saw earlier, but instead of calling the
SaveRaw method, it calls
public bool UseThumbnailFile(string thumbnailFileName,
out String outServerID, out int rawWidth,
out int rawHeight)
this.serverID = thumbnailFileName.Substring(
outServerID = serverID;
getRawFilePath(), out rawWidth,
out rawHeight, false, out dummy);
IImageProvider implementation can always be trusted to obtain the server ID from a filename, as it always will have generated that filename from the server ID in the first place.
So, whether the user uploads a new file, or chooses a thumbnail to reuse a previously uploaded file, the code in
ImageProviderImpl will arrive at
getRawInfo to return the details of the image to the control, optionally creating a thumbnail while it has the image loaded. Generating the thumbnail gives us a first look at the way the code generates the various images it will require in its different stages:
private string CreateThumbnail(Image img, WebImageFormat format)
string thumbFileName = null;
Rectangle rawRect = new Rectangle(0, 0, img.Width, img.Height);
Rectangle thumbRect = new Rectangle();
float fWidth = (float)img.Width;
float fHeight = (float)img.Height;
float fThumbSize = (float)thumbnailSize;
float aspectRatio = fWidth / fHeight;
if (aspectRatio > 1)
thumbRect.Width = thumbnailSize;
thumbRect.X = 0;
thumbRect.Height = Convert.ToInt32((fThumbSize /
fWidth) * fHeight);
thumbRect.Y = (thumbnailSize - thumbRect.Height) / 2;
thumbRect.Height = thumbnailSize;
thumbRect.Y = 0;
thumbRect.Width = Convert.ToInt32((fThumbSize /
fHeight) * fWidth);
thumbRect.X = (thumbnailSize - thumbRect.Width) / 2;
using (Bitmap thumb = new Bitmap(
using (Graphics g = Graphics.FromImage(thumb))
g.DrawImage(img, thumbRect, rawRect,
string filepath = getFilePath(
thumbFileName = Path.GetFileName(filepath);
The thumbnails the control generates for its own UI are always square (it only offers one
ThumbnailSize property) but uploaded images will only coincidentally be square. We need to scale down the uploaded image so that its longest dimension matches the
ThumbnailSize specified, and then centre this scaled rectangular image in the square thumbnail. So, as well as working out the width and height we need to scale the original image to, we need to work out how far from either the left or the top of the square thumbnail the rectangular scaled image needs to be positioned. We use a
thumbRect to hold this information as it maintains both size (width, height) and position (X, Y).
Once we know where the scaled image will sit within our thumbnail, we create a new square bitmap for the thumbnail:
using (Bitmap thumb = new Bitmap(
and from that bitmap, we obtain a drawing surface
using (Graphics g = Graphics.FromImage(thumb));
setGraphicsQuality method called next is used several times by
ImageProviderImpl. To present a simple API to the developer, we defined an enumeration that is independent of any graphics API and is obvious to the user:
public enum WebImageQuality
High, Medium, Low
The desired image quality of the final web image is then easily set by the developer as a property of the control:
... Quality="High" ...
When using a particular library like GDI+, we need to translate this general concept of quality into settings specific to the library, hence:
private void setGraphicsQuality(Graphics g, WebImageQuality quality)
High quality in this case corresponds to the highest possible quality settings offered by GDI+.
In the case of thumbnail generation, we actually ignore the user's settings and always go for the highest possible settings, because reducing a large image to such a small size will result in a very poor quality thumbnail otherwise. But elsewhere (in canvas generation and in final web image generation), we use the control property.
Back in the thumbnail generation code, we paint the drawing surface white and then draw the reduced image in place using an overload of
g.DrawImage(img, thumbRect, rawRect, GraphicsUnit.Pixel);
thumbRect is the destination rectangle and
rawRect is the source rectangle with respect to
img (the raw image). In this case,
rawRect is the entire raw image, but later on, we'll use a similar technique to obtain a crop.
We then call
getFilePath to work out where we're going to save the thumbnail and what we're going to call it. This method makes use of the constants defined in the control:
public const string RawImageDirName = "raw";
public const string CanvasImageDirName = "canvas";
public const string ThumbnailImageDirName = "thumbnails";
public const string WebImageDirName = "web";
These name the subdirectories under the main working directory where the images of each type will be stored. The
getFilePath method uses the server ID and the format (GIF, PNG or JPG) to name the file. If the user is making a new image from a previously uploaded one, then the server ID of the original raw image will have already been used to name at least one file in the canvas and web directories, so we also check for an existing file and rename the file if necessary, using the Windows convention of a version number in parentheses (e.g., myfile(3).jpg).
We then save the thumbnail in the format specified.
getGDIFormat(..) is similar to
getGraphicsQuality(..) in that it transforms our simple
WebImageFormat enumeration into something GDI+ specific, in this case, a simple mapping onto GDI+
- 22nd February, 2006: Initial post
- 17th February, 2009: Updated demo project