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

Dealing with images in content management systems (page 5)

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

(Download the source code for this article)

Resizing

The middle condition in the move function's if statement handles resizing. To change the size of the selection by moving the right hand edge to the right or the bottom edge downwards is relatively straightforward, as all you need to do is increase the width or height of the selection div by the amount the user has moved the mouse. To give the appearance of moving the top of the selection upwards or the left of the selection to the left is more complicated. In these cases you want to have the bottom or right edges appear to stay where they are, so you need to simultaneously increase the size of the selection by the amount the user has moved the mouse and move it up or leftwards by the same amount. This makes the experience feel natural to the user and is what they will be expecting.

... else if(bResizing) { var dx = p.x - downPoint.x; var dy = p.y - downPoint.y; if(resizeXMode == "E") { selection.w = 0 + originalRect.w + dx; if(selection.w < minimumSelectionSize) { selection.w = minimumSelectionSize; bResizing = false; } } if(resizeXMode == "W") { selection.w = 0 + originalRect.w - dx; selection.x = 0 + originalRect.x + dx; if(selection.w < minimumSelectionSize) { dx = selection.w - minimumSelectionSize; selection.w = minimumSelectionSize; selection.x += dx; bResizing = false; } } if(resizeYMode == "S") { selection.h = 0 + originalRect.h + dy; if(selection.h < minimumSelectionSize) { selection.h = minimumSelectionSize; bResizing = false; } } if(resizeYMode == "N") { selection.h = 0 + originalRect.h - dy; selection.y = 0 + originalRect.y + dy; if(selection.h < minimumSelectionSize) { dy = selection.h - minimumSelectionSize; selection.h = minimumSelectionSize; selection.y += dy; bResizing = false; } } constrain(selection); setSelection(selection); checkConfine(selection); } ...

Each direction is handled separately. If the user grabbed a corner hotspot then two of the four conditions will be met. Moving "W" or "N" is more complicated than moving "E" or "S" as discussed above. We also cancel the resizing operation (by setting bResizing to false) if the selection ends up too small (this script uses a default minimum selection size of 40 pixels square, which leaves enough to be able to still grab all the hotspots and move the selection around).

Once we have given the selection to new shape according to the user's mouse movements, we need to constrain it to the required aspect ratio if this is being enforced:

function constrain(rect) { if(bConstrain && rect) { var newRatio = rect.w / rect.h; if(newRatio > aspectRatio) { // it's too "landscapey" - // keep the height the same but reduce the width // in accordance with the required aspectRatio var correctWidth = Math.round(aspectRatio * rect.h); if(correctWidth >= minimumSelectionSize) { if(resizeXMode == "W") { var rightPos = rect.x + rect.w; rect.x = rightPos - correctWidth; } rect.w = correctWidth; } else { // the constrained selection will be too small rect.w = minimumSelectionSize; var newH = Math.round(minimumSelectionSize / aspectRatio); var dy = newH - rect.h; rect.h = newH; if(resizeYMode == "N") { rect.y -= dy; } } } else { // it's too "portraity" - //keep the width the same but reduce the height // in accordance with the required aspectRatio var correctHeight = Math.round(rect.w / aspectRatio); if(correctHeight >= minimumSelectionSize) { if(resizeYMode == "N") { var bottomPos = rect.y + rect.h; rect.y = bottomPos - correctHeight; } rect.h = correctHeight; } else { // the constrained selection will be too small rect.h = minimumSelectionSize; var newW = Math.round(minimumSelectionSize * aspectRatio); var dx = newW - rect.w; rect.w = newW; if(resizeXMode == "W") { rect.x -= dx; } } } } }

First of all we have to decide whether the current aspect ratio is wider (more landscapey) or taller more (portrait-y) than the required aspect ratio. This tells us which dimension can stay as it is and which one needs to be altered to bring the overall shape back into proportion. Again, we have to keep the apparent position of the right or bottom edges constant if the user is resizing to the left or top. We also need to make sure that in constraining a dimension (which will always reduce the size) we're not bringing it under the minimumSelectionSize. If so, we need to set the dimension to the minimum and increase the other dimension to compensate.

There are various ways in which this constrain function could work – for example, it could work the other way round and always increase the other dimension rather than reduce it. However, this set of rules seems to produce the most intuitive results when actually manipulating the selection.

Creating the final web image

Pressing the OK button causes the current selection information to be written to a hidden form field:

function storeSelectionInfo(hiddenFieldID) { var field = document.getElementById(hiddenFieldID); // get the selection dimensions relative to the canvas - x,y,w,h var selection = rectangle(oSelection); field.value = (selection.x - canvasRect.x) + "," + (selection.y - canvasRect.y) + "," + selection.w + "," + selection.h; }

Then the form posts back to the server. This takes us into the OK button's server-side event handler:

void confirmSelection_Click(object sender, EventArgs e) { string[] clientDimensions = hiddenField.Value.Split(new char[] { ',' }); int x = Convert.ToInt32(clientDimensions[0]); int y = Convert.ToInt32(clientDimensions[1]); int w = Convert.ToInt32(clientDimensions[2]); int h = Convert.ToInt32(clientDimensions[3]); // now we have the x,y,w,h of the selection relative to the canvas, so // we have to scale the selection so that it is a selection from the // original raw image: float scaleFactor = (float)canvasWidth / (float)rawWidth; Rectangle transformedSelection = new Rectangle( (int)(x / scaleFactor), (int)(y / scaleFactor), (int)(w / scaleFactor), (int)(h / scaleFactor)); // transformedSelection now represents the user's selected crop on the // raw image rather than the canvas image // now determine what the dimensions of the final image should be. If // ImageWidth and ImageHeight were both set then we already know, but // if one of them was "*" then we need to work out what it should // proportionally be from the user's selected crop shape: float selectionAspectRatio = (float)w / (float)h; int reqdWidth = intImageWidth; int reqdHeight = intImageHeight; // these should never be both <= 0 if (reqdWidth <= 0) { reqdWidth = (int)(selectionAspectRatio * reqdHeight); } else if (reqdHeight <= 0) { reqdHeight = (int)(reqdWidth / selectionAspectRatio); } // now we have everything we need: what area (transformedSelection) to crop // out of the raw image, and what dimensions this cropped area should be // resized to: webImageName = ImageProvider.CropAndScale( transformedSelection, webImageFormat, webImageQuality, reqdWidth, reqdHeight); targetImage.Src = getImageSource(webImageName, "web"); controlMode = ControlMode.Changed; }

Using the coordinates from the client, we create a new System.Drawing.Rectangle called transformedSelection, which represents the user's selection on the canvas transformed to the coordinate system of the raw image. Then, if only one of reqdWidth and reqdHeight has been set as a property of the control (i.e., evaluates as a positive integer rather than "*"), we need to work out what the other dimension of the final web image should be. We use the aspect ratio of the selection to determine this. Once we have all this information we can call the IImageProvider's CropAndScale method:

public string CropAndScale(System.Drawing.Rectangle transformedSelection, WebImageFormat format, WebImageQuality quality, int reqdWidth, int reqdHeight) { string webFileName = null; Rectangle dest = new Rectangle(0, 0, reqdWidth, reqdHeight); using (Image rawImg = Image.FromFile(getRawFilePath())) { using (Bitmap webImage = new Bitmap( reqdWidth, reqdHeight, PixelFormat.Format24bppRgb)) { using (Graphics g = Graphics.FromImage(webImage)) { setGraphicsQuality(g, quality); g.DrawImage( rawImg, dest, transformedSelection, GraphicsUnit.Pixel); string filePath = getFilePath( WebImageMaker.WebImageDirName, format); webImage.Save(filePath, getGDIFormat(format)); webFileName = Path.GetFileName(filePath); } } } return webFileName; }

This is very similar to the GDI+ code we've already seen. We make a new rectangle to represent the final web image (dest) then draw the transformed selection onto it:

g.DrawImage(rawImg, dest, transformedSelection, GraphicsUnit.Pixel);

We then set the control's image to this new web image and change the controlMode to ControlMode.Changed. The developer can later query the control for the file path of the new web image by accessing its WebImagePath property.

Possible extra features

There are many ways this control's features could be added to. One example would be to provide a hook into the web image creation stage that allowed external code to process the image before saving, for example, adding a border or a copyright notice.

Ajax

There are a few places in this code where the user experience might be a little improved with the use of some script callbacks. However, the "elephant in the room" with this control is the requirement to get the raw file back to the server, which is likely to be the mother of all postbacks. Saving a few postbacks elsewhere seems a bit trivial after that.


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