Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider option to remove blank space around signature #49

Closed
efc opened this issue Mar 10, 2014 · 66 comments
Closed

Consider option to remove blank space around signature #49

efc opened this issue Mar 10, 2014 · 66 comments

Comments

@efc
Copy link

efc commented Mar 10, 2014

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
Copy link
Owner

szimek commented Mar 10, 2014

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
Copy link
Author

efc commented Mar 10, 2014

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!)

@efc efc closed this as completed Mar 10, 2014
@efc efc reopened this Mar 10, 2014
@nathanbertram
Copy link

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

@contactmatts
Copy link

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
Copy link

fmp777 commented Dec 16, 2014

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

@szimek
Copy link
Owner

szimek commented Dec 17, 2014

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
Copy link

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
Copy link

BasitAli commented Sep 7, 2015

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
Copy link
Owner

szimek commented Sep 7, 2015

@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
Copy link

BasitAli commented Sep 8, 2015

Ah, yes, ofcourse. Thanks :).

@szimek
Copy link
Owner

szimek commented Nov 10, 2015

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.

@szimek szimek closed this as completed Nov 10, 2015
@rodrigovallades
Copy link

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
Copy link

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
Copy link

mikemclin commented Dec 18, 2015

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
Copy link
Author

efc commented Dec 18, 2015

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
Copy link

@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
Copy link

yarnball commented May 18, 2016

@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 <ng-controller="app"> 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
Copy link

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

@mikemclin
Copy link

@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
Copy link
Contributor

agilgur5 commented Sep 2, 2016

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
Copy link
Author

efc commented Sep 2, 2016

Thanks, @agilgur5. That's great!

@nathanbertram
Copy link

+1 @agilgur5

@tillifywebb
Copy link

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
Copy link

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
Copy link

this save my life thank you @efc

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

@shawe
Copy link

shawe commented Apr 30, 2017

#49 (comment) This is exactly what I'm looking for. Thanks for sharing!

@hariomgoyal64
Copy link

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

@agilgur5
Copy link
Contributor

@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
Copy link

hariomgoyal64 commented Aug 1, 2019

@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 trimmed canvas return copy.canvas; }

@ghwrivas
Copy link

ghwrivas commented Jul 8, 2020

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

@ustincameron
Copy link

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
Copy link
Contributor

agilgur5 commented Jul 24, 2020

@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
Copy link

the-hotmann commented Oct 17, 2020

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
Copy link
Contributor

agilgur5 commented Oct 17, 2020

@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 agilgur5/react-signature-canvas#49 (comment) for more details

@the-hotmann
Copy link

the-hotmann commented Oct 18, 2020

@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
Copy link
Contributor

@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
Copy link

ghwrivas commented Oct 20, 2020

@POOLEworks
Copy link

When using Signature pad in a form, what is the best way to return the completed signature base64 data to an input?
Looking at a jquery on submit or maybe it's put in a hidden text input once there's a exit from the canvas. I don't want to chase my tail if that's not it. I have a ton of other information going through this form.
Sending it to a MySQL database as BLOB. This isn't going to be a database called upon daily so I'm not worried about the weight. It's more important to keep the signature with the other submitted info.

@vivdroid
Copy link

vivdroid commented Jun 8, 2021

In case someone wants to have this with white background, just add this little change inside for loop.
Also I have added some extra transparent padding to the final image.

Final Code

const signPad = new SignaturePad(canvasRef.current, {
  backgroundColor: 'rgb(255, 255, 255)' // adding background color to signature pad
});

// function to remove blanks
export function removeBlanks(canvas) {
  let croppedCanvas = document.createElement('canvas');
  let croppedCtx = 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) {
      if (imageData.data[index] === 255 
        && imageData.data[index + 1] === 255 
        && imageData.data[index + 2] === 255 
        && imageData.data[index + 3] === 255 
        ) {
        continue;
      }
      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 + 40; // extra width
  croppedCanvas.height = h + 40; // extra height
  croppedCtx.putImageData(cut, 20, 20); // extra height/width

  return croppedCanvas.toDataURL();
}

@Riba-Kit
Copy link

jaredatch's code with small modifications becomes 3-4x faster

function getCroppedCanvasImage(canvas: HTMLCanvasElement) {

	let originalCtx = canvas.getContext('2d');

	let originalWidth = canvas.width;
	let originalHeight = canvas.height;
	let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight);

	let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;

	for (y = 0; y < originalHeight; y++) {
		for (x = 0; x < originalWidth; x++) {
			currentPixelColorValueIndex = (y * originalWidth + x) * 4;
			let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
			if (currentPixelAlphaValue > 0) {
				if (minX > x) minX = x;
				if (maxX < x) maxX = x;
				if (minY > y) minY = y;
				if (maxY < y) maxY = y;
			}
		}
	}

	let croppedWidth = maxX - minX;
	let croppedHeight = maxY - minY;
	if (croppedWidth < 0 || croppedHeight < 0) return null;
	let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);

	let croppedCanvas = document.createElement('canvas'),
		croppedCtx    = croppedCanvas.getContext('2d');

	croppedCanvas.width = croppedWidth;
	croppedCanvas.height = croppedHeight;
	croppedCtx.putImageData(cuttedImageData, 0, 0);

	return croppedCanvas.toDataURL();
}

@cnuarin
Copy link

cnuarin commented Sep 23, 2021

Thanks.
To center the signature without cropping (works for my need):

Replace the code:

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

with:

    this._ctx.clearRect(0, 0, imgWidth, imgHeight);
    this._ctx.putImageData(relevantData, (imgWidth - (cropRight-cropLeft))/2, (imgHeight - (cropBottom-cropTop))/2);

@larrymcp
Copy link

Fantastic @Riba-Kit : many thanks!

One question, should the line:

if (croppedWidth < 0 || croppedHeight < 0) return null;

...include the zero also? I modified it to:

if (croppedWidth <= 0 || croppedHeight <= 0) return null;

...because I found documentation which says that getImageData will throw an exception if either of those is zero.

@Riba-Kit
Copy link

@larrymcp Thanks! You found a bug in my production)

@xitude
Copy link

xitude commented Apr 20, 2022

jaredatch's code with small modifications becomes 3-4x faster

function getCroppedCanvasImage(canvas: HTMLCanvasElement) {

	let originalCtx = canvas.getContext('2d');

	let originalWidth = canvas.width;
	let originalHeight = canvas.height;
	let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight);

	let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;

	for (y = 0; y < originalHeight; y++) {
		for (x = 0; x < originalWidth; x++) {
			currentPixelColorValueIndex = (y * originalWidth + x) * 4;
			let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
			if (currentPixelAlphaValue > 0) {
				if (minX > x) minX = x;
				if (maxX < x) maxX = x;
				if (minY > y) minY = y;
				if (maxY < y) maxY = y;
			}
		}
	}

	let croppedWidth = maxX - minX;
	let croppedHeight = maxY - minY;
	if (croppedWidth < 0 || croppedHeight < 0) return null;
	let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);

	let croppedCanvas = document.createElement('canvas'),
		croppedCtx    = croppedCanvas.getContext('2d');

	croppedCanvas.width = croppedWidth;
	croppedCanvas.height = croppedHeight;
	croppedCtx.putImageData(cuttedImageData, 0, 0);

	return croppedCanvas.toDataURL();
}

This returns it as .png :(

@tonyalfaro
Copy link

tonyalfaro commented May 1, 2022

This returns it as .png :(

If you need a jpeg image, you must change :
return croppedCanvas.toDataURL();

to :
return croppedCanvas.toDataURL('image/jpeg', 1.0);

@xjlin0
Copy link

xjlin0 commented Sep 8, 2022

If exporting SVG are required, @efc 's signaturePad based solution is still the only one works without other dependencies. For signature_pad.umd.min.js v 4.0.7, it needs to modified two lines, from

this._canvas.width = cropRight-cropLeft;
this._canvas.height = cropBottom-cropTop;

to

this._ctx.canvas.width = cropRight-cropLeft;
this._ctx.canvas.height = cropBottom-cropTop;

If other file formats are allowed, one can check first which MIME types are working in browsers before using canvas based solutions such as @Riba-Kit 's. However when exporting in jpg (either signaturePad.toDataURL("image/jpeg", 0.5) or canvas.toDataURL("image/jpeg", 0.5)), none of blanks were trimmed. 😞

@Andrei-Fogoros
Copy link

Hi Jared @jaredatch,
Could you please tell us what is the license for the above function? :)

Regards,
Andrei

@ryangriggs
Copy link

It would be really helpful for the signature object to also return the bounding rectangle of the signature (no need to actually crop the image). Then we could do the processing ourselves. Something like signature.bounds() which returns { left, top, width, height } of the actual signature image. This could be calculated while the user is drawing the sig, thus eliminating the need to scan the entire image afterward.

@merbin2012
Copy link

@efc Thanks for your modification, this modification works perfectly if the background is transparent, but if we change the background color of the canvas, it doesn't work. Can you please help me?

@efc
Copy link
Author

efc commented Mar 21, 2023

@merbin2012, yes, this requires a transparent background. But instead of coloring the background of this element, why not try wrapping it in another div to which you apply your background color?

@merbin2012
Copy link

@efc Thanks for your suggestion, I will try and let you know.

@tylerhsu
Copy link

tylerhsu commented Apr 4, 2023

As others have mentioned, the solutions earlier in this thread produce bitmap images. For those looking to obtain a cropped SVG, here's the function I use. First call signaturePad.toSVG(), then pass the resulting string to this function, which returns a cropped version.

It manipulates the svg's viewBox attribute to "zoom in" on the appropriate area. It determines the appropriate area by calling the builtin getBBox() function on the SVG element.

function cropSVG(svgText: string): string {
  // First convert the svg string into an html element so we can
  // call getBBox() on it.
  const tempElement = document.createElement('div');
  tempElement.setAttribute('style', 'visibility: hidden');
  // getBBox() only works if the element is rendered into the page.
  document.body.appendChild(tempElement);
  tempElement.innerHTML = svgText;
  const svgElement = tempElement.getElementsByTagName('svg')[0]
  // This gets the bounding box around the signature.
  const bbox = svgElement.getBBox();
  // Use the bounding box's coordinates as the svg's viewBox.
  // This eliminates the whitespace by "zooming in" on the
  // bounding box.  Include 5px extra, as the signature's
  // edges get slightly clipped otherwise.
  const viewBox = [bbox.x - 5, bbox.y - 5, bbox.width + 10, bbox.height + 10].join(" ");
  svgElement.setAttribute("viewBox", viewBox);
  // Let the svg have the size and aspect ratio of the
  // cropped result, not the original canvas.
  svgElement.removeAttribute("width");
  svgElement.removeAttribute("height");
  const croppedSVG = svgElement.outerHTML;
  document.body.removeChild(tempElement);
  return croppedSVG;
}

Use as follows:

const croppedSVGString = cropSVG(signaturePad.toSVG());

@LucasBourgeois
Copy link

Hi,
Im using @jaredatch function and facing a weird issue..
ive this error from error reports from users but i can't really reproduce it by myself :
TypeError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': Value is not of type 'long'.: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': Value is not of type 'long'.

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

The only thing i can find if that : either pix.x[0], pix.y[0], w or h is undefined or NaN..
but i can't find why.
Of course, i only run this function when canvas has been touched by user and has pixel on it.
Anyone had this issue before ?

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests