How to compress an image via Javascript in the browser?

179,124

Solution 1

In short:

  • Read the files using the HTML5 FileReader API with .readAsArrayBuffer
  • Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
  • Create new Image element and set it's src to the file blob url
  • Send the image to the canvas. The canvas size is set to desired output size
  • Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
  • Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
  • On backend, read the dataURI, decode from Base64, and save it

Source: code.

Solution 2

I see two things missing from the other answers:

  • canvas.toBlob (when available) is more performant than canvas.toDataURL, and also async.
  • the file -> image -> canvas -> file conversion loses EXIF data; in particular, data about image rotation commonly set by modern phones/tablets.

The following script deals with both points:

// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
    Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
        value: function(callback, type, quality) {

            var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
                len = binStr.length,
                arr = new Uint8Array(len);

            for (var i = 0; i < len; i++) {
                arr[i] = binStr.charCodeAt(i);
            }

            callback(new Blob([arr], {type: type || 'image/png'}));
        }
    });
}

window.URL = window.URL || window.webkitURL;

// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
    // Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
    if (file.slice) {
        file = file.slice(0, 131072);
    } else if (file.webkitSlice) {
        file = file.webkitSlice(0, 131072);
    }

    var reader = new FileReader();
    reader.onload = function(e) {
        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8) {
            callback(-2);
            return;
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    callback(-1);
                    return;
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;
                for (var i = 0; i < tags; i++)
                    if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                        callback(view.getUint16(offset + (i * 12) + 8, little));
                        return;
                    }
            }
            else if ((marker & 0xFF00) != 0xFF00) break;
            else offset += view.getUint16(offset, false);
        }
        callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
    var canvas = document.createElement('canvas');
    if (orientation > 4) {
        canvas.width = rawHeight;
        canvas.height = rawWidth;
    } else {
        canvas.width = rawWidth;
        canvas.height = rawHeight;
    }

    if (orientation > 1) {
        console.log("EXIF orientation = " + orientation + ", rotating picture");
    }

    var ctx = canvas.getContext('2d');
    switch (orientation) {
        case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
        case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
        case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
        case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
        case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
        case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
    }
    ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
    return canvas;
}

function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
    if (file.size <= acceptFileSize) {
        callback(file);
        return;
    }
    var img = new Image();
    img.onerror = function() {
        URL.revokeObjectURL(this.src);
        callback(file);
    };
    img.onload = function() {
        URL.revokeObjectURL(this.src);
        getExifOrientation(file, function(orientation) {
            var w = img.width, h = img.height;
            var scale = (orientation > 4 ?
                Math.min(maxHeight / w, maxWidth / h, 1) :
                Math.min(maxWidth / w, maxHeight / h, 1));
            h = Math.round(h * scale);
            w = Math.round(w * scale);

            var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
            canvas.toBlob(function(blob) {
                console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
                callback(blob);
            }, 'image/jpeg', quality);
        });
    };
    img.src = URL.createObjectURL(file);
}

Example usage:

inputfile.onchange = function() {
    // If file size > 500kB, resize such that width <= 1000, quality = 0.9
    reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
        let body = new FormData();
        body.set('file', blob, blob.name || "file.jpg");
        fetch('/upload-image', {method: 'POST', body}).then(...);
    });
};

Solution 3

@PsychoWoods' answer is good. I would like to offer my own solution. This Javascript function takes an image data URL and a width, scales it to the new width, and returns a new data URL.

// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
    "use strict";
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;

    // Provide default values
    imageType = imageType || "image/jpeg";
    imageArguments = imageArguments || 0.7;

    // Create a temporary image so that we can compute the height of the downscaled image.
    image = new Image();
    image.src = dataUrl;
    oldWidth = image.width;
    oldHeight = image.height;
    newHeight = Math.floor(oldHeight / oldWidth * newWidth)

    // Create a temporary canvas to draw the downscaled image on.
    canvas = document.createElement("canvas");
    canvas.width = newWidth;
    canvas.height = newHeight;

    // Draw the downscaled image on the canvas and return the new data URL.
    ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, newWidth, newHeight);
    newDataUrl = canvas.toDataURL(imageType, imageArguments);
    return newDataUrl;
}

This code can be used anywhere you have a data URL and want a data URL for a downscaled image.

Solution 4

You can take a look at image-conversion,Try it here --> demo page

enter image description here

Solution 5

I find that there's simpler solution compared to the accepted answer.

  • Read the files using the HTML5 FileReader API with .readAsArrayBuffer
  • Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
  • Create new Image element and set it's src to the file blob url
  • Send the image to the canvas. The canvas size is set to desired output size
  • Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
  • Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
  • On backend, read the dataURI, decode from Base64, and save it

As per your question:

Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it

My solution:

  1. Create a blob with the file directly with URL.createObjectURL(inputFileElement.files[0]).

  2. Same as accepted answer.

  3. Same as accepted answer. Worth mentioning that, canvas size is necessary and use img.width and img.height to set canvas.width and canvas.height. Not img.clientWidth.

  4. Get the scale-down image by canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Setting 'image/jpg' has no effect. image/png is also supported. Make a new File object inside the callbackfunction body with let compressedImageBlob = new File([blob]).

  5. Add new hidden inputs or send via javascript . Server doesn't have to decode anything.

Check https://javascript.info/binary for all information. I came up the solution after reading this chapter.


Code:

    <!DOCTYPE html>
    <html>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
      Select image to upload:
      <input type="file" name="fileToUpload" id="fileToUpload" multiple>
      <input type="submit" value="Upload Image" name="submit">
    </form>
    </body>
    </html>

This code looks far less scary than the other answers..

Update:

One has to put everything inside img.onload. Otherwise canvas will not be able to get the image's width and height correctly as the time canvas is assigned.

    function upload(){
        var f = fileToUpload.files[0];
        var fileName = f.name.split('.')[0];
        var img = new Image();
        img.src = URL.createObjectURL(f);
        img.onload = function(){
            var canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            canvas.toBlob(function(blob){
                    console.info(blob.size);
                    var f2 = new File([blob], fileName + ".jpeg");
                    var xhr = new XMLHttpRequest();
                    var form = new FormData();
                    form.append("fileToUpload", f2);
                    xhr.open("POST", "upload.php");
                    xhr.send(form);
            }, 'image/jpeg', 0.5);
        }
    }

3.4MB .png file compression test with image/jpeg argument set.

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |
Share:
179,124
pomeh
Author by

pomeh

Note to all SO beginner: Please read (and embrace!) Jon Skeet's helpful hints on how to write a good question (or at least his short version here) - one that has a chance that someone can answer it. I'm a French developer, I like to improve my knowledge in JavaScript (mainly jQuery and Node.js,), PHP (Symfony), HTML5 and C#

Updated on July 08, 2022

Comments

  • pomeh
    pomeh almost 2 years

    TL;DR;

    Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it ? I'm pretty sure JavaScript can do this, but I can't find a way to achieve it.


    Here's the full scenario I would like to implement:

    • the user goes to my website, and choose an image via an input type="file" element,
    • this image is retrieved via JavaScript, we do some verification such as correct file format, maximum file size etc,
    • if every thing is OK, a preview of the image is displayed on the page,
    • the user can do some basic operations such as rotate the image by 90°/-90°, crop it following a pre-defined ratio, etc, or the user can upload another image and return to step 1,
    • when the user is satisfied, the edited image is then compressed and "saved" locally (not saved to a file, but in the browser memory/page),-
    • the user fill a form with data like name, age etc,
    • the user click on the "Finish" button, then the form containing datas + compressed image is sent to the server (without AJAX),

    The full process up to the last step should be done client side, and should be compatible on latest Chrome and Firefox, Safari 5+ and IE 8+. If possible, only JavaScript should be used (but I'm pretty sure this is not possible).

    I've not code anything right now, but I've thought about it already. File reading locally is possible via File API, image previewing and editing could be done using Canvas element, but I can't find a way to do the image compression part.

    According to html5please.com and caniuse.com, supporting those browser is quite hard (thanks to IE), but could be done using polyfill such as FlashCanvas and FileReader.

    Actually, the goal is to reduce file size, so I see image compression as a solution. But, I know that uploaded images are going to be displayed on my website, every time at the same place, and I know the dimension of this display area (eg. 200x400). So, I could resize the image to fit those dimensions, thus reducing file size. I have no idea what would be the compression ratio for this technique.

    What do you think ? Do you have any advice to tell me ? Do you know any way to compress an image browser-side in JavaScript ? Thanks for your replies.

  • pomeh
    pomeh over 11 years
    Thanks a lot ! This is what I was looking for. Do you know how good the compression ratio is with this technique ?
  • psychowood
    psychowood over 11 years
    Apart from the network transmission (you are sending Base64 encoded content, which is not the best one), the image compression alghorithm is one of the standard ones, the size depends on the quality and format you choose.
  • user1111929
    user1111929 about 9 years
    @NicholasKyriakides I can confirm that canvas.toDataURL("image/jpeg",0.7) effectively compresses it, it saves JPEG with quality 70 (as opposed to the default, quality 100).
  • nicholaswmin
    nicholaswmin about 9 years
    @user1111929 If you can revert it back to quality 100 then it is compression, otherwise if it's a one way street and you can't get it back to quality 100, it is downscaling - Unless I got my terms and understanding mixed up, not sure about that.
  • user1111929
    user1111929 about 9 years
    I'm sorry, I understood downscaling as just lowering the height and width, and compression as actually reducing the quality. Not sure about my terms either, but at least we agree on the contents. :)
  • Billybobbonnet
    Billybobbonnet about 9 years
    @Nicholas Kyriakides, this is not good distinction to make. Most of the codecs are not lossless, so they would fit into your "downscaling" definition (i.e. you cant revert to 100).
  • Tatarize
    Tatarize about 8 years
    You should convert from Base64 on the front end, append it to the FormData and submit it in binary as a file. Then you aren't adding pointless 33% to the file size and limited by the postdata size etc.
  • Tatarize
    Tatarize about 8 years
    Downscaling refers to making images of a smaller size in terms of height and width. This really is compression. It's lossy compression but certainly compression. It isn't downscaling the pixels, it just nudges some of the pixels to being the same color so that the compression can hit those colors in fewer bits. JPEG has built in compression for the pixels anyway, but but in lossy mode it says that a few colors off can be called the same color. That's still compression. Downscaling with regard to graphics typically refers to a change in the actual size.
  • hellol11
    hellol11 over 7 years
    I just want to say this: the file can go straight to URL.createObjectUrl() without turning the file into a blob; the file counts as a blob.
  • lifeisbeautiful
    lifeisbeautiful over 7 years
    without reducing the quality
  • visulo
    visulo about 7 years
    please, can you give me more detail about this example, how to call the function and how returned the result?
  • Vivian River
    Vivian River about 7 years
    Here's an example: danielsadventure.info/Html/scaleimage.html Be sure to read the source of the page to see how it works.
  • Ricardo Ruiz Romero
    Ricardo Ruiz Romero almost 7 years
    ToBlob did the trick for me, creating a file and recieving in the $_FILES array on the server. Thank you!
  • hostingutilities.com
    hostingutilities.com about 6 years
    Perhaps something has changed since you posted this, but the second argument to that canvas.toDataURL function you mentioned is the amount of compression you want to apply.
  • Barabas
    Barabas almost 6 years
    I would add the explanation of quality, resolution and imageType (format of this)
  • Cedric Arnould
    Cedric Arnould almost 6 years
    web.archive.org/web/20171226190510/danielsadventure.info/Htm‌​l/… For other people who wants to read the link propose by @DanielAllenLangdon
  • SanSolo
    SanSolo over 5 years
    Please add some information about the linked resources
  • Matt Pope
    Matt Pope over 5 years
    As a heads up, sometimes image.width/height will return 0 since it hasn't loaded. You might need to convert this into an async function and listen to image.onload to get the correct image with and height.
  • Garvit Jain
    Garvit Jain almost 5 years
    Looks great! But will this work on all browsers, web and mobile? (Let's ignore IE)
  • gabrielstuff
    gabrielstuff almost 4 years
    The demo page link is broken. You can test here: demo.wangyulue.com/image-conversion
  • user1432181
    user1432181 over 3 years
    I've added a demonstrator jsfiddle for psychowood's code sample should anyone need: jsfiddle.net/Abeeee/0wxeugrt/9
  • mangeshbhuskute
    mangeshbhuskute about 3 years
    I like your solution, but How can we preserve EXIF data?
  • Artem Dumanov
    Artem Dumanov over 2 years
    This is so simple and useful. Guys, don't even spend your time reading other answers...
  • Adel Mourad
    Adel Mourad over 2 years
    It is the best plugin so far, super easy to use and powerful. This can be the best answer. Thanks for sharing
  • Zac
    Zac over 2 years
    Best answer here so far. Clean code as well
  • Adelin
    Adelin over 2 years
    still can't understand a couple of things about this, like the relation between all values like accuracy and size for example ..
  • Nico Serrano
    Nico Serrano over 2 years
    I used this function but was getting application/octet-stream instead of image/jpeg in the server. To solve it I sent the blob itself instead of new File(...)
  • Artur Müller Romanov
    Artur Müller Romanov over 2 years
    -1 because as soon as image.src is assigned, the image gets loaded asynchronously. If you have an image that is larger than tiny your code will not run and won't throw any error messages. Had to figure this out the hard way.