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