Resizing and Cropping Images with Canvas

Learn how to resize and crop images using JavaScript and the HTML5 Canvas element using controls, commonly seen in photo editing applications.

ImageResizingCroppingCanvas

In this tutorial we’re going to learn how to resize and crop an image using the HTML5 <canvas> element, and while we’re at it, let’s create some fancy controls for resizing, commonly seen in photo editing applications.

In a real world example a website or app might use a technique like this to resize and frame a profile picture before uploading. Whilst we could do this on the server, it would require the transfer of a potentially large file, which is slow. Instead we can resize the image on the client side before uploading it, which is fast.

We do this by creating an HTML5 <canvas> element and drawing the image to the canvas at a particular size, then extracting the new image data from the canvas as a data URI. Most browsers have good support for these methods, so you can probably use this technique right now, however just be aware of some limitations unrelated to browser support such as quality and performance.

Resizing very large images can cause the browser to slow down or in some cases, even crash. It makes sense to set reasonable limits on the file size just as you would when uploading a file. If quality is important you may find the resized image looks undesirable due to how the browser resampled it. There are some techniques to improve the quality of images downscaled with canvas, but they are not covered in this tutorial.

Take a look at the final result in this demo or download the ZIP file.

With that in mind, let’s get started!

The Markup

In our demo we’re going to start with an existing image:

<img class="resize-image" src="image.jpg" alt="Image" />

That’s it! That’s all the HTML we need for this demo.

The CSS

The CSS is also very minimal. First, define the styles for the resize-container and the image.

.resize-container {
    position: relative;
    display: inline-block;
    cursor: move;
    margin: 0 auto;
}

.resize-container img {
    display: block
}

.resize-container:hover img,
.resize-container:active img {
    outline: 2px dashed rgba(222,60,80,.9);
}

Next, define the position and style for each of the ‘resize handles’. These are the little squares at each corner of the image that we drag to resize.

.resize-handle-ne,
.resize-handle-ne,
.resize-handle-se,
.resize-handle-nw,
.resize-handle-sw {
    position: absolute;
    display: block;
    width: 10px;
    height: 10px;
    background: rgba(222,60,80,.9);
    z-index: 999;
}

.resize-handle-nw {
    top: -5px;
    left: -5px;
    cursor: nw-resize;
}

.resize-handle-sw {
    bottom: -5px;
    left: -5px;
    cursor: sw-resize;
}

.resize-handle-ne {
    top: -5px;
    right: -5px;
    cursor: ne-resize;
}

.resize-handle-se {
    bottom: -5px;
    right: -5px;
    cursor: se-resize;
}

The JavaScript

With the JavaScript start by defining some of the variables and initializing the Canvas and the target image.

var resizeableImage = function(image_target) {
    var $container,
    orig_src = new Image(),
    image_target = $(image_target).get(0),
    event_state = {},
    constrain = false,
    min_width = 60,
    min_height = 60,
    max_width = 800,
    max_height = 900,
    resize_canvas = document.createElement('canvas');
});

resizeableImage($('.resize-image'));

Next, we create the init function that will be called immediately. This function wraps the image with a container, creates resize handles and makes a copy of the original image used for resizing. We also assign the jQuery object for the container element to a variable so we can refer to it later and add a mousedown event listener to detect when someone begins dragging one of the handles.

var resizeableImage = function(image_target) {

// ...
    init = function(){

        // Create a new image with a copy of the original src
        // When resizing, we will always use this original copy as the base
        orig_src.src=image_target.src;

        // Add resize handles
        $(image_target).wrap('<div class="resize-container"></div>')
        .before('<span class="resize-handle resize-handle-nw"></span>')
        .before('<span class="resize-handle resize-handle-ne"></span>')
        .after('<span class="resize-handle resize-handle-se"></span>')
        .after('<span class="resize-handle resize-handle-sw"></span>');

        // Get a variable for the container
        $container =  $(image_target).parent('.resize-container');

        // Add events
        $container.on('mousedown', '.resize-handle', startResize);
    };

//...

    init();
}

The startResize and endResize functions do very little other than tell the browser to start paying attention to where the mouse is moving and when to stop paying attention.

startResize = function(e){
    e.preventDefault();
    e.stopPropagation();
    saveEventState(e);
    $(document).on('mousemove', resizing);
    $(document).on('mouseup', endResize);
};

endResize = function(e){
    e.preventDefault();
    $(document).off('mouseup touchend', endResize);
    $(document).off('mousemove touchmove', resizing);
};

Before we start tracking the mouse position we want to take a snapshot of the container dimensions and other key data points. We store these in a variable named event_state and use them later as a point of reference while resizing to work out the change in height and width.

saveEventState = function(e){
  // Save the initial event details and container state
  event_state.container_width = $container.width();
  event_state.container_height = $container.height();
  event_state.container_left = $container.offset().left; 
  event_state.container_top = $container.offset().top;
  event_state.mouse_x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft(); 
  event_state.mouse_y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();

  // This is a fix for mobile safari
  // For some reason it does not allow a direct copy of the touches property
  if(typeof e.originalEvent.touches !== 'undefined'){
	event_state.touches = [];
	$.each(e.originalEvent.touches, function(i, ob){
	  event_state.touches[i] = {};
	  event_state.touches[i].clientX = 0+ob.clientX;
	  event_state.touches[i].clientY = 0+ob.clientY;
	});
  }
  event_state.evnt = e;
}

The resizing function is where most of the action happens. This function is constantly invoked while the user is dragging one of the resize handles. Every time this function is called we work out the new width and height by taking the current position of the mouse relative to the initial position of the corner we are dragging.

resizing = function(e){ 
    var mouse={},width,height,left,top,offset=$container.offset();
    mouse.x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft(); 
    mouse.y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();

    width = mouse.x - event_state.container_left;
    height = mouse.y  - event_state.container_top;
    left = event_state.container_left;
    top = event_state.container_top;

    if(constrain || e.shiftKey){
        height = width / orig_src.width * orig_src.height;
    }

    if(width > min_width && height > min_height && width < max_width && height < max_height){
      resizeImage(width, height);  
      // Without this Firefox will not re-calculate the the image dimensions until drag end
      $container.offset({'left': left, 'top': top});        
    }
}

Next we add the option to constrain the image dimensions when toggled using the shift key or a variable.

Finally, we resize the image, but only if the new width and height are not outside the bounds of the min and max variables we initially set.

Note: Because we’re actually resizing the image and not just changing the height and width attributes, you might consider limiting how often resizeImage is called to improve performance. This is called debouncing or throttling.

Actually resizing the image

Drawing an image to the Canvas is as easy as drawImage. We set the height and width of the canvas first and always use the original copy of the full-sized image. We then use toDataURL on the Canvas to get a Base64-encoded version of the newly resized image and place this on the page.

There is a full explanation for all the parameters that can be used with the drawImage method in the cropping section of this tutorial.

resizeImage = function(width, height){
    resize_canvas.width = width;
    resize_canvas.height = height;
    resize_canvas.getContext('2d').drawImage(orig_src, 0, 0, width, height);   
    $(image_target).attr('src', resize_canvas.toDataURL("image/png"));  
};

Too simple? There is one small proviso: the image must be on the same domain as the page or on a server with cross-origin resource sharing (CORS) enabled. If it’s not, you might run into problems with an error about a ‘tainted canvas’.

Resizing from different corners

You should now have a working demo. But it’s not complete. At the moment, no matter which corner of the image we resize, it behaves as though we are resizing it from the bottom right. We want to be able to resize the image from any corner. To do this we need to understand how it should behave.

When resizing, the corner we’re dragging as well as its adjacent edges should move, while the corner directly opposite and its adjacent edges should remain fixed.

Shows which corner remains fixed when resizing an image

When we change the width and height of an image, the right and bottom edges move, while the top and left edges will stay the same. This means that by default, an image is resized from its bottom right corner.

We can’t change this default behavior, but when resizing from any corner other than the bottom right we can change the overall position of the image so that it appears as though the opposite corner and edges remain fixed. Let’s update our resizing function:

resizing = function(e){
  var mouse={},width,height,left,top,offset=$container.offset();
  mouse.x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft(); 
  mouse.y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();
  
  // Position image differently depending on the corner dragged and constraints
  if( $(event_state.evnt.target).hasClass('resize-handle-se') ){
    width = mouse.x - event_state.container_left;
    height = mouse.y  - event_state.container_top;
    left = event_state.container_left;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-sw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = mouse.y  - event_state.container_top;
    left = mouse.x;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-nw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = mouse.x;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  } else if($(event_state.evnt.target).hasClass('resize-handle-ne') ){
    width = mouse.x - event_state.container_left;
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = event_state.container_left;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  }

  // Optionally maintain aspect ratio
  if(constrain || e.shiftKey){
    height = width / orig_src.width * orig_src.height;
  }

  if(width > min_width && height > min_height && width < max_width && height < max_height){
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height);  
    // Without this Firefox will not re-calculate the the image dimensions until drag end
    $container.offset({'left': left, 'top': top});
  }
}

We are now checking to see which resize-handle has been dragged and we’re moving the image while resizing it so that it appears as though the correct corner remains fixed.

Moving the image

Now that we can resize the image from any of its corners you might have noticed we can inadvertently change its position on the page. We need to give users the ability to move the image back into the center of frame. In the init function let’s add another event listener similar to the one we did earlier.

init = function(){

    //...

    $container.on('mousedown', 'img', startMoving);
}

We now add startMoving and endMoving functions similar to startResize and endResize.

startMoving = function(e){
    e.preventDefault();
    e.stopPropagation();
    saveEventState(e);
    $(document).on('mousemove', moving);
    $(document).on('mouseup', endMoving);
};

endMoving = function(e){
    e.preventDefault();
    $(document).off('mouseup', endMoving);
    $(document).off('mousemove', moving);
};

In the function moving we need to work out the new position of the top left edge of the container. This will be equal to the current position of the mouse, offset by the distance the mouse was from the top left corner when we started dragging the image.

moving = function(e){
    var  mouse={};
    e.preventDefault();
    e.stopPropagation();
    mouse.x = (e.clientX || e.pageX) + $(window).scrollLeft();
    mouse.y = (e.clientY || e.pageY) + $(window).scrollTop();
    $container.offset({
        'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
        'top': mouse.y - ( event_state.mouse_y - event_state.container_top ) 
    });
};

Cropping the image

Now that we can resize the image we might want to crop it as well. Rather than allowing users to crop the image to any size and shape, let’s create a frame that is the exact dimensions we require and ask users to position the image inside that frame. This gives them control over the zoom and framing, yet ensures the final image always the same size and shape.

To do this we need to add a the following HTML:

<div class="overlay">
    <div class="overlay-inner">
    </div>
</div>
<button class="btn-crop js-crop">Crop</button>

The styles for the overlay box are important, particularly it’s position, width and height as they are used to determine what part of the image is cropped. It’s also important to remember that the frame should always be visible on any background color. That is why I used a semi transparent white outline around the main box in my example.

.overlay {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
    z-index: 999;
    width: 200px;
    height: 200px;
    border: solid 2px rgba(222,60,80,.9);
    box-sizing: content-box;
    pointer-events: none;
}

.overlay:after,
.overlay:before {
    content: '';
    position: absolute;
    display: block;
    width: 204px;
    height: 40px;
    border-left: dashed 2px rgba(222,60,80,.9);
    border-right: dashed 2px rgba(222,60,80,.9);
}

.overlay:before {
    top: 0;
    margin-left: -2px;
    margin-top: -40px;
}

.overlay:after {
    bottom: 0;
    margin-left: -2px;
    margin-bottom: -40px;
}

.overlay-inner:after,
.overlay-inner:before {
    content: '';
    position: absolute;
    display: block;
    width: 40px;
    height: 204px;
    border-top: dashed 2px rgba(222,60,80,.9);
    border-bottom: dashed 2px rgba(222,60,80,.9);
}

.overlay-inner:before {
    left: 0;
    margin-left: -40px;
    margin-top: -2px;
}

.overlay-inner:after {
    right: 0;
    margin-right: -40px;
    margin-top: -2px;
}

.btn-crop {
    position: absolute;
    vertical-align: bottom;
    right: 5px;
    bottom: 5px;
    padding: 6px 10px;
    z-index: 999;
    background-color: rgb(222,60,80);
    border: none;
    border-radius: 5px;
    color: #FFF;
}

Update the JavaScript with the following function and event listener:

init = function(){

    //...

    $('.js-crop').on('click', crop);
  
};

crop = function(){
    var crop_canvas,
        left = $('.overlay').offset().left - $container.offset().left,
        top =  $('.overlay').offset().top - $container.offset().top,
        width = $('.overlay').width(),
        height = $('.overlay').height();
        
    crop_canvas = document.createElement('canvas');
    crop_canvas.width = width;
    crop_canvas.height = height;
    
    crop_canvas.getContext('2d').drawImage(image_target, left, top, width, height, 0, 0, width, height);
    window.open(crop_canvas.toDataURL("image/png"));
}

The crop function is similar to the resizeImage function however instead of passing it height and width values we get the height and width from the overlay element.

For cropping, the canvas drawImage method requires nine parameters. The first parameter is the source image. The next four parameters indicate what part of the source image is used (the clipping box). The final four parameters indicate where on the canvas to start drawing the image and at what size.

Adding touch events and gesture detection

We’ve added mouse events, now let’s add support for touch enabled devices.

For mousedown and mouseup there are equivalent touch events, touchstart and touchend and for mousemove there is the equivalent touchmove. Someone obviously lacks a sense of humor because these events could have easily been named “touchdown” and “touchup”.

Let’s add touchstart and touchend everywhere we have a mousedown and mouseup event listener and touchmove everywhere we have mousemove.

// In init()...
$container.on('mousedown touchstart', '.resize-handle', startResize);
$container.on('mousedown touchstart', 'img', startMoving);

//In startResize() ...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//In endResize()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

//In  startMoving()...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//In endMoving()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

Since we’re resizing an image it might be reasonable to expect that some users will try common gestures like pinch zoom. There is a library called Hammer that provides a lot of convenience when working with gestures. But since we only need pinch zoom, in our case, it might be a more like a sledge hammer. Let me show you how easy it is to detect pinch zoom without any library.

You might have notice that in the saveEventState function we already stored the initial touch data; this will come in handy now.

First we check if the event contains two “touches” and measure the distance between them. We note this as the initial distance and then constantly measure how much this distance changes while moving. Let’s update the moving function:

moving = function(e){
  var  mouse={}, touches;
  e.preventDefault();
  e.stopPropagation();
  
  touches = e.originalEvent.touches;
  mouse.x = (e.clientX || e.pageX || touches[0].clientX) + $(window).scrollLeft(); 
  mouse.y = (e.clientY || e.pageY || touches[0].clientY) + $(window).scrollTop();
  $container.offset({
    'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
    'top': mouse.y - ( event_state.mouse_y - event_state.container_top ) 
  });
  // Watch for pinch zoom gesture while moving
  if(event_state.touches && event_state.touches.length > 1 && touches.length > 1){
    var width = event_state.container_width, height = event_state.container_height;
    var a = event_state.touches[0].clientX - event_state.touches[1].clientX;
    a = a * a; 
    var b = event_state.touches[0].clientY - event_state.touches[1].clientY;
    b = b * b; 
    var dist1 = Math.sqrt( a + b );
    
    a = e.originalEvent.touches[0].clientX - touches[1].clientX;
    a = a * a; 
    b = e.originalEvent.touches[0].clientY - touches[1].clientY;
    b = b * b; 
    var dist2 = Math.sqrt( a + b );

    var ratio = dist2 /dist1;

    width = width * ratio;
    height = height * ratio;
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height);
  }
};

We divide the current distance by initial distance to get the ratio and how much to scale the image. We work out the new width and height and then resize the image.

ImageResizingCroppingCanvas01

And that’s it. Take a look at the demo or download the ZIP file.

In my testing, Chrome prevented the default browser response for pinch zoom, which is to change the page zoom, but Firefox didn’t.

I hope you found this tutorial useful. I suggest further reading on drag and drop and file upload techniques and look forward to see how people might combine these techniques to create some beautiful user interfaces.

Previous:
Next:

Tagged with:

Mike is web developer living in Merimbula on the south east coast of Australia. He loves the web and sometimes writes about it. He is honored that you are reading his bio.

View all contributions by

Website: http://madebymike.com.au

Related Articles

CSS Reference

Learn about all important CSS properties from the basics with our extensive and easy-to-read CSS Reference.

It doesn't matter if you are a beginner or intermediate, start learning CSS now.

Feedback 108

  1. 2

    Great article.
    When uploading a .png the background turns black. How would I go about changing this to white?
    Many thanks

  2. 3

    I am using HTML5 FileReader – Extracting the image into a DataURL and using canvas to crop the image. Once the crop done, I extract the image from canvas using toDataURL and put into the image tag. At this point the image quality drastically reduces. has anyone faced this?

  3. 4

    Great widget! Easy to customize to my needs (different style on overlay etc).

    My only real concern is the very low performance when zooming on a mobile device, so I’m wondering whether there’s a more efficient way than to convert the image to a base64 “blob” (if now that’s the main contributor). Save it as a binary file and refer to it as such? Not that Javascript is a fan of files.

    Regards,
    Anders

  4. 5

    Hi, excellent job! Though when image exceeds certain dimensions resize does not work any longer. I tried to edit max_width and max_height options but that didn’t solve the issue. Could you help me?
    Thanks

    • 6

      Hi, I faced same issue and made some changes on this part of the code helps me :

      if(width > min_width && height > min_height /*&& width < max_width && height < max_height*/){
      // To improve performance you might limit how often resizeImage() is called
      resizeImage(width, height);
      // Without this Firefox will not re-calculate the the image dimensions until drag end
      $container.offset({'left': left, 'top': top});
      }

      Hope this will help you

  5. 7

    Hi, very cool post.
    One question, when I load a png image resize is no longer possible, do I need to change or add something?
    thanks and happy new year.

  6. 9

    It’s working . But i want to know that how can i save it after crop? Does it need PHP for saving a crop image with your this image resizing technique?
    Thank you

  7. 15

    Hello, I am having difficulties with large images (resolution 8mp), can anyone help me with this? Anyone had the same problem?

  8. 18

    To save the image, I did this in the .js file:


    // resize from 128x128 to 64x64
    crop_canvas.getContext('2d').drawImage(image_target, left, top, width, height, 0, 0, 64, 64);

    var Pic = crop_canvas.toDataURL("image/png");

    // Generate the image data
    Pic = Pic.replace(/^data:image\/(png|jpg);base64,/, "")

    // make an AJAX call to move the file
    var url = '(place your url here)';

    // Send the image data to Server
    $.ajax({
    type: 'POST',
    url: url,
    dataType: 'text',
    data: {
    imageData : Pic
    },
    success: function (msg) {

    }
    });

    And in the .php file, try this:


    if (array_key_exists('imageData',$_REQUEST)) {

    $imgData = base64_decode($_REQUEST['imageData']);

    // Path where the image is going to be saved
    $filePath = '(put your url + filename here)';

    // Write $imgData into the image file
    $file = fopen($filePath, 'w');
    fwrite($file, $imgData);
    fclose($file);
    }

    I had some problems with image quality after resizing from 128×128 to 64×64. Any clues? Thanks in advance.

    • 19

      OK, got it figured out. Make the changes below.

      – Be sure you use parseInt(left) and parseInt(top) to be sure you are resizing/cropping on the pixel!

      crop_canvas = document.createElement('canvas');
      crop_canvas.width = 64;
      crop_canvas.height = 64;

      crop_canvas.getContext('2d').drawImage(image_target, parseInt(left), parseInt(top), width, height, 0, 0, 64, 64);

      var Pic = crop_canvas.toDataURL("image/png");

      // Generate the image data
      Pic = Pic.replace(/^data:image\/(png|jpg);base64,/, "")

      Thanks for the great tutorial!!

      (By the way, I noticed the image gets cropped a pixel or two to the left of the box.)

    • 21

      Hello, i have been success insert it to database but hard to do update the images, did you have any idea how to update cause i use onclick to save it to database then after click it will write to folder adn insert, please guide me

  9. 22

    Great stuff! but I have an issue in Firefox, when the image is resized smaller then the bounding box, it fails to crop…
    need help on this….

  10. 24

    Hello,

    I really love this “pluggin” I made many changes on it I can updload, rename and tag the image and also save file.

    But there is one thing I am facing today.
    Maybe you facing same issue. When you move the image outside the global area (not the one inside the red square), you loose the image and there is no possibility to catch it again.

    How can we avoid this? Is there a possibility to stop moving when the mouse is close to the border – 10px (10 should be a parameter)?

  11. 26

    How I can do to re-center the image on a click button?
    I mean if I lost my image I want to reinit the position of the image if there a possibility to do that?
    thanks

  12. 27

    answer to my own question, maybe someone else needs this.
    Add the following function on a click (or whatever your want)

    recenter = function(e){
    $container.offset({
    'left': event_state.container_left,
    'top': event_state.container_top
    });
    }

Follow this discussion

Leave a Comment