0

I am setting up a canvas and then drawing an image into that canvas, followed by calling canvas.toBlob(function(blob)...), but I am finding the blob argument inside the toBlob is sometimes null.

Why would this be? Should I be waiting for something after drawImage or something (even though in the snippet - you can see I wait for the image to be loaded before proceeding)?

//--------------------------------------------------
function doImageFileInsert(fileinput) {
  var newImg = document.createElement('img');

  var img = fileinput.files[0];
  let reader = new FileReader();
  reader.onload = (e) => {
    let base64 = e.target.result;
    newImg.src = base64;
    doTest(newImg);
  };
  reader.readAsDataURL(img);

  fileinput.value = ''; // reset ready for another file
}
//--------------------------------------------------
function doTest(imgElem) {
  console.log('doTest');
  var canvas = document.createElement("canvas");
  var w = imgElem.width;
  var h = imgElem.height;
  canvas.width = w;
  canvas.height = h;
  var ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(imgElem, 0, 0, w, h);

  canvas.toBlob(function(blob) {
    if (blob) {
      console.log('blob is good');
    } else {
      console.log('blob is null');
      alert('blob is null');
    }
  }, 'image/jpeg', 0.9);
}
canvas, div {
    border:solid 1px grey;
    padding:10px;
    margin:5px;
    border-radius:9px;
}
img {
    width:100%;
    height:auto;
}
<input type='file' value='Add' onChange='doImageFileInsert(this)'>

Also available at https://jsfiddle.net/Abeeee/rtwcge5h/24/.

If you add images via the Choose-File button enough times then you get the problem (alert('blob is null')).

Kaiido
  • 123,334
  • 13
  • 219
  • 285
user1432181
  • 918
  • 1
  • 9
  • 24
  • [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask): Include a [mcve] _in the question itself_ and not only an excerpt and a link to an external resource (that might not be reachable or change its content for whatever reason). – Andreas May 26 '21 at 17:57
  • According to the [specification](https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob) there's only one step that could be a problem in this case: _"If `result` is _non-null_, then set result to a [serialization of `result` as a file](https://html.spec.whatwg.org/multipage/canvas.html#a-serialisation-of-the-bitmap-as-a-file) with type and quality if given"_ + _"**If an error occurs** during the creation of the image file (e.g. an internal encoder error), then **the result of the serialization is `null`**."_ – Andreas May 26 '21 at 18:00
  • @Andreas you missed the step 3, which is where the result is **not** set in OP's case (since their canvas is 0x0px). – Kaiido May 26 '21 at 22:52
  • @Kaiido No, I didn't missed that step, but I missed that OP is only waiting for the `FileReader` and not for the `Image` :/ – Andreas May 27 '21 at 04:54
  • @Andreas Well, why say "there's only one step..." then? Clearly that's at least two. – Kaiido May 27 '21 at 05:13
  • @Kaiido Because... _"I missed that OP is only waiting for the `FileReader`"_ o.O – Andreas May 27 '21 at 06:47

3 Answers3

2

There are only a few reasons why toBlob would produce null:

  • A bug in the browser's encoder (never seen it myself).
  • A canvas whose area is bigger than the maximum supported by the UA.
  • A canvas whose width or height is 0.

Since you are not waiting for the image to load, its width and height properties are still 0, and you fall in the third bullet above since you do set the canvas's size to these.


So to fix your error, wait for the image has loaded before doing anything with it.
Also, note that you should almost never use FileReader.readAsDataURL(), and certainly not to display media files from a Blob, instead generate a blob:// URI from these Blobs using URL.createObjectURL().

But in your case, you can even use the better createImageBitmap API which will take care of loading the image in your Blob and will generate a memory friendly ImageBitmap object which is ready to be drawn by the GPU without any more computation.
Only Safari hasn't implemented the basics of this API yet, but they should soon (it's exposed behind flags, and unflagged in TP version), and I wrote a polyfill you can use to fix the holes in various implementations.

const input = document.querySelector("input");
input.oninput = async (evt) => {
  const img = await createImageBitmap(input.files[0]);
  const canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  // I hope you'll do something more here,
  // reencoding an iamge to JPEG through the Canvas API is a bad idea
  // Canvas encoders aim at speed, not quality
  canvas.toBlob( (blob) => {
    console.log( blob );
  }, "image/jpeg", 0.9 );
};
<!-- createImageBitmap polyfill for Safari -->
<script src="https://cdn.jsdelivr.net/gh/Kaiido/createImageBitmap/dist/createImageBitmap.js"></script>
<input type="file" accept="image/*">
Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

Okay, it looks like it was the caller to the canvas work that was the problem - and indeed the image had not been loaded fully by the time the drawImage (probably) was run.

The original call to doTest() was

function doImageFileInsert(fileinput) {
    var contenteditable = document.getElementById('mydiv');
    var newImg = document.createElement('img');
    //contenteditable.appendChild(newImg);


    var img = fileinput.files[0];
    let reader = new FileReader();
    reader.onload = (e) => {
      let base64 = e.target.result;
      newImg.src = base64;
      doTest(newImg);  <-----
    };
    reader.readAsDataURL(img);
 }

but it was the call that was at fault

changing it to

function doImageFileInsert(fileinput) {
    var contenteditable = document.getElementById('mydiv');
    var newImg = document.createElement('img');
    //contenteditable.appendChild(newImg);


    var img = fileinput.files[0];
    let reader = new FileReader();
    reader.onload = (e) => {
      let base64 = e.target.result;
      newImg.src = base64;
      //doTest(newImg);  
      newImg.onload = (e) => { doTest(newImg); };   <-----
    };
    reader.readAsDataURL(img);
 }

seems to fix it. A working version can be seen in https://jsfiddle.net/Abeeee/rtwcge5h/25/

user1432181
  • 918
  • 1
  • 9
  • 24
0

Here is a more modern approach if you wish to use it

/**
 * @param {HTMLInputElement} fileInput 
 */
async function doImageFileInsert (fileInput) {
  const img = new Image()
  img.src = URL.createObjectURL(fileInput.files[0])
  await img.decode()
  doTest(img)
}

or this that don't work in all browser (would require a polyfill)

/**
 * @param {HTMLInputElement} fileInput 
 */
function doImageFileInsert (fileInput) {
  createImageBitmap(fileInput.files[0]).then(doTest)
}

The FileReader is such a legacy pattern now days when there is new promise based read methods on the blob itself and that you can also use URL.createObjectURL(file) instead of wasting time encoding a file to base64 url (only to be decoded back to binary again by the <img>) it's a waste of time, processing, and RAM.

Endless
  • 34,080
  • 13
  • 108
  • 131
  • From a Blob better use `createImageBitmap`. [Here is a polyfill](https://github.com/Kaiido/createImageBitmap) for Safari (that I wrote), though they should add support soon enough. Also it would make a way better answer explaining why the response was `null`. – Kaiido May 26 '21 at 22:49