How should I crop an image at client side using jcrop and upload it?
Asked Answered
R

2

13

I am working on a component in which there is file-upload HTML control, upon selecting an image using the file-upload element, the image would be rendered on the HTML5 Canvas element.

Here is JSFiddle with sample code: https://jsfiddle.net/govi20/spmc7ymp/

id=target => selector for jcrop element
id=photograph => selector for fileupload element
id=preview => selector for canvas element
id=clear_selection => selector for a button which would clear the canvas

Third-party JS libraries used:

<script src="./js/jquery.min.js"></script>
<script src="./js/jquery.Jcrop.js"></script>
<script src="./js/jquery.color.js"></script>

Setting up the JCrop:

<script type="text/javascript">

jQuery(function($){
 
var api;

$('#target').Jcrop({
  // start off with jcrop-light class
  bgOpacity: 0.5,
  keySupport: false,
  bgColor: 'black',
  minSize:[240,320],
  maxSize:[480,640],
  onChange : updatePreview,
  onSelect : updatePreview, 
  height:160,
  width:120,
  addClass: 'jcrop-normal'
},function(){
  api = this;
  api.setSelect([0,0,240,320]);
  api.setOptions({ bgFade: true });
  api.ui.selection.addClass('jcrop-selection');
  });

});

clear canvas event which will be triggered on clear button click event:

jQuery('#clear_selection').click(function(){
  $('#target').Jcrop({    
      
      setSelect: [0,0,0,0],
    });
});

code that renders image on HTML5 Canvas:

function readURL(input) {
    
    if (input.files && input.files[0]) {
        var reader = new FileReader();
        reader.onload = function (e) {
            $('#target').attr('src', e.target.result);
            setProperties();       
        }
        reader.readAsDataURL(input.files[0]);
    }
}

function setProperties(){
   $('#target').Jcrop({         
              setSelect: [0,0,240,320]
        }); 
}
$("#photograph").change(function(){
    readURL(this);     
});

code to crop and render an image on the canvas:

    var canvas = document.getElementById('preview'),
    context = canvas.getContext('2d');

    make_base();
    function updatePreview(c) {
        console.log("called");
        if(parseInt(c.w) > 0) {
            // Show image preview
            var imageObj = $("#target")[0];
            var canvas = $("#preview")[0];
            var context = canvas.getContext("2d");
            context.drawImage(imageObj, c.x, c.y, c.w, c.h, 0, 0, canvas.width, canvas.height);
        }
    };

    function make_base() {
        console.log("make_base called");
        var base_image = new Image();
        base_image.src = '';
        base_image.onload = function () {
            context.drawImage(base_image, 0, 0);
        }
    }

Here are a bunch of issues I am facing with the above setup:

  1. updatePreview function is not getting called on selection, hence the canvas is not getting rendered.
  2. crop selection box is not draggable (I am using bootstrap CSS, I suspect it is due to missing/mismatching dependency).
  3. Canvas is HTML5 element, which means the end-user must have an HTML5 compatible browser, I am working on an app that has millions of users. Forcing users to use the latest browser is not a feasible option. What should be the fallback mechanism here?
Ralph answered 7/1, 2016 at 9:4 Comment(0)
B
19

Seahorsepip's answer is fantastic. I made a lot of improvements on the non-fallback answer.

http://jsfiddle.net/w1Lh4w2t/

I would recommend not doing that strange hidden png thing, when an Image object works just as well (so long as we're not supporting fallbacks).

var jcrop_api;
var canvas;
var context;
var image;
var prefsize;

Though even then we are, you're better off getting that data out of the canvas at the end and putting it in that field only at the end.

function loadImage(input) {
  if (input.files && input.files[0]) {
    var reader = new FileReader();
    reader.onload = function(e) {
      image = new Image();
      image.src = e.target.result;
      validateImage();
    }
    reader.readAsDataURL(input.files[0]);
  }
}

But, if you want more functions than just crop, if we attach the jcrop to an inserted canvas (which we destroy with the jcrop on refresh). We can easily do anything we can do with a canvas, then validateImage() again and have the updated image visible in place.

function validateImage() {
  if (canvas != null) {
    image = new Image();
    image.src = canvas.toDataURL('image/png');
  }
  if (jcrop_api != null) {
    jcrop_api.destroy();
  }
  $("#views").empty();
  $("#views").append("<canvas id=\"canvas\">");
  canvas = $("#canvas")[0];
  context = canvas.getContext("2d");
  canvas.width = image.width;
  canvas.height = image.height;
  context.drawImage(image, 0, 0);
  $("#canvas").Jcrop({
    onSelect: selectcanvas,
    onRelease: clearcanvas,
    boxWidth: crop_max_width,
    boxHeight: crop_max_height
  }, function() {
    jcrop_api = this;
  });
  clearcanvas();
}

Then on submit we submit any pending operations, like applyCrop() or applyScale(), adding data into hidden fields for fallback stuff, if we have those things needed. We then have a system we can easily just modify the canvas, in any way, then when we submit the canvas data gets sent properly.

function applyCrop() {
  canvas.width = prefsize.w;
  canvas.height = prefsize.h;
  context.drawImage(image, prefsize.x, prefsize.y, prefsize.w, prefsize.h, 0, 0, canvas.width, canvas.height);
  validateImage();
}

The canvas is added to a div views.

 <div id="views"></div>

To catch the attached file in PHP (drupal), I used something like:

    function makeFileManaged() {
        if (!isset($_FILES['croppedfile']))
            return NULL;
        $path = $_FILES['croppedfile']['tmp_name'];
        if (!file_exists($path))
            return NULL;
        $result_filename = $_FILES['croppedfile']['name'];
        $uri = file_unmanaged_move($path, 'private://' . $result_filename, FILE_EXISTS_RENAME);
        if ($uri == FALSE)
            return NULL;
        $file = File::Create([
                    'uri' => $uri,
        ]);
        $file->save();
        return $file->id();
    }
Berns answered 12/6, 2016 at 9:36 Comment(22)
Mostly try the jfiddle, should be fully functional save the upload to jfiddle thing.Berns
Two things 1) I am uploading an Image using upload controller, bootstrap modal is not showing Image, then I am clicking on crop button now it is showing the Image. Now If I close the modal and upload the Image again it's showing an old image.Ralph
The image is being divided up from the canvas, and it's saving the canvas. The image being uploaded is built from the modified canvas. You either need to update the image from the bootstrap modal by sending the data back to the page and rebuilding the canvas there, or submit the modified image directly from the bootstrap modal.Berns
The way mine is setup is wysiwyg with regard to the current canvas. You can simply refresh the canvas and apply modifications to it. And it'll display the currentmost version of the image as well as the canvas. But, you need to share the modifications back to the other canvas/image etc, if that is where the uploading happens.Berns
My guess is that firefox is destroying the items inside the bootstrap modal after it's closed. This includes the canvas object that we have a reference to. So I theory if that's the case pressing Canvas To Main after that closes will properly add that canvas to both Mozilla and Chrome. But, in either case, adding the create a new canvas and paint the image on that canvas should give it a canvas to save.Berns
and why it is not working without that alert on chrome too? weird, not getting what is happening here,Ralph
Oh, hadn't checked that. Lemme see.Berns
Oh, the bug where it doesn't have a proper length and height and shrinks down to nothing on both Chrome and Firefox?Berns
If I had to guess, its because it's not loaded yet. It doesn't trigger on image load to set the canvas size so it jumps that step and doesn't have the image size at that moment.Berns
The alert didn't help, but it did stall for time which caused it to trigger correctly.Berns
@piechuckerr, This should work now. jsfiddle.net/spmc7ymp/26 I think the error is it trying to add it to the canvas before the div where the canvas is to exist, does not yet, exist. So it needs the modal fully loaded, before it can apply this stuff to the canvas as such.Berns
Let us continue this discussion in chat.Ralph
@Tatarize, i am trying to implement your given code, however i have run into problems.After form submit, how do i capture the blob data in 'whatever.php'? For testing purpose when i do print_r($_POST['cropped_image[]']); it says undefined index cropped_image[]. Please, please tell me with an example how do i capture the blob data from php page and convert it an image and save it in a folder on the server. Also when i do print_r($_POST']); it shows an empty array.Bounden
I managed to do it but it's in like a $files element in the PHP. It uploads which is the main thing but I had to fetch it from the globals in an odd way.Berns
I'm pretty sure it totally already is a file. It got posted as appended to the post data and was uploaded you fetch it with $_FILES somehow but I had to debug to find exactly where that was. @gomeshmunda. I might be able to check if the old catch in some PHP was still there. But, it will have already uploaded, you need to rename it and move it to the right sections in the PHP, triggered by the post. But, it's stored in $_FILES rather than in $_POST as all uploaded files appended to post would be apt to do.Berns
Yep,you were right about $_FILES.But its showing some weird values i guess.I would love to show you the screen shot, but don't no how to here...Bounden
I think my PHP to catch it was like: if (!isset($_FILES['croppedfile'])) return NULL; $path = $_FILES['croppedfile']['tmp_name']; if (!file_exists($path)) return NULL; $result_filename = $_FILES['croppedfile']['name'];Berns
@gomeshmunda I was using it for Drupal so I used something like pastebin.com/dsKfqdnY to move the file and put it into managed usage. The values are kinda weird but I think you can set them somewhat, but the file go uploaded without any server side cropping which was the big thing.Berns
Added to the answer. Hope it helps, though that's where the file is and beyond that is beyond the scope of the original question by a bit too far. It uploads and gets put in the webservers temp files, and needs to be grabbed from the POST by accessing the stuff in $_FILES. Debugging the PHP helped a lot.Berns
@Berns thanks for the codes, let me study them and hopefully i will be able to convert it into an image and save it on a folder.Thanks again for replying to my questions :)Bounden
Is it possible that the code no longer works for jQuery 3.3.1? I copied the code exactly as it is, also saved the Jcrop CSS and JS source files, but when I select an image, nothing happensPantomimist
I changed the jQuery on the posted fiddle, and at 3.3.1 it worked fine. Loaded image, cropped, rotated, flipped, perfectly fine.Berns
J
20

Here's basic html 5 code:

https://jsfiddle.net/zm7e0jev/

This code crops the image, shows a preview and sets the value of an input element to the base64 encoded cropped image.

You can fetch the image file in php the following way:

//File destination
$destination = "/folder/cropped_image.png";
//Get convertable base64 image string
$image_base64 = $_POST["png"];
$image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
$image_base64 = str_replace(" ", "+", $image_base64);
//Convert base64 string to image data
$image = base64_decode($image_base64);
//Save image to final destination
file_put_contents($destination, $image);

Submitting base64 image string as a post variable has it's server post size limits and base64 encoding makes the cropped image file size even bigger (~33%) then the raw data of the cropped image would be which makes the upload take even longer.

To set the post size limit: What is the size limit of a post request?

Keep in mind that an increased post size limit can be abused for a DoS attack as example.

Instead I suggest converting the base64 cropped image to a data blob and then add it to the form on submit as a file:

https://jsfiddle.net/g3ysk6sf/

Then you can fetch the image file in php the following way:

//File destination
$destination = "/folder/cropped_image.png";
//Get uploaded image file it's temporary name
$image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
//Move temporary file to final destination
move_uploaded_file($image_tmp_name, $destination);

Update:

FormData() is only partially support in IE10 and not supported in older versions of IE

So I suggest sending the base64 string as a fallback, though this will cause problems with bigger images so it needs to check the filesize and show an error popup when the image is above a specific size.

I'll post an update with the fallback code below when I've got it working.

Update 2:

I added a fallback for IE10 and below:

https://jsfiddle.net/oupxo3pu/

The only limitation is the image size that can be submitted when using IE10 and below, in case the image size is too big the js code will throw an error. The maximum size to work for post values is different between each server, the js code has a variable to set the maximum size.

The php code below is adapted to work with above fallback:

//File destination
$destination = "/folder/cropped_image.png";
if($_POST["png"]) {//IE10 and below
    //Get convertable base64 image string
    $image_base64 = $_POST["png"];
    $image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
    $image_base64 = str_replace(" ", "+", $image_base64);
    //Convert base64 string to image data
    $image = base64_decode($image_base64);
    //Save image to final destination
    file_put_contents($destination, $image);
} else if($_FILES["cropped_image"]) {//IE11+ and modern browsers
    //Get uploaded image file it's temporary name
    $image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
    //Move temporary file to final destination
    move_uploaded_file($image_tmp_name, $destination);
}
There is no fallback code for the canvas element yet, I'm looking into it.
The post size limitation in the fallback for older browsers is one of the reasons I dropped support for older browsers myself.

Update 3:

The fallback I recommend for the canvas element in IE8:

http://flashcanvas.net/

It supports all the canvas functions the cropping code needs.

Keep in mind it requires flash. There is a canvas fallback (explorercanvas) that does not require flash but it does not support the function toDataURL() which we need to save our cropped image.

Jahn answered 10/1, 2016 at 14:29 Comment(14)
At my phone currently, I'll be home in a hour or two :/ Traveling by train is a horrible slow experience....Jahn
Also a client side cropped image uploading is a tricky thing that took me some work, in first instant I made it a base64 string but that ran into issues on submit because of it's length. After that I added code to make the base64 string into an image file blob and then I added that to the form using some code that is html5(so that would also require a fallback). I ended dropping older browsers all together in the project because I kept running into these limitations and making fallbacks for each limitation took too much time and work.Jahn
Back, gimme a minute to make ajs fiddle.Jahn
Took a bit longer to remove all unnecessary code and bring it back to the basics :PJahn
@piechuckerr Updated the answer with all the info you need, if you have any more questions leave a comment :DJahn
Okay will try the stuff you posted and will let you know result.Ralph
I didnt test all the php code so let me know if it works, the php code is also very basic, depending on the image requirements I highly suggest checks, at least make sure to check if the image is a png file since you can't trust client side code alone in security, always double check server side.Jahn
Yes I will port that to java and offer you the bounty asapRalph
Thanks, so you're using java server side? That's kinda rare :PJahn
Thanks for the bounty :DJahn
facing few problems please have a look at it. #37828855Ralph
I would recommend a fallback for canvas be simply not allowing a crop, and submitting the file through the typical file object. Do not prevent default, do not allow cropping, simply submit the file in the form. Don't tell them cropping would be available if they had a better browser. Also, this should be the non-modified submission too. If they don't want to crop, send the binary file unmolested.Berns
@Jahn Here is a suggestion to make this work on Firefox. Basically 2 changes. First, I added trueSize to Jcrop(), and next I used Math.max() in drawImage() to prevent zero coordinates which will cause an error. Here is the updated jsfiddle: jsfiddle.net/v6t72f99/13Expanse
no longer works with firefox or edge. replace the context.draw with this: " if (coords.w !== 0 && coords.h !== 0) { context.drawImage(imageObj, coords.x, coords.y, coords.w, coords.h, 0, 0, canvas.width, canvas.height); }"Orlandoorlanta
B
19

Seahorsepip's answer is fantastic. I made a lot of improvements on the non-fallback answer.

http://jsfiddle.net/w1Lh4w2t/

I would recommend not doing that strange hidden png thing, when an Image object works just as well (so long as we're not supporting fallbacks).

var jcrop_api;
var canvas;
var context;
var image;
var prefsize;

Though even then we are, you're better off getting that data out of the canvas at the end and putting it in that field only at the end.

function loadImage(input) {
  if (input.files && input.files[0]) {
    var reader = new FileReader();
    reader.onload = function(e) {
      image = new Image();
      image.src = e.target.result;
      validateImage();
    }
    reader.readAsDataURL(input.files[0]);
  }
}

But, if you want more functions than just crop, if we attach the jcrop to an inserted canvas (which we destroy with the jcrop on refresh). We can easily do anything we can do with a canvas, then validateImage() again and have the updated image visible in place.

function validateImage() {
  if (canvas != null) {
    image = new Image();
    image.src = canvas.toDataURL('image/png');
  }
  if (jcrop_api != null) {
    jcrop_api.destroy();
  }
  $("#views").empty();
  $("#views").append("<canvas id=\"canvas\">");
  canvas = $("#canvas")[0];
  context = canvas.getContext("2d");
  canvas.width = image.width;
  canvas.height = image.height;
  context.drawImage(image, 0, 0);
  $("#canvas").Jcrop({
    onSelect: selectcanvas,
    onRelease: clearcanvas,
    boxWidth: crop_max_width,
    boxHeight: crop_max_height
  }, function() {
    jcrop_api = this;
  });
  clearcanvas();
}

Then on submit we submit any pending operations, like applyCrop() or applyScale(), adding data into hidden fields for fallback stuff, if we have those things needed. We then have a system we can easily just modify the canvas, in any way, then when we submit the canvas data gets sent properly.

function applyCrop() {
  canvas.width = prefsize.w;
  canvas.height = prefsize.h;
  context.drawImage(image, prefsize.x, prefsize.y, prefsize.w, prefsize.h, 0, 0, canvas.width, canvas.height);
  validateImage();
}

The canvas is added to a div views.

 <div id="views"></div>

To catch the attached file in PHP (drupal), I used something like:

    function makeFileManaged() {
        if (!isset($_FILES['croppedfile']))
            return NULL;
        $path = $_FILES['croppedfile']['tmp_name'];
        if (!file_exists($path))
            return NULL;
        $result_filename = $_FILES['croppedfile']['name'];
        $uri = file_unmanaged_move($path, 'private://' . $result_filename, FILE_EXISTS_RENAME);
        if ($uri == FALSE)
            return NULL;
        $file = File::Create([
                    'uri' => $uri,
        ]);
        $file->save();
        return $file->id();
    }
Berns answered 12/6, 2016 at 9:36 Comment(22)
Mostly try the jfiddle, should be fully functional save the upload to jfiddle thing.Berns
Two things 1) I am uploading an Image using upload controller, bootstrap modal is not showing Image, then I am clicking on crop button now it is showing the Image. Now If I close the modal and upload the Image again it's showing an old image.Ralph
The image is being divided up from the canvas, and it's saving the canvas. The image being uploaded is built from the modified canvas. You either need to update the image from the bootstrap modal by sending the data back to the page and rebuilding the canvas there, or submit the modified image directly from the bootstrap modal.Berns
The way mine is setup is wysiwyg with regard to the current canvas. You can simply refresh the canvas and apply modifications to it. And it'll display the currentmost version of the image as well as the canvas. But, you need to share the modifications back to the other canvas/image etc, if that is where the uploading happens.Berns
My guess is that firefox is destroying the items inside the bootstrap modal after it's closed. This includes the canvas object that we have a reference to. So I theory if that's the case pressing Canvas To Main after that closes will properly add that canvas to both Mozilla and Chrome. But, in either case, adding the create a new canvas and paint the image on that canvas should give it a canvas to save.Berns
and why it is not working without that alert on chrome too? weird, not getting what is happening here,Ralph
Oh, hadn't checked that. Lemme see.Berns
Oh, the bug where it doesn't have a proper length and height and shrinks down to nothing on both Chrome and Firefox?Berns
If I had to guess, its because it's not loaded yet. It doesn't trigger on image load to set the canvas size so it jumps that step and doesn't have the image size at that moment.Berns
The alert didn't help, but it did stall for time which caused it to trigger correctly.Berns
@piechuckerr, This should work now. jsfiddle.net/spmc7ymp/26 I think the error is it trying to add it to the canvas before the div where the canvas is to exist, does not yet, exist. So it needs the modal fully loaded, before it can apply this stuff to the canvas as such.Berns
Let us continue this discussion in chat.Ralph
@Tatarize, i am trying to implement your given code, however i have run into problems.After form submit, how do i capture the blob data in 'whatever.php'? For testing purpose when i do print_r($_POST['cropped_image[]']); it says undefined index cropped_image[]. Please, please tell me with an example how do i capture the blob data from php page and convert it an image and save it in a folder on the server. Also when i do print_r($_POST']); it shows an empty array.Bounden
I managed to do it but it's in like a $files element in the PHP. It uploads which is the main thing but I had to fetch it from the globals in an odd way.Berns
I'm pretty sure it totally already is a file. It got posted as appended to the post data and was uploaded you fetch it with $_FILES somehow but I had to debug to find exactly where that was. @gomeshmunda. I might be able to check if the old catch in some PHP was still there. But, it will have already uploaded, you need to rename it and move it to the right sections in the PHP, triggered by the post. But, it's stored in $_FILES rather than in $_POST as all uploaded files appended to post would be apt to do.Berns
Yep,you were right about $_FILES.But its showing some weird values i guess.I would love to show you the screen shot, but don't no how to here...Bounden
I think my PHP to catch it was like: if (!isset($_FILES['croppedfile'])) return NULL; $path = $_FILES['croppedfile']['tmp_name']; if (!file_exists($path)) return NULL; $result_filename = $_FILES['croppedfile']['name'];Berns
@gomeshmunda I was using it for Drupal so I used something like pastebin.com/dsKfqdnY to move the file and put it into managed usage. The values are kinda weird but I think you can set them somewhat, but the file go uploaded without any server side cropping which was the big thing.Berns
Added to the answer. Hope it helps, though that's where the file is and beyond that is beyond the scope of the original question by a bit too far. It uploads and gets put in the webservers temp files, and needs to be grabbed from the POST by accessing the stuff in $_FILES. Debugging the PHP helped a lot.Berns
@Berns thanks for the codes, let me study them and hopefully i will be able to convert it into an image and save it on a folder.Thanks again for replying to my questions :)Bounden
Is it possible that the code no longer works for jQuery 3.3.1? I copied the code exactly as it is, also saved the Jcrop CSS and JS source files, but when I select an image, nothing happensPantomimist
I changed the jQuery on the posted fiddle, and at 3.3.1 it worked fine. Loaded image, cropped, rotated, flipped, perfectly fine.Berns

© 2022 - 2024 — McMap. All rights reserved.