Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Electronic Signature with Canvas and JavaScript

0.00/5 (No votes)
5 Feb 2015 1  
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).

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

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

<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

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 = [];       // 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

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

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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here