digital expertise network :: about the guild :: case studies :: contact the guild :: articles :: second life :: tools

Dealing with images in content management systems (page 3)

Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples

(Download the source code for this article)

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 dimensions 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 upload Button'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 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 ServerID, the image dimensions (rawWidth and rawHeigth) and the name of the thumbnail file it creates for this image. In this implementation the generated ServerID 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 ServerID. 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 ServerID from a filename as it always will have generated that filename from the ServerID 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 images of each type will be stored. The getFilePath method uses the ServerID 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 ServerID 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.

Generating the canvas

Regardless of whether the user uploaded a new raw image or picked a thumbnail, we have now supplied the control with information about the image's width and height, and what the ServerID is (either newly generated or inferred from the thumbnail).

If we were in uploadButton_Click, the new thumbnail is added in to the list of thumbnail names in the session:

SessionImages.Add(thumbnailFileName);

Then, both uploadButton_Click and btnThumb_Command call UseRaw(). This method first checks to see if the raw image has the same dimensions as the control's ImageWidth and ImageHeight and if so, assumes the user has uploaded a web ready image. In this case the IImageProvider's SaveRawImageAsWebImage is called, which effectively copies the raw image to the web image directory, only doing any graphics redrawing if the uploaded format differs from the specified format. The mode of the control is then set to ControlMode.Changed and its work is done.

Usually, the image won't be the right size, so the control calls CanvasFromRaw() to generate the user's canvas on which they can make a selection.:

private void CanvasFromRaw() { // we're going to need to do some further work with this image // so let's work out a few things about it aspectRatio = (float)rawWidth / (float)rawHeight; // the image is not the right size so we need to make a canvas string[] clientDimensions = hiddenField.Value.Split(new char[] { ',' }); // we'll allow the canvas to be up to 80% // of the user's current browser window size: int clientX = Convert.ToInt32(clientDimensions[0]) * 4 / 5; int clientY = Convert.ToInt32(clientDimensions[1]) * 4 / 5; float clientRatio = (float)clientX / (float)clientY; // which is the dimension that should constrain the canvas size? if (clientRatio > aspectRatio) { // y axis constrains the canvas size canvasHeight = clientY; canvasWidth = rawWidth * clientY / rawHeight; } else { canvasWidth = clientX; canvasHeight = rawHeight * clientX / rawWidth; } canvasImageName = ImageProvider.CreateCanvas( webImageFormat, webImageQuality, canvasWidth, canvasHeight); canvas.Src = getImageSource(canvasImageName, "canvas"); canvas.Width = canvasWidth; canvas.Height = canvasHeight; controlMode = ControlMode.Canvas; }

This is where we make use of the viewport dimensions we captured earlier. We work out the aspect ratio of the uploaded image

aspectRatio = (float)rawWidth / (float)rawHeight;)

and that of the browser viewport

float clientRatio = (float)clientX / (float)clientY;

Comparing them allows us to determine which dimension should constrain the canvas so that it fits in the user's browser. We also multiply the client dimensions by 4/5 so the canvas has some space around it and doesn't go right to the edge of the browser, which would be ugly and not allow any room for additional UI.

The call to IImageProvider.CreateCanvas is more straightforward than the earlier thumbnail example:

public string CreateCanvas(WebImageFormat format, WebImageQuality quality, int canvasWidth, int canvasHeight) { purge(WebImageMaker.CanvasImageDirName); string canvasFileName = null; using (Image rawImg = Image.FromFile(getRawFilePath())) { using (Bitmap canvas = new Bitmap( canvasWidth, canvasHeight, PixelFormat.Format24bppRgb)) { using (Graphics g = Graphics.FromImage(canvas)) { setGraphicsQuality(g, quality); g.DrawImage(rawImg, 0, 0, canvasWidth, canvasHeight); string filePath = getFilePath( WebImageMaker.CanvasImageDirName, format); canvas.Save(filePath, getGDIFormat(format)); canvasFileName = Path.GetFileName(filePath); } } } return canvasFileName; }

The overload of Graphics.DrawImage used here simply redraws the source image on the whole of the canvas, scaling appropriately. The calls to setGraphicsQuality, getFilePath and getGDIFormat are the same as in the thumbnail case.

Back in the control we set the source of the canvas image using

getImageSource(canvasImageName, "canvas");

This method generates the image url appropriately depending on whether the handler is being used or the control itself will be serving the image.

We are now in ControlMode.Canvas and the control will render the canvas UI when the Render method is called.

Inserting client-side script references

We ensure that the client side UI has the correct JavaScript in the OnPreRender phase. The conditional compilation in this method has already been discussed. The end result is that usually, WebImageMaker_normal.js will be referenced. This has script to position the thumbnails selector and control its visibility (particularly when there may be multiple instances of the control on the page at any one time), as well as the setViewportDimensions function discussed earlier. When the control is in canvas mode, WebImageMaker_canvas.js will be referenced instead. We also need to inject and additional piece of javascript to initialise the canvas in the browser:

Page.ClientScript.RegisterStartupScript( this.GetType(), this.ClientID, getInitScript(), true); private string getInitScript() { string s = @" function init{0}() {{ initialise('{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}'); }} window.onload = init{0}; "; return String.Format(s, this.ClientID, popupDiv.ClientID, canvas.ClientID, selectionBox.ClientID, imageWidth, imageHeight, targetImage.ClientID, confirmSelection.ClientID, lblDebugInfo.ClientID); }

(The double "{{" are to escape a single "{" when using String.Format).


Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples