Click here to Skip to main content
15,884,629 members
Articles / Web Development / HTML
Tip/Trick

Electronic Signature with Canvas and JavaScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
5 Feb 2015CPOL2 min read 28K   20   3
This article will show you how to implement a complete solution of electronic signature by using HTML5 canvas in ASP.NET (VB).

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

JavaScript
//Copyright (C) 2011 by Michael Bleigh and Intridea, Inc.
//

(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.el.width = this.canvas.width();
                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':
                        //return this.context.globalCompositeOperation = oldcomposite;
                    case 'mouseout':
                    case 'mouseleave':
                    case 'touchend':
                        //this.stopPainting();
                    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.

JavaScript
function copyCanvas(frm, to) {

    //get data from bigger pad
    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;

    //get data from smaller pad
    var myCanvas = document.getElementById(to);
    var ctx = myCanvas.getContext('2d');

    //draw data from bigger pad to smaller pad
    img.onload = function () {
        ctx.drawImage(img, 0, 0, 300, 150); // Or at whatever offset you like
    };
}

OK, now you have JS ready to use. This is how I create the signature pad.

HTML

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>
    &nbsp;|&nbsp; <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

JavaScript
var sign1 = $("#container1").sketch({ defaultColor: "#000", defaultSize: 2 });

Bigger Pad

JavaScript
<div class="div_signature1big">
  <canvas id="container1big" width="1000" height="500"></canvas>
  <br />
  <a class="clear1big">Clear</a>
</div>
HTML
var sign1big = $("#container1big").sketch({ defaultColor: "#000", defaultSize: 5 });

Load Image to Canvas (I keep image data in img1, and Load it Back to Canvas)

JavaScript
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

JavaScript
$(".clear1").click(function () {
       sign1.sketch().action = null;
       sign1.sketch().actions = [];       // this line empties the 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

JavaScript
$('.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).

JavaScript
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); //image data 
    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);
            //alert('Signature 1 saved');
            break;
        }
    }
}

How I Check and Force User to Sign Before Saving Data

JavaScript
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.NET
<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:

JavaScript
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:

JavaScript
dim sImg as String = datarow("Img1").Tostring

Some more things to do to make it work well with iPad:

VB.NET
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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Singapore Singapore
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionsorry, I haven t got enough knowledge to mount it Pin
Member 918772225-Nov-16 0:46
Member 918772225-Nov-16 0:46 
GeneralInteresting Pin
Nitij7-Feb-15 6:00
professionalNitij7-Feb-15 6:00 
GeneralRe: Interesting Pin
Miller Nguyen8-Feb-15 15:50
Miller Nguyen8-Feb-15 15:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.