Cropping Images To A Specific Aspect Ratio With JavaScript
In this 3 minute tutorial we’ll write a tiny JavaScript function that helps us crop images to various aspect ratios. Super useful for cropping photos before posting to social media timelines or uploading profile pictures as these are often required to be of a certain aspect ratio.
In this tutorial we’ll by modifying image data. For example, when a user is about to upload a an image we crop it to a certain aspect ratio. If we just want to present images in a certain aspect ratio we can use a CSS only solution.
Loading the Image Data
To get started we’ll need a source image. Let’s use a generic image URL as our source.
const imageURL = 'path/to/our/image.jpeg';
To crop an image we need to access the actual image data. We can get to this data by loading the URL to an <img>
element.
const inputImage = new Image();
inputImage.src = imageURL;
Our next step is drawing the image to a <canvas>
, the canvas will allow us to modify the image data. We’ll add the onload
callback right before setting the src
so we can capture the moment the image has loaded.
// this image will hold our source image data
const inputImage = new Image();
// we want to wait for our image to load
inputImage.onload = () => {
// create a canvas that will present the output image
const outputImage = document.createElement('canvas');
// set it to the same size as the image
outputImage.width = inputImage.naturalWidth;
outputImage.height = inputImage.naturalHeight;
// draw our image at position 0, 0 on the canvas
const ctx = outputImage.getContext('2d');
ctx.drawImage(inputImage, 0, 0);
// show both the image and the canvas
document.body.appendChild(inputImage);
document.body.appendChild(outputImage);
};
// start loading our image
inputImage.src = imageURL;
Running this code results in a <canvas>
that is presenting the same image as the image located at our imageURL
.
Cropping The Image to a Square Aspect Ratio
Now that we have gained access to the image data we can start manipulating it.
Let’s start with a square crop. A square crop has an aspect ratio of 1:1. This means each side of the output image has the same length. We can represent this numerically as an aspect ratio of 1
. The aspect ratio of a 200x200 image is 1
, the aspect ratio of a 400x300 image can be calculated by dividing the width and height, which equals 1.333
(400/300).
Let’s edit our code and view the results.
// the desired aspect ratio of our output image (width / height)
const outputImageAspectRatio = 1;
// this image will hold our source image data
const inputImage = new Image();
// we want to wait for our image to load
inputImage.onload = () => {
// let's store the width and height of our image
const inputWidth = inputImage.naturalWidth;
const inputHeight = inputImage.naturalHeight;
// get the aspect ratio of the input image
const inputImageAspectRatio = inputWidth / inputHeight;
// if it's bigger than our target aspect ratio
let outputWidth = inputWidth;
let outputHeight = inputHeight;
if (inputImageAspectRatio > outputImageAspectRatio) {
outputWidth = inputHeight * outputImageAspectRatio;
} else if (inputImageAspectRatio < outputImageAspectRatio) {
outputHeight = inputWidth / outputImageAspectRatio;
}
// create a canvas that will present the output image
const outputImage = document.createElement('canvas');
// set it to the same size as the image
outputImage.width = outputWidth;
outputImage.height = outputHeight;
// draw our image at position 0, 0 on the canvas
const ctx = outputImage.getContext('2d');
ctx.drawImage(inputImage, 0, 0);
// show both the image and the canvas
document.body.appendChild(inputImage);
document.body.appendChild(outputImage);
};
// start loading our image
inputImage.src = imageURL;
The result is a square image, great! But let’s take a closer look. It seems our crop is not positioned in the center of the input image. This is because we have not update the drawImage
call. The drawImage
call takes 3 (or more) arguments, the inputImage
, and the x
and y
position to draw the image at.
Our input image is still drawn at location 0
, 0
we need to adjust this so we get a center crop instead of a top left crop.
To do this we need to move the image slightly to the left. Suppose our input image is 400
pixels wide and our output image is 300
pixels wide, to center it we’d need to move the input image 50
pixels to the left (negative 50 pixels). -50
pixels is 300
minus 400
divided by 2
. This results in the following code snippet.
const outputX = (outputWidth - inputWidth) * .5
Let’s update the code snippet, we can use the same code for both the x
and the y
offset.
// the desired aspect ratio of our output image (width / height)
const outputImageAspectRatio = 1;
// this image will hold our source image data
const inputImage = new Image();
// we want to wait for our image to load
inputImage.onload = () => {
// let's store the width and height of our image
const inputWidth = inputImage.naturalWidth;
const inputHeight = inputImage.naturalHeight;
// get the aspect ratio of the input image
const inputImageAspectRatio = inputWidth / inputHeight;
// if it's bigger than our target aspect ratio
let outputWidth = inputWidth;
let outputHeight = inputHeight;
if (inputImageAspectRatio > outputImageAspectRatio) {
outputWidth = inputHeight * outputImageAspectRatio;
} else if (inputImageAspectRatio < outputImageAspectRatio) {
outputHeight = inputWidth / outputImageAspectRatio;
}
// calculate the position to draw the image at
const outputX = (outputWidth - inputWidth) * 0.5;
const outputY = (outputHeight - inputHeight) * 0.5;
// create a canvas that will present the output image
const outputImage = document.createElement('canvas');
// set it to the same size as the image
outputImage.width = outputWidth;
outputImage.height = outputHeight;
// draw our image at position 0, 0 on the canvas
const ctx = outputImage.getContext('2d');
ctx.drawImage(inputImage, outputX, outputY);
// show both the image and the canvas
document.body.appendChild(inputImage);
document.body.appendChild(outputImage);
};
// start loading our image
inputImage.src = imageURL;
With this update our crop is now centered on the input image.
Creating a Reusable JavaScript Crop Function
As a final step let’s turn our code into a reusable function so we can quickly crop images with various crop aspect ratios. Our JavaScript snippet is already suitable to be used for any aspect ratio, not just squares.
/**
* @param {string} url - The source image
* @param {number} aspectRatio - The aspect ratio
* @return {Promise<HTMLCanvasElement>} A Promise that resolves with the resulting image as a canvas element
*/
function crop(url, aspectRatio) {
// we return a Promise that gets resolved with our canvas element
return new Promise((resolve) => {
// this image will hold our source image data
const inputImage = new Image();
// we want to wait for our image to load
inputImage.onload = () => {
// let's store the width and height of our image
const inputWidth = inputImage.naturalWidth;
const inputHeight = inputImage.naturalHeight;
// get the aspect ratio of the input image
const inputImageAspectRatio = inputWidth / inputHeight;
// if it's bigger than our target aspect ratio
let outputWidth = inputWidth;
let outputHeight = inputHeight;
if (inputImageAspectRatio > aspectRatio) {
outputWidth = inputHeight * aspectRatio;
} else if (inputImageAspectRatio < aspectRatio) {
outputHeight = inputWidth / aspectRatio;
}
// calculate the position to draw the image at
const outputX = (outputWidth - inputWidth) * 0.5;
const outputY = (outputHeight - inputHeight) * 0.5;
// create a canvas that will present the output image
const outputImage = document.createElement('canvas');
// set it to the same size as the image
outputImage.width = outputWidth;
outputImage.height = outputHeight;
// draw our image at position 0, 0 on the canvas
const ctx = outputImage.getContext('2d');
ctx.drawImage(inputImage, outputX, outputY);
resolve(outputImage);
};
// start loading our image
inputImage.src = url;
});
}
Our new and shiny crop
function can be called like so:
crop('path/to/our/image.jpeg', 1);
Or, to get a “16:9” crop:
crop('path/to/our/image.jpeg', 16 / 9);
As the function returns a Promise
we can get the results like this:
crop('path/to/our/image.jpeg', 16 / 9).then((canvas) => {
// `canvas` is the resulting image
});
Or, using async/await:
const canvas = await crop('path/to/our/image.jpeg', 16 / 9);
View a demo of the end result on CodePen
Conclusion
By using the HTML canvas API and some basic math we build a tiny crop helper function that makes it easy to quickly crop images in various aspect ratios. This helps us prepare images for social media posts, profile pictures, familiar document sizes, or other popular media formats.
To keep the article concise our current solution does not cover these edge cases:
- Browsers being confused by mobile photos EXIF orientation header.
- Canvas memory overflowing on mobile devices for very big images.
- Poor image quality when downscaling images.
If you need a solution for these issues you could explore Pintura Image Editor, an easy to use image editor that solves these edge cases and features a wide range of additional functionality.