Closed efc closed 8 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.
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 super useful, thanks! I like the idea of doing it client side and saving the server from having to process this.
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.
I agree, this should be merged into the project. THANK YOU MUCH EFC!
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.
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.
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.
@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.
Ah, yes, ofcourse. Thanks :).
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.
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!
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 ?
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...
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>
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.
@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!
@mikemclin
Can you give a few more instructions on setting this up? I presume
That's what I've done so far- and but it does not work.
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
Just adjusted the canvas size and signature pad size in the CSS. Worked for me. :)
@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.
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)
Thanks, @agilgur5. That's great!
+1 @agilgur5
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.
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();
},
this save my life thank you @efc
signaturePad.removeBlanks(); $('#base64Data').val(signaturePad.toDataURL());
https://github.com/szimek/signature_pad/issues/49#issuecomment-260976909 This is exactly what I'm looking for. Thanks for sharing!
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
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!
Thanks for the snippet @jaredatch! Very helpful!
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.
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 :)
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!
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?
@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)
@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 validString
prop/method will do)
Maybe I am not getting something...is it supposed to remove white space around the signature?
Yes @crypto789, it is supposed to remove as much of the bounding rectangle as possible without removing any of the signature itself.
not working on ipad. is there any solution or am i miss something?
@shaangidwani my example demo works fine on my iPad. Uses trim-canvas
under the hood.
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!!!!
Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The source width is 0. I am getting this error using above code @jaredatch
@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
@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; }
@hariomdebut it work perfectly, in my code I replaced:
return this._signaturePad.toDataURL(type);
with:
return this.trimCanvas(this._canvas).toDataURL('image/png');
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.
@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 import
s/require
s
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.
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
@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
@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.
@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.
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
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.
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.