|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionMost 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 <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. 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
The file selector is a standard 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 Floating above the generated canvas is the selection rectangle. This is another
A client side script responds to mouse move events, so that when the pointer is near the edges of the
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
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. So how does it all work?There are eight source files:
Embedded ResourcesThese 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
" #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 filesThe 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 imagesApart 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 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 The control has a private property ( 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 Child ControlsThe control derives from the new ASP.NET 2.0 The control's children are all straightforward Web controls and HTML controls with a few literals. Some of the properties of the
Here, 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 hirearchy
...
// 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. The complete control hierarchy as built by
Client-side JavaScript and CSS are responsible for displaying the The control keeps track of its current state via its public enum ControlMode
{
Normal, Canvas, Changed
}
At a lower level, the state of the control is kept track of by using the new ASP.NET 2.0 protected override void LoadControlState(object savedState)...
protected override object SaveControlState()...
In this control, Page.RegisterRequiresControlState(this);
This ensures that the Renderingprotected 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 The if (SessionImages.Count > 0)
{
thumbnailButton.RenderControl(writer);
writer.WriteBreak();
thumbnailsDiv.RenderControl(writer)
}
Uploading the imageIn 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 " Back on the server, the 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 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
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 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;
}
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 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 So, whether the user uploads a new file, or chooses a thumbnail to reuse a previously uploaded file, the code in 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 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 using (Graphics g = Graphics.FromImage(thumb));
The 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 g.Clear(Color.White);
g.DrawImage(img, thumbRect, rawRect, GraphicsUnit.Pixel);
We then call 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 We then save the thumbnail in the format specified.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||