Tuesday, 5 February 2013

Using MVC3, Razor Helpers, and jCrop to upload and crop images.

Using MVC3, Razor Helpers, and jCrop to upload and crop images.


A common requirement for websites that allow user registration is the uploading of user profile images.  In many cases, this can be a bother for both user and developer.  The implementation of image cropping functionality has traditionally been somewhat cumbersome for developers, and users are generally pained by having to crop an image offline before uploading it, especially more inexperienced computer users.

We’ll explore how to quickly and easily create a solution using ASP.NET MVC3 and 2 of the new Razor helpers, the FileUpload and WebImage helpers and jCrop, an excellent jQuery plugin for image cropping.
Downloading/Installing Jcrop

Jcrop is available for download from the following link: Jcrop Webpage



Just drop your CSS and the Jcrop.gif file in your Content foler, and include the jQuery.jCrop.min.js in your Scripts folder.
FileUpload and WebImage for uploading images
Adding the Microsoft.Web.Helpers Library

To add the Microsoft.Web.Helpers library, install NuGet, and install the Microsoft.Web.Helpers package.  Also, include the System.Web.Helpers package included with ASP.NET MVC 3.

We’re creating a very simple solution for displaying and cropping images here.

First, we’ll need a view model for our profile editing view.
Code Snippet

    public class ProfileViewModel
    {
        [UIHint("ProfileImage")]
        public string ImageUrl { get; set; }
    }

You can see we’re using a UIHint to simplify the naming of our Display and Editor templates.

Next, we’ll create an editor template for our image:
Code Snippet

    @model System.String
    
        <img src="@(String.IsNullOrEmpty(Model) ? "" : Model)" id="profileImage" />



Now, lets create our index page.
Code Snippet

    @model WebHelpers.Models.WebImage.ProfileViewModel
    @using Microsoft.Web.Helpers;
    
    @{
        ViewBag.Title = "Index";
    }
    
    <h2>Index</h2>
    
       @using (Html.BeginForm("Upload", "WebImage", FormMethod.Post, new { @encType = "multipart/form-data" }))
       {
         @Html.DisplayFor(x => x.ImageUrl)<br/>
        
         <input type="submit" name="submit" text="upload" />
       }



We’ve got a basic HTML form here that will post to the Upload action method with the encoding type set to multipart/form-data for file upload.  We’ll need to add in the code for the upload box.  For this, we’ll use the FileUpload helper.
The FileUpload helper
Code Snippet

    @FileUpload.GetHtml(initialNumberOfFiles:1, allowMoreFilesToBeAdded: true, includeFormTag: false, addText: "Add Files", uploadText: "Upload File")<br />

The FileUpload helper gives developers an extremely quick and flexible way to add multiple file upload support to a form, using javascript to handle adding multiple file upload controls.  The GetHtml method is used to render a file upload control using the optional parameters shown above.  The snippet above results in the upload control displayed below.

image
Code Snippet

    public ActionResult Upload(ProfileViewModel model)
    {
        var image = WebImage.GetImageFromRequest();
    
        if (image != null)
        {
            if (image.Width > 500)
            {
                image.Resize(500, ((500 * image.Height) / image.Width));
            }
    
            var filename = Path.GetFileName(image.FileName);
            image.Save(Path.Combine("../TempImages", filename));
            filename = Path.Combine("~/TempImages", filename);
    
            model.ImageUrl = Url.Content(filename);
    
        }
    
        return View("Index", model);
    }

For our purposes, we’ll use the following FileUpload command for a single file upload:
Code Snippet

    @FileUpload.GetHtml(initialNumberOfFiles: 1, includeFormTag: false, uploadText: "Upload File")<br />


Receiving Uploaded Files

To receive and save the uploaded file, we’ll implement the Upload action method using the WebImage helper to save the file:
Code Snippet

    public ActionResult Upload(ProfileViewModel model)
    {
        var image = WebImage.GetImageFromRequest();
    
        if (image != null)
        {
            if (image.Width > 500)
            {
                image.Resize(500, ((500 * image.Height) / image.Width));
            }
    
            var filename = Path.GetFileName(image.FileName);
            image.Save(Path.Combine("../TempImages", filename));
            filename = Path.Combine("~/TempImages", filename);
    
            model.ImageUrl = Url.Content(filename);
    
        }
    
        return View("Index", model);
    }

Using the GetImageFromRequest static method on the WebImage class, we retrieve a WebImage object representing the uploaded file.  Since we’re constraining all images to a maximum of 500 pixels in width, we’ll use the Width property to check the size, and use the Resize functionality of the WebImage helper to resize the image in memory. We the get the filename and use the Save function of the WebImage helper to svae the file to a TempImages folder. We then retrieve the image url and set it back to the ImageUrl property of our ProfileViewModel instance and return the model to our view.   This results in the following:





In our app, we’re displaing a profile picture constrained to a 1 to 1 aspect ratio.  this causes most images we’d upload to appear tiny and squished.  This results in an ugly profile picture.  Lets fix this problem by giving our users the ability to crop their image using jCrop.

Cropping using jCrop and WebImage

jCrop requires a few image properties initially to set the initial crop window.  We’ll create an Editor input model that will give us the ability to both supply those values, and to retrieve the final cropping dimensions to perform the cropping action.
Code Snippet

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    
    namespace WebHelpers.Models.WebImage
    {
        public class EditorInputModel
        {
    
            public ProfileViewModel Profile { get; set; }
            public double Top { get; set; }
            public double Bottom { get; set; }
            public double Left { get; set; }
            public double Right { get; set; }
            public double Width { get; set; }
            public double Height { get; set; }
        }
    }

Out EditorInputModel contains our original ProfileViewModel which supplies our ImageUrl property, but may contain other profile information for the solution you’re implementing.  It also contains both size and position properties for the crop selection.  We’re going to set these properties to set the initial cropping indicator.
Updating our Upload action method:

Paste the following code at Line 17 in our Upload action method above:
Code Snippet

    var editModel = new EditorInputModel()
                    {
                        Profile = model,
                        Width = image.Width,
                        Height = image.Height,
    
                        Top = image.Height * 0.1,
                        Left = image.Width * 0.9,
                        Right = image.Width * 0.9,
                        Bottom = image.Height * 0.9
                    };
                    return View("Editor", editModel);

This code populates our EditorInputModel with the initial information we need to supply jCrop with to do the cropping operation.  We’re passing this information then to the “Editor” view.
The editor view

First, we’re going to use jCrop to allow us to crop our image with a live thumbnail preview.  To do that, we need to display our image twice.  We’ll use the following editor template:
Code Snippet

    @model System.String
    <div id="cropContainer">
        <div id="cropPreview">
            <img src="@(String.IsNullOrEmpty(Model) ? "" : Model)" id="preview" />
        </div>
        <div id="cropDisplay">
            <img src="@(String.IsNullOrEmpty(Model) ? "" : Model)" id="profileImageEditor" />
        </div>
    </div>



This simple editor template creates 2 img tags displaying the image, one named “preview”and one named “profileImageEditor”.  The size of preview is constrained to a 1 to 1 aspect ratio using CSS.

Next, we’ll need our Editor view.
Code Snippet

    @model WebHelpers.Models.WebImage.EditorInputModel
    
    @{
        ViewBag.Title = "Editor";
    }
    @section HeaderSection
    {
        <link href="@Url.Content("~/Content/jquery.Jcrop.css")" rel="stylesheet" type="text/css" />
        <script src="@Url.Content("~/Scripts/jquery.Jcrop.js")" type="text/javascript"></script>
    }
    <h2>Editor</h2>
    <div id="mainform">
    @using(Html.BeginForm("Edit","WebImage", FormMethod.Post))
    {
        @Html.EditorFor(x=>x.Profile.ImageUrl)
       
        @Html.HiddenFor(x=>x.Left)
        @Html.HiddenFor(x=>x.Right)
        @Html.HiddenFor(x=>x.Top)
        @Html.HiddenFor(x=>x.Bottom)
        @Html.HiddenFor(x => x.Profile.ImageUrl)
        <input type='submit' name='action' value='Crop' />
    }
    </div>



First, we’re referencing our jCrop css and javascript files, alogn with creating the form for passing back our cropped image values.

We display our editor for the image, and create hidden HTML fields for the Left, Right, Top, and Bottom properties of the cropping indicator.  These we pass back to perform the cropping operation.

Next, we’ll need to add some javascript in a script tag at the bottom of this view:
Code Snippet

    $(function () {
            jQuery('#profileImageEditor').Jcrop({
                onChange: showPreview,
                onSelect: showPreview,
                setSelect: [@Model.Top, @Model.Left, @Model.Right, @Model.Bottom],
                aspectRatio: 1
            });
        });
    
    
        function showPreview(coords)
        {
            if (parseInt(coords.w) > 0)
            {
                $('#Top').val(coords.y);
                $('#Left').val(coords.x);
                $('#Bottom').val(coords.y2);
                $('#Right').val(coords.x2);
    
                var width = @Model.Width;
                var height = @Model.Height;
                var rx = 100 / coords.w;
                var ry = 100 / coords.h;
       
                jQuery('#preview').css({
                    width: Math.round(rx * width) + 'px',
                    height: Math.round(ry * height) + 'px',
                    marginLeft: '-' + Math.round(rx * coords.x) + 'px',
                    marginTop: '-' + Math.round(ry * coords.y) + 'px'
                });
            }
        }

here we’re calling .Jcrop on our profileImageEditor image tag, setting the jCrop onChange and onSelect callsback to the showPreview function.  We’re also setting the Model.Top, model.left, Model.Right, and Model.Bottom properties to the html form values being automatically set by jCrop.  We’re also constraining the selector to an aspect ratio of 1.

In the showPreview function, we’re grabbing the crop selectors values and updating our thumbnail window to show a preview of the cropping operation.

Now, after launching our app and submitting an image for upload, we’re given the following view:
image

On the left, we have our main image display with our jCrop cropping tool, and our live thumbail perview to the left, along with the “Crop” button to submit our post request.

The last thing we need to do is to implement our Edit action method, which will do the actual cropping using the WebImage helper.
The edit action method


Code Snippet

    public ActionResult Edit(EditorInputModel editor)
            {
                var image = new WebImage("~" + editor.Profile.ImageUrl);
                var height = image.Height;
                var width = image.Width;
    
    
                image.Crop((int)editor.Top, (int)editor.Left, (int)(height – editor.Bottom), (int)(width – editor.Right));
                var originalFile = editor.Profile.ImageUrl;
                editor.Profile.ImageUrl = Url.Content("~/ProfileImages/" + Path.GetFileName(image.FileName));
                image.Resize(100, 100, true, false);
                image.Save(@"~" + editor.Profile.ImageUrl);
                System.IO.File.Delete(Server.MapPath(originalFile));
                return View("Index", editor.Profile);
            }

First, we load the original image form disk using the ImageUrl we had stored in our ProfileViewModel instance. We retrieve the width and eight from the loaded image, as these are our initial width and height values we’ll need to perform our cropping.

We then call the Crop method of the WebImage instance, supplying the top and left values on the original image where the crop begins, and calculating the width and height of the resulting image by subtracting the bottom of the crop tool from the height of the image, and the rigth of the crop tool from the width of the original image.

We then grab the original source image URL before setting the ImageURL property to the new location we’ll be saving our cropped image.  Now that we’ve cropped our image to a 1 to 1 aspect ratio, we’ll resize to the final profile image size, 100 x 100.  We then save the image to our new ImageURL, delete the original source image, and pass the cropped image back to the index page.

After our cropping operation, we’re returned to our index view with our new cropped image.

image

No comments:

Post a Comment