Saturday, July 30, 2011

Image Upload, Crop and Resize with ASP.NET MVC jQuery Uploadify and jCrop

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

image

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

12 comments:

lap jai said...

Very helpful!

mateus said...

hey my man! cool post, I had to do something similar a while ago. Quick question. Why did you choose to use Cache instead of Session?

beebul said...

Thanks I'm going to try and implement this for a new project.

PSD to Magento said...

It's been a pleasure reading your blog. I have bookmarked your website so that I can come back & read more in the future as well.

Nathanael Jones said...

Please dispose your System.Drawing instances, they always crash the server.... They're seen as 10KB by the garbage collector instead of the 10-100MB they actually occupy. Also, unless you're resizing during upload, don't re-encode. Validation is fine, but just keep the original bytes and require the extension be correct.

Remember *some* people copy & paste sample code without checking it for memory leaks. It's technically their responsibility, but us bloggers should do our part.

Tony said...

Tried to open the project in VS 2010 but got an error that it couldn't open ImageUploadAndCrop.csproj, any ideas?

Aaron Schnieder said...

@Nathanael Jones

Hey Nathanael, all of the System.Drawing.* objects are disposed via using statements. Did I miss something?

Aaron Schnieder said...

@Tony

I just grabbed a new copy of the download project in a new VM and it works great. Something might be wrong with your instances of VS.

Aaron Schnieder said...

@mateus

Hey, howzit! Cache vs. Session just depends on the application scope that you want the images to be available to. I used cache to make it more generic and available throughout an app. If you have an app that uses sessions to track individual users and each user should get a different image rendered, then Session would be the way to go.

Magento Developer said...

Hey, howzit! Cache vs. Session just depends on the application scope that you want the images to be available to. I used cache to make it more generic and available throughout an app. If you have an app that uses sessions to track individual users and each user should get a different image rendered, then Session would be the way to go.


Magento Developer

Scott McClain said...

Thanks for sharing. I will use this a a base for something I am working on.

Jeniffer Shamlin said...

I need both resize and crop image while upload.... Someone help me to do this....