4

I'm trying to resize an image on a mobile device. (Using ionic 2 framework, based on angular 2).

My maximum file size is 5MB and some devices capture images bigger than that. So I'm currently trying to resize the image using canvas.toDataURL() but this is extremely slow. (App doesn't respond for 15-20 seconds).

My current resize function is the following:

private resize(outputFormat, sourceImgObj, quality) {
    let mimeType = "image/jpeg";

    let cvs = document.createElement('canvas');
    cvs.width = sourceImgObj.naturalWidth;
    cvs.height = sourceImgObj.naturalHeight;
    let ctx = cvs.getContext("2d").drawImage(sourceImgObj, 0, 0);

    let newImageData = cvs.toDataURL(mimeType, quality/100);
    return newImageData;
}

Which (I believe) at the time was based on, if not the same as, j-i-c.

This function does work. In the browser it's decent but still slow (Chrome). But when running this function on a device while for example selecting an image of 8 MB, the app will basically crash.

Is there any way to make this compression/resizing of the image faster?


Additional info

I'm getting the file itself by using cordova-plugin-camera which is a direct link to the file on the user's device. So this is not a base64 image (but I do am able to obtain one easily if necessary).

Ivar Reukers
  • 7,560
  • 9
  • 56
  • 99
  • 2
    Did you try to use `cvs.toBlob` instead of `cvs.toDataURL`? It might be better because the Blob doesn't need a new conversion to (probably) base64 string. – Dekel Aug 12 '17 at 23:47
  • 1
    See https://nodeca.github.io/pica/demo/ Hopefully it helps! – TheChetan Aug 14 '17 at 05:11
  • Do you absolutely need to keep the image's **dimensions** ? I guess your 8MB image is quite large and if it comes from the device camera, the compression might already be quite decent. So when you draw it on the canvas, you are actually producing a new raw image (without any compression), with the same amount of pixels than in your original file. This means that your canvas itself will be way larger than the original 8MB in memory. When calling toDataURL, it will have to extract this data, process it etc, eating even more memory. And it's not even sure that at the end you'll get a lighter file – Kaiido Aug 16 '17 at 07:55
  • So all in all, instead of trying to reduce the quality, is it possible for you to first try to reduce the dimensions of these images ? – Kaiido Aug 16 '17 at 07:57
  • No that doesnt matter as long as the aspect ratio is maintained – Ivar Reukers Aug 16 '17 at 07:57
  • You should also know about canvas size limits - https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element – Alex Nikulin Aug 17 '17 at 11:12

3 Answers3

3

Your resize function really is badly named. What it does is to change the quality of the jpeg lossy algorithm. It doesn't really resize your images, and I guess your 8MB image is quite large and if it comes from the device camera, the compression might already be quite decent.

So when you draw it on the canvas, you are actually producing a new raw image (without any compression), with the same amount of pixels than in your original file.

This means that your canvas itself will be way larger than the original 8MB in memory. When calling toDataURL, it will have to extract this data, process it etc, eating even more memory.

And it's not even sure that at the end you'll get a lighter file...

If you're ok to really resize (i.e change the dimensions of) your images, then it will be easier for your device to handle it :

function resize(sourceImgObj) {
    const mimeType = "image/jpeg"; // would probably be better to keep the one of the file...
    let quality = .92; // default;
    const cvs = document.createElement('canvas');
    const MAX_SIZE = 500; // in px for both height and width
    const w = sourceImgObj.naturalWidth;
    const h = sourceImgObj.naturalHeight;
    let ratio = MAX_SIZE / Math.max(w, h);
    if(ratio > 1){ // if it's smaller than our defined MAX_SIZE
      ratio = 1;
      quality = .5; // lower the jpeg quality
      }
    cvs.width = ratio * w;
    cvs.height = ratio * h;
    let ctx = cvs.getContext("2d").drawImage(sourceImgObj, 0, 0, cvs.width, cvs.height);

    // note : if it's not a problem to convert it to async then toBlob would be a better choice
    let newImageData = cvs.toDataURL(mimeType, quality);
    return newImageData;
}

inp.onchange = e => {
  const MAX_SIZE = 100000; // 100KB
  let img = new Image();
  img.src = URL.createObjectURL(inp.files[0]);
  img.onload = e => {
    // if it's not too big, return the image's current src
    let dataURL = (inp.files[0].size > MAX_SIZE) ?
    resize(img) : img.src;
    let out = new Image();
    out.src = dataURL;
    document.body.appendChild(out);
    };
  }
<input type="file" id="inp">

edit (ivaro18)

I just wanted to add in my modified version of this answer. I don't like reducing the quality parameter to 50%, so I stayed with the ratio and modified it to suit my needs. This resizes an 8.1MB image within 600ms to 900kB.

resize(sourceImgObj, callback) {
   const mimeType = "image/jpeg";
   let quality = .92; // default;
   const cvs = document.createElement('canvas');
   const MAX_SIZE = 1024; // in px for both height and width
   const w = sourceImgObj.naturalWidth;
   const h = sourceImgObj.naturalHeight;
   let ratio = MAX_SIZE / Math.max(w, h);
   if(ratio > 1) {
       let percentage = (w >= h) ? ((w - MAX_SIZE) / w * 100) : ((h - MAX_SIZE) / h * 100);
       cvs.width = (percentage + 100)/100 * w;
       cvs.height = (percentage + 100)/100 * h;

   } else {
       cvs.width = w * ratio;
       cvs.height = h * ratio;
   }

   let ctx = cvs.getContext("2d").drawImage(sourceImgObj, 0, 0, cvs.width, cvs.height);

   let newImageData = cvs.toDataURL(mimeType, quality);
   callback(newImageData);
}
Ivar Reukers
  • 7,560
  • 9
  • 56
  • 99
Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

If the sole purpose of compression is to reduce the file size, then you can specify the image quality in the plugin's config, at the time of taking a pic.

You can try several different values and see which one best works out for you.

https://github.com/apache/cordova-plugin-camera#cameracameraoptions--object

Abhishek Jain
  • 445
  • 1
  • 4
  • 11
  • But how will I know the image size beforehand? I only want to downscale the image if its above 5MB, everything below would keep their original quality. – Ivar Reukers Aug 16 '17 at 07:24
0

There is already a library for doing this, no need to re-invent the wheel

https://www.npmjs.com/package/ngx-image-compress

You can try it here https://stackblitz.com/edit/ngx-compress-sample

David Faure
  • 1,336
  • 14
  • 25