1

I am trying to create an image compressor without any third-party library.

Here is my code(it will finally returns the blob of compressed image):

 async function CompressImage(File, Quality, FileType) {
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var img = new Image;
        img.onload = function () {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0, img.width, img.height);                        
                canvas.toBlob(function (blob) {                    
                    return blob;
                }, FileType, Quality);            
            }
        }
        img.src = URL.createObjectURL(File);
    }

It seems there is nothing wrong with my code while always returns an undefined value.

I found out maybe it is the asynchronous problem. The img.onload and the canvas.toBlob are asynchronous methods.

I think the promise may help this while I don't know how to order these methods run one by one and finally returns a value.

How can I solve this? Thank you.

Melon NG
  • 2,568
  • 6
  • 27
  • 52
  • Does this answer your question? [access blob value outside of canvas.ToBlob() async function](https://stackoverflow.com/questions/42458849/access-blob-value-outside-of-canvas-toblob-async-function) – Mellet Feb 20 '21 at 01:37
  • @Mellet It is similar while not the same, I have to draw the file to the canvas first and the img.onload is asynchronous also. – Melon NG Feb 20 '21 at 08:50
  • @MelonNG See https://stackoverflow.com/q/45788934/1048572 for how to wait for the image being loaded – Bergi Feb 21 '21 at 15:11

1 Answers1

1

Use a Promise instead of async, in the end is the same but syntactically are not, in a Promise you get 2 functions as parameters (typically called resolve and reject), these functions can be run inside any callback no problem, but in an async function becomes problematic because there's no way to await for a function that receives a callback (because is not a Promise that you can await).

TLDR; async functions do not play well executing functions that run callbacks, use a Promise directly so you can run resolve() anywhere, even if is on a callback function.

For example:

function CompressImage(File, Quality, FileType) {
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  var img = new Image;
  var prom = new Promise(function(resolve) {
    img.onload = function () {
      canvas.width = img.width;
      canvas.height = img.height;
      ctx.drawImage(img, 0, 0, img.width, img.height);                        
          canvas.toBlob(function (blob) {                    
              return resolve(blob);
          }, FileType, Quality);            
      }
    }
    img.src = URL.createObjectURL(File);
  });
  return prom;
}

That should do the trick, look how I'm creating the promise to resolve it with the callback of canvas.toBlob, if you notice, the function is not async because is returning a Promise directly, but in your code, you can treat it just like an async function.

Another way to do it with a little more updated syntax can be:

const CompressImage = async (File, Quality, FileType) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image;
  return await (new Promise(resolve => {
    img.onload = () => {
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0, img.width, img.height);                        
            canvas.toBlob(resolve, FileType, Quality);            
        }
    }
    img.src = URL.createObjectURL(File);
  }));
};

And should be the same.

I haven't tested this but if is broken is pretty close.

UPDATE:

Well, after playing with the jsfiddle you provided I have an interesting finding:

$(function () {
    $("#FileUploader").change(function () {
        StartCompress(this.files[0]);
    });
    function StartCompress(input) {
        $("#beforesize").text(input.size);
        CompressFile(input, 0.8).then(blob => $("#afteresize").text(blob.size));
        
    }
    function CompressFile(file, quality) {
      var canvas = document.createElement('canvas');
      var ctx = canvas.getContext('2d');
      var img = new Image();
      var prom = new Promise(resolve => {
        img.onload = () => {
          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img, 0, 0);
          canvas.toBlob(blob => {
            resolve(blob)
          }, file.type, quality);
        }
        img.src = URL.createObjectURL(file);
      });
      return prom;  
    }
});

I modified a little bit the code so my ocd can be in peace hahaha, but also, the problem is the MIME (FileType), because you are not providing canvas with the file type, is just grabbing it and converting it to png and that's a lossless format, so it will be bigger if you are selecting a jpg image (that allows loss) and basically scaling it to fit the same size.

Try the code now (I'm using file.type to provide the type of the file to canvas.toBlob), It goes to hell with png and I suppose is related to the issue I pointed out.

Hope it helps.

Sebastián Espinosa
  • 2,123
  • 13
  • 23