Building an HTML5 Icon Editor in Drupal


A while back I threw together a proof of concept icon editor using Drupal 7 and an HTML5 canvas, and it was way too neat to not share with the rest of the world.

This tutorial is a basic walk-through of how to make the canvas editor, which allows a user to edit the pixel values of an image on a Drupal node and save it back to the same node.

Before I get into any code, this tutorial assumes that you know your way around PHP and Javascript, are familiar with Drupal 7, and are comfortable building custom Drupal modules. If you have never built a custom Drupal module take a look at the module developer's guide here. It's a good starting point and will familiarize you with some of the terms and functions explored later.

For brevity, this tutorial also assumes that you already have Drupal 7 installed and have at least one content type set up that needs an editable icon field. This icon field should be a Drupal image field and, for the sake of this tutorial, I'm going to assume it is named 'field_icon'. I limited this image to 32x32 pixels since the image data is passed as POST data (and therefore is limited bymax_post_size), but also because I show each pixel as a 10x10 pixel square, so the 32x32 pixel image becomes more than 350 pixels square when grid lines are added.

The icon editor itself is pretty simple and will only require two pages in Drupal, one for the actual editor and one to save the canvas image data, and a single javascript file to control drawing on the canvas. So the module file starts out pretty simply, with a hook_menu() with two items (for this tutorial the module will be called icon_editor).

[prism:php] function icon_editor_menu() { $items['node/%node/icons'] = array( 'title' => t('Edit Icons'), 'description' => t('Edit HTML5 canvas icon'), 'page callback' => 'icon_editor_icon_page', 'page arguments' =>; array(1), 'type' =>; MENU_NORMAL_ITEM, ); $items['node/%node/icon-save'] = array( 'title' => t('Save Icons'), 'description' => t('Save icon from HTML5 canvas'), 'page callback' => 'icon_editor_icon_save', 'page arguments' => array(1), 'type' => MENU_NORMAL_ITEM, ); return $items; } [/prism:php]

The icon editor page itself is more complicated and, after some trial and error, I found that it was easiest to have two separate canvases on the page: one is the large “editor” canvas that displays a zoomed version of the icon and allows the user to alter pixel colors, and the other is a smaller canvas the size of the original image for loading and saving. The editor page also needs to have the image from the node rendered as an img element (so that our Javascript can render it into a canvas) and another element (in this case a button) to trigger the save function. Here's my page callback for the editor:

[prism:php] function icon_editor_editor_page($node) { $output = ''; if(isset($node->field_icon[LANGUAGE_NONE][0]['uri'])) { //hardcoded for the sake of the tutorial. $address = file_create_url($node->field_icon[LANGUAGE_NONE][0]['uri']); //the editor javascript will use the id of this img element to load the image into a canvas $output .= " />'; } //smaller load/save canvas $output.='';//primary edit canvas, the width and height are assuming 10x10 pixel pixels and lines in between $output .= ''; //don't forget a save button! $output .= 'Save'; //add the javascript file that will control the canvas $path = drupal_get_path('module', 'icon_editor'); drupal_add_js($path, 'icon_editor.js'); //add our node id to the Drupal settings object for our icon save function drupal_add_js(array('icon_editor' => array('icon_node' => $node->nid)), 'setting'); return $output; } [/prism:php]

The next step is to build the Javascript controller for the editor, which will render a larger 'zoomed' version of the icon, alter the pixel on user click, and send the altered icon image off to our save page for eventual saving back to the node.

[prism:javascript] //global variables var controllerCanvas; var iconCanvas; (function ($, Drupal, window, document) { Drupal.behaviors.iconEditor = { //grab the canvas objects controllerCanvas = document.getElementById('temp-canvas').getContext('2d'); iconCanvas = document.getElementById('icon-canvas').getContext('2d'); //copy the icon from the image to the temp canvas getDefaultIcon(); //draw the contents of the temp canvas to the main icon canvas drawIconCanvas(); //handle clicks on the canvas $('#icon-canvas').mousedown(function(e) { //set the color to black iconCanvas.fillStyle ='#000'; var xVal = Math.floor(e.offsetX/11); var yVal = Math.floor(e.offsetY/11); //fill a 10x10 rectangle with color, making sure to remember to offset the render and click by the grid lines too iconCanvas.fillRect((xVal*10)+(xVal+1), (yVal*10)+(yVal+1), 10, 10); }); //handle clicks on the save button $('#save-icon').click(function() { //copy the data from the icon canvas back to the temp canvas copyCanvasData(); //get the base64 canvas data from the temp canvas. This handy function allows us to pass encoded image data (it ends up being encoded as a png) through a POST var canvData = document.getElementById('temp-canvas').toDataURL(); //send the canvas data to the save icon page. Here is where we use the Drupal.settings node id variable set in our icon page function in the module //also, note that we take the base64 encoded data from above and pass it through as our POST variable imagedata var request = $.ajax({ type: "POST", url: "/node/"+Drupal.settings.icon_editor.icon_node+"/icon-save", data: {imagedata: canvData}, success: function(data, text_status) { //this would be a good place to handle errors from the save page, for now we reload the current page, which refreshes with our saved icon location.reload(); } }); }); } /** * renders the node image on the controller canvas */ function getDefaultIcon() { //grab the node field_icon image var thisimg = document.getElementById('node-icon'); controllerCanvas.drawImage(thisiimg, 0, 0); } /** * renders the icon canvas */ function drawIconCanvas() { //fill the canvas with a gray background iconCanvas.fillStyle = '#999999'; iconCanvas.fillRect(0, 0, 354, 354); //add our grid lines iconCanvas.strokeStyle = '#AAAAAA'; iconCanvas.lineWidth = .5; iconCanvas.beginPath(); for(var n=0; n<33; n++) { iconCanvas.moveTo(0, (n*10)+(n+1)); iconCanvas.lineTo(354, (n*10)+(n+1)); iconCanvas.moveTo((n*10)+(n+1), 0); iconCanvas.lineTo((n*10)+(n+1), 354); } iconCanvas.stroke(); drawImageDataOnCanvas(); } /** * draws the image data from the temp icon onto the canvas */ function drawImageDataOnCanvas() { var thisColor; for(var x=0; x<32; x++) { for(var y=0; y<32; y++) { //grab the color data from pixel x,y var thisData = controllerCanvas.getImageData(x, y, 1, 1); //turn the data into an rgb value thisColor = 'rgb('[0]+','[1]+','[2]+')'; //set the canvas fill style iconCanvas.fillStyle = thisColor; //fill a 10x10 square in this color, leaving grid lines iconCanvas.fillRect((x*10)+(x+1), (y*10)+(y+1), 10, 10); } } } /** * copies data from the icon editor to the temp canvas */ function copyCanvasData() { var thisColor; for(var x=0; x<32; x++) { for(var y=0; y<32; y++) { //loop through each pixel, copying to the save canvas var thisData = iconCanvas.getImageData((x*10)+(x+1), (y*10)+(y+1), 1, 1); thisColor = 'rgb('[0]+','[1]+','[2]+')'; controllerCanvas.fillStyle = thisColor; controllerCanvas.fillRect(x, y, 1, 1); } } } })(jQuery, Drupal, this, this.document); [/prism:javascript]

To sum up what's happening in the javascript, I add a behavior that inits the various canvases, handles clicks on the editor canvas and the save button, and then adds functions to draw to all the canvases. The most important lines to notice here are where I set all the clicked pixels to black — this is definitely the first place to improve the functionality, but that can wait for another tutorial.

In the mean time, there is one last missing module function to tie everything together: the save page function, called when Drupal looks for node/%node/icon-save. This will process the encoded POST data from the editor page and save the resulting image to the node (so it's kind of important to the whole process).

[prism:php] function icon_editor_icon_save($node) { //grab our raw image data passed javascript $raw_data = $_POST['imagedata']; $filtered_data = explode(',', $raw_data); //this data is posted as "data:image/png;base64,", so we explode on comma if(isset($filtered_data[1])) { //make sure there is data //get the raw data $unecoded = base64_decode($filtered_data[1]); //save the file to the filesystem, alter this as necessary for your file system and Drupal's needs $file = file_save_data($unecoded, 'public://'.$node->nid.'_icon.png', FILE_EXISTS_RENAME); //set the field_icon to the file object you get from file_save_data() $node->field_icon[LANGUAGE_NONE][0] = (array)$file; //you could easily wrap this node_save in a try/catch and return a value to the javascript to handle save errors node_save($node); } } [/prism:php]

And that's it! You've now got a very basic MVP for an icon editor, ready for more features (like adding a color picker like Spectrum)

Banner image courtesy of Flicker user [Arthur Hanna](