40

I have a canvas in my webpage; I create a new Image data in this canvas then I modify some pixel through myImgData.data[] array. Now I would like to scale this image and make it bigger. I tried by scaling the context but the image remains small. Is it possible to do this? Thanks

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Masiar
  • 20,450
  • 31
  • 97
  • 140

6 Answers6

44

You could draw the imageData to a new canvas, scale the original canvas and then draw the new canvas to the original canvas.

Something like this should work:

var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = $("<canvas>")
    .attr("width", imageData.width)
    .attr("height", imageData.height)[0];
    
newCanvas.getContext("2d").putImageData(imageData, 0, 0);
    
context.scale(1.5, 1.5);
context.drawImage(newCanvas, 0, 0);

Here's a functioning demo http://jsfiddle.net/Hm2xq/2/.

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Castrohenge
  • 8,525
  • 5
  • 39
  • 66
  • 21
    I would just like to add that you could do this without creating another canvas as ctx.drawImage(ctx.canvas,0,0); draws the canvas onto itself, and you can scale as you draw this. – Overcode May 12 '13 at 19:01
  • 3
    but that scale also messes up coordinates 10 x is 1.5*10. How do you compensate for that – Muhammad Umer Nov 07 '14 at 04:14
  • i have same problem with this case: the newcanvas have alpha = 0 in some pixel. when drawing newcanvas to the original canvas. the alpha become black. how to solve this – billyzaelani Mar 15 '17 at 17:13
  • this is great tkhs! – wcandillon May 28 '23 at 20:52
20

I needed to do it without the interpolation that putImageData() causes, so I did it by scaling the image data into a new, resized ImageData object. I can't think of any other time I've thought that using 5 nested for loops was a good idea:

function scaleImageData(imageData, scale) {
  var scaled = c.createImageData(imageData.width * scale, imageData.height * scale);

  for(var row = 0; row < imageData.height; row++) {
    for(var col = 0; col < imageData.width; col++) {
      var sourcePixel = [
        imageData.data[(row * imageData.width + col) * 4 + 0],
        imageData.data[(row * imageData.width + col) * 4 + 1],
        imageData.data[(row * imageData.width + col) * 4 + 2],
        imageData.data[(row * imageData.width + col) * 4 + 3]
      ];
      for(var y = 0; y < scale; y++) {
        var destRow = row * scale + y;
        for(var x = 0; x < scale; x++) {
          var destCol = col * scale + x;
          for(var i = 0; i < 4; i++) {
            scaled.data[(destRow * scaled.width + destCol) * 4 + i] =
              sourcePixel[i];
          }
        }
      }
    }
  }

  return scaled;
}

I hope that at least one other programmer can copy and paste this into their editor while muttering, "There but for the grace of god go I."

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Casey Rodarmor
  • 14,878
  • 5
  • 30
  • 33
18

I know it's an old subject, but since people like may find it useful, I add my optimization to the code of rodarmor :

function scaleImageData(imageData, scale) {
    var scaled = ctx.createImageData(imageData.width * scale, imageData.height * scale);
    var subLine = ctx.createImageData(scale, 1).data
    for (var row = 0; row < imageData.height; row++) {
        for (var col = 0; col < imageData.width; col++) {
            var sourcePixel = imageData.data.subarray(
                (row * imageData.width + col) * 4,
                (row * imageData.width + col) * 4 + 4
            );
            for (var x = 0; x < scale; x++) subLine.set(sourcePixel, x*4)
            for (var y = 0; y < scale; y++) {
                var destRow = row * scale + y;
                var destCol = col * scale;
                scaled.data.set(subLine, (destRow * scaled.width + destCol) * 4)
            }
        }
    }

    return scaled;
}

This code uses less loops and runs roughly 30 times faster. For instance, on a 100x zoom of a 100*100 area this codes takes 250 ms while the other takes more than 8 seconds.

user3079576
  • 181
  • 1
  • 2
17

You can scale the canvas using the drawImage method.

context = canvas.getContext('2d');
context.drawImage( canvas, 0, 0, 2*canvas.width, 2*canvas.height );

This would scale the image to double the size and render the north-west part of it to the canvas. Scaling is achieved with the third and fourth parameters to the drawImage method, which specify the resulting width and height of the image.

See docs at MDN https://developer.mozilla.org/en-US/docs/DOM/CanvasRenderingContext2D#drawImage%28%29

Sixtease
  • 790
  • 8
  • 18
1

@Castrohenge's answer works, but as Muhammad Umer points out, it messes up the mouse coordinates on the original canvas after that. If you want to maintain the ability to perform additional scales (for cropping, etc.) then you need to utilize a second canvas (for scaling) and then fetch the image data from the second canvas and put that into the original canvas. Like so:

function scaleImageData(imageData, scale){
    var newCanvas = $("<canvas>")
      .attr("width", imageData.width)
      .attr("height", imageData.height)[0];

    newCanvas.getContext("2d").putImageData(imageData, 0, 0);

    // Second canvas, for scaling
    var scaleCanvas = $("<canvas>")
      .attr("width", canvas.width)
      .attr("height", canvas.height)[0];

    var scaleCtx = scaleCanvas.getContext("2d");

    scaleCtx.scale(scale, scale);
    scaleCtx.drawImage(newCanvas, 0, 0);

    var scaledImageData =  scaleCtx.getImageData(0, 0, scaleCanvas.width, scaleCanvas.height);

    return scaledImageData;
}
Chris Holmes
  • 11,444
  • 12
  • 50
  • 64
  • 4
    for, `canvas.width`, and `canvas.height`, where is the variable `canvas` defined? –  May 11 '17 at 23:26
0

Nowadays, the best way to render a scaled ImageData object is generally to create an ImageBitmap from it.
All modern browsers finally do support it.

This will use a faster path to render the ImageData's content to a bitmap readily available to be painted by drawImage(), theoretically a lot faster than the second best option of putting the ImageData on a secondary <canvas> and redraw that <canvas>.

The main catch-up is that createImageBitmap() is asynchronous*, and thus it may not fit in an animation frame very well.

(async () => {
  // here I'll just make some noise in my ImageData
  const imagedata = new ImageData(100, 100);
  const arr = new Uint32Array(imagedata.data.buffer);
  for( let i = 0; i < arr.length; i++ ) {
    arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
  }
  // now to render it bigger
  const bitmap = await createImageBitmap(imagedata);
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  ctx.imageSmoothingEnabled = false; // keep pixel perfect
  ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
})();
<canvas width="1000" height="1000"></canvas>

* Technically, only Firefox does implement createImageBitmap(<ImageData>) asynchronously, Chrome and Safari will resolve the returned Promise synchronously, and there, it's safe to use it in an animation frame: https://jsfiddle.net/vraz3xcg/

Kaiido
  • 123,334
  • 13
  • 219
  • 285