How should I crop the image at client side using jcrop and upload it?

41,811

Solution 1

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();
    }

Solution 2

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.

Share:
41,811
Govinda Sakhare
Author by

Govinda Sakhare

Keep asking the dumb questions! It is better to look like a fool, than to be one.

Updated on February 13, 2022

Comments

  • Govinda Sakhare
    Govinda Sakhare over 2 years

    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?
  • seahorsepip
    seahorsepip over 8 years
    At my phone currently, I'll be home in a hour or two :/ Traveling by train is a horrible slow experience....
  • seahorsepip
    seahorsepip over 8 years
    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.
  • seahorsepip
    seahorsepip over 8 years
    Back, gimme a minute to make ajs fiddle.
  • seahorsepip
    seahorsepip over 8 years
    Took a bit longer to remove all unnecessary code and bring it back to the basics :P
  • seahorsepip
    seahorsepip over 8 years
    @piechuckerr Updated the answer with all the info you need, if you have any more questions leave a comment :D
  • Govinda Sakhare
    Govinda Sakhare over 8 years
    Okay will try the stuff you posted and will let you know result.
  • seahorsepip
    seahorsepip over 8 years
    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.
  • Govinda Sakhare
    Govinda Sakhare over 8 years
    Yes I will port that to java and offer you the bounty asap
  • seahorsepip
    seahorsepip over 8 years
    Thanks, so you're using java server side? That's kinda rare :P
  • seahorsepip
    seahorsepip over 8 years
    Thanks for the bounty :D
  • Tatarize
    Tatarize almost 8 years
    Mostly try the jfiddle, should be fully functional save the upload to jfiddle thing.
  • Govinda Sakhare
    Govinda Sakhare almost 8 years
    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.
  • Tatarize
    Tatarize almost 8 years
    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.
  • Tatarize
    Tatarize almost 8 years
    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.
  • Govinda Sakhare
    Govinda Sakhare almost 8 years
    facing few problems please have a look at it. stackoverflow.com/questions/37828355/…
  • Tatarize
    Tatarize almost 8 years
    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.
  • Govinda Sakhare
    Govinda Sakhare almost 8 years
    and why it is not working without that alert on chrome too? weird, not getting what is happening here,
  • Tatarize
    Tatarize almost 8 years
    Oh, hadn't checked that. Lemme see.
  • Tatarize
    Tatarize almost 8 years
    Oh, the bug where it doesn't have a proper length and height and shrinks down to nothing on both Chrome and Firefox?
  • Tatarize
    Tatarize almost 8 years
    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.
  • Tatarize
    Tatarize almost 8 years
    The alert didn't help, but it did stall for time which caused it to trigger correctly.
  • Tatarize
    Tatarize almost 8 years
    @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.
  • Govinda Sakhare
    Govinda Sakhare almost 8 years
  • Tatarize
    Tatarize almost 8 years
    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.
  • Max Stern
    Max Stern over 7 years
    @seahorsepip 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/13
  • CountMurphy
    CountMurphy over 7 years
    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); }"
  • gomesh munda
    gomesh munda over 6 years
    @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.
  • Tatarize
    Tatarize over 6 years
    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.
  • Tatarize
    Tatarize over 6 years
    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.
  • gomesh munda
    gomesh munda over 6 years
    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...
  • Tatarize
    Tatarize over 6 years
    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'];
  • Tatarize
    Tatarize over 6 years
    @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.
  • Tatarize
    Tatarize over 6 years
    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.
  • gomesh munda
    gomesh munda over 6 years
    @Tatarize 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 :)
  • Admin
    Admin about 6 years
    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 happens
  • Tatarize
    Tatarize about 6 years
    I changed the jQuery on the posted fiddle, and at 3.3.1 it worked fine. Loaded image, cropped, rotated, flipped, perfectly fine.