Compress An Image Before Upload With JavaScript
In this quick tutorial we’ll use JavaScript to compress images selected with a file input element. We’ll compress images and save them back to the file input ready for upload.
To make sure users can upload their images and prevent super big images from being uploaded we can compress image data before upload without bothering the user with all kinds of requirements.
If you’re in a hurry, or find it easier to read the code itself, you can jump to the final code snippet here
Getting The Selected Image Files
In the example below we’ll accept all kinds of files but will only compress the images. The multiple
attribute enables selection of more than one file.
To prevent other files from being added we can optionally set the file input accept
attribute to "image/*"
Let’s set up a file input field, we’ll listen for the "change"
to detect when the user has selected one or more files. We’ll handle the TODO
items in the next section.
<input type="file" multiple class="my-image-field" />
<script>
// Get the selected files from the file input
const input = document.querySelector('.my-image-field');
input.addEventListener('change', (e) => {
// Get the files
const { files } = e.target;
// No files selected
if (!files.length) return;
// For every file in the files list
for (const file of files) {
// We don't have to compress files that aren't images
if (!file.type.startsWith('image')) {
// TODO: Not an image
}
// TODO: Compress the image
}
// TODO: Store the files
});
</script>
Our code now runs when a user selects one or more image files. Next up is compressing the images.
Since we’ve got some todo’s to take care of… I’ve been building a macOS daily Todo tracking app called Hotlist (a fun side project to learn app development), who knows, maybe it’s just what you’ve been looking for.
Anyhow… Let’s get back to image compression!
Compressing The Images
We’ll implement the compressImage
function. This function will convert the passed File
object to an ImageBitmap
, draw it to a <canvas>
element, and then the canvas returns a compressed JPEG using the toBlob
API.
Please note that we could also ask for a WEBP but that this (at the time of this writing) isn’t support on Safari 🤷♂️
Let’s get to it.
<input type="file" multiple class="my-image-field" />
<script>
const compressImage = async (file, { quality = 1, type = file.type }) => {
// Get as image data
const imageBitmap = await createImageBitmap(file);
// Draw to canvas
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
// Turn into Blob
return await new Promise((resolve) =>
canvas.toBlob(resolve, type, quality)
);
};
// Get the selected file from the file input
const input = document.querySelector('.my-image-field');
input.addEventListener('change', async (e) => {
// Get the files
const { files } = e.target;
// No files selected
if (!files.length) return;
// For every file in the files list
for (const file of files) {
// We don't have to compress files that aren't images
if (!file.type.startsWith('image')) {
// TODO: Not an image
}
// We compress the file by 50%
const compressedFile = await compressImage(file, {
// 0: is maximum compression
// 1: is no compression
quality: 0.5,
// We want a JPEG file
type: 'image/jpeg',
});
}
// TODO: Store the files
});
</script>
The compressedFile
variable will now contain the compressed image file.
Next up is saving the compressed image file (and the ignored files), back to the file input.
Saving The Compressed Image Back To The File Input
We’ll create a DataTransfer
object with which we’ll create our new files list. This list we can then later save back to the file input
The DataTransfer
object only accepts File
objects so we need to turn our Blob into a File, let’s update the compressImage
function.
<input type="file" multiple class="my-image-field" />
<script>
const compressImage = async (file, { quality = 1, type = file.type }) => {
// Get as image data
const imageBitmap = await createImageBitmap(file);
// Draw to canvas
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
// Turn into Blob
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, type, quality)
);
// Turn Blob into File
return new File([blob], file.name, {
type: blob.type,
});
};
// Get the selected file from the file input
const input = document.querySelector('.my-image-field');
input.addEventListener('change', async (e) => {
// Get the files
const { files } = e.target;
// No files selected
if (!files.length) return;
// We'll store the files in this data transfer object
const dataTransfer = new DataTransfer();
// For every file in the files list
for (const file of files) {
// We don't have to compress files that aren't images
if (!file.type.startsWith('image')) {
// Ignore this file, but do add it to our result
dataTransfer.items.add(file);
continue;
}
// We compress the file by 50%
const compressedFile = await compressImage(file, {
quality: 0.5,
type: 'image/jpeg',
});
// Save back the compressed file instead of the original file
dataTransfer.items.add(compressedFile);
}
// Set value of the file input to our new files list
e.target.files = dataTransfer.files;
});
</script>
Select one or more image files in the file input below to test out the code, no worries, nothing is uploaded, everything stays in the browser.
Conclusion
We’ve learned how to use canvas and DataTransfer to set up a tiny unobtrusive script to help us compress images before upload. This script will make it easier for our users to upload images.
Of course there are still a lot of improvements to make, for example we could:
- Resize the image to fit a maximum bounding box.
- Crop the image to a certain aspect ratio.
- Better deal with erroneous input and show fitting error messages.
- Make sure we handle Safari memory issues.
If we’re looking for a more robust solution we could use FilePond to handle the file upload, it can do image resizing, compression, and more.
If we need even more control we can use Pintura to enable our users to edit images before upload, Pintura is also a drop-in replacement for our compressImage
function.