ASP.NET MVC: Crop image by using Jcrop

ASP.NET MVC: Crop image by using JcropHi all, I’d like to show you today very very simple example of how to crop image by using Jcrop library. Jcrop is a very powerful jQuery image cropping plugin. So, why I chose this one – because this plugin supports many browsers and devices like Google Chrome, Mozilla FireFox, IE 8 (even for IE 8 for Windows XP!), 9, 10, 11, Safari (Mac OS), iPad, iPhone and Android. To avoid confusing, especially of newbies in jQuery and ASP.NET MVC, I decided to split current topic by three separated articles. The main goal in these three articles is to show how to use Jcrop in ASP.NET MVC web application and please take a look on what will be described:

1) This post: First and very simple cropping example with static image that we already have on the page.

2) Second post: On the page we only have select file input, after selection image, system uploads file by using ajax file upload as described here and shows image on the page and after that we can crop this one.

3) Third post: The most interesting from my point, there will be all things from the second post, but only difference is that after selecting and uploading, image will be shown in the popup. And also will describe how to crop image with a big aspect ratio like 1900 x 1900 and submit data to the server. Those who do not want read entire article, scroll down to the end of the page and download ASP.NET MVC Demo solution.

OK, let’s begin.

First off we have to download Jcrop library (in my example I use Jcrop-v0.9.1)

And step-by-step

1) Download jquery.Jcrop-0.9.12.zip

2) Create NOT empty ASP.NET MVC solution

3) Remove all HTML created by default in the ~/Views/Home/Index.cshtml and ~/Views/Shared/_Layout.cshtml

4) Include jquery-1.11.1.min.js (please, notice that by default this plugin uses jQuery v1.9.0, i.e. if something will not work, try to include jQuery v1.9.0)

5) Include jquery.Jcrop.min.css and jquery.Jcrop.min.js

6) Download test image At this moment, you have to have next things: Code in the ~/Views/Shared/_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Testing cropping for images</title>

</head>
<body>
    @RenderBody()

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>

    <link href="~/JsLibraries/Jcrop-v0.9.12/css/jquery.Jcrop.min.css" rel="stylesheet" />
    <script src="~/JsLibraries/Jcrop-v0.9.12/js/jquery.Jcrop.min.js"></script>

    @RenderSection("scripts", required: false)
</body>
</html>

Code in the ~/Views/Home/Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

Please, notice one important thing, due to testing purposes, I directly added .js and .css files to the page and do not use Bundling and Minification, in the real-world web applications you must use bundling and add all .js and .css ones in the App_Start/BundleConfig.cs

7) Let’s add to the ~/Views/Home/Index.cshtml next html code which contains “Crop Image” link, origin image and hidden container for displaying cropped image.

<p><a href="#" id="hl-crop-image">Crop Image</a></p>

<p>
    <img id="my-origin-image" src="~/Images/funny_cat_v1.jpg" />
</p>
<p>
    <img id="my-cropped-image" src="#" style="display:none;" />
</p>

8) To the ~/Views/Home/Index.cshtml inside of

@section scripts{

    <script type="text/javascript">


    </script>
}

let’s add javascript code and entire Index.cshtml should looks like next:

@{
    ViewBag.Title = "Home Page";
}

<p><a href="#" id="hl-crop-image">Crop Image</a></p>

<p>
    <img id="my-origin-image" src="~/Images/funny_cat_v1.jpg" />
</p>
<p>
    <img id="my-cropped-image" src="#" style="display:none;" />
</p>

@section scripts{

    <script type="text/javascript">

        var imageCropWidth = 0;
        var imageCropHeight = 0;
        var cropPointX = 0;
        var cropPointY = 0;

        $(document).ready(function () {
            initCrop();      
        });

        $("#hl-crop-image").on("click", function (e) {
            e.preventDefault();
            cropImage();
        });

        function initCrop() {
            $('#my-origin-image').Jcrop({
                onChange: setCoordsAndImgSize,
                aspectRatio: 1
            });
        }

        function setCoordsAndImgSize(e) {

            imageCropWidth = e.w;
            imageCropHeight = e.h;

            cropPointX = e.x;
            cropPointY = e.y;
        }

        function cropImage() {

            if (imageCropWidth == 0 && imageCropHeight == 0) {
                alert("Please select crop area.");
                return;
            }

            $.ajax({
                url: '/Image/CropImage',
                type: 'POST',
                data: {
                    imagePath: $("#my-origin-image").attr("src"),
                    cropPointX: cropPointX,
                    cropPointY: cropPointY,
                    imageCropWidth: imageCropWidth,
                    imageCropHeight: imageCropHeight
                },
                success: function (data) {
                    $("#my-cropped-image")
                        .attr("src", data.photoPath + "?t=" + new Date().getTime())
                        .show();
                },
                error: function (data) { }
            });
        }

    </script>
}

And little description of what is going on in the our javascript. Global variables such as imageCropWidth and imageCropHeight store width and heigh of selected image area and we will be able to send these parameters to the server. CropPointX with cropPointY store coordinates of the point on the figure from which we have to start calculation to be able to crop proper part of image.

ASP.NET MVC: Crop image by using Jcrop

Inside of “document ready”, calls initCrop() method to initialize cropping functionality for our image. After we have attached click event handler for the Crop Image link. Each time, when user changes area of cropping, setCoordsAndImgSize() method fills global variables with needed data that was described above. cropImage() method does main work on the client side, it sends data to the ImageController and in the success section displays new cropped image by using url received from ImageController. Btw. as you noticed, I’ve added t parameter with current timespan to the image url, it helps to refresh cache of the browser, otherwise you will see the same cached image that was before.

9) Move to the ImageController.

Code of ImageController.cs

public class ImageController : Controller
{
    [HttpPost]
    public virtual ActionResult CropImage(string imagePath, int? cropPointX, int? cropPointY, int? imageCropWidth, int? imageCropHeight)
    {
        if (string.IsNullOrEmpty(imagePath) || !cropPointX.HasValue || !cropPointY.HasValue || !imageCropWidth.HasValue || !imageCropHeight.HasValue)
        {
            return new HttpStatusCodeResult((int)HttpStatusCode.BadRequest);
        }

        byte[] imageBytes = System.IO.File.ReadAllBytes(Server.MapPath(imagePath));
        byte[] croppedImage = ImageHelper.CropImage(imageBytes, cropPointX.Value, cropPointY.Value, imageCropWidth.Value, imageCropHeight.Value);

        string tempFolderName = Server.MapPath("~/" + ConfigurationManager.AppSettings["Image.TempFolderName"]);
        string fileName = Path.GetFileName(imagePath);

        try
        {
            FileHelper.SaveFile(croppedImage, Path.Combine(tempFolderName, fileName));
        }
        catch (Exception ex)
        {
            //Log an error     
            return new HttpStatusCodeResult((int)HttpStatusCode.InternalServerError);
        }

        string photoPath = string.Concat("/", ConfigurationManager.AppSettings["Image.TempFolderName"], "/", fileName);
        return Json(new { photoPath = photoPath }, JsonRequestBehavior.AllowGet);
    }
}

CropImage() method crops images regarding to input parameters, but it one uses additional classes that helps work with images by using standard net framework libraries: FileHelper.cs

public static class FileHelper
{
    public static void SaveFile(byte[] content, string path)
    {
        string filePath = GetFileFullPath(path);
        if (!Directory.Exists(Path.GetDirectoryName(filePath)))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
        }

        //Save file
        using (FileStream str = File.Create(filePath))
        {                
            str.Write(content, 0, content.Length);
        }
    }

    public static string GetFileFullPath(string path)
    {
        string relName = path.StartsWith("~") ? path : path.StartsWith("/") ? string.Concat("~", path) : path;

        string filePath = relName.StartsWith("~") ? HostingEnvironment.MapPath(relName) : relName;

        return filePath;
    }
}

and ImageHelper.cs

public static class ImageHelper
{
    public static byte[] CropImage(byte[] content, int x, int y, int width, int height)
    {
        using (MemoryStream stream = new MemoryStream(content))
        {
            return CropImage(stream, x, y, width, height);
        }
    }

    public static byte[] CropImage(Stream content, int x, int y, int width, int height)
    {
        //Parsing stream to bitmap
        using (Bitmap sourceBitmap = new Bitmap(content))
        {
            //Get new dimensions
            double sourceWidth = Convert.ToDouble(sourceBitmap.Size.Width);
            double sourceHeight = Convert.ToDouble(sourceBitmap.Size.Height);
            Rectangle cropRect = new Rectangle(x, y, width, height);

            //Creating new bitmap with valid dimensions
            using (Bitmap newBitMap = new Bitmap(cropRect.Width, cropRect.Height))
            {
                using (Graphics g = Graphics.FromImage(newBitMap))
                {
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                    g.SmoothingMode = SmoothingMode.HighQuality;
                    g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                    g.CompositingQuality = CompositingQuality.HighQuality;

                    g.DrawImage(sourceBitmap, new Rectangle(0, 0, newBitMap.Width, newBitMap.Height), cropRect, GraphicsUnit.Pixel);

                    return GetBitmapBytes(newBitMap);
                }
            }
        }
    }

    public static byte[] GetBitmapBytes(Bitmap source)
    {
        //Settings to increase quality of the image
        ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders()[4];
        EncoderParameters parameters = new EncoderParameters(1);
        parameters.Param[0] = new EncoderParameter(Encoder.Quality, 100L);

        //Temporary stream to save the bitmap
        using (MemoryStream tmpStream = new MemoryStream())
        {
            source.Save(tmpStream, codec, parameters);

            //Get image bytes from temporary stream
            byte[] result = new byte[tmpStream.Length];
            tmpStream.Seek(0, SeekOrigin.Begin);
            tmpStream.Read(result, 0, (int)tmpStream.Length);

            return result;
        }
    }
}

and let’s add Image.TempFolderName key to appSettings section in the web.config

<appSettings>
  <add key="Image.TempFolderName" value="Temp" />    
</appSettings>

 

Little tip:

Probably you noticed that if you remove aspectRatio: 1 from initCrop() method, you will be able to select random cropping area on the image, like this:

2014-09-02_2130

in this case, we have image with width = 370 and height = 90. So, be carefully if you are using crop plugin without aspectRatio: 1 parameter, as in the future you will need to display cropped images on the webpages and it can be nightmare if you have many images with different width and height. Because, (most likely), you will need display images in the div with 100 x 100 px or process it somehow and it will be really difficult and result will be like next:

Image stretched by width

image stretched by width

Image stretched by height

Image stretched by height

As you can see, you cannot correctly show image with different aspect ration in the 100 x 100 or 300 x 300px div. To avoid troubles, in most cases you have to use aspectRatio: 1 parameter and user will select image with correct aspect ration.

That’s it!

So, in the next article I will show you how to use Jcrop plugin with Ajax File Uploader.

Have a good coding!

Download DEMO (Visual Studio 2013)

This work, “ASP.NET MVC: Crop image by using Jcrop”, uses image from flickr that was added by Laura Bittner, used under CC BY 4.0. “ASP.NET MVC: Crop image by using Jcrop” is licensed under CC BY 4.0 by Sergey Boiko.

 

  • Nemo

    I applied your code but as a result, the cropped image is not as the preview.. Do you think i need to resize image first on server before cropping it?

    • http://powerdotnetcore.com Sergey Boiko

      1) the cropped image is not as the preview – sorry, I don’t understand what do you mean…
      2) Do you think i need to resize image first on server before creating it? – it depends on case, but anyway resizing and cropping should be on server side.

  • http://www.code-hound.com/ J Khalaf

    Thanks for the article. How about when a user must first upload an image? In my application, users need to upload images, and the images must maintain a certain aspect ration, which is why I’m looking into using JCrop. Do I temporarily store the image somewhere, then load it back up onto a page and do all the above so that the user can crop to correct size? Then delete image from old temp location?

  • Russell Cash

    I had to change the callback from onChange to onSelect in order to get this to work. For me, onChange was firing continuously so the button click event never fired. Switching over seemed to solve the issue.

  • Jim

    Thanks for the great article and the example project. I was able to get the example project to work as expected, but wasn’t able to incorporate the code into my project.

    I can display the image, but I don’t get the ‘crop’ frame when I start clicking on the image. If I click on the hyperlink “Crop Image” I get the appropriate error from the code: “Please select crop area.”

    I’ve tried different versions of Jquery and currently set to jquery-1.9.0.min.js

    I also changed the “onChange” to “onSelect” in the function initCrop… didn’t seem to make a difference but onChange doesn’t appear to be a recognized keyword by intellisense.

    My project is MVC5 & bootstrap.