I need a slick way for my customers to upload, crop and resize photos within my Church Management Software. After doing some research it seems that a bunch of existing libraries and utilities need to be combined to create a functional and slick user experience for this seemingly mundane task. Specifically the platform I want to use is ASP.NET MVC (although this code will work equally well in Java with a few changes) and the best tools for uploading and cropping images seem to be Uploadify and jCrop powered by jQuery (of course.)
I found a lot of examples of each different library used in ASP.NET (and a few in MVC) but the examples where rather simplistic and didn’t really provide an elegant end to end solution tying together these great libraries. So I went ahead and created a nice ASP.NET MVC based sample application that accomplishes the following…
- Uploads images with Uploadify
- Stores uploaded images in Cache
- Crops images using jCrop
- Resizing images
- All interactions handled by jQuery AJAX for a smooth UX
Uploading Images
I used a great blog post by James McCormack as the base for uploading images with Uploadify and ASP.NET MVC. Basically Uploadify is doing the heavy lifting and I just tied the onComplete event to an ASP.NET MVC Controller method.
$("#fuFileUploader").uploadify({
'hideButton': true, // We use a trick below to overlay a fake html upload button with this hidden flash button
'wmode': 'transparent',
'uploader': '<%= Url.Content("~/Uploadify/uploadify.swf") %>',
'cancelImg': '<%= Url.Content("~/Uploadify/cancel.png") %>',
'buttonText': 'Upload File',
'script': '<%= Url.Action("FileUpload", "Media") %>',
'multi': true,
'auto': true,
'fileExt': '*.jpg;*.gif;*.png;*.jpeg',
'fileDesc' : 'Image Files',
'scriptData': { RequireUploadifySessionSync: true, SecurityToken: UploadifyAuthCookie, SessionId: UploadifySessionId },
'onComplete': function (event, ID, fileObj, response, data) {
response = $.parseJSON(response);
if (response.Status == 'OK') {
$("#pnlUpload").hide();
$("#pnlUploadedImage").show();
$("#imgUploadedImage").attr("src", imageHandler + response.Id);
$('#imgUploadedImage').Jcrop({
onChange: setCoords,
onSelect: setCoords
});
}
}
});
The controller method grabs the image that was uploaded…
public ActionResult FileUpload(MediaAssetUploadModel uploadedFileMeta)
{
Guid newImageId = new Guid();
try
{
newImageId = ProcessUploadedImage(uploadedFileMeta);
}
catch (Exception ex)
{
string errorMsg = string.Format("Error processing image: {0}", ex.Message);
Response.StatusCode = 500;
Response.Write(errorMsg);
return Json(string.Empty);
}
return Json(new { Id = newImageId, Status = "OK" });
}
And then processes it….
private Guid ProcessUploadedImage(MediaAssetUploadModel uploadedFileMeta)
{
// Get the file extension
WorkingImageExtension = Path.GetExtension(uploadedFileMeta.Filename).ToLower();
string[] allowedExtensions = { ".png", ".jpeg", ".jpg", ".gif" }; // Make sure it is an image that can be processed
if (allowedExtensions.Contains(WorkingImageExtension))
{
WorkingImageId = Guid.NewGuid();
Image workingImage = new Bitmap(uploadedFileMeta.fileData.InputStream);
WorkingImage = ImageHelper.ImageToByteArray(workingImage);
}
else
{
throw new Exception("Cannot process files of this type.");
}
return WorkingImageId;
}
And stores the image converted to a byte array in Cache…
#region cached properties
private byte[] WorkingImage
{
get
{
byte[] img = null;
if (HttpContext.Cache[WorkingImageCacheKey] != null)
img = (byte[])HttpContext.Cache[WorkingImageCacheKey];
return img;
}
set
{
HttpContext.Cache.Add(WorkingImageCacheKey,
value,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 40, 0),
System.Web.Caching.CacheItemPriority.Low,
null);
}
}
private byte[] ModifiedImage
{
get
{
byte[] img = null;
if (HttpContext.Cache[ModifiedImageCacheKey] != null)
img = (byte[])HttpContext.Cache[ModifiedImageCacheKey];
return img;
}
set
{
HttpContext.Cache.Add(ModifiedImageCacheKey,
value,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 40, 0),
System.Web.Caching.CacheItemPriority.Low,
null);
}
}
#endregion
The image is assigned in id, which is used as the Cache key and is returned back out via json. Once the call is successfully completed an img src attribute is updated to point to a ASHX image handler I wrote that accepts the id as a querystring parameter.
$("#imgUploadedImage").attr("src", imageHandler + response.Id);
The image handler grabs the image out of the cache and then displays it.
public void ProcessRequest (HttpContext context) {
byte[] image = GetImage(context.Request.QueryString["id"], context);
context.Response.Clear();
context.Response.ContentType = "image/pjpeg";
context.Response.BinaryWrite(image);
context.Response.End();
}
Cropping Images
Once the image has been uploaded, jCrop is initialized to allow for image crop selection.
$('#imgUploadedImage').Jcrop({
onChange: setCoords,
onSelect: setCoords
});
If the user selects a region to crop and clicks the crop button a jQuery AJAX call is made to the server. The x, y and width, height crop values are posted to the server and then the server crops the image as specified. A new modified image id is returned via json to the view and then the new cropped image is displayed via the image handler ashx once again.
function cropImage() {
$.ajax({
url: "/Media/CropImage",
type: "POST",
data: { x: x, y: y, w: w, h: h },
success: function (data) {
$('#lblMethodError').hide();
$("#pnlNewImage").show();
$("#imgNewImage").attr("src", imageHandler + data);
},
error: function (xhr, status, error) {
// Show the error
$('#lblMethodError').text(xhr.responseText);
$('#lblMethodError').show();
}
});
}
public JsonResult CropImage(int x, int y, int w, int h)
{
try
{
if (w == 0 && h == 0) // Make sure the user selected a crop area
throw new Exception("A crop selection was not made.");
string imageId = ModifyImage(x, y, w, h, ImageModificationType.Crop);
return Json(imageId);
}
catch (Exception ex)
{
string errorMsg = string.Format("Error cropping image: {0}", ex.Message);
Response.StatusCode = 500;
Response.Write(errorMsg);
return Json(string.Empty);
}
}
Resizing Images
Resizing images follows the same basic pattern. I have hard coded the new image dimensions in the example, but you could easily add a couple of inputs to the view and allow the user to specify the new dimensions.
Download the Example
I put together a full working example that includes all of the code above and is the basis for what I will end up implemented in my SaaS. I hope you find this helpful.
Aaron Schnieder
http://www.churchofficeonline.com