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.
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 DIV
appropriately, 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 (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.
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 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.
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)
{
...
...
...
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)
{
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();
...
targetImage.ID = this.ID + "_img";
popupDiv.Attributes.Add("class", "webImageMaker_popup");
...
this.Controls.Add(popupDiv);
this.Controls.Add(hiddenField);
popupDiv.Controls.Add(canvas);
popupDiv.Controls.Add(selectionBox);
...
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:
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)
{
if (!upload.HasFile)
{
lblMessages.Text = "No file present. Might be too big.";
lblMessages.Visible = true;
return;
}
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;
}
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 ImageButton
s 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