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:

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.

I share web dev tips on Twitter, if you found this interesting and want to learn more, follow me there

Or join my newsletter

More articles More articles