Using Base64 Canvas Data In jQuery To Create ColdFusion Images

Posted March 10, 2010 at 9:49 AM by Ben Nadel

Tags: ColdFusion, Javascript / DHTML

Last week, I explored the HTML Canvas element for the first time. In that exploration, I created a "finger painting" demo for the iPhone that would post drawing commands to the server where the image would be re-created as a PNG in ColdFusion. That was a nice approach because it gave me some flexibility in how the ColdFusion image was created (using anti-aliasing and a thicker pen stroke). But, I wanted to see if there was a way to extract image data from the canvas without having to track every single drawing action performed by the user.

 
 
 
 
 
 
 
 
 
 

As it turns out, the HTML canvas element has a toDataURL( mimeType ) method. This method takes the current canvas display and returns a base64-encoded data URL of the pixel data for the given mime-type. This sounded like something that would play very nicely with ColdFusion's ImageReadBase64() method, which can create a new image object from a base64-encoded data URL.

Taking my iPhone drawing demo from last week, I refactored it to use the toDataURL() method. Now, rather than storing every single drawing command, I simply export the base64 image data and store it in a hidden form field before I post the form to the ColdFusion server.

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>iPhone Touch Events With jQuery</title>
  • <meta
  • name="viewport"
  • content="width=device-width, user-scalable=no, initial-scale=1"
  • />
  • <style type="text/css">
  •  
  • body {
  • margin: 5px 5px 5px 5px ;
  • padding: 0px 0px 0px 0px ;
  • }
  •  
  • canvas {
  • border: 1px solid #999999 ;
  • -webkit-touch-callout: none ;
  • -webkit-user-select: none ;
  • }
  •  
  • a {
  • background-color: #CCCCCC ;
  • border: 1px solid #999999 ;
  • color: #333333 ;
  • display: block ;
  • height: 40px ;
  • line-height: 40px ;
  • text-align: center ;
  • text-decoration: none ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="jquery-1.4.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • // When the window has loaded, scroll to the top of the
  • // visible document.
  • jQuery( window ).load(
  • function(){
  •  
  • // When scrolling the document, using a timeout to
  • // create a slight delay seems to be necessary.
  • // NOTE: For the iPhone, the window has a native
  • // method, scrollTo().
  • setTimeout(
  • function(){
  • window.scrollTo( 0, 0 );
  • },
  • 50
  • );
  •  
  • }
  • );
  •  
  •  
  • // When The DOM loads, initialize the scripts.
  • jQuery(function( $ ){
  •  
  • // Get a refernce to the canvase.
  • var canvas = $( "canvas" );
  •  
  • // Get a reference to our form.
  • var form = $( "form" );
  •  
  • // Get a reference to our base64 image data input;
  • // this is where we will need to save the encoded
  • // image before we submit the form.
  • var imageData = form.find( "input[ name = 'imageData' ]" );
  •  
  • // Get a reference to the export link.
  • var exportGraphic = $( "a" );
  •  
  • // Get the rendering context for the canvas (curently,
  • // 2D is the only one available). We will use this
  • // rendering context to perform the actually drawing.
  • var pen = canvas[ 0 ].getContext( "2d" );
  •  
  • // Create a variable to hold the last point of contact
  • // for the pen (so that we can draw FROM-TO lines).
  • var lastPenPoint = null;
  •  
  • // This is a flag to determine if we using an iPhone.
  • // If not, we want to use the mouse commands, not the
  • // the touch commands.
  • var isIPhone = (new RegExp( "iPhone", "i" )).test(
  • navigator.userAgent
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // I take the event X,Y and translate it into a local
  • // coordinate system for the canvas.
  • var getCanvasLocalCoordinates = function( pageX, pageY ){
  • // Get the position of the canvas.
  • var position = canvas.offset();
  •  
  • // Translate the X/Y to the canvas element.
  • return({
  • x: (pageX - position.left),
  • y: (pageY - position.top)
  • });
  • };
  •  
  •  
  • // I get appropriate event object based on the client
  • // environment.
  • var getTouchEvent = function( event ){
  • // Check to see if we are in the iPhont. If so,
  • // grab the native touch event. By its nature,
  • // the iPhone tracks multiple touch points; but,
  • // to keep this demo simple, just grab the first
  • // available touch event.
  • return(
  • isIPhone ?
  • window.event.targetTouches[ 0 ] :
  • event
  • );
  • };
  •  
  •  
  • // I handle the touch start event. With this event,
  • // we will be starting a new line.
  • var onTouchStart = function( event ){
  • // Get the native touch event.
  • var touch = getTouchEvent( event );
  •  
  • // Get the local position of the touch event
  • // (taking into account scrolling and offset).
  • var localPosition = getCanvasLocalCoordinates(
  • touch.pageX,
  • touch.pageY
  • );
  •  
  • // Store the last pen point based on touch.
  • lastPenPoint = {
  • x: localPosition.x,
  • y: localPosition.y
  • };
  •  
  • // Since we are starting a new line, let's move
  • // the pen to the new point and beign a path.
  • pen.beginPath();
  • pen.moveTo( lastPenPoint.x, lastPenPoint.y );
  •  
  • // Now that we have initiated a line, we need to
  • // bind the touch/mouse event listeners.
  • canvas.bind(
  • (isIPhone ? "touchmove" : "mousemove"),
  • onTouchMove
  • );
  •  
  • // Bind the touch/mouse end events so we know
  • // when to end the line.
  • canvas.bind(
  • (isIPhone ? "touchend" : "mouseup"),
  • onTouchEnd
  • );
  • };
  •  
  •  
  • // I handle the touch move event. With this event, we
  • // will be drawing a line from the previous point to
  • // the current point.
  • var onTouchMove = function( event ){
  • // Get the native touch event.
  • var touch = getTouchEvent( event );
  •  
  • // Get the local position of the touch event
  • // (taking into account scrolling and offset).
  • var localPosition = getCanvasLocalCoordinates(
  • touch.pageX,
  • touch.pageY
  • );
  •  
  • // Store the last pen point based on touch.
  • lastPenPoint = {
  • x: localPosition.x,
  • y: localPosition.y
  • };
  •  
  • // Draw a line from the last pen point to the
  • // current touch point.
  • pen.lineTo( lastPenPoint.x, lastPenPoint.y );
  •  
  • // Render the line.
  • pen.stroke();
  • };
  •  
  •  
  • // I handle the touch end event. Here, we are basically
  • // just unbinding the move event listeners.
  • var onTouchEnd = function( event ){
  • // Unbind event listeners.
  • canvas.unbind(
  • (isIPhone ? "touchmove" : "mousemove")
  • );
  •  
  • // Unbind event listeners.
  • canvas.unbind(
  • (isIPhone ? "touchend" : "mouseup")
  • );
  • };
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Bind the export link to simply submit the form.
  • exportGraphic.click(
  • function( event ){
  • // Prevent the default behavior.
  • event.preventDefault();
  •  
  • // Get the bsae64 image data form the canvas.
  • // This will be availble as a DATA URL in the
  • // form of:
  • //
  • // data:image/png;base64,iVBORw0KGgog==
  • //
  • // ColdFusion should be able to use the data
  • // URL complete with the headers.
  • imageData.val(
  • canvas[ 0 ].toDataURL( "image/png" )
  • );
  •  
  • // Submit the form.
  • form.submit();
  • }
  • );
  •  
  •  
  • // Bind the touch start event to the canvas. With
  • // this event, we will be starting a new line. The
  • // touch event is NOT part of the jQuery event object.
  • // We have to get the Touch even from the native
  • // window object.
  • canvas.bind(
  • (isIPhone ? "touchstart" : "mousedown"),
  • function( event ){
  • // Pass this event off to the primary event
  • // handler.
  • onTouchStart( event );
  •  
  • // Return FALSE to prevent the default behavior
  • // of the touch event (scroll / gesture) since
  • // we only want this to perform a drawing
  • // operation on the canvas.
  • return( false );
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <!--- This is where we draw. --->
  • <canvas
  • id="canvas"
  • width="308"
  • height="358">
  • </canvas>
  •  
  • <!---
  • This is the form that will post the drawing information
  • back to the server.
  • --->
  • <form action="export.cfm" method="post">
  •  
  • <!--- The base64 image data exported from canvas. --->
  • <input type="hidden" name="imageData" value="" />
  •  
  • <!--- This is the export feature. --->
  • <a href="#">Export Graphic</a>
  •  
  • </form>
  •  
  • </body>
  • </html>

As you can see, clicking on the "Export Graphic" button stores the base64 image data to the form field, "imageData". Because the base64 data contains all information about the image, including the height and width, I no longer need to post the canvas dimensions along with the form submission.

Once we post to the ColdFusion server, the code server-side has been greatly simplified; we are no longer re-creating the image step-by-step, we are simply creating an image object based on the submitted base64-encoded image data URL:

  • <!--- Param the base64 encoded image data value. --->
  • <cfparam name="form.imageData" type="string" default="" />
  •  
  • <!--- Create a new ColdFusion image with the given dimensions. --->
  • <cfset image = imageReadBase64( form.imageData ) />
  •  
  •  
  • <!---
  • Now that we have drawn the image, write it to the browser
  • as a PNG file.
  • --->
  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>iPhone Touch Events With jQuery</title>
  • <meta
  • name="viewport"
  • content="width=device-width, initial-scale=1"
  • />
  • </head>
  • <body>
  •  
  • <h1>
  • Your iPhone Touch Drawing
  • </h1>
  •  
  • <p>
  • <cfimage
  • action="writetobrowser"
  • source="#image#"
  • style="border: 1px solid ##999999 ;"
  • />
  • </p>
  •  
  • <p>
  • <a href="./index.cfm">Draw Another Image</a>
  • </p>
  •  
  • </body>
  • </html>

As you can see, the ColdFusion image is created with a single line of code - one call to imageReadBase64(). This is extremely simple; but, at the same time, as I mentioned above, we have much less flexibility as to how the image is rendered. In my previous experiment, the re-created ColdFusion image had "nicer" anti-aliasing and a thicker pen stroke. Of course, "nicer" is a highly subjective term.

Using this approach, the following drawing:

 
 
 
 
 
 
Using jQuery And The Canvas Element To Draw. 
 
 
 

... gets exported and re-created as this:

 
 
 
 
 
 
Use Base64 Canvas Data To Re-Create An Image In ColdFusion Using ImageReadBase64(). 
 
 
 

As you can see, the re-created ColdFusion image is a bit jagged in its lines.

The Canvas element is a lot of fun to play with; of course, keep in mind that the Canvas element is not yet supported by all the major browsers (think: IE). As such, it can only really be used in research and development - probably not as a mission-critical solution.




Reader Comments

Mar 10, 2010 at 10:00 AM // reply »
304 Comments

FYI, I think for your iPhone UA check you also want to match "ipod" - that will then work with the ipod touch.


Mar 10, 2010 at 10:06 AM // reply »
10,638 Comments

@Raymond,

Ah, good call! For some reason, I had it in my mind that iPhone would cover all the apple devices - clearly not thinking through that one very effectively :)


Mar 10, 2010 at 11:00 AM // reply »
30 Comments

Have you seen this project? http://code.google.com/p/explorercanvas/ It brings the canvas element to IE. Haven't had a chance to try it yet, but seems promising.


Mar 10, 2010 at 11:04 AM // reply »
10,638 Comments

@Ryan,

I have not seen that - thanks for posting it here. I'll be sure to check it out.


Mar 10, 2010 at 11:47 AM // reply »
2 Comments

This is awesome. Is there a way to implement this on any other touchscreen device, Example BB Storm?


Mar 10, 2010 at 9:23 PM // reply »
10,638 Comments

@Sanjay,

That's a good question, and one that I don't know the answer to. The canvas is a "new" HTML element, so theoretically, if the Storm supports it in the browser, it should work. The biggest question would be which kind of "event" to support. This currently supports Mouse and "Touch" events. I don't know how proprietary the "touch" events are, or if they are common to most mobile smart devices.


Mar 10, 2010 at 9:49 PM // reply »
6 Comments

hum - that should work for signature capture


Mar 10, 2010 at 9:51 PM // reply »
10,638 Comments

@Scott,

I've got an idea along those lines for a fun little project.


Mar 10, 2010 at 11:08 PM // reply »
53 Comments

Everytime I see one of these iPhone-specific pages, I'm going to try to port it to my Nexus one.

The event object for Android is identical to a browser (specifically, event.pageX, event.pageY) so no code modification required to detect the events.

I added a new function:
var isAndroid = (new RegExp( "Android", "i")).test(navigator.userAgent);

var isTouch = (isIPhone || isAndroid);

onTouchStart should use isTouch in place of isIPhone. The duplicate unbind in onTouchEnd was not required for android and actually messed it up somewhat.

link: http://drewwells.net/demo/draw.html


Mar 12, 2010 at 4:15 AM // reply »
14 Comments

Just seen a very similar concept here:

http://mrdoob.com/projects/harmony/

Which they say:

"As it works on webkit, he made sure it worked on the mobile Android and iPhone browsers. No multi-touch as yet, but the touch UI still makes a nice input mechanism."

Very cool stuff ... just need to find something to actually use it for :)


Mar 15, 2010 at 10:00 AM // reply »
10,638 Comments

@Drew,

Oh cool - that's great to know; looks like the "touch" events are becoming standard for these mobile smart devices.

@Tom,

Yeah, I've seen Harmony come across on Twitter - really some stunning visuals! I especially love the way it seems to be cognizant of where existing lines are (and works with them).


Sep 28, 2010 at 1:58 AM // reply »
1 Comments

This does not work on the Android Nexus because it does not support the todataURL function.

Does anyone have a work around for this?

Cheers

Guysa


Oct 18, 2010 at 2:44 PM // reply »
1 Comments

Wondering how to do the server-side part in PHP or Ruby or Python?


Oct 20, 2010 at 10:28 AM // reply »
10,638 Comments

@Josh,

Can't help you on that, sorry. ColdFusion makes working with images pretty easy, but I wouldn't know how to handle that in other languages.



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 3, 2012 at 10:49 PM
How I Got Node.js Running On A Linux Micro Instance Using Amazon EC2
Wow this was really helpful! Only thing I would add is you need to update your .bash_profile after you edit the secure_path. This is what I did: $ . ~/.bash_profile Otherwise, NPM won't be found. ... read »
Feb 3, 2012 at 10:14 PM
Pushing Base64-Encoded Images Over HTML5 WebSockets With Pusher And ColdFusion
@Ben, Just wanted to let you know that pusher are soon to start limiting sizes on messages. This was the detail that came through in the Feb dispatch: "However, we will soon be limiting the s ... read »
Feb 3, 2012 at 5:05 PM
Regular Expressions Make CSV Parsing In ColdFusion So Much Easier (And Faster)
I tried using your RegEx in my C# program, but it was matching an extra empty-string at the end and so I would end up with an extra field that doesn't exist, so I changed it to this: (^|,)("(?: ... read »
Feb 3, 2012 at 3:47 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
Josh Cyr posted this on Twitter just a little bit ago. Thought it was appropriate. http://stackoverflow.com/questions/1619152/how-to-create-rest-urls-without-verbs/1619677#1619677 ... read »
Feb 3, 2012 at 2:28 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
@Michael, You definitely make a good point (and extra points for quoting movies - I love movies). When you use a return() statement to define the object's public API, it does provide a consistent a ... read »
Feb 3, 2012 at 2:04 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
To quote Jurassic Park: "Just because you can doesn't mean you should". I completely, utterly disagree with the thought that this is more readable. Consider the current module pattern: if ... read »
Feb 3, 2012 at 1:10 PM
REST API Design Rulebook By Mark Masse
@Jordan, Yeah, WRML was created by Mark Masse (author of the book). I also found it to be a bit convoluted. I suppose it is intended to allow the Client to be able to programmaticaly respond to cha ... read »
Feb 3, 2012 at 1:08 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
@Jason, To be honest, I don't have good answers for that kinds of stuff. And, to the point, that is specifically why I *really* liked the REST API Design Rulebook by Mark Masse - he just cuts throu ... read »