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 leanirng CSS now.

Feedback 105

  1. 1

    I’m back again! This tutorial was awesome and really helped me a lot. But now I’ve changed my template to something responsive, which means things resize really well on mobile units.

    Now the stuff aint’ working. It works on my computer but not on my smartphone. The thing is, the window is no longer set up in pixels, but in percents, so it can be responsive to different touchscreens. Example: height: auto; width: 100%.

    I’m not sure why, but the picture crop isn’t relative to the window. It seems to be cropping in absolute position. Any help?

  2. 2

    hey ,
    Your code is great and very helpful. I am new to the php and jquery so I have some problem one of them is how can i use result image in my website please give solution soon …

  3. 3

    I want keep original ratio width/height when scale image.
    What need code change?
    thank you

  4. 4

    hi, great article.
    i have some issues, i want use this script in my site , with image upload and on change of input file assign the image to src of img tag , every things work but when i want to resize the image with handle bars i get this error :
    Uncaught InvalidStateError: Failed to execute ‘drawImage’ on ‘CanvasRenderingContext2D’: The HTMLImageElement provided is in the ‘broken’ state.
    how can i resolve this? could you help me please.

    • 5

      error raised on line 125 of compnent.js file.
      i really appreciate your answer.

    • 6

      hi again, i resolve my previous issue, but when i want to resize a new picture , img src change to the prev image that i select with file input, code that i use for change is :

      function readURL(input) {

      if (input.files && input.files[0]) {
      var reader = new FileReader();

      reader.onload = function (e) {
      $('#resize-image').attr('src', e.target.result);
      }

      reader.readAsDataURL(input.files[0]);
      }
      }

      $("#fileInput").change(function(){
      readURL(this);
      });

      how can i resolve this? pleaaaase help me.

    • 7

      Hi, will realy like o know how you solved your previous issue (CanvasRenderingContext2D’: The HTMLImageElement provided is in the ‘broken’ state.). I’m having the same issue and did not find how to fix it yet. Thks for your help.

  5. 8

    Since you can use a two touch for the pinch to zoom is it possible to have the nodes scale top, bottom and left, right only? also what if you have more then one image how would you get it to switch between both images?

  6. 9

    Hi! great demo… If I try to crop a 5000px image, it takes forever to resize… any tips on how I can resize the image (keeping the ratio) so I can then move it around easily for cropping?

  7. 12

    Hi, Thanks for such a nice Tutorial.

    How can we Save the resized and coppred image? and Hide the image from outside the Center box?

  8. 13

    oFReader.onload = function (oFREvent) {
    var img = new Image();
    img.onload = function () {
    var canvas = document.createElement(“canvas”);
    var ctx = canvas.getContext(“2d”);
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
    document.getElementById(“resize-image”).src = canvas.toDataURL();
    }
    }

    this is my code to show uploaded image, I am using high dimentsion image, the same I need to display in my Canvas half of its size, When i do this by img.width/2 , img.height/2
    the quality of the picture loosing.. how i can show the image on canvas in a reduced size with out loosing quality during crop

  9. 14

    hi i want to resize 3 images in same page but its not working ,
    it works with only one image , please suggest for this issue .

  10. 16

    Okay, I’ve modified a bit your component to work with multiple images at the same page. Here’s the code: http://pastebin.com/tzQt6YA1
    Little bit of explanation. Now you should create structure like this:

    Component set’s image to the maximun of containers size and it transparent at start, you should resize it to see image. This is the problem I can’t beat.
    Another options – I removed max width and heigth, you can implement it if you want using this article.
    Also, I’ve add dynamic cropping and saving to the hidden input field.

Follow this discussion

Leave a Comment