Automatically Crop HTML5 canvas to contents

Viewed 15299

Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.

+---------------+
|               |
|               |
|     (.Y. )    |
|      /_       |
|     \____/    |
|               |
|               |
+---------------+

What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.

+------+
|(.Y. )|
| /_   |
|\____/|
+------+

Thanks!

5 Answers

Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.

function trimCanvas(canvas){
    const context = canvas.getContext('2d');

    const topLeft = {
        x: canvas.width,
        y: canvas.height,
        update(x,y){
            this.x = Math.min(this.x,x);
            this.y = Math.min(this.y,y);
        }
    };

    const bottomRight = {
        x: 0,
        y: 0,
        update(x,y){
            this.x = Math.max(this.x,x);
            this.y = Math.max(this.y,y);
        }
    };

    const imageData = context.getImageData(0,0,canvas.width,canvas.height);

    for(let x = 0; x < canvas.width; x++){
        for(let y = 0; y < canvas.height; y++){
            const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
            if(alpha !== 0){
                topLeft.update(x,y);
                bottomRight.update(x,y);
            }
        }
    }

    const width = bottomRight.x - topLeft.x;
    const height = bottomRight.y - topLeft.y;

    const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
    canvas.width = width;
    canvas.height = height;
    context.putImageData(croppedCanvas,0,0);

    return canvas;
}

The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:

var img = new Image;
img.onload = () => {

  var canvas = document.getElementById('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);

  document.getElementById('button').addEventListener('click', ()=>{
    autoCropCanvas(canvas, ctx);
    document.getElementById('button').remove();
  });

};
img.src = 'data:image/png;base64,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';


function autoCropCanvas(canvas, ctx) {
  var bounds = {
   left: 0,
   right: canvas.width,
   top: 0,
   bottom: canvas.height
  };
  var rows = [];
  var cols = [];
  var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  for (var x = 0; x < canvas.width; x++) {
   cols[x] = cols[x] || false;
   for (var y = 0; y < canvas.height; y++) {
    rows[y] = rows[y] || false;
    const p = y * (canvas.width * 4) + x * 4;
    const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
    var isEmptyPixel = Math.max(r, g, b, a) === 0;
    if (!isEmptyPixel) {
     cols[x] = true;
     rows[y] = true;
    }
   }
  }
  for (var i = 0; i < rows.length; i++) {
   if (rows[i]) {
    bounds.top = i ? i - 1 : i;
    break;
   }
  }
  for (var i = rows.length; i--; ) {
   if (rows[i]) {
    bounds.bottom = i < canvas.height ? i + 1 : i;
    break;
   }
  }
  for (var i = 0; i < cols.length; i++) {
   if (cols[i]) {
    bounds.left = i ? i - 1 : i;
    break;
   }
  }
  for (var i = cols.length; i--; ) {
   if (cols[i]) {
    bounds.right = i < canvas.width ? i + 1 : i;
    break;
   }
  }
  var newWidth = bounds.right - bounds.left;
  var newHeight = bounds.bottom - bounds.top;
  var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
  canvas.width = newWidth;
  canvas.height = newHeight;
  ctx.putImageData(cut, 0, 0);
 }
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>

Here's code in ES syntax, short, fast and concise:

/**
 * Trim a canvas.
 * 
 * @author Arjan Haverkamp (arjan at avoid dot org)
 * @param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
 * @param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
 * @returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
 */
const trimCanvas = (canvas, threshold = 0) => {
    const ctx = canvas.getContext('2d'),
        w = canvas.width, h = canvas.height,
        imageData = ctx.getImageData(0, 0, w, h),
        tlCorner = { x:w+1, y:h+1 },
        brCorner = { x:-1, y:-1 };

    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
                tlCorner.x = Math.min(x, tlCorner.x);
                tlCorner.y = Math.min(y, tlCorner.y);
                brCorner.x = Math.max(x, brCorner.x);
                brCorner.y = Math.max(y, brCorner.y);
            }
        }
    }

    const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);

    canvas.width = brCorner.x - tlCorner.x;
    canvas.height = brCorner.y - tlCorner.y;

    ctx.putImageData(cut, 0, 0);

    return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
Related