javascriptjquerycssimagejcrop

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


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?

Solution

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