Introduction
This solution solved the requirement of Electronic Signature in ASP.NET. So, you can use it in tablet, PC... without any additional device needed. You may find many solutions here and there, but you also may get in trouble when you put it in ASP.NET to reload the signatures, postback, saving data, and failed to make it work in iPad, Android (Safari, Chrome...)
Background
In this article, I'm using some of the code that I found from everywhere. So, I'd like to profusely thank all coders (if you find your code appears here).
Using the Code
I found this code from the internet and I modified to make it work well in iPad. Especially, the signature won't disappear when postback in iPad (it doesn't disappear in desktop).
(function () {
var __slice = [].slice;
(function ($) {
var Sketch;
$.fn.sketch = function () {
var args, key, sketch;
key = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
if (this.length > 1) {
$.error('Sketch.js can only be called on one element at a time.');
}
sketch = this.data('sketch');
if (typeof key === 'string' && sketch) {
if (sketch[key]) {
if (typeof sketch[key] === 'function') {
return sketch[key].apply(sketch, args);
} else if (args.length === 0) {
return sketch[key];
} else if (args.length === 1) {
return sketch[key] = args[0];
}
} else {
return $.error('Sketch.js did not recognize the given command.');
}
} else if (sketch) {
return sketch;
} else {
this.data('sketch', new Sketch(this.get(0), key));
return this;
}
};
Sketch = (function () {
function Sketch(el, opts) {
this.el = el;
this.canvas = $(el);
this.context = el.getContext('2d');
this.options = $.extend({
toolLinks: true,
defaultTool: 'marker',
defaultColor: '#000000',
defaultSize: 2
}, opts);
this.painting = false;
this.color = this.options.defaultColor;
this.size = this.options.defaultSize;
this.tool = this.options.defaultTool;
this.actions = [];
this.action = [];
this.canvas.bind('click mousedown mouseup mousemove mouseleave
mouseout touchstart touchmove touchend touchcancel', this.onEvent);
if (this.options.toolLinks) {
$('body').delegate("a[href=\"#" +
(this.canvas.attr('id')) + "\"]", 'click', function (e) {
var $canvas, $this, key, sketch, _i, _len, _ref;
$this = $(this);
$canvas = $($this.attr('href'));
sketch = $canvas.data('sketch');
_ref = ['color', 'size', 'tool'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
key = _ref[_i];
if ($this.attr("data-" + key)) {
sketch.set(key, $(this).attr("data-" + key));
}
}
if ($(this).attr('data-download')) {
sketch.download($(this).attr('data-download'));
}
return false;
});
}
}
Sketch.prototype.download = function (format) {
var mime;
format || (format = "png");
if (format === "jpg") {
format = "jpeg";
}
mime = "image/" + format;
return window.open(this.el.toDataURL(mime));
};
Sketch.prototype.set = function (key, value) {
this[key] = value;
return this.canvas.trigger("sketch.change" + key, value);
};
Sketch.prototype.startPainting = function () {
this.painting = true;
return this.action = {
tool: this.tool,
color: this.color,
size: parseFloat(this.size),
events: []
};
};
Sketch.prototype.stopPainting = function () {
if (this.action) {
this.actions.push(this.action);
}
this.painting = false;
this.action = null;
return this.redraw();
};
Sketch.prototype.onEvent = function (e) {
if (e.originalEvent && e.originalEvent.targetTouches) {
e.pageX = e.originalEvent.targetTouches[0].pageX;
e.pageY = e.originalEvent.targetTouches[0].pageY;
}
$.sketch.tools[$(this).data('sketch').tool].onEvent.call($(this).data('sketch'), e);
e.preventDefault();
return false;
};
Sketch.prototype.redraw = function () {
var sketch;
this.context = this.el.getContext('2d');
sketch = this;
$.each(this.actions, function () {
if (this.tool) {
return $.sketch.tools[this.tool].draw.call(sketch, this);
}
});
if (this.painting && this.action) {
return $.sketch.tools[this.action.tool].draw.call(sketch, this.action);
}
};
return Sketch;
})();
$.sketch = {
tools: {}
};
$.sketch.tools.marker = {
onEvent: function (e) {
switch (e.type) {
case 'mousedown':
case 'touchstart':
if (this.painting) {
this.stopPainting();
}
this.startPainting();
break;
case 'mouseup':
case 'mouseout':
case 'mouseleave':
case 'touchend':
case 'touchcancel':
this.stopPainting();
}
if (this.painting) {
this.action.events.push({
x: e.pageX - this.canvas.offset().left,
y: e.pageY - this.canvas.offset().top,
event: e.type
});
return this.redraw();
}
},
draw: function (action) {
var event, previous, _i, _len, _ref;
this.context.lineJoin = "round";
this.context.lineCap = "round";
this.context.beginPath();
this.context.moveTo(action.events[0].x, action.events[0].y);
_ref = action.events;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
event = _ref[_i];
this.context.lineTo(event.x, event.y);
previous = event;
}
this.context.strokeStyle = action.color;
this.context.lineWidth = action.size;
return this.context.stroke();
}
};
return $.sketch.tools.eraser = {
onEvent: function (e) {
return $.sketch.tools.marker.onEvent.call(this, e);
},
draw: function (action) {
var oldcomposite;
oldcomposite = this.context.globalCompositeOperation;
this.context.globalCompositeOperation = "destination-out";
action.color = "rgba(0,0,0,1)";
$.sketch.tools.marker.draw.call(this, action);
return this.context.globalCompositeOperation = oldcomposite;
}
};
})(jQuery);
}).call(this);
There's a small problem when you create a signature pad in webpage. It cannot be too big in the webform, but if it's small, it's hard to sign. So I made a bigger pad for the user to sign and transfer the image from Bigger pad to smaller pad (in webform). This is the code of JS to do that job and ensure the image won't be distorted.
function copyCanvas(frm, to) {
var canvas = document.getElementById(frm);
var sigData = canvas.toDataURL("image/png");
var w = canvas.width;
var h = canvas.height;
var img = new Image;
img.src = sigData;
var myCanvas = document.getElementById(to);
var ctx = myCanvas.getContext('2d');
img.onload = function () {
ctx.drawImage(img, 0, 0, 300, 150);
};
}
OK, now you have JS ready to use. This is how I create the signature pad.
HTML
<div id="div_signature1"
runat="server" style="background-color:yellow">
<canvas id="container1" class="signBox"></canvas>
<br />
<asp:HiddenField runat="server" ID="img1" Value="" />
<a class="clear1">[Clear]</a>
| <a class="signpad1">[Open]</a>
</div>
<asp:Image ID="imgSign1" runat="server" Visible="false" />
I made runat="server"
because I want to hide it later if I want to print the page.
JS
var sign1 = $("#container1").sketch({ defaultColor: "#000", defaultSize: 2 });
Bigger Pad
<div class="div_signature1big">
<canvas id="container1big" width="1000" height="500"></canvas>
<br />
<a class="clear1big">Clear</a>
</div>
var sign1big = $("#container1big").sketch({ defaultColor: "#000", defaultSize: 5 });
Load Image to Canvas (I keep image data in img1, and Load it Back to Canvas)
var canvas = document.getElementById("container1");
var ctx = canvas.getContext("2d");
var image = new Image();
image.src = img1;
ctx.drawImage(image, 0, 0);
How Clear Button Works
$(".clear1").click(function () {
sign1.sketch().action = null;
sign1.sketch().actions = [];
var myCanvas = document.getElementById("container1");
var ctx = myCanvas.getContext("2d");
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);
$("[id$=img1]").val("");
});
How Bigger Pad Works When You Click [Open] Button
$('.signpad1').click(function () {
$('.div_signature1big').dialog("open");
});
$(".div_signature1big").dialog({
autoOpen: false,
modal: true,
resizable: false,
height: "600",
width: "1000",
close: function () {
copyCanvas("container1big", "container1");
}
});
When the Bigger pad closes, it will copy its image to smaller pad.
Postback Problem
In Webform, postback will remove the canvas data... so when a Save button is clicked, I need to transfer data to a hiddenfield
(img1
), then redraw the canvas (that's why I have an addition step Load image to canvas).
function getImg1() {
var canvas = document.getElementById("container1");
var sigData = canvas.toDataURL("image/png");
if (!canvas.getContext) return;
var ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;
var drawn = null;
var d = ctx.getImageData(0, 0, w, h);
var len = d.data.length;
for (var i = 0; i < len; i++) {
if (!d.data[i]) {
drawn = false;
} else if (d.data[i]) {
drawn = true;
var sigData = canvas.toDataURL("image/png");
$('[id$=img1]').val(sigData);
break;
}
}
}
How I Check and Force User to Sign Before Saving Data
var img1 = $("[id$=img1]").val();
if (img1 == "" || img1 == null ) {
alert("Please complete all signatures");
return false;
}
else
{
return true;
}
I combined these 2 functions in one function call getData().
Then Webform button will be:
<asp:Button ID="btnSave" runat="server" Text="Save" CssClass="myButton"
CausesValidation="false" OnClientClick="getData();" OnClick="btnSave_Click" />
in btnSave_Click
, you can retrieve data of canvas with one line:
dim sImg as string = img1.Value.ToString
Actually, you're saving canvas
data as base64 image.
Then, to load image back to canvas can be as simple as:
dim sImg as String = datarow("Img1").Tostring
Some more things to do to make it work well with iPad:
Protected Sub Page_PreInit(sender As Object, e As EventArgs) Handles Me.PreInit
Try
Dim ua As String = Request.UserAgent
If ua IsNot Nothing AndAlso (ua.IndexOf_
("iPhone", StringComparison.CurrentCultureIgnoreCase) >= 0 _
OrElse ua.IndexOf("iPad", StringComparison.CurrentCultureIgnoreCase) >= 0 _
OrElse ua.IndexOf("iPod", StringComparison.CurrentCultureIgnoreCase) >= 0) _
AndAlso ua.IndexOf("Safari", StringComparison.CurrentCultureIgnoreCase) < 0 Then
Me.ClientTarget = "uplevel"
End If
Catch ex As Exception
ShowError("Cannot create mobile page: " & ex.Message)
End Try
End Sub
I didn't include the code file here. But I hope the solution is clear enough to understand.
I hope you can understand my bad English and structures...
Thanks for reading my article!