How to render a blob on a canvas element?
Asked Answered
A

3

14

How to render an image blob to a canvas element?

So far i have these two (simplified) functions to capture an image, transform it to a blob and eventually render the blob on a canvas in this codepen, it just returns the default black image.

var canvas = document.getElementById('canvas');
var input = document.getElementById('input');
var ctx = canvas.getContext('2d');
var photo;


function picToBlob() {
  var file = input.files[0];

  canvas.toBlob(function(blob) {
    var newImg = document.createElement("img"),
      url = URL.createObjectURL(blob);

    newImg.onload = function() {
      ctx.drawImage(this, 0, 0);
      photo = blob;
      URL.revokeObjectURL(url);
    };

    newImg.src = url;
  }, file.type, 0.5);

  canvas.renderImage(photo);
}

HTMLCanvasElement.prototype.renderImage = function(blob) {

  var canvas = this;
  var ctx = canvas.getContext('2d');
  var img = new Image();

  img.onload = function() {
    ctx.drawImage(img, 0, 0)
  }
  img.src = URL.createObjectURL(blob);
}

input.addEventListener('change', picToBlob, false);
Animatism answered 24/6, 2016 at 2:51 Comment(1)
You are calling canvas.toBlobbefore you draw on it (in your canvas.renderImage, which is async btw) and you wonder why it outputs a transparent image? I'd say it's because you didn't drawn anything on your canvas at the time you tried to export it. Ps: in your renderImage, don't forget to also revoke the URL object, and you'll probably need to resize your canvas.Hortatory
E
20

I think you need to tidy up your code a bit. It's hard to know what you are trying to achieve because there are many unnecessary lines of code. The main problem is that blob is coming undefined here

HTMLCanvasElement.prototype.renderImage = function(blob){

because photo never gets initialized here inside the toBlob function...which is unnecessary for what you are trying to achieve.

Here's a simplified working version of your code snippet

var canvas = document.getElementById('canvas');
var input = document.getElementById('input');


  function picToBlob() {
    canvas.renderImage(input.files[0]);
  }

HTMLCanvasElement.prototype.renderImage = function(blob){
  
  var ctx = this.getContext('2d');
  var img = new Image();

  img.onload = function(){
    ctx.drawImage(img, 0, 0)
  }

  img.src = URL.createObjectURL(blob);
};

input.addEventListener('change', picToBlob, false);
<input type='file' accept='image' capture='camera' id='input'>
<canvas id = 'canvas'></canvas>
Epilate answered 24/6, 2016 at 3:44 Comment(6)
I too was looking at this approach, hoping to stick with raw data (from getImageData), but since URL.createObjectURL just ends up creating a string anyways I ended up skipping a step and going straight to toDataURL instead for my image src.Cockade
I don't understand why such a bad practice is upvoted, modifying prototype is a bad practice and leads to problems.Stidham
@Stidham because the context of the question is not about best practices...if you don't like the answer then feel free to contribute by adding yoursEpilate
@Cockade You are mistaken: createObjectURL returns a short URI that contains a handle (aka alias, or pointer, or reference) to the already-loaded binary image data from blob, so it's blazing fast and doesn't consume any extra RAM, whereas using toDataURL is the worst way to load images and other binary data in JavaScript because it's a blocking, synchronous function (so it freezes the browser's page thread!) and more than doubles your RAM usage for each file (as data: URIs use Base64 encoding, which requires 125% the memory of the source binary).Elisavetgrad
Be careful when using the code in this answer: if you ever change img.src after renderImage is used then ensure that you use URL.revokeObjectURL on the old img.src otherwise it will have a resource-leakage bug. MDN has a good guide on Using Object URLs.Elisavetgrad
@Elisavetgrad you're absolutely right, I had been using toDataURL for HTML elsewhere on the project and didn't understand it fully. I ended up doing exactly as you stated with createObjectURL for subsequent projects.Cockade
M
10

You can also use createImageBitmap to directly render a blob into the canvas:

createImageBitmap(blob).then(imageBitmap=>{ctx.drawImage(imageBitmap,0,0)})

var canvas = document.getElementById('canvas');
var input = document.getElementById('input');


function blobToCanvas() {
  createImageBitmap(input.files[0]).then(imageBitmap => {
    console.log(imageBitmap);
    canvas.getContext('2d').drawImage(imageBitmap, 0, 0)
  })
}


input.addEventListener('change', blobToCanvas, false);
<input type='file' accept='image' capture='camera' id='input'>
<canvas id='canvas'></canvas>
Merrill answered 3/2, 2022 at 9:7 Comment(1)
If you have issue rendering larger images from files (via URL.createObjectURL) the detour via canvas helped!Eyeleteer
Q
3

You can use it as below

function renderImage(canvas, blob) {
  const ctx = canvas.getContext('2d')
  const img = new Image()
  img.onload = (event) => {
    URL.revokeObjectURL(event.target.src) // 👈 This is important. If you are not using the blob, you should release it if you don't want to reuse it. It's good for memory.
    ctx.drawImage(event.target, 0, 0)
  }
  img.src = URL.createObjectURL(blob)
}

below is an example

/**
 * @param {HTMLCanvasElement} canvas: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
 * @param {Blob} blob: https://developer.mozilla.org/en-US/docs/Web/API/Blob
 * */
function renderImage(canvas, blob) {
  const ctx = canvas.getContext('2d')
  switch (blob.type) {
    case "image/jpeg": // Normally, you don't need it (switch), but if you have a special case, then you can consider it.
    case "image/png":
      const img = new Image()
      img.onload = (event) => {
        URL.revokeObjectURL(event.target.src) // Once it loaded the resource, then you can free it at the beginning.
        ctx.drawImage(event.target, 0, 0)
      }
      img.src = URL.createObjectURL(blob)
      break  
  }
}

// 👇 below is test
(() => {
  const canvas = document.querySelector('canvas')
  const input = document.querySelector('input')
  input.addEventListener('change',
    (event) => {
      const file = event.target.files[0]
      const blob = new Blob(
        [file],
        {"type": file.type} // If the type is unknown, default is empty string.
      )
      renderImage(canvas, blob)
    }
  )
})()
<div><input type='file' accept='.png,.jpg'></div>
<canvas></canvas>

another example to show you What effect of the revokeObjectURL.

<div></div>
<canvas width="477" height="600"></canvas>
<script>
  async function renderImage(canvas, blob, isNeedRevoke=true) {
    const ctx = canvas.getContext('2d')
    const img = new Image() // The upper part of the painting.
    const img2 = new Image() // The lower part of the painting.

    await new Promise(resolve => {
      img.onload = (event) => {
        if (isNeedRevoke) {
          URL.revokeObjectURL(event.target.src)
        }
        ctx.drawImage(event.target,
          0, 0, 477, 300,
          0, 0, 477, 300
        )
        resolve()
      }
      img.src = URL.createObjectURL(blob)
      setTimeout(resolve, 2000)
    }).then(() => {
      img2.onload = (event) => {
        ctx.drawImage(event.target,
          0, 300, 477, 300,
          0, 300, 477, 300
        )
      }
      img2.src = img.src // 👈 If URL.revokeObjectURL(img.src) happened, then img2.src can't find the resource, such that img2.onload will not happen.
    })
  }

  function CreateTestButton(canvas, btnText, isNeedRevoke) {
    const button = document.createElement("button")
    button.innerText = btnText
    button.onclick = async (event) => {
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height)  // clear canvas
      fetch("https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/PNG_Test.png/477px-PNG_Test.png")
        .then(async response=>{
          const blob = await response.blob()
          renderImage(canvas, blob, isNeedRevoke)
        }).catch(err=>console.error(err))
    }
    return button
  }

  (() => {
    window.onload = () => {
      const canvas = document.querySelector('canvas')
      const div = document.querySelector('div')
      const btn1 = CreateTestButton(canvas, "Without URL.revokeObjectURL", false)
      const btn2 = CreateTestButton(canvas, "URL.revokeObjectURL", true)
      div.append(btn1, btn2)
    }
  })()
</script>
Quadrivalent answered 23/7, 2021 at 10:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.