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?
Govinda Sakhare
  • 5,009
  • 6
  • 33
  • 74

2 Answers2

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.

Community
  • 1
  • 1
seahorsepip
  • 4,519
  • 1
  • 19
  • 30
  • At my phone currently, I'll be home in a hour or two :/ Traveling by train is a horrible slow experience.... – seahorsepip Jan 10 '16 at 17:27
  • 1
    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 Jan 10 '16 at 17:33
  • Back, gimme a minute to make ajs fiddle. – seahorsepip Jan 10 '16 at 21:48
  • Took a bit longer to remove all unnecessary code and bring it back to the basics :P – seahorsepip Jan 10 '16 at 23:10
  • @piechuckerr Updated the answer with all the info you need, if you have any more questions leave a comment :D – seahorsepip Jan 11 '16 at 00:46
  • 1
    Okay will try the stuff you posted and will let you know result. – Govinda Sakhare Jan 11 '16 at 04:57
  • 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. – seahorsepip Jan 11 '16 at 05:07
  • Yes I will port that to java and offer you the bounty asap – Govinda Sakhare Jan 11 '16 at 05:46
  • Thanks, so you're using java server side? That's kinda rare :P – seahorsepip Jan 11 '16 at 14:42
  • Thanks for the bounty :D – seahorsepip Jan 14 '16 at 06:02
  • facing few problems please have a look at it. http://stackoverflow.com/questions/37828355/image-converted-to-base64-not-showing-on-bootstrap-modals-canvas – Govinda Sakhare Jun 16 '16 at 04:33
  • 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. – Tatarize Jun 17 '16 at 11:02
  • @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: https://jsfiddle.net/v6t72f99/13/ – Max Stern Dec 04 '16 at 23:53
  • 1
    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); }" – CountMurphy Jan 04 '17 at 20:32
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();
    }
Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • 1
    Mostly try the jfiddle, should be fully functional save the upload to jfiddle thing. – Tatarize Jun 12 '16 at 13:20
  • 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. – Govinda Sakhare Jun 14 '16 at 08:18
  • 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 Jun 15 '16 at 00:38
  • 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. – Tatarize Jun 15 '16 at 00:40
  • 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. – Tatarize Jun 16 '16 at 04:56
  • and why it is not working without that `alert` on chrome too? weird, not getting what is happening here, – Govinda Sakhare Jun 16 '16 at 05:03
  • Oh, hadn't checked that. Lemme see. – Tatarize Jun 16 '16 at 05:06
  • Oh, the bug where it doesn't have a proper length and height and shrinks down to nothing on both Chrome and Firefox? – Tatarize Jun 16 '16 at 05:08
  • 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 Jun 16 '16 at 05:10
  • The alert didn't help, but it did stall for time which caused it to trigger correctly. – Tatarize Jun 16 '16 at 05:13
  • @piechuckerr, This should work now. https://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. – Tatarize Jun 16 '16 at 06:06
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/114807/discussion-between-piechuckerr-and-tatarize). – Govinda Sakhare Jun 16 '16 at 06:10
  • @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. – gomesh munda Sep 24 '17 at 19:35
  • 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 Sep 24 '17 at 19:39
  • 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. – Tatarize Sep 24 '17 at 19:43
  • 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... – gomesh munda Sep 24 '17 at 19:47
  • 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 Sep 24 '17 at 19:53
  • @gomeshmunda I was using it for Drupal so I used something like https://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 Sep 24 '17 at 19:59
  • 1
    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. – Tatarize Sep 24 '17 at 20:02
  • 1
    @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 :) – gomesh munda Sep 24 '17 at 20:06
  • 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 –  Apr 11 '18 at 06:31
  • I changed the jQuery on the posted fiddle, and at 3.3.1 it worked fine. Loaded image, cropped, rotated, flipped, perfectly fine. – Tatarize Apr 11 '18 at 08:17