197

Here's a noodle scratcher.

Bearing in mind we have HTML5 local storage and xhr v2 and what not. I was wondering if anyone could find a working example or even just give me a yes or no for this question:

Is it possible to Pre-size an image using the new local storage (or whatever), so that a user who does not have a clue about resizing an image can drag their 10mb image into my website, it resize it using the new localstorage and THEN upload it at the smaller size.

I know full well you can do it with Flash, Java applets, active X... The question is if you can do with Javascript + Html5.

Looking forward to the response on this one.

Ta for now.

Jimmyt1988
  • 20,466
  • 41
  • 133
  • 233

10 Answers10

196

Yes, use the File API, then you can process the images with the canvas element.

This Mozilla Hacks blog post walks you through most of the process. For reference here's the assembled source code from the blog post:

// from an input element
var filesToUpload = input.files;
var file = filesToUpload[0];

var img = document.createElement("img");
var reader = new FileReader();  
reader.onload = function(e) {img.src = e.target.result}
reader.readAsDataURL(file);

var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);

var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;

if (width > height) {
  if (width > MAX_WIDTH) {
    height *= MAX_WIDTH / width;
    width = MAX_WIDTH;
  }
} else {
  if (height > MAX_HEIGHT) {
    width *= MAX_HEIGHT / height;
    height = MAX_HEIGHT;
  }
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);

var dataurl = canvas.toDataURL("image/png");

//Post dataurl to the server with AJAX
robertc
  • 74,533
  • 18
  • 193
  • 177
  • 6
    Why thank you good sir. I will have a play tonight... With the file api that is. I got the drag and drop upload to work and I realised this would also be a really nice feature to include. Yippee. – Jimmyt1988 Apr 26 '12 at 13:20
  • Thank you sir! These two lines are not necessary in my opinion: var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); ---- as you already get the context and drawImage at the end... – Qlimax Aug 22 '12 at 12:28
  • 3
    Don't we need to use this mycanvas.toDataURL("image/jpeg", 0.5) to make sure the quality is brought in order to decrease the file size. I came across this comment on the mozilla blog post and saw the bug resolution here https://bugzilla.mozilla.org/show_bug.cgi?id=564388 – sbose Jan 15 '13 at 10:58
  • @SHOUBHIKBOSE I don't know, that depends on whether your app requires you to sacrifice quality for file size. – robertc Jan 15 '13 at 13:20
  • 1
    Thanks @robertc Okay I'm a bit confused . I need to bring down the file size not the just the dimensions. So what should I be doing then? :) – sbose Jan 15 '13 at 14:14
  • @SHOUBHIKBOSE If you have your own question that's not answered by the above, you should be asking your own question, not commenting on this answer. – robertc Jan 15 '13 at 14:48
  • 1
    Sorry @robertc for trying to clarify my doubt here! – sbose Jan 15 '13 at 20:35
  • 37
    You should wait for `onload` on the image (attach handler _before_ setting `src`), because browser is allowed to do decoding asynchronously and pixels may not be available before `drawImage`. – Kornel Jun 20 '13 at 14:02
  • 7
    You may want to put the canvas code into the onload function of the filereader - there is a chance that large images will not be loaded before you `ctx.drawImage()` near the end. – Greg Jan 09 '14 at 10:16
  • @robertc : Please see my similar question & help with an answer . http://stackoverflow.com/questions/22282971/html5-jquery-to-resize-multiple-images – logan Mar 09 '14 at 13:52
  • 4
    be carefull, you need additionnal magic to make it work on IOS : http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios – flo850 Jul 13 '14 at 13:11
  • be attention at file image size, that may slow down or hang the browsers – albanx Oct 19 '14 at 09:46
  • 2
    You can use **synchronous** `URL.createObjectURL(File)` instead of the asynchronous `FileReader.readAsDataURL(File)` to make a URL to the image. – Константин Ван Feb 18 '16 at 04:36
  • It was supposed to reduce the filesize, but canvas output a larger image than my image with this method with lower dimension – SooCheng Koh Apr 11 '17 at 12:22
  • 2
    `var ctx = canvas.getContext("2d"); // Uncaught ReferenceError: canvas is not defined`. Did you forget to define `canvas`? – Alex G Aug 30 '17 at 03:18
  • In my case i had to put everything inside the reader's "onload" function and a new img's onload function. But Great response! thank you! – Marco Dec 04 '17 at 17:19
117

I tackled this problem a few years ago and uploaded my solution to github as https://github.com/rossturner/HTML5-ImageUploader

robertc's answer uses the solution proposed in the Mozilla Hacks blog post, however I found this gave really poor image quality when resizing to a scale that was not 2:1 (or a multiple thereof). I started experimenting with different image resizing algorithms, although most ended up being quite slow or else were not great in quality either.

Finally I came up with a solution which I believe executes quickly and has pretty good performance too - as the Mozilla solution of copying from 1 canvas to another works quickly and without loss of image quality at a 2:1 ratio, given a target of x pixels wide and y pixels tall, I use this canvas resizing method until the image is between x and 2 x, and y and 2 y. At this point I then turn to algorithmic image resizing for the final "step" of resizing down to the target size. After trying several different algorithms I settled on bilinear interpolation taken from a blog which is not online anymore but accessible via the Internet Archive, which gives good results, here's the applicable code:

ImageUploader.prototype.scaleImage = function(img, completionCallback) {
    var canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

    while (canvas.width >= (2 * this.config.maxWidth)) {
        canvas = this.getHalfScaleCanvas(canvas);
    }

    if (canvas.width > this.config.maxWidth) {
        canvas = this.scaleCanvasWithAlgorithm(canvas);
    }

    var imageData = canvas.toDataURL('image/jpeg', this.config.quality);
    this.performUpload(imageData, completionCallback);
};

ImageUploader.prototype.scaleCanvasWithAlgorithm = function(canvas) {
    var scaledCanvas = document.createElement('canvas');

    var scale = this.config.maxWidth / canvas.width;

    scaledCanvas.width = canvas.width * scale;
    scaledCanvas.height = canvas.height * scale;

    var srcImgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
    var destImgData = scaledCanvas.getContext('2d').createImageData(scaledCanvas.width, scaledCanvas.height);

    this.applyBilinearInterpolation(srcImgData, destImgData, scale);

    scaledCanvas.getContext('2d').putImageData(destImgData, 0, 0);

    return scaledCanvas;
};

ImageUploader.prototype.getHalfScaleCanvas = function(canvas) {
    var halfCanvas = document.createElement('canvas');
    halfCanvas.width = canvas.width / 2;
    halfCanvas.height = canvas.height / 2;

    halfCanvas.getContext('2d').drawImage(canvas, 0, 0, halfCanvas.width, halfCanvas.height);

    return halfCanvas;
};

ImageUploader.prototype.applyBilinearInterpolation = function(srcCanvasData, destCanvasData, scale) {
    function inner(f00, f10, f01, f11, x, y) {
        var un_x = 1.0 - x;
        var un_y = 1.0 - y;
        return (f00 * un_x * un_y + f10 * x * un_y + f01 * un_x * y + f11 * x * y);
    }
    var i, j;
    var iyv, iy0, iy1, ixv, ix0, ix1;
    var idxD, idxS00, idxS10, idxS01, idxS11;
    var dx, dy;
    var r, g, b, a;
    for (i = 0; i < destCanvasData.height; ++i) {
        iyv = i / scale;
        iy0 = Math.floor(iyv);
        // Math.ceil can go over bounds
        iy1 = (Math.ceil(iyv) > (srcCanvasData.height - 1) ? (srcCanvasData.height - 1) : Math.ceil(iyv));
        for (j = 0; j < destCanvasData.width; ++j) {
            ixv = j / scale;
            ix0 = Math.floor(ixv);
            // Math.ceil can go over bounds
            ix1 = (Math.ceil(ixv) > (srcCanvasData.width - 1) ? (srcCanvasData.width - 1) : Math.ceil(ixv));
            idxD = (j + destCanvasData.width * i) * 4;
            // matrix to vector indices
            idxS00 = (ix0 + srcCanvasData.width * iy0) * 4;
            idxS10 = (ix1 + srcCanvasData.width * iy0) * 4;
            idxS01 = (ix0 + srcCanvasData.width * iy1) * 4;
            idxS11 = (ix1 + srcCanvasData.width * iy1) * 4;
            // overall coordinates to unit square
            dx = ixv - ix0;
            dy = iyv - iy0;
            // I let the r, g, b, a on purpose for debugging
            r = inner(srcCanvasData.data[idxS00], srcCanvasData.data[idxS10], srcCanvasData.data[idxS01], srcCanvasData.data[idxS11], dx, dy);
            destCanvasData.data[idxD] = r;

            g = inner(srcCanvasData.data[idxS00 + 1], srcCanvasData.data[idxS10 + 1], srcCanvasData.data[idxS01 + 1], srcCanvasData.data[idxS11 + 1], dx, dy);
            destCanvasData.data[idxD + 1] = g;

            b = inner(srcCanvasData.data[idxS00 + 2], srcCanvasData.data[idxS10 + 2], srcCanvasData.data[idxS01 + 2], srcCanvasData.data[idxS11 + 2], dx, dy);
            destCanvasData.data[idxD + 2] = b;

            a = inner(srcCanvasData.data[idxS00 + 3], srcCanvasData.data[idxS10 + 3], srcCanvasData.data[idxS01 + 3], srcCanvasData.data[idxS11 + 3], dx, dy);
            destCanvasData.data[idxD + 3] = a;
        }
    }
};

This scales an image down to a width of config.maxWidth, maintaining the original aspect ratio. At the time of development this worked on iPad/iPhone Safari in addition to major desktop browsers (IE9+, Firefox, Chrome) so I expect it will still be compatible given the broader uptake of HTML5 today. Note that the canvas.toDataURL() call takes a mime type and image quality which will allow you to control the quality and output file format (potentially different to input if you wish).

The only point this doesn't cover is maintaining the orientation information, without knowledge of this metadata the image is resized and saved as-is, losing any metadata within the image for orientation meaning that images taken on a tablet device "upside down" were rendered as such, although they would have been flipped in the device's camera viewfinder. If this is a concern, this blog post has a good guide and code examples on how to accomplish this, which I'm sure could be integrated to the above code.

ndequeker
  • 7,932
  • 7
  • 61
  • 93
Ross Taylor-Turner
  • 3,687
  • 2
  • 24
  • 33
30

Correction to above:

<img src="" id="image">
<input id="input" type="file" onchange="handleFiles()">
<script>

function handleFiles()
{
    var filesToUpload = document.getElementById('input').files;
    var file = filesToUpload[0];

    // Create an image
    var img = document.createElement("img");
    // Create a file reader
    var reader = new FileReader();
    // Set the image once loaded into file reader
    reader.onload = function(e)
    {
        img.src = e.target.result;

        var canvas = document.createElement("canvas");
        //var canvas = $("<canvas>", {"id":"testing"})[0];
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);

        var MAX_WIDTH = 400;
        var MAX_HEIGHT = 300;
        var width = img.width;
        var height = img.height;

        if (width > height) {
          if (width > MAX_WIDTH) {
            height *= MAX_WIDTH / width;
            width = MAX_WIDTH;
          }
        } else {
          if (height > MAX_HEIGHT) {
            width *= MAX_HEIGHT / height;
            height = MAX_HEIGHT;
          }
        }
        canvas.width = width;
        canvas.height = height;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, width, height);

        var dataurl = canvas.toDataURL("image/png");
        document.getElementById('image').src = dataurl;     
    }
    // Load files into file reader
    reader.readAsDataURL(file);


    // Post the data
    /*
    var fd = new FormData();
    fd.append("name", "some_filename.jpg");
    fd.append("image", dataurl);
    fd.append("info", "lah_de_dah");
    */
}</script>
Justin Levene
  • 1,630
  • 19
  • 17
  • 2
    How do you handle the PHP part of it after you add it to the FormData()? You wouldn't be looking for $_FILES['name']['tmp_name'][$i], for example? I'm trying if(isset($_POST['image']))... but the `dataurl` not there. – denikov Aug 02 '14 at 14:08
  • 2
    it doesn't work on firefox the first time you try to upload an image. The next time you try it works. How to fix it? – Fred Dec 16 '16 at 15:47
  • return if file is array is null or empty. – Sheelpriy Jun 02 '17 at 06:50
  • 3
    In Chrome when trying to access `img.width` and `img.height` initially they are `0`. You should put the rest of the function inside `img.addEventListener('load', function () { // now the image has loaded, continue... });` – Inigo Nov 06 '17 at 15:58
  • 2
    @Justin can you clarify "the above", as in, what is this post a correction of? Cheers – Martin Feb 14 '18 at 18:40
  • @Inigo you my dear, deserve a medal. =) – Andrey Bulezyuk May 10 '19 at 13:59
16

Modification to the answer by Justin that works for me:

  1. Added img.onload
  2. Expand the POST request with a real example

function handleFiles()
{
    var dataurl = null;
    var filesToUpload = document.getElementById('photo').files;
    var file = filesToUpload[0];

    // Create an image
    var img = document.createElement("img");
    // Create a file reader
    var reader = new FileReader();
    // Set the image once loaded into file reader
    reader.onload = function(e)
    {
        img.src = e.target.result;

        img.onload = function () {
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0);

            var MAX_WIDTH = 800;
            var MAX_HEIGHT = 600;
            var width = img.width;
            var height = img.height;

            if (width > height) {
              if (width > MAX_WIDTH) {
                height *= MAX_WIDTH / width;
                width = MAX_WIDTH;
              }
            } else {
              if (height > MAX_HEIGHT) {
                width *= MAX_HEIGHT / height;
                height = MAX_HEIGHT;
              }
            }
            canvas.width = width;
            canvas.height = height;
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, width, height);

            dataurl = canvas.toDataURL("image/jpeg");

            // Post the data
            var fd = new FormData();
            fd.append("name", "some_filename.jpg");
            fd.append("image", dataurl);
            fd.append("info", "lah_de_dah");
            $.ajax({
                url: '/ajax_photo',
                data: fd,
                cache: false,
                contentType: false,
                processData: false,
                type: 'POST',
                success: function(data){
                    $('#form_photo')[0].reset();
                    location.reload();
                }
            });
        } // img.onload
    }
    // Load files into file reader
    reader.readAsDataURL(file);
}
Martin
  • 22,212
  • 11
  • 70
  • 132
Will
  • 1,989
  • 20
  • 19
9

If you don't want to reinvent the wheel you may try plupload.com

nanospeck
  • 3,388
  • 3
  • 36
  • 45
  • 3
    I also use plupload for client side resizing. The best thing about it is it can use flash, html5, silverlight, etc. Whatever the user has available, because every user has a different setup. If you want only html5, then I think it wont work on some old browsers and Opera. – jnl Jul 19 '14 at 21:55
8

Typescript

async resizeImg(file: Blob): Promise<Blob> {
    let img = document.createElement("img");
    img.src = await new Promise<any>(resolve => {
        let reader = new FileReader();
        reader.onload = (e: any) => resolve(e.target.result);
        reader.readAsDataURL(file);
    });
    await new Promise(resolve => img.onload = resolve)
    let canvas = document.createElement("canvas");
    let ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    let MAX_WIDTH = 1000;
    let MAX_HEIGHT = 1000;
    let width = img.naturalWidth;
    let height = img.naturalHeight;
    if (width > height) {
        if (width > MAX_WIDTH) {
            height *= MAX_WIDTH / width;
            width = MAX_WIDTH;
        }
    } else {
        if (height > MAX_HEIGHT) {
            width *= MAX_HEIGHT / height;
            height = MAX_HEIGHT;
        }
    }
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, width, height);
    let result = await new Promise<Blob>(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); });
    return result;
}
jales cardoso
  • 591
  • 7
  • 11
  • 2
    Anyone that may come across this post, this promise based answer is much more reliable in my opinion. I converted to normal JS and it works like a charm. – gbland777 Dec 06 '19 at 17:08
7

The accepted answer works great, but the resize logic ignores the case in which the image is larger than the maximum in only one of the axes (for example, height > maxHeight but width <= maxWidth).

I think the following code takes care of all cases in a more straight-forward and functional way (ignore the typescript type annotations if using plain javascript):

private scaleDownSize(width: number, height: number, maxWidth: number, maxHeight: number): {width: number, height: number} {
    if (width <= maxWidth && height <= maxHeight)
      return { width, height };
    else if (width / maxWidth > height / maxHeight)
      return { width: maxWidth, height: height * maxWidth / width};
    else
      return { width: width * maxHeight / height, height: maxHeight };
  }
Taro
  • 1,432
  • 1
  • 16
  • 15
5
fd.append("image", dataurl);

This will not work. On PHP side you can not save file with this.

Use this code instead:

var blobBin = atob(dataurl.split(',')[1]);
var array = [];
for(var i = 0; i < blobBin.length; i++) {
  array.push(blobBin.charCodeAt(i));
}
var file = new Blob([new Uint8Array(array)], {type: 'image/png', name: "avatar.png"});

fd.append("image", file); // blob file
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
robot
  • 51
  • 1
  • 1
5

Resizing images in a canvas element is generally bad idea since it uses the cheapest box interpolation. The resulting image noticeable degrades in quality. I'd recommend using http://nodeca.github.io/pica/demo/ which can perform Lanczos transformation instead. The demo page above shows difference between canvas and Lanczos approaches.

It also uses web workers for resizing images in parallel. There is also WEBGL implementation.

There are some online image resizers that use pica for doing the job, like https://myimageresizer.com

Ivan Nikitin
  • 3,578
  • 27
  • 39
0

You can use dropzone.js if you want to use simple and easy upload manager with resizing before upload functions.

It has builtin resize functions, but you can provide your own if you want.

Michał Zalewski
  • 3,123
  • 2
  • 24
  • 37