Cropper.js & ImageSharp

Cropper.js & ImageSharp

A simple demo showing the usage of Cropper.js in combination with the backend processing of the SixLabors ImageSharp library.

Download VS Project

Code Snippets

I wanted to use the data generated with the Advanced Cropper.js Canvas and process the image server side. This demo can Rotate, Crop, Flip horizontal and vertical, zoom out and crop an area larger than the original image. The JS can crop the image and send the data to the server also as seen here , but I also want to do the cropping etc on already saved images or in a Windows enviroment.

When looking for a working demo and/or example I found out there were none. And the few examples I could find did not handle rotation or cropping with dimesions larger than the source image (correctly).

So I created it using SixLabors ImageSharp. This example uses MVC, but it can easily be adapted for other .Net purposes. For simplicity there is no error checking or input validation for the correct file type etc.

Note that you also need the ImageSharp Drawing library.

Cropper.js generates data that looks like this. I deserialize this in the backend to perform the cropping actions. The class CropperData is used for that. Note that this example uses the "basic" cropper as seen here.

{
    "x": 377.4249874715656,
    "y": 524.5012332206522,
    "width": 1285.0810512058467,
    "height": 722.8580913032888,
    "rotate": 45,
    "scaleX": 1,
    "scaleY": 1
}

So first a Class to Deserialize the Cropper data into.

public class CropperData
{
    public double x { get; set; }
    public double y { get; set; }
    public double width { get; set; }
    public double height { get; set; }
    public int rotate { get; set; }
    public int scaleX { get; set; }
    public int scaleY { get; set; }
}

The method that does all the work.

using System;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.IO;

public byte[] doCropperStuff(CropperData cropperData, byte[] oldImage)
{
    //some variables
    int cropX = (int)Math.Abs(cropperData.x);
    int cropY = (int)Math.Abs(cropperData.y);
    int cropWidth = (int)cropperData.width;
    int cropHeight = (int)cropperData.height;
    int imageWidth = 0;
    int imageHeight = 0;
    int posX = 0;
    int posY = 0;

    //the background color used when the crop dimensions are outside the image
    var fillColor = new Rgba32(96, 111, 145);

    //create a new memorystream and load the image
    using (var stream = new MemoryStream())
    using (var image = Image.Load(new MemoryStream(oldImage)))
    {
        //auto orient the image
        image.Mutate(x => x.AutoOrient());

        //flip horizontal
        if (cropperData.scaleX == -1)
        {
            image.Mutate(x => x.Flip(FlipMode.Horizontal));
        }

        //flip vertical
        if (cropperData.scaleY == -1)
        {
            image.Mutate(x => x.Flip(FlipMode.Vertical));
        }

        //rotate
        if (cropperData.rotate != 0)
        {
            image.Mutate(x => x.Rotate(cropperData.rotate).BackgroundColor(fillColor));
        }

        imageWidth = image.Width;
        imageHeight = image.Height;

        //check the dimensions and position of the crop and calculate image size and crop position
        //X-axis
        if (cropperData.x < 0 && cropX + cropWidth > imageWidth)
        {
            imageWidth = cropWidth;
            posX = cropX;
            cropX = 0;
        }
        else if (cropperData.x < 0)
        {
            imageWidth = imageWidth + cropX;
            posX = cropX;
            cropX = 0;
        }
        else if (cropX + cropWidth > imageWidth)
        {
            imageWidth = cropX + cropWidth;
        }

        //Y-axis
        if (cropperData.y < 0 && cropY + cropHeight > imageHeight)
        {
            imageHeight = cropHeight;
            posY = cropY;
            cropY = 0;
        }
        else if (cropperData.y < 0)
        {
            imageHeight = imageHeight + cropY;
            posY = cropY;
            cropY = 0;
        }
        else if (cropY + cropHeight > imageHeight)
        {
            imageHeight = cropY + cropHeight;
        }

        //create a new image with the correct dimension for the image and the crop
        using (var newImage = new Image<Rgba32>(Configuration.Default, imageWidth, imageHeight, fillColor))
        {
            //position the image onto the new one
            newImage.Mutate(x => x.DrawImage(image, new Point(posX, posY), 1));

            //now do the actual cropping
            newImage.Mutate(x => x.Crop(new Rectangle(cropX, cropY, cropWidth, cropHeight)).BackgroundColor(fillColor));

            //set the compression level and save the image to a memory stream (100 = best quality image)
            newImage.Save(stream, new JpegEncoder { Quality = 100 });

            //maak van de stream een byte array
            return stream.ToArray();
        }
    }
}
                    

The Controller.

using System;
using System.Web.Mvc;
using WebApplication1.Models;
using System.IO;
using Newtonsoft.Json;

namespace WebApplication1.Controllers
{
    public class ExampleController : Controller
    {
        public ActionResult Index()
        {
            return View(new ExampleViewModel());
        }


        [HttpPost]
        public ActionResult Index(ExampleViewModel model)
        {
            //check if the values are present
            if (model.image_upload == null || string.IsNullOrEmpty(model.cropper_data))
            {
                return View(model);
            }

            //use newtonsoft.json to deserialize the posted cropper data into a custom class
            var cropperData = JsonConvert.DeserializeObject<CropperData>(model.cropper_data);

            //use a memorystream to get the uploaded image as a byte array
            using (MemoryStream stream = new MemoryStream())
            {
                model.image_upload.InputStream.Position = 0;
                model.image_upload.InputStream.CopyTo(stream);

                //get the cropped image as byte array
                var binary_image = doCropperStuff(cropperData, stream.ToArray());

                //make the image a base64 stream to display on the page
                model.cropper_image = string.Format("data:image/jpeg;base64,{0}", Convert.ToBase64String(binary_image));
            }

            return View(model);
        }
    } 
}
                    

The Model.

using System;
using System.ComponentModel.DataAnnotations;
using System.Web;

namespace WebApplication1.Models
{
    public class ExampleViewModel
    {
        [Display(Name = "Upload an image")]
        public HttpPostedFileBase image_upload { get; set; }

        public string cropper_data { get; set; }

        public string cropper_image { get; set; }
    }
}

The front-end HTML in Razor.

@model WebApplication1.Models.ExampleViewModel

@using (Html.BeginForm("Index", "Example", FormMethod.Post, new { @enctype = "multipart/form-data", @role = "form" }))
{
    <div class="container">

        <div class="row">
            <div class="col-6">

                <div class="form-group">
                    @Html.LabelFor(m => m.image_upload)

                    <div class="custom-file">
                        @Html.TextBoxFor(m => m.image_upload, new { type = "file", @class = "custom-file-input", capture = "camera", accept = ".bmp,.gif,.jpg,.jpeg,.png" })
                        <label class="custom-file-label"></label>
                    </div>
                </div>

            </div>
        </div>

        <div class="row">
            <div class="col pt-3 pb-4">

                <button class="btn btn-primary" type="submit">
                    Upload and Crop
                </button>

            </div>
        </div>

        <div class="row">
            <div class="col-6">

                @if (!string.IsNullOrEmpty(Model.cropper_image))
                {
                    <img src="@Model.cropper_image" class="img-fluid img-thumbnail" />
                }

                <img id="image">

            </div>
        </div>

    </div>

    @Html.HiddenFor(m => m.cropper_data)
}

<link href="/cropper.css" rel="stylesheet" />
<script src="/cropper.js"></script>

And finally the Javascript.

//check if a image is selected
$('.custom-file-input').change(function () {
    var file = $(this)[0].files[0];
    var $cropper_image = $('#image');

    //read the image and show on the page
    var reader = new FileReader();
    reader.onload = function (e) {
        $cropper_image.attr('src', e.target.result);
        $cropper_image.show();

        //start cropper.js
        startCropper();
    };

    reader.readAsDataURL(file);
});

//this initializes the cropper
function startCropper() {
    const image = document.getElementById('image');
    const cropper = new Cropper(image, {
        aspectRatio: NaN,
        crop(event) {
            var cropperdata = JSON.stringify(cropper.getData(true));
            $('#cropper_data').val(cropperdata);
        },
    });
}

When implementing the code in the actual project I needed it for, the line using (var newImage = new Image(Configuration.Default, 800, 600)) kept giving the following error when compiling (Visible in Output Window in VS)

"The type 'SixLabors.ImageSharp.PixelFormats.Rgba32' cannot be used as type parameter 'TPixel' in the generic type or method 'Image'. There is no boxing conversion from 'SixLabors.ImageSharp.PixelFormats.Rgba32' to '?'."

While it does work in other projects. Solved it by creating a separate Class Library containing doCropperStuff() and adding it to the project.