szimek / signature_pad

HTML5 canvas based smooth signature drawing
http://szimek.github.io/signature_pad/
MIT License
10.8k stars 2.11k forks source link

Consider option to remove blank space around signature #49

Closed efc closed 8 years ago

efc commented 10 years ago

I need to use the sig image later in my app and the blank space around the sig can be a bit of a pain. I added an option to remove this blank space so that the image returned when I get the data is the cropped signature. I'm not sure if this would be useful to others, and I am not able to git at the moment, so I'll just offer it here in case it proves helpful.

    SignaturePad.prototype.removeBlanks = function () {
        var imgWidth = this._ctx.canvas.width;
        var imgHeight = this._ctx.canvas.height;
        var imageData = this._ctx.getImageData(0, 0, imgWidth, imgHeight),
        data = imageData.data,
        getAlpha = function(x, y) {
            return data[(imgWidth*y + x) * 4 + 3]
        },
        scanY = function (fromTop) {
            var offset = fromTop ? 1 : -1;

            // loop through each row
            for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {

                // loop through each column
                for(var x = 0; x < imgWidth; x++) {
                    if (getAlpha(x, y)) {
                        return y;                        
                    }      
                }
            }
            return null; // all image is white
        },
        scanX = function (fromLeft) {
            var offset = fromLeft? 1 : -1;

            // loop through each column
            for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {

                // loop through each row
                for(var y = 0; y < imgHeight; y++) {
                    if (getAlpha(x, y)) {
                        return x;                        
                    }      
                }
            }
            return null; // all image is white
        };

        var cropTop = scanY(true),
        cropBottom = scanY(false),
        cropLeft = scanX(true),
        cropRight = scanX(false);

        var relevantData = this._ctx.getImageData(cropLeft, cropTop, cropRight-cropLeft, cropBottom-cropTop);
        this._canvas.width = cropRight-cropLeft;
        this._canvas.height = cropBottom-cropTop;
        this._ctx.clearRect(0, 0, cropRight-cropLeft, cropBottom-cropTop);
        this._ctx.putImageData(relevantData, 0, 0);
    };

FYI, credit for most of this code goes to http://stackoverflow.com/questions/12175991/crop-image-white-space-automatically-using-jquery

I imagine my need for this feature also grows from my using signature_pad on a larger tablet instead of a phone-size device.

szimek commented 10 years ago

Thanks! While it might be useful to be able to trim it on the client side (to display it later to the user, or to upload less data), if it's not actually needed there, it's simpler to trim it on the server side using e.g. ImageMagick - convert -trim input.jpg output.jpg.

As it's not specific to the library, but rather a generic image processing operation, I'd like to avoid merging it. However, I'll leave it opened for now, so that it's easier to find for others.

efc commented 10 years ago

I agree that if the server-side is accessible, that is a better place to put trimming. However, I am working with a server that does not (and will not) have ImageMagick installed. Still, even in this situation, the trimming could be placed in javascript outside the library, so you are probably right to keep it out of the library itself. (Apologies, flailing a bit with git comments!)

nathanbertram commented 10 years ago

@efc super useful, thanks! I like the idea of doing it client side and saving the server from having to process this.

contactmatts commented 9 years ago

Not only is it useful, but it's needed. I'm working with Salesforce (cloud based) and have no access to perform image trimming on the server.

fmp777 commented 9 years ago

I agree, this should be merged into the project. THANK YOU MUCH EFC!

szimek commented 9 years ago

I don't think I'll ever merge it into this library. Like I mentioned before, removing white space around an image can be moved into a separate library, because it's not specific to signature pad library and doesn't use any part of it.

Feel free to create a small library with the code posted by @efc and I'll add info about it to readme file. The code would probably still need to be modified to support e.g. non-transparent background colors.

mikemclin commented 9 years ago

This can also be done easily in PHP using the Intervention library... http://image.intervention.io/api/trim

UPDATE - While this works, I found it to be VERY processor intensive, especially when submitting a large signature pad image (I was using GD library). I am now trimming in the browser, which seems "instant", and doesn't use up server resources.

BasitAli commented 9 years ago

Could this be merged in? It's not a bad idea to have an additional utility function. If someone needs client side trimming (as I do), they can use this method. Currently I rely on a fork, but having it in the main repository makes upgrading straightforward.

szimek commented 9 years ago

@BasitAli It won't be merged. You don't have to rely on a fork - you can simply use the function provided by @efc. It doesn't access any data from the library - only the canvas, so it doesn't even have to be on SignaturePad class. You can simply add it as e.g. window.imageUtils.trim or something similar and use that.

BasitAli commented 9 years ago

Ah, yes, ofcourse. Thanks :).

szimek commented 8 years ago

I've just added info about trimming images to readme file and linked to the code provided by @efc (thanks!), so I'm finally closing this issue.

rodrigovallades commented 8 years ago

I disagree that this funcionality should be handled by a separate code/library/component. Don't get me wrong, please. But the signature pad exists to capture a users' signature, and the blank space around it is not part of the signature. Also, in many development teams this is a job for a frontend developer, and most times a frontend developer doesn't have access to backend funcionality to trim the imagem server-side. Signature-pad should handle it, in my opinion.

As I said, please don't get me wrong!

rodrigovallades commented 8 years ago

I was able to implement this in my project, it crops the image as it should and sends the cropped image to the server. The problem is that my canvas content gets distorted on screen. I only want to send the cropped image to my server; the changes cannot be made on screen.

I'm trying to create a temporary canvas and crop this instead, but I'm having a hard time.

Can someone help me? Maybe @efc ?

mikemclin commented 8 years ago

The removeBlanks method simply resizes your canvas element. So, after you call it and submit your data to wherever, you need to resize your canvas back to normal size. Here is how I accomplished it in Angular. I know you probably aren't using Angular, but maybe this will give you an idea of how to potentially tackle this problem...

In this implementation, a button triggers the submit() method, which trims the canvas, saves the data, clears the canvas, and then resizes the canvas. The same function that is used to size the canvas on page load, is re-used to size after saving the data.

Hope this helps...

Javascript (Angular Directive)

angular
    .module('app')
    .directive('psSignature', psSignature);

  function psSignature($window, $timeout) {
    return {
      restrict: 'E',
      scope: {
        onCancel: '&',
        onSubmit: '&',
        submitting: '='
      },
      templateUrl: 'signature.html',
      link: function (scope, element, attrs) {
        var canvas = element.find('canvas')[0];
        var canvasWrapper = element.find('.signature__body')[0];
        var signaturePad = new SignaturePad(canvas, {onEnd: makeDirty});
        var dirty = false;

        scope.cancel = scope.onCancel;
        scope.clear = clear;
        scope.submit = submit;
        scope.isDirty = isDirty;
        scope.showCancel = showCancel;

        activate();

        ////////////

        function activate() {
          addListeners();
          $timeout(resizeCanvas, 500);
        }

        function addListeners() {
          // Add
          angular.element($window).on('resize', resizeCanvas);
          // Clean up
          scope.$on('$destroy', function () {
            angular.element($window).off('resize', resizeCanvas);
          });
        }

        function makeDirty() {
          scope.$apply(function () {
            dirty = true;
          });
        }

        function clear() {
          signaturePad.clear();
          dirty = false;
        }

        function submit() {
          signaturePad.removeBlanks();
          scope.onSubmit({contents: signaturePad.toDataURL()});
          clear();
          resizeCanvas();
        }

        function isDirty() {
          return dirty;
        }

        function showCancel() {
          return !!(attrs.onCancel);
        }

        function resizeCanvas() {
          canvas.width = canvasWrapper.offsetWidth;
          canvas.height = canvasWrapper.offsetHeight;
        }
      }
    };
  }

signature.html HTML for directive

<div class="signature">
    <div class="signature__toolbar">
        <div class="signature__cancel">
            <button ng-click="cancel()" ng-show="showCancel()">
                <span class="icon-chevron-left-thin"></span> Cancel
            </button>
        </div>
        <div class="signature__clear">
            <button ng-click="clear()" ng-disabled="!isDirty()">
                Clear
            </button>
        </div>
        <div class="signature__submit">
            <button ng-click="submit()" ng-disabled="!isDirty()">
                <span class="icon-check"></span> Submit
            </button>
        </div>
    </div>
    <div class="signature__body">
        <div class="signature__label">Please Sign Here</div>
        <canvas class="signature__pad"></canvas>
    </div>
</div>
efc commented 8 years ago

Looks like @mikemclin provided a great answer, @rodrigovallades. I don't have to bother resizing the canvas in my app since it loads a whole new HTML page with a fresh canvas, so I'm afraid I can't really improve on Mike's suggestion.

rodrigovallades commented 8 years ago

@mikemclin @efc

Actually I'm using angular in my project, indeed. I managed to accomplish what I wanted by duplicating the canvas and trimming the new one, so the canvas showing in my page stays untouched.

But I'll try your suggestion later @mikemclin , thank you!

yarnball commented 8 years ago

@mikemclin

Can you give a few more instructions on setting this up? I presume

  1. AngularJS added above head
  2. Create a new Angular file with your sample code
  3. Add and your code to HTML

That's what I've done so far- and but it does not work.

  1. The buttons is not clickable
  2. If I remove ng-disabled="!isDirty()" and click the button- nothing happens.

Not sure if it is related- I have gotten the error "Cannot read property 'addEventListener' of null" when troubleshooting/

I made a plunker- https://plnkr.co/edit/NOZG5Y4dgrpCZKU5Xkja?p=preview

Is there an update/fix? Thanks

prathprabhudesai commented 8 years ago

Just adjusted the canvas size and signature pad size in the CSS. Worked for me. :)

mikemclin commented 8 years ago

@yarnball I modified my answer slightly to better describe what the code snippets are for. The JavaScript is my Angular directive. The HTML provided was the HTML template for the directive.

Now I would use that directive in my code; something like this...

<ps-signature on-submit="vm.submit(contents)" submitting="vm.submitting">Please sign here...</ps-signature>

You're going to need to have a good understanding of Angular directives to actually use my code in your app. And realistically, it was built for my app's purposes and might not be very flexible for reusability.

agilgur5 commented 8 years ago

For anyone who's looking for a tiny library that does this, I've created one here: https://github.com/agilgur5/trim-canvas based off @efc's code

Side note: It's also used in my React implementation of signature_pad, in case you're looking for one of those: https://github.com/agilgur5/react-signature-canvas (based off https://github.com/blackjk3/react-signature-pad)

efc commented 8 years ago

Thanks, @agilgur5. That's great!

nathanbertram commented 8 years ago

+1 @agilgur5

tillifywebb commented 8 years ago

I agree. Since this is for a signature in some respects it makes sense that the white space should be trimmed, or added as a flag: trimWhiteSpace: true. Just seems so sensible to do it client-side and within the library.

jaredatch commented 7 years ago

Here's what we ended up using.

The big difference and thing to note is that running this does NOT modify the primary canvas. It duplicates it, then trims the signature on the duplicate, and returns the data url. This is because I didn't want to modify the canvas the user sees.

Our use case is we have the signature field and a hidden field, which is for the data URL. Every time onEnd fires, we run the signature through the crop function (which returns the trimmed data) and then insert it/update the hidden field.

Works very well. Hopefully it helps someone, took a bit of fiddling to get worked out :)

        /**
         * Crop signature canvas to only contain the signature and no whitespace.
         *
         * @since 1.0.0
         */
        cropSignatureCanvas: function(canvas) {

            // First duplicate the canvas to not alter the original
            var croppedCanvas = document.createElement('canvas'),
                croppedCtx    = croppedCanvas.getContext('2d');

                croppedCanvas.width  = canvas.width;
                croppedCanvas.height = canvas.height;
                croppedCtx.drawImage(canvas, 0, 0);

            // Next do the actual cropping
            var w         = croppedCanvas.width,
                h         = croppedCanvas.height,
                pix       = {x:[], y:[]},
                imageData = croppedCtx.getImageData(0,0,croppedCanvas.width,croppedCanvas.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 = pix.x[n] - pix.x[0];
            h = pix.y[n] - pix.y[0];
            var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);

            croppedCanvas.width = w;
            croppedCanvas.height = h;
            croppedCtx.putImageData(cut, 0, 0);

            return croppedCanvas.toDataURL();
        },
marmangarcia commented 7 years ago

this save my life thank you @efc

signaturePad.removeBlanks(); $('#base64Data').val(signaturePad.toDataURL());

shawe commented 7 years ago

https://github.com/szimek/signature_pad/issues/49#issuecomment-260976909 This is exactly what I'm looking for. Thanks for sharing!

Forehon commented 6 years ago

Hi, I have to store signatures (base64) in a database using SignaturePad. To big ! (60ko) This script seems perfect to solve a part of my problem but i don't know how to use it ! :o( (not a js expert)

can you explain how to integrate this ? Where should I copy this great code ? Where should I call it ? A small tuto Please

moritzgloeckl commented 6 years ago

I translated @jaredatch's solution to Angular 2 / Typescript:

let canvas: HTMLCanvasElement = document.getElementsByTagName("canvas")[0];
let croppedCanvas:HTMLCanvasElement = document.createElement('canvas');
let croppedCtx:CanvasRenderingContext2D = croppedCanvas.getContext("2d");

croppedCanvas.width = canvas.width;
croppedCanvas.height = canvas.height;
croppedCtx.drawImage(canvas, 0, 0);

let w = croppedCanvas.width;
let h = croppedCanvas.height;
let pix = {x:[], y:[]};
let imageData = croppedCtx.getImageData(0,0,w,h);
let index = 0;

for (let y = 0; y < h; y++) {
    for (let 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((a,b) => a-b);
pix.y.sort((a,b) => a-b);
let n = pix.x.length-1;

w = pix.x[n] - pix.x[0];
h = pix.y[n] - pix.y[0];
var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);

croppedCanvas.width = w;
croppedCanvas.height = h;
croppedCtx.putImageData(cut, 0, 0);

return croppedCanvas.toDataURL();

Works great!

troygrosfield commented 6 years ago

Thanks for the snippet @jaredatch! Very helpful!

hoangnguyen459 commented 6 years ago

Thank you very much for a cool solution to removeBlanks(). I have successfully saved signature without extra blanks. The problem I am encountering is re-loading the signature from database and displaying on signaturePad. The trimmed signature is auto stretching to fit the canvas. This looks not too good. Is there a way to add blanks to the trimmed signature so it will display correctly on the canvas? Thank you very much.

mochsner commented 6 years ago

Running into issues with tests for all non-angular versions of this program... is it still up-to-date? Been a late night, so sorry for any potentially major oversights...

For all the images, the loops don't seem to be catching where "text" is -- anything that's white seems to be text. It's simply cropping the canvas by 1 pixel for each iteration. Any help would be appreciated :)

minhnhatspk commented 5 years ago

croppedCanvas

Here's what we ended up using.

The big difference and thing to note is that running this does NOT modify the primary canvas. It duplicates it, then trims the signature on the duplicate, and returns the data url. This is because I didn't want to modify the canvas the user sees.

Our use case is we have the signature field and a hidden field, which is for the data URL. Every time onEnd fires, we run the signature through the crop function (which returns the trimmed data) and then insert it/update the hidden field.

Works very well. Hopefully it helps someone, took a bit of fiddling to get worked out :)

        /**
         * Crop signature canvas to only contain the signature and no whitespace.
         *
         * @since 1.0.0
         */
        cropSignatureCanvas: function(canvas) {

            // First duplicate the canvas to not alter the original
            var croppedCanvas = document.createElement('canvas'),
                croppedCtx    = croppedCanvas.getContext('2d');

                croppedCanvas.width  = canvas.width;
                croppedCanvas.height = canvas.height;
                croppedCtx.drawImage(canvas, 0, 0);

            // Next do the actual cropping
            var w         = croppedCanvas.width,
                h         = croppedCanvas.height,
                pix       = {x:[], y:[]},
                imageData = croppedCtx.getImageData(0,0,croppedCanvas.width,croppedCanvas.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 = pix.x[n] - pix.x[0];
            h = pix.y[n] - pix.y[0];
            var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);

            croppedCanvas.width = w;
            croppedCanvas.height = h;
            croppedCtx.putImageData(cut, 0, 0);

            return croppedCanvas.toDataURL();
        },

It doesn't work for me. When I draw a small signature, the base64 string is broken and can not display on the browser. If I draw a big one, there's no problem!

ghost commented 5 years ago

I need to use the sig image later in my app and the blank space around the sig can be a bit of a pain. I added an option to remove this blank space so that the image returned when I get the data is the cropped signature. I'm not sure if this would be useful to others, and I am not able to git at the moment, so I'll just offer it here in case it proves helpful.

    SignaturePad.prototype.removeBlanks = function () {
        var imgWidth = this._ctx.canvas.width;
        var imgHeight = this._ctx.canvas.height;
        var imageData = this._ctx.getImageData(0, 0, imgWidth, imgHeight),
        data = imageData.data,
        getAlpha = function(x, y) {
            return data[(imgWidth*y + x) * 4 + 3]
        },
        scanY = function (fromTop) {
            var offset = fromTop ? 1 : -1;

            // loop through each row
            for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {

                // loop through each column
                for(var x = 0; x < imgWidth; x++) {
                    if (getAlpha(x, y)) {
                        return y;                        
                    }      
                }
            }
            return null; // all image is white
        },
        scanX = function (fromLeft) {
            var offset = fromLeft? 1 : -1;

            // loop through each column
            for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {

                // loop through each row
                for(var y = 0; y < imgHeight; y++) {
                    if (getAlpha(x, y)) {
                        return x;                        
                    }      
                }
            }
            return null; // all image is white
        };

        var cropTop = scanY(true),
        cropBottom = scanY(false),
        cropLeft = scanX(true),
        cropRight = scanX(false);

        var relevantData = this._ctx.getImageData(cropLeft, cropTop, cropRight-cropLeft, cropBottom-cropTop);
        this._canvas.width = cropRight-cropLeft;
        this._canvas.height = cropBottom-cropTop;
        this._ctx.clearRect(0, 0, cropRight-cropLeft, cropBottom-cropTop);
        this._ctx.putImageData(relevantData, 0, 0);
    };

FYI, credit for most of this code goes to http://stackoverflow.com/questions/12175991/crop-image-white-space-automatically-using-jquery

I imagine my need for this feature also grows from my using signature_pad on a larger tablet instead of a phone-size device.

Can someone please explain how to actually call this function?

I am trying to call it this way: var data = signaturePad.toDataURL('image/png').removeBlanks(); but it does not work. It gives me an error:

Uncaught TypeError: signaturePad.toDataURL(...).removeBlanks is not a function at

HTMLButtonElement.

How should I use it?

elegisandi commented 5 years ago

@crypto789 you have to do two separate statements to achieve that:

signaturePad.removeBlanks();
var data = signaturePad.toDataURL('image/png');

NOTES: toDataURL method returns a string, so you can't chain any method/props of signaturePad prototype (though any valid String prop/method will do)

ghost commented 5 years ago

@crypto789 you have to do two separate statements to achieve that:

signaturePad.removeBlanks();
var data = signaturePad.toDataURL('image/png');

NOTES: toDataURL method returns a string, so you can't chain any method/props of signaturePad prototype (though any valid String prop/method will do)

Maybe I am not getting something...is it supposed to remove white space around the signature?

efc commented 5 years ago

Yes @crypto789, it is supposed to remove as much of the bounding rectangle as possible without removing any of the signature itself.

shaangidwani commented 5 years ago

not working on ipad. is there any solution or am i miss something?

agilgur5 commented 5 years ago

@shaangidwani my example demo works fine on my iPad. Uses trim-canvas under the hood.

leonetosoft commented 5 years ago

I translated @jaredatch's solution to Angular 2 / Typescript:

let canvas: HTMLCanvasElement = document.getElementsByTagName("canvas")[0];
let croppedCanvas:HTMLCanvasElement = document.createElement('canvas');
let croppedCtx:CanvasRenderingContext2D = croppedCanvas.getContext("2d");

croppedCanvas.width = canvas.width;
croppedCanvas.height = canvas.height;
croppedCtx.drawImage(canvas, 0, 0);

let w = croppedCanvas.width;
let h = croppedCanvas.height;
let pix = {x:[], y:[]};
let imageData = croppedCtx.getImageData(0,0,w,h);
let index = 0;

for (let y = 0; y < h; y++) {
  for (let 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((a,b) => a-b);
pix.y.sort((a,b) => a-b);
let n = pix.x.length-1;

w = pix.x[n] - pix.x[0];
h = pix.y[n] - pix.y[0];
var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);

croppedCanvas.width = w;
croppedCanvas.height = h;
croppedCtx.putImageData(cut, 0, 0);

return croppedCanvas.toDataURL();

Works great!

PERFECT!!!!

hariomgoyal64 commented 5 years ago

Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The source width is 0. I am getting this error using above code @jaredatch

agilgur5 commented 5 years ago

@hariomdebut that error is a bit cryptic, but it means the canvas has 0 width. That probably means your original canvas (that is cloned) is incorrectly sized

hariomgoyal64 commented 5 years ago

@agilgur5 I tried many solutions for this but none worked. I don't know how it works for some people. Still the best solution which works for me : trimCanvas(c) { const ctx = c.getContext('2d'); const copy = document.createElement('canvas').getContext('2d'); const pixels = ctx.getImageData(0, 0, c.width, c.height); const l = pixels.data.length; let i; const bound = { top: null, left: null, right: null, bottom: null }; let x; let y; // Iterate over every pixel to find the highest // and where it ends on every axis () for (i = 0; i < l; i += 4) { if (pixels.data[i + 3] !== 0) { x = (i / 4) % c.width; // tslint:disable-next-line: no-bitwise y = ~~((i / 4) / c.width); if (bound.top === null) { bound.top = y; } if (bound.left === null) { bound.left = x; } else if (x < bound.left) { bound.left = x; } if (bound.right === null) { bound.right = x; } else if (bound.right < x) { bound.right = x; } if (bound.bottom === null) { bound.bottom = y; } else if (bound.bottom < y) { bound.bottom = y; } } } // Calculate the height and width of the content const trimHeight = bound.bottom - bound.top; const trimWidth = bound.right - bound.left; const trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight); copy.canvas.width = trimWidth; copy.canvas.height = trimHeight; copy.putImageData(trimmed, 0, 0); // Return trimmedcanvas return copy.canvas; }

ghwrivas commented 4 years ago

@hariomdebut it work perfectly, in my code I replaced: return this._signaturePad.toDataURL(type); with: return this.trimCanvas(this._canvas).toDataURL('image/png');

ustincameron commented 4 years ago

Please consider merging this in since the whitespace is not part of a signature. Trying to use this within React client-side pdf generation. Third-party packages like trim-canvas are outdated and fail to load @babel/core correctly.

agilgur5 commented 4 years ago

@ustincameron hi, dude who made trim-canvas here, which was built from this thread (see the comments). All of the comments in this thread are virtually identical to it and each other.

Third-party packages like trim-canvas are outdated

Just because a package (like trim-canvas) hasn't published a new version in years, doesn't mean it's outdated. trim-canvas gets ~37k downloads every single week and is used inside of react-signature-canvas (also made by me) and vue-signature-canvas among others, which are actively used as well. It has 100% test coverage as does react-signature-canvas, which also has a few live, working examples.

and fail to load @babel/core correctly.

Well trim-canvas was built before @babel/core (Babel 7) existed, so it doesn't even use it. babel-core (Babel 6) is a dev dependency and there are no (prod) dependencies. Can look at the build on UNPKG which doesn't make any imports/requires

If you've got a problem with trim-canvas, I'd recommend filing an issue in that repo with a detailed reproduction or failing test case.

the-hotmann commented 3 years ago

Here's what we ended up using.

The big difference and thing to note is that running this does NOT modify the primary canvas. It duplicates it, then trims the signature on the duplicate, and returns the data url. This is because I didn't want to modify the canvas the user sees.

Our use case is we have the signature field and a hidden field, which is for the data URL. Every time onEnd fires, we run the signature through the crop function (which returns the trimmed data) and then insert it/update the hidden field.

Works very well. Hopefully it helps someone, took a bit of fiddling to get worked out :)

        /**
         * Crop signature canvas to only contain the signature and no whitespace.
         *
         * @since 1.0.0
         */
        cropSignatureCanvas: function(canvas) {

            // First duplicate the canvas to not alter the original
            var croppedCanvas = document.createElement('canvas'),
                croppedCtx    = croppedCanvas.getContext('2d');

                croppedCanvas.width  = canvas.width;
                croppedCanvas.height = canvas.height;
                croppedCtx.drawImage(canvas, 0, 0);

            // Next do the actual cropping
            var w         = croppedCanvas.width,
                h         = croppedCanvas.height,
                pix       = {x:[], y:[]},
                imageData = croppedCtx.getImageData(0,0,croppedCanvas.width,croppedCanvas.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 = pix.x[n] - pix.x[0];
            h = pix.y[n] - pix.y[0];
            var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);

            croppedCanvas.width = w;
            croppedCanvas.height = h;
            croppedCtx.putImageData(cut, 0, 0);

            return croppedCanvas.toDataURL();
        },

Sorry for this late reply, but is there an option to use this for SVG? For me this always exports a PNG even if I replace the last line with:

return croppedCanvas.toDataURL('image/svg+xml');

Really searching for a way to trim a Canvas and export it as SVG and not as any bitmap

agilgur5 commented 3 years ago

@MartinH0 that's not really possible as no browser natively supports Canvas to SVG. signature_pad (not the underlying Canvas) can only do it because it records the raw point data and has an algorithm to output SVG from that.

Can see https://github.com/agilgur5/react-signature-canvas/issues/49#issuecomment-627726585 for more details

the-hotmann commented 3 years ago

@agilgur5 hm ok, thank you. Maybe a bit offtopic, but do you know how to (in JavaScript, or PHP) trim normal SVGs? I really cant find anything related to trim SVGs. As when I download the SVG if will always have white-borders as the SVG which gets exported was bigger of course.

This then also could (should) be implemented in signature_pad as I think this will be a good feature. But maybe just as option.

agilgur5 commented 3 years ago

@MartinH0 I found a good few results in a quick search, basically all pertaining to changing the SVG's viewBox. Off the top of my head, not sure if that's the best answer as I haven't done raw SVG manipulation in like 4-6 years (Data Visualization / D3 work).

This then also could (should) be implemented in signature_pad as I think this will be a good feature. But maybe just as option.

The above code is only a few lines of user-land code. Theoretically I could make trim-svg as a counterpart to trim-canvas and would be supportive of either making it into signature_pad, but that's previously been rejected. react-signature-canvas has a trimCanvas method built-in because of the frequency of these requests. Trimming SVG out-of-the-box is a bit more of a clunky API since one is Canvas => Canvas and the other SVG => SVG, but trimCanvasAsSVG or something could work.

ghwrivas commented 3 years ago

I'm using that in angular component https://github.com/ghwrivas/ngx-signature-pad/blob/master/projects/ngx-signature-pad/src/lib/ngx-signature-pad.component.ts