Search code examples
javascriptcsshtmlcanvas

Cropping an HTML canvas to the width/height of its visible pixels (content)?


Can an HTML canvas element be internally cropped to fit its content?

For example, if I have a 500x500 pixel canvas with only a 10x10 pixel square at a random location inside it, is there a function which will crop the entire canvas to 10x10 by scanning for visible pixels and cropping?


Edit: this was marked as a duplicate of Javascript Method to detect area of a PNG that is not transparent but it's not. That question details how to find the bounds of non-transparent content in the canvas, but not how to crop it. The first word of my question is "cropping" so that's what I'd like to focus on.


Solution

  • A better trim function.

    Though the given answer works it contains a potencial dangerous flaw, creates a new canvas rather than crop the existing canvas and (the linked region search) is somewhat inefficient.

    Creating a second canvas can be problematic if you have other references to the canvas, which is common as there are usually two references to the canvas eg canvas and ctx.canvas. Closure could make it difficult to remove the reference and if the closure is over an event you may never get to remove the reference.

    The flaw is when canvas contains no pixels. Setting the canvas to zero size is allowed (canvas.width = 0; canvas.height = 0; will not throw an error), but some functions can not accept zero as an argument and will throw an error (eg ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height); is common practice but will throw an error if the canvas has no size). As this is not directly associated with the resize this potencial crash can be overlooked and make its way into production code.

    The linked search checks all pixels for each search, the inclusion of a simple break when an edge is found would improve the search, there is still an on average quicker search. Searching in both directions at the same time, top and bottom then left and right will reduce the number of iterations. And rather than calculate the address of each pixel for each pixel test you can improve the performance by stepping through the index. eg data[idx++] is much quicker than data[x + y * w]

    A more robust solution.

    The following function will crop the transparent edges from a canvas in place using a two pass search, taking in account the results of the first pass to reduce the search area of the second.

    It will not crop the canvas if there are no pixels, but will return false so that action can be taken. It will return true if the canvas contains pixels.

    There is no need to change any references to the canvas as it is cropped in place.

    // ctx is the 2d context of the canvas to be trimmed
    // This function will return false if the canvas contains no or no non transparent pixels.
    // Returns true if the canvas contains non transparent pixels
    function trimCanvas(ctx) { // removes transparent edges
        var x, y, w, h, top, left, right, bottom, data, idx1, idx2, found, imgData;
        w = ctx.canvas.width;
        h = ctx.canvas.height;
        if (!w && !h) { return false } 
        imgData = ctx.getImageData(0, 0, w, h);
        data = new Uint32Array(imgData.data.buffer);
        idx1 = 0;
        idx2 = w * h - 1;
        found = false; 
        // search from top and bottom to find first rows containing a non transparent pixel.
        for (y = 0; y < h && !found; y += 1) {
            for (x = 0; x < w; x += 1) {
                if (data[idx1++] && !top) {  
                    top = y + 1;
                    if (bottom) { // top and bottom found then stop the search
                        found = true; 
                        break; 
                    }
                }
                if (data[idx2--] && !bottom) { 
                    bottom = h - y - 1; 
                    if (top) { // top and bottom found then stop the search
                        found = true; 
                        break;
                    }
                }
            }
            if (y > h - y && !top && !bottom) { return false } // image is completely blank so do nothing
        }
        top -= 1; // correct top 
        found = false;
        // search from left and right to find first column containing a non transparent pixel.
        for (x = 0; x < w && !found; x += 1) {
            idx1 = top * w + x;
            idx2 = top * w + (w - x - 1);
            for (y = top; y <= bottom; y += 1) {
                if (data[idx1] && !left) {  
                    left = x + 1;
                    if (right) { // if left and right found then stop the search
                        found = true; 
                        break;
                    }
                }
                if (data[idx2] && !right) { 
                    right = w - x - 1; 
                    if (left) { // if left and right found then stop the search
                        found = true; 
                        break;
                    }
                }
                idx1 += w;
                idx2 += w;
            }
        }
        left -= 1; // correct left
        if(w === right - left + 1 && h === bottom - top + 1) { return true } // no need to crop if no change in size
        w = right - left + 1;
        h = bottom - top + 1;
        ctx.canvas.width = w;
        ctx.canvas.height = h;
        ctx.putImageData(imgData, -left, -top);
        return true;            
    }