Using Base64 Canvas Data In jQuery To Create ColdFusion Images

Posted March 10, 2010 at 9:49 AM

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 »
262 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 »
8,777 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 »
27 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 »
8,777 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 »
1 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 »
8,777 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 »
8,777 Comments

@Scott,

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


Mar 10, 2010 at 11:08 PM // reply »
8 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 »
8,777 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).


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:

Formatting: <strong>bold</strong> <em>italic<em>







  • Help Wanted - Find Your Next ColdFusion Job
Recent Blog Comments
Sep 3, 2010 at 5:48 AM
Scope Behavior When Using CFThread Inside Of ColdFusion Components
Thanks Ben, Excellent article and very precise explanation. Cheers Philip A question on invoking asynchronous save or some task and returning response back to the calling page. Using cfThread is ... read »
Sep 3, 2010 at 3:04 AM
Long Polling Experiment With jQuery And ColdFusion
@Ben, Thank you for your answer. If you are interested in - I solved the problem. It was, as you said, a buffer issue. Now when I'm getting a new request, the first thing I do is I'm sending some fa ... read »
Sep 3, 2010 at 1:29 AM
Using jQuery's SlideUp() and SlideDown() Methods With Bottom-Positioned Elements
Hey Ben, Thanks for clearing this up! Also, is there a way for the container to be open when you first load the page, so that when u click on the link it will slideUp? ... read »
Sep 3, 2010 at 12:29 AM
Bidirectional Data Exchange With ColdFusion's CFThread Tag
Thanks for posting this example, Ben. I plan to put something like this to use. I want to spawn up a thread to insert several records (possibly 1000s) into a database incrementing a counter upon ea ... read »
Sep 2, 2010 at 11:23 PM
Experimenting With HTML5's Cache Manifest For Offline Web Applications
Hi Ben, having checked all articles on Html5's appCache, is there a solution to just update newer files, using the manifest file? I am looking into using application cache to actually have an offline ... read »
Sep 2, 2010 at 3:11 PM
Long Polling Experiment With jQuery And ColdFusion
@Alex, It looks like some of the browsers implement some sort of buffering on the data request. I was definitely finding different behavior across browsers. I want to come back and figure this code ... read »
Sep 2, 2010 at 3:09 PM
Creating Base64-Encoded Data URLs For Images In ColdFusion
@Randall, At the very least, I think Chrome won't be able to close windows unless it opens them up, right? I am not sure. ... read »
Sep 2, 2010 at 2:17 PM
ColdFusion NumberFormat() Exploration
Ben - I have same question as Jim and I think maybe you misread it? I want numbers with non-zero decimal places to display the decimal, but those that have no decimal to display w/o the decimal poin ... read »