Click here to Skip to main content
Click here to Skip to main content

Dealing with Images in Content Management Systems, Part 1

, 17 Feb 2009
Rate this:
Please Sign up or sign in to vote.
Browser-based resizing and optimisation of images
Prize winner in Competition "ASP.NET Jan 2006"

Introduction

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 CompositeControl class, which makes it easier to compose new server controls out of existing, simpler controls. This control makes heavy use of JavaScript, DOM, and CSS on the client to power the user interface, and requires a modern web browser. It will work in Firefox across all platforms, IE6 on Windows, and Safari on MacOSX. The control looks like this when used on an aspx page:

<guild:WebImageMaker id="wim_main" runat="server" width="250" 
    BorderStyle="Solid" BorderColor="#c0c0c0" BorderWidth="1px" 
    CancelButtonText="Cancel" ConfirmButtonText="OK"
    UploadButtonText="upload image..." 
    ImageWidth="203" ImageHeight="*"
    ImageUrl=" " 
    WorkingDirectory="C:\imagemaker_workingdir" 
    Format="jpg" Quality="High"  />

The key attributes here are the last six. ImageWidth and 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. Format and 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.

Control in default state

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.

Canvas

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.

selection rectangle

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 (ImageWidth and 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.

proportional resizing of selection

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.

finished web image

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 ImageWidth and 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.

thumbnails UI

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:

  • WebImageMaker.cs

    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 WebImageFormat and WebImageQuality, which are also attributes of the control and allow the developer to enforce how the final web image is created.

  • IImageProvider.cs

    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.

  • ImageProviderImpl.cs

    An implementation of IImageProvider that uses the GDI+ libraries in System.Drawing.

  • WebImageMakerImageHelper.cs

    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.

  • WebImageMakerHandler.ashx

    Optional HttpHandler to use if you don't want the control itself to handle the serving of its generated images.

  • WebImageMaker.css

    Client side stylesheet that sets the CSS attributes for the various elements rendered by the control.

  • WebImageMaker_canvas.js

    Client-side script that powers the user interface when the control is in Canvas mode.

  • WebImageMaker_normal.js

    Client-side script that helps display thumbnails for previously uploaded raw image files, when the control is not in Canvas mode.

Embedded Resources

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:

#if BuildAsControlLibrary
cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), 
         "Guild.WebControls.WebImageMaker.css");
#else
cssUrl = Page.ResolveUrl("~/WebImageMaker.css");
#endif

"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:

#if BuildAsControlLibrary
[assembly: WebResource("Guild.WebControls.WebImageMaker.css", 
    "text/css")]
[assembly: WebResource("Guild.WebControls.WebImageMaker_canvas.js", 
    "application/x-javascript")]
[assembly: WebResource("Guild.WebControls.WebImageMaker_normal.js", 
    "application/x-javascript")]
#endif

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
/define:BuildAsControlLibrary

# embed the resources
/resource:WebImageMaker.css,
          Guild.WebControls.WebImageMaker.css 
/resource:WebImageMaker_canvas.js, 
          Guild.WebControls.WebImageMaker_canvas.js 
/resource:WebImageMaker_normal.js,
          Guild.WebControls.WebImageMaker_normal.js 

# output as a library into our BuildOutput directory
/target:library /out:BuildOutput\
                 Guild.WebControls.WebImageMaker.dll

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)
    {
        ...
        // get the details of the image from the querystring
        ...
        // write the image out to the response
        ...
        Response.End();
    }
    ...
}

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.

Child Controls

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:

[Bindable(false)]
[Category("Appearance")]
[DefaultValue("Confirm Selection")]
[Themeable(false)]
public string ConfirmButtonText
{
    get
    {
        EnsureChildControls();
        return confirmSelection.Text;
    }
    set
    {
        if (!IsServingImage) // child controls won't be instantiated
        {
             EnsureChildControls();
             confirmSelection.Text = value;
        }
    }
}

Here, 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.

CreateChildControls()

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(); 
    // etc... and the rest of the controls
    ...

    targetImage.ID = this.ID + "_img"; 
    popupDiv.Attributes.Add("class", "webImageMaker_popup");
    // etc... add attributes to controls
    // that need them for css and script
    ...

    this.Controls.Add(popupDiv);
    this.Controls.Add(hiddenField);            
    popupDiv.Controls.Add(canvas);
    popupDiv.Controls.Add(selectionBox);
    // etc... add the controls to the hierarchy
    ...

    // some controls need to fire both
    // client- and server-side events:
    confirmSelection.OnClientClick = 
        "storeSelectionInfo('" + 
        hiddenField.ClientID + "')";
    confirmSelection.Click += 
      new EventHandler(confirmSelection_Click);
    ...

    foreach (string thumbnailFilename in SessionImages)
    {
        ImageButton thumbBtn = 
               getThumbnailButton(thumbnailFilename);
        thumbBtn.Command += new 
               CommandEventHandler(btnThumb_Command);
        thumbnailsDiv.Controls.Add(thumbBtn);
    }
    ...

    thumbnailButton.Attributes.Add("onclick", 
        "showThumbnailDiv('" + thumbnailsDiv.ClientID + "');");
    uploadButton.OnClientClick 
        = "setViewportDimensions('" + hiddenField.ClientID + "')";
    this.Controls.Add(upload);
    this.Controls.Add(uploadButton);
    uploadButton.Click += new 
          EventHandler(uploadButton_Click);
    ...

    this.ChildControlsCreated = true;
}

All child controls that might be used by the control are instantiated and built into the control hierarchy in this method, even though some of them don't end up being rendered later on. Some are given specific IDs and style attributes so the rendered elements can work with the client-side JavaScript and CSS. All controls need to be present to respond to any events that might get fired a little later. 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:

Control layout

Client-side JavaScript and CSS are responsible for displaying the 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 Canvas and Changed but can't return to Normal. The state can go from Normal to Canvas and back again if the user cancels in the Canvas state.

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 OnInit method:

Page.RegisterRequiresControlState(this);

This ensures that the Load and Save method pair will be called by the Page.

Rendering

protected override void Render(HtmlTextWriter writer)
{
   AddAttributesToRender(writer);
   writer.RenderBeginTag(HtmlTextWriterTag.Div);
           
  if (this.controlMode == ControlMode.Canvas)
  {
      popupDiv.RenderControl(writer)
  }
  ...
  ...
}

In the rendering phase, the control selectively asks each of its child controls to render themselves to the supplied HtmlTextWriter. The 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 DIV element.

The 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)
{
    thumbnailButton.RenderControl(writer);
    writer.WriteBreak();
    thumbnailsDiv.RenderControl(writer)
}

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

In CreateChildControls(), we set the uploadButton's OnClientClick property so that some client-side script will be called before the form is submitted:

uploadButton.OnClientClick 
    = "setViewportDimensions('" + hiddenField.ClientID + "')";

This calls the following function in WebImageMaker_normal.js:

function setViewportDimensions(hiddenFieldID)
{
    var field = document.getElementById(hiddenFieldID);
    var width;
    var height;
    
    if (window.innerWidth)
    {
        width = window.innerWidth;
        height = window.innerHeight;
    }
    else if (document.documentElement && 
             document.documentElement.clientWidth)
    {
        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 "if..." statement accommodate various browser differences. There is an excellent discussion of the viewport at www.quirksmode.org, a fantastic resource for CSS, JavaScript, and browser idiosyncrasies.

Back on the server, the uploadButton's click event is handled by this handler:

void uploadButton_Click(object sender, EventArgs e)
{
    // do we have a file?
    if (!upload.HasFile)
    {
        lblMessages.Text = "No file present. Might be too big.";
        lblMessages.Visible = true;
        return;
    }

    // is it an image?
    string thumbnailFileName;
    bool imageOK = 
         ImageProvider.SaveRaw(upload.PostedFile, 
         out serverImgID, out rawWidth, out rawHeight, 
         out thumbnailFileName);
    if (!imageOK)
    {
        lblMessages.Text = "File is not an image" + 
                           " that the system understands.";
        lblMessages.Visible = true;
        return;
    }

    // we've got this far, so make
    // a thumbnail and store the Guid in the user's 
    // session, so they can reuse
    // the image without having to upload it again.
    SessionImages.Add(thumbnailFileName);

    CanvasFromRaw();            
}

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 IImageProvider implementation:

private IImageProvider __imageProvider;
private IImageProvider ImageProvider
{
    get
    {
        if (__imageProvider == null)
        {
            __imageProvider = new ImageProviderImpl();
            __imageProvider.WorkingDirectory = this.workingDirectory;
            __imageProvider.ServerID = this.serverImgID;
            __imageProvider.ThumbnailSize = this.thumbnailSize;
            __imageProvider.PurgeStrategy = this.purgeStrategy;
        }
        return __imageProvider;
    }
}

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 ControlState.

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)
{
    purge(WebImageMaker.RawImageDirName);
    this.serverID = outServerID = Guid.NewGuid().ToString();
    string filepath = getRawFilePath();
    postedFile.SaveAs(filepath);
    return getRawInfo(
       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 (rawWidth and 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 getRawInfo:

private bool getRawInfo(string filepath, out int rawWidth, 
        out int rawHeight, bool createThumbnail, 
        out string thumbnailFileName)
{
    thumbnailFileName = "";
    bool result = false;
    rawWidth = 0;
    rawHeight = 0;
    try
    {
        using (Image img = Image.FromFile(filepath))
        {
            rawWidth = img.Width;
            rawHeight = img.Height;
            rawFormat = img.RawFormat;
            result = true;
            if (createThumbnail)
            {
                thumbnailFileName = CreateThumbnail(img, 
                                    WebImageFormat.Jpg);
            }
        }
    }
    catch
    {
        result = false;
    }
    return result;
}

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 = 
         ImageProvider.UseThumbnailFile(
         e.CommandArgument.ToString(), 
         out serverImgID, out rawWidth, 
         out rawHeight);
    if (!imageOK)
    {
        lblMessages.Text = 
            "Could not find the uploaded image" + 
            " corresponding to the thumbnail.";
        lblMessages.Visible = true;
        return;
    }
    CanvasFromRaw();
}

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 ImageProvider's SaveRaw method, it calls UseThumbnailFile:

public bool UseThumbnailFile(string thumbnailFileName, 
       out String outServerID, out int rawWidth, 
       out int rawHeight)
{
    this.serverID = thumbnailFileName.Substring(
        0, thumbnailFileName.LastIndexOf("."));
    outServerID = serverID;
    string dummy; 
    return getRawInfo(
        getRawFilePath(), out rawWidth, 
        out rawHeight, false, out dummy);
}

The 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;
    }
    else
    {
        thumbRect.Height = thumbnailSize;
        thumbRect.Y = 0;
        thumbRect.Width = Convert.ToInt32((fThumbSize / 
                                    fHeight) * fWidth);
        thumbRect.X = (thumbnailSize - thumbRect.Width) / 2;
    }
    using (Bitmap thumb = new Bitmap(
        thumbnailSize, thumbnailSize, 
        PixelFormat.Format24bppRgb))
    {
        using (Graphics g = Graphics.FromImage(thumb))
        {
            setGraphicsQuality(g, WebImageQuality.High);
            g.Clear(Color.White);
            g.DrawImage(img, thumbRect, rawRect, 
                        GraphicsUnit.Pixel);
            string filepath = getFilePath(
                WebImageMaker.ThumbnailImageDirName, format);
            thumb.Save(filepath, getGDIFormat(format));
            thumbFileName = Path.GetFileName(filepath);
        }
    }
    return thumbFileName;
}

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 System.Drawing.Rectangle called 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(
    thumbnailSize, thumbnailSize, 
    PixelFormat.Format24bppRgb));

and from that bitmap, we obtain a drawing surface g (a System.Drawing.Graphics instance):

using (Graphics g = Graphics.FromImage(thumb));

The 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)
{
    switch (quality)
    {
        case WebImageQuality.High:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.HighQuality;
        break;

        case WebImageQuality.Medium:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.Default;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.Half;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        break;

        case WebImageQuality.Low:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.Low;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.HighSpeed;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
        break;
    }
}

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 drawImage:

g.Clear(Color.White);
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+ ImageFormat instances.

History

  • 22nd February, 2006: Initial post
  • 17th February, 2009: Updated demo project

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Tom Crane
Web Developer
United Kingdom United Kingdom
Tom Crane is a software developer from London. He likes to code in C# but has been known to implement Java-based content management systems for public sector clients.
 
Most of the time he does web programming and tries to make complex tasks seem easy through friendly UI.
 


Comments and Discussions

 
GeneralMy vote of 5 PinmemberMember 870994616-May-13 20:41 
GeneralMy vote of 5 Pinmembermanoj kumar choubey4-Apr-12 0:10 
GeneralMy vote of 5 PinmemberFilip D'haene13-Nov-11 12:34 
GeneralMy vote of 5 PinmemberSergio Andrés Gutiérrez Rojas3-Nov-11 17:45 
GeneralMy vote of 5 PinmemberMagnus Gudmundsson15-Jan-11 5:15 
GeneralGet File Name PinmemberDescendz2-Aug-10 2:23 
GeneralHi !! nice code....But Won;t work when master page is inclued !!! Pinmemberbhushan13in1312-Jun-10 0:15 
QuestionGreat work but how about cleaning up the canvas and raw folders? PinmemberWouter Spelt15-Oct-09 10:29 
GeneralCropping images Pinmembernace2k216-Jun-09 21:38 
GeneralWebImageMaker control in Web Application Projects Pinmembertamasanm24-Jan-09 4:53 
GeneralRe: WebImageMaker control in Web Application Projects PinmemberITMaiO3-Apr-09 4:55 
AnswerGREAT control. I have one addtional improvement which I added - resizing jpg in 100% quality PinmemberfitITOren12-Dec-08 12:18 
General[Message Removed] Pinmemberstonber6-Oct-08 7:35 
GeneralBest Image Processing Pinmembersonesh30-Sep-08 21:11 
GeneralRe: Best Image Processing PinmemberfitITOren12-Dec-08 19:16 
Generalhelp! PinmemberRod DeValcourt30-Aug-08 5:56 
GeneralRe: help! PinmemberRod DeValcourt30-Aug-08 6:14 
Generalunknown server tag error PinmemberJames Woodmacy27-Aug-08 6:20 
GeneralChanging the apperance of the popupdiv Pinmembermcselasvegas29-Jul-08 2:44 
GeneralGUID image name PinmemberWarmdownhere16-Jul-08 10:37 
QuestionRe: GUID image name PinmemberThaRobster22-Aug-08 0:29 
GeneralRe: GUID image name Pinmembersanicovan10-Dec-08 18:13 
GeneralProblem with WorkingDirectory Pinmembernasser.f16-Apr-08 1:36 
QuestionCan we use in ASP.Net 1.0 Version PinmemberGayuDams6-Feb-08 1:49 
GeneralProgrammatically set properties seem to be lost during postback PinmemberN0rthernlights16-Jan-08 6:18 
GeneralProblem with Control Library Pinmemberrdissell14-Jan-08 22:01 
GeneralRe: Problem with Control Library PinmemberN0rthernlights16-Jan-08 6:21 
QuestionRenaming the saved file Pinmembersmarv0124-May-07 4:43 
GeneralImage resize values Pinmemberfetty4106-Apr-07 0:44 
QuestionHow to use it inside a UserControl? PinmemberCultx2-Apr-07 3:02 
GeneralProblems with FormView Pinmemberkallekula7418-Mar-07 6:06 
GeneralGreat, Well Written Article PinmemberTom John13-Feb-07 23:58 
GeneralSave the Image in the user specified name Pinmemberarunpillai30-Jan-07 20:44 
GeneralRe: Save the Image in the user specified name Pinmemberkallekula7418-Mar-07 6:02 
GeneralRe: Save the Image in the user specified name PinmemberMember 212873023-Oct-08 1:06 
GeneralFantastic article Pinmemberjoshcdsi30-Nov-06 1:17 
QuestionUnknown server tag 'guild:WebImageMaker'. Pinmemberfredpalma10-Nov-06 13:24 
QuestionRe: Unknown server tag 'guild:WebImageMaker'. PinmemberAndrea763-Apr-08 2:43 
GeneralRe: Unknown server tag 'guild:WebImageMaker'. Pinmemberdenii20-Apr-08 20:31 
QuestionStrange code on the original files PinmemberPascal Harvey13-Oct-06 1:53 
QuestionProblem with Build_Control_Library PinmemberBububear19-Jul-06 13:44 
QuestionRe: Problem with Build_Control_Library PinmemberGayuDams6-Feb-08 1:43 
Jokebravooo PinmemberRecep GUVEN18-Jul-06 8:25 
GeneralI have some problem setting property at runtime Pinmembertriplex_ita13-Jul-06 0:42 
GeneralProblem wiht master pages PinmemberPriya Rajasekaran9-May-06 4:16 
GeneralRe:Here is the solution PinmemberPriya Rajasekaran9-May-06 7:12 
GeneralRe:Here is the solution Pinmemberhelloclarice25-May-06 6:03 
GeneralRe:Here is the solution Pinmemberbevrigy6-Jun-06 10:46 
GeneralRe:Here is the solution Pinmemberwrt54gfr27-Aug-06 1:16 
GeneralRe:Here is the solution Pinmemberpoteet7-Jun-06 14:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 17 Feb 2009
Article Copyright 2006 by Tom Crane
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid