简体   繁体   中英

Automatically Crop HTML5 canvas to contents

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!

Edited (see comments )

function cropImageFromCanvas(ctx) {
  var canvas = ctx.canvas, 
    w = canvas.width, h = canvas.height,
    pix = {x:[], y:[]},
    imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
    x, y, index;
  
  for (y = 0; y < h; y++) {
    for (x = 0; x < w; x++) {
      index = (y * w + x) * 4;
      if (imageData.data[index+3] > 0) {
        pix.x.push(x);
        pix.y.push(y);
      } 
    }
  }
  pix.x.sort(function(a,b){return a-b});
  pix.y.sort(function(a,b){return a-b});
  var n = pix.x.length-1;
  
  w = 1 + pix.x[n] - pix.x[0];
  h = 1 + pix.y[n] - pix.y[0];
  var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);

  canvas.width = w;
  canvas.height = h;
  ctx.putImageData(cut, 0, 0);
        
  var image = canvas.toDataURL();
}

If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).

Here is how I'll do it.

  1. Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.

  2. Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.

  3. Store that region of pixeldata.

  4. Resize your canvas to its new dimensions (the ones of the region we got at step 2.)

  5. Paste the saved region back to the canvas.

Et, voilá :)

Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.

I prepared a small sketch in jsFiddle that you can use as starting point for your code.

Working sample at jsFiddle

Hope I've helped you.
c:.

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};
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM