1 /*
  2  * Copyright (C) 2008, 2009, 2010 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2010-06-26 21:44:56 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview The main PaintWeb application code.
 26  */
 27 
 28 /**
 29  * @class The PaintWeb application object.
 30  *
 31  * @param {Window} [win=window] The window object to use.
 32  * @param {Document} [doc=document] The document object to use.
 33  */
 34 function PaintWeb (win, doc) {
 35   var _self = this;
 36 
 37   if (!win) {
 38     win = window;
 39   }
 40   if (!doc) {
 41     doc = document;
 42   }
 43 
 44   /**
 45    * PaintWeb version.
 46    * @type Number
 47    */
 48   this.version = 0.9; //!
 49 
 50   /**
 51    * PaintWeb build date (YYYYMMDD).
 52    * @type Number
 53    */
 54   this.build = -1; //!
 55 
 56   /**
 57    * Holds all the PaintWeb configuration.
 58    * @type Object
 59    */
 60   this.config = {
 61     showErrors: true
 62   };
 63 
 64   /**
 65    * Holds all language strings used within PaintWeb.
 66    */
 67   // Here we include a minimal set of strings, used in case the language file will 
 68   // not load.
 69   this.lang = {
 70     "noComputedStyle": "Error: window.getComputedStyle is not available.",
 71     "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.",
 72     "noCanvasSupport": "Error: Your browser does not support Canvas.",
 73     "guiPlaceholderWrong": "Error: The config.guiPlaceholder property must " +
 74       "reference a DOM element!",
 75     "initHandlerMustBeFunction": "The first argument must be a function.",
 76     "noConfigFile": "Error: You must point to a configuration file by " +
 77       "setting the config.configFile property!",
 78     "failedConfigLoad": "Error: Failed loading the configuration file.",
 79     "failedLangLoad": "Error: Failed loading the language file."
 80   };
 81 
 82   /**
 83    * Holds the buffer canvas and context references.
 84    * @type Object
 85    */
 86   this.buffer = {canvas: null, context: null};
 87 
 88   /**
 89    * Holds the current layer ID, canvas and context references.
 90    * @type Object
 91    */
 92   this.layer = {id: null, canvas: null, context: null};
 93 
 94   /**
 95    * The instance of the active tool object.
 96    *
 97    * @type Object
 98    *
 99    * @see PaintWeb.config.toolDefault holds the ID of the tool which is 
100    * activated when the application loads.
101    * @see PaintWeb#toolActivate Activate a drawing tool by ID.
102    * @see PaintWeb#toolRegister Register a new drawing tool.
103    * @see PaintWeb#toolUnregister Unregister a drawing tool.
104    * @see pwlib.tools holds the drawing tools.
105    */
106   this.tool = null;
107 
108   /**
109    * Holds references to DOM elements.
110    *
111    * @private
112    * @type Object
113    */
114   this.elems = {};
115 
116   /**
117    * Holds the last recorded mouse coordinates and the button state (if it's 
118    * down or not).
119    *
120    * @private
121    * @type Object
122    */
123   this.mouse = {x: 0, y: 0, buttonDown: false};
124 
125   /**
126    * Holds all the PaintWeb extensions.
127    *
128    * @type Object
129    * @see PaintWeb#extensionRegister Register a new extension.
130    * @see PaintWeb#extensionUnregister Unregister an extension.
131    * @see PaintWeb.config.extensions Holds the list of extensions to be loaded 
132    * automatically when PaintWeb is initialized.
133    */
134   this.extensions = {};
135 
136   /**
137    * Holds all the PaintWeb commands. Each property in this object must 
138    * reference a simple function which can be executed by keyboard shortcuts 
139    * and/or GUI elements.
140    *
141    * @type Object
142    * @see PaintWeb#commandRegister Register a new command.
143    * @see PaintWeb#commandUnregister Unregister a command.
144    */
145   this.commands = {};
146 
147   /**
148    * The graphical user interface object instance.
149    * @type pwlib.gui
150    */
151   this.gui = null;
152 
153   /**
154    * The document element PaintWeb is working with.
155    *
156    * @private
157    * @type Document
158    * @default document
159    */
160   this.doc = doc;
161 
162   /**
163    * The window object PaintWeb is working with.
164    *
165    * @private
166    * @type Window
167    * @default window
168    */
169   this.win = win;
170 
171   /**
172    * Holds image information: width, height, zoom and more.
173    *
174    * @type Object
175    */
176   this.image = {
177     /**
178      * Image width.
179      *
180      * @type Number
181      */
182     width: 0,
183 
184     /**
185      * Image height.
186      *
187      * @type Number
188      */
189     height: 0,
190 
191     /**
192      * Image zoom level. This property holds the current image zoom level used 
193      * by the user for viewing the image.
194      *
195      * @type Number
196      * @default 1
197      */
198     zoom: 1,
199 
200     /**
201      * Image scaling. The canvas elements are scaled from CSS using this value 
202      * as the scaling factor. This value is dependant on the browser rendering 
203      * resolution and on the user-defined image zoom level.
204      *
205      * @type Number
206      * @default 1
207      */
208     canvasScale: 1,
209 
210     /**
211      * Tells if the current image has been modified since the initial load.
212      *
213      * @type Boolean
214      * @default false
215      */
216     modified: false
217   };
218 
219   /**
220    * Resolution information.
221    *
222    * @type Object
223    */
224   this.resolution = {
225     /**
226      * The DOM element holding information about the current browser rendering 
227      * settings (zoom / DPI).
228      *
229      * @private
230      * @type Element
231      */
232     elem: null,
233 
234     /**
235      * The ID of the DOM element holding information about the current browser 
236      * rendering settings (zoom / DPI).
237      *
238      * @private
239      * @type String
240      * @default 'paintweb_resInfo'
241      */
242     elemId: 'paintweb_resInfo',
243 
244     /**
245      * The styling necessary for the DOM element.
246      *
247      * @private
248      * @type String
249      */
250     cssText: '@media screen and (resolution:96dpi){' +
251              '#paintweb_resInfo{width:96px}}' +
252              '@media screen and (resolution:134dpi){' +
253              '#paintweb_resInfo{width:134px}}' +
254              '@media screen and (resolution:200dpi){' +
255              '#paintweb_resInfo{width:200px}}' +
256              '@media screen and (resolution:300dpi){' +
257              '#paintweb_resInfo{width:300px}}' +
258              '#paintweb_resInfo{' +
259              'display:block;' +
260              'height:100%;' +
261              'left:-3000px;' +
262              'position:fixed;' +
263              'top:0;' +
264              'visibility:hidden;' +
265              'z-index:-32}',
266 
267     /**
268      * Optimal DPI for the canvas elements.
269      *
270      * @private
271      * @type Number
272      * @default 96
273      */
274     dpiOptimal: 96,
275 
276     /**
277      * The current DPI used by the browser for rendering the entire page.
278      *
279      * @type Number
280      * @default 96
281      */
282     dpiLocal: 96,
283 
284     /**
285      * The current zoom level used by the browser for rendering the entire page.
286      *
287      * @type Number
288      * @default 1
289      */
290     browserZoom: 1,
291 
292     /**
293      * The scaling factor used by the browser for rendering the entire page. For 
294      * example, on Gecko using DPI 200 the scale factor is 2.
295      *
296      * @private
297      * @type Number
298      * @default -1
299      */
300     scale: -1
301   };
302 
303   /**
304    * The image history.
305    *
306    * @private
307    * @type Object
308    */
309   this.history = {
310     /**
311      * History position.
312      *
313      * @type Number
314      * @default 0
315      */
316     pos: 0,
317 
318     /**
319      * The ImageDatas for each history state.
320      *
321      * @private
322      * @type Array
323      */
324     states: []
325   };
326 
327   /**
328    * Tells if the browser supports the Canvas Shadows API.
329    *
330    * @type Boolean
331    * @default true
332    */
333   this.shadowSupported = true;
334 
335   /**
336    * Tells if the current tool allows the drawing of shadows.
337    *
338    * @type Boolean
339    * @default true
340    */
341   this.shadowAllowed = true;
342 
343   /**
344    * Image in the clipboard. This is used when some selection is copy/pasted.  
345    * 
346    * @type ImageData
347    */
348   this.clipboard = false;
349 
350   /**
351    * Application initialization state. This property can be in one of the 
352    * following states:
353    *
354    * <ul>
355    *   <li>{@link PaintWeb.INIT_NOT_STARTED} - The initialization is not 
356    *   started.
357    *
358    *   <li>{@link PaintWeb.INIT_STARTED} - The initialization process is 
359    *   running.
360    *
361    *   <li>{@link PaintWeb.INIT_DONE} - The initialization process has completed 
362    *   successfully.
363    *
364    *   <li>{@link PaintWeb.INIT_ERROR} - The initialization process has failed.
365    * </ul>
366    *
367    * @type Number
368    * @default PaintWeb.INIT_NOT_STARTED
369    */
370   this.initialized = PaintWeb.INIT_NOT_STARTED;
371 
372   /**
373    * Custom application events object.
374    *
375    * @type pwlib.appEvents
376    */
377   this.events = null;
378 
379   /**
380    * Unique ID for the current PaintWeb instance.
381    *
382    * @type Number
383    */
384   this.UID = 0;
385 
386   /**
387    * List of Canvas context properties to save and restore.
388    *
389    * <p>When the Canvas is resized the state is lost. Using context.save/restore 
390    * state does work only in Opera. In Firefox/Gecko and WebKit saved states are 
391    * lost after resize, so there's no state to restore. As such, PaintWeb has 
392    * its own simple state save/restore mechanism. The property values are saved 
393    * into a JavaScript object.
394    *
395    * @private
396    * @type Array
397    *
398    * @see PaintWeb#stateSave to save the canvas context state.
399    * @see PaintWeb#stateRestore to restore a canvas context state.
400    */
401   this.stateProperties = ['strokeStyle', 'fillStyle', 'globalAlpha', 
402     'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'shadowOffsetX', 
403     'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation', 
404     'font', 'textAlign', 'textBaseline'];
405 
406   /**
407    * Holds the keyboard event listener object.
408    *
409    * @private
410    * @type pwlib.dom.KeyboardEventListener
411    * @see pwlib.dom.KeyboardEventListener The class dealing with the 
412    * cross-browser differences in the DOM keyboard events.
413    */
414   var kbListener_ = null;
415 
416   /**
417    * Holds temporary state information during PaintWeb initialization.
418    *
419    * @private
420    * @type Object
421    */
422   var temp_ = {onInit: null, toolsLoadQueue: 0, extensionsLoadQueue: 0};
423 
424   // Avoid global scope lookup.
425   var MathAbs   = Math.abs,
426       MathFloor = Math.floor,
427       MathMax   = Math.max,
428       MathMin   = Math.min,
429       MathRound = Math.round,
430       pwlib     = null,
431       appEvent  = null,
432       lang      = this.lang;
433 
434   /**
435    * Element node type constant.
436    *
437    * @constant
438    * @type Number
439    */
440   this.ELEMENT_NODE = window.Node ? Node.ELEMENT_NODE : 1;
441 
442   /**
443    * PaintWeb pre-initialization code. This runs when the PaintWeb instance is 
444    * constructed.
445    * @private
446    */
447   function preInit() {
448     var d = new Date();
449 
450     // If PaintWeb is running directly from the source code, then the build date 
451     // is always today.
452     if (_self.build === -1) {
453       var dateArr = [d.getFullYear(), d.getMonth()+1, d.getDate()];
454 
455       if (dateArr[1] < 10) {
456         dateArr[1] = '0' + dateArr[1];
457       }
458       if (dateArr[2] < 10) {
459         dateArr[2] = '0' + dateArr[2];
460       }
461 
462       _self.build = dateArr.join('');
463     }
464 
465     _self.UID = d.getMilliseconds() * MathRound(Math.random() * 100);
466     _self.elems.head = doc.getElementsByTagName('head')[0] || doc.body;
467   };
468 
469   /**
470    * Initialize PaintWeb.
471    *
472    * <p>This method is asynchronous, meaning that it will return much sooner 
473    * before the application initialization is completed.
474    *
475    * @param {Function} [handler] The <code>appInit</code> event handler. Your 
476    * event handler will be invoked automatically when PaintWeb completes 
477    * loading, or when an error occurs.
478    *
479    * @returns {Boolean} True if the initialization has been started 
480    * successfully, or false if not.
481    */
482   this.init = function (handler) {
483     if (this.initialized === PaintWeb.INIT_DONE) {
484       return true;
485     }
486 
487     this.initialized = PaintWeb.INIT_STARTED;
488 
489     if (handler && typeof handler !== 'function') {
490       throw new TypeError(lang.initHandlerMustBeFunction);
491     }
492 
493     temp_.onInit = handler;
494 
495     // Check Canvas support.
496     if (!doc.createElement('canvas').getContext) {
497       this.initError(lang.noCanvasSupport);
498       return false;
499     }
500 
501     // Basic functionality used within the Web application.
502     if (!window.getComputedStyle) {
503       try {
504         win.getComputedStyle(doc.createElement('div'), null);
505       } catch (err) {
506         this.initError(lang.noComputedStyle);
507         return false;
508       }
509     }
510 
511     if (!window.XMLHttpRequest) {
512       this.initError(lang.noXMLHttpRequest);
513       return false;
514     }
515 
516     if (!this.config.configFile) {
517       this.initError(lang.noConfigFile);
518       return false;
519     }
520 
521     if (typeof this.config.guiPlaceholder !== 'object' || 
522         this.config.guiPlaceholder.nodeType !== this.ELEMENT_NODE) {
523       this.initError(lang.guiPlaceholderWrong);
524       return false;
525     }
526 
527     // Silently ignore any wrong value for the config.imageLoad property.
528     if (typeof this.config.imageLoad !== 'object' || 
529         this.config.imageLoad.nodeType !== this.ELEMENT_NODE) {
530       this.config.imageLoad = null;
531     }
532 
533     // JSON parser and serializer.
534     if (!window.JSON) {
535       this.scriptLoad(PaintWeb.baseFolder + 'includes/json2.js', 
536           this.jsonlibReady);
537     } else {
538       this.jsonlibReady();
539     }
540 
541     return true;
542   };
543 
544   /**
545    * The <code>load</code> event handler for the JSON library script.
546    * @private
547    */
548   this.jsonlibReady = function () {
549     if (window.pwlib) {
550       _self.pwlibReady();
551     } else {
552       _self.scriptLoad(PaintWeb.baseFolder + 'includes/lib.js', 
553           _self.pwlibReady);
554     }
555   };
556 
557   /**
558    * The <code>load</code> event handler for the PaintWeb library script.
559    * @private
560    */
561   this.pwlibReady = function () {
562     pwlib = window.pwlib;
563     appEvent = pwlib.appEvent;
564 
565     // Create the custom application events object.
566     _self.events = new pwlib.appEvents(_self);
567     _self.configLoad();
568   };
569 
570   /**
571    * Report an initialization error.
572    *
573    * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
574    *
575    * @private
576    *
577    * @param {String} msg The error message.
578    *
579    * @see pwlib.appEvent.appInit
580    */
581   this.initError = function (msg) {
582     switch (this.initialized) {
583       case PaintWeb.INIT_ERROR:
584       case PaintWeb.INIT_DONE:
585       case PaintWeb.INIT_NOT_STARTED:
586         return;
587     }
588 
589     this.initialized = PaintWeb.INIT_ERROR;
590 
591     var ev = null;
592 
593     if (this.events && 'dispatch' in this.events &&
594         appEvent    && 'appInit'  in appEvent) {
595 
596       ev = new appEvent.appInit(this.initialized, msg);
597       this.events.dispatch(ev);
598     }
599 
600     if (typeof temp_.onInit === 'function') {
601       if (!ev) {
602         // fake an event dispatch.
603         ev = {type: 'appInit', state: this.initialized, errorMessage: msg};
604       }
605 
606       temp_.onInit.call(this, ev);
607     }
608 
609     if (this.config.showErrors) {
610       alert(msg);
611     } else if (window.console && console.log) {
612       console.log(msg);
613     }
614   };
615 
616   /**
617    * Asynchronously load the configuration file. This method issues an 
618    * XMLHttpRequest to load the JSON file.
619    *
620    * @private
621    *
622    * @see PaintWeb.config.configFile The configuration file.
623    * @see pwlib.xhrLoad The library function being used for creating the 
624    * XMLHttpRequest object.
625    */
626   this.configLoad = function () {
627     pwlib.xhrLoad(PaintWeb.baseFolder + this.config.configFile, 
628         this.configReady);
629   };
630 
631   /**
632    * The configuration reader. This is the event handler for the XMLHttpRequest 
633    * object, for the <code>onreadystatechange</code> event.
634    *
635    * @private
636    *
637    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
638    *
639    * @see PaintWeb#configLoad The method which issues the XMLHttpRequest request 
640    * for loading the configuration file.
641    */
642   this.configReady = function (xhr) {
643     /*
644      * readyState values:
645      *   0 UNINITIALIZED open() has not been called yet.
646      *   1 LOADING send() has not been called yet.
647      *   2 LOADED send() has been called, headers and status are available.
648      *   3 INTERACTIVE Downloading, responseText holds the partial data.
649      *   4 COMPLETED Finished with all operations.
650      */
651     if (!xhr || xhr.readyState !== 4) {
652       return;
653     }
654 
655     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
656       _self.initError(lang.failedConfigLoad);
657       return;
658     }
659 
660     var config = pwlib.jsonParse(xhr.responseText);
661     pwlib.extend(_self.config, config);
662 
663     _self.langLoad();
664   };
665 
666   /**
667    * Asynchronously load the language file. This method issues an XMLHttpRequest 
668    * to load the JSON file.
669    *
670    * @private
671    *
672    * @see PaintWeb.config.lang The language you want for the PaintWeb user 
673    * interface.
674    * @see pwlib.xhrLoad The library function being used for creating the 
675    * XMLHttpRequest object.
676    */
677   this.langLoad = function () {
678     var id   = this.config.lang,
679         file = PaintWeb.baseFolder;
680 
681     // If the language is not available, always fallback to English.
682     if (!(id in this.config.languages)) {
683       id = this.config.lang = 'en';
684     }
685 
686     if ('file' in this.config.languages[id]) {
687       file += this.config.languages[id].file;
688     } else {
689       file += this.config.langFolder + '/' + id + '.json';
690     }
691 
692     pwlib.xhrLoad(file, this.langReady);
693   };
694 
695   /**
696    * The language file reader. This is the event handler for the XMLHttpRequest 
697    * object, for the <code>onreadystatechange</code> event.
698    *
699    * @private
700    *
701    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
702    *
703    * @see PaintWeb#langLoad The method which issues the XMLHttpRequest request 
704    * for loading the language file.
705    */
706   this.langReady = function (xhr) {
707     if (!xhr || xhr.readyState !== 4) {
708       return;
709     }
710 
711     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
712       _self.initError(lang.failedLangLoad);
713       return;
714     }
715 
716     pwlib.extend(_self.lang, pwlib.jsonParse(xhr.responseText));
717 
718     if (_self.initCanvas() && _self.initContext()) {
719       // Start GUI load now.
720       _self.guiLoad();
721     } else {
722       _self.initError(lang.errorInitCanvas);
723     }
724   };
725 
726   /**
727    * Initialize the PaintWeb commands.
728    *
729    * @private
730    * @returns {Boolean} True if the initialization was successful, or false if 
731    * not.
732    */
733   this.initCommands = function () {
734     if (this.commandRegister('historyUndo',    this.historyUndo) &&
735         this.commandRegister('historyRedo',    this.historyRedo) &&
736         this.commandRegister('selectAll',      this.selectAll) &&
737         this.commandRegister('selectionCut',   this.selectionCut) &&
738         this.commandRegister('selectionCopy',  this.selectionCopy) &&
739         this.commandRegister('clipboardPaste', this.clipboardPaste) &&
740         this.commandRegister('imageSave',      this.imageSave) &&
741         this.commandRegister('imageClear',     this.imageClear) &&
742         this.commandRegister('swapFillStroke', this.swapFillStroke) &&
743         this.commandRegister('imageZoomIn',    this.imageZoomIn) &&
744         this.commandRegister('imageZoomOut',   this.imageZoomOut) &&
745         this.commandRegister('imageZoomReset', this.imageZoomReset)) {
746       return true;
747     } else {
748       this.initError(lang.errorInitCommands);
749       return false;
750     }
751   };
752 
753   /**
754    * Load th PaintWeb GUI. This method loads the GUI markup file, the stylesheet 
755    * and the script.
756    *
757    * @private
758    *
759    * @see PaintWeb.config.guiStyle The interface style file.
760    * @see PaintWeb.config.guiScript The interface script file.
761    * @see pwlib.gui The interface object.
762    */
763   this.guiLoad = function () {
764     var cfg    = this.config,
765         gui    = this.config.gui,
766         base   = PaintWeb.baseFolder + cfg.interfacesFolder + '/' + gui + '/',
767         style  = base + cfg.guiStyle,
768         script = base + cfg.guiScript;
769 
770     this.styleLoad(gui + 'style', style);
771 
772     if (pwlib.gui) {
773       this.guiScriptReady();
774     } else {
775       this.scriptLoad(script, this.guiScriptReady);
776     }
777   };
778 
779   /**
780    * The <code>load</code> event handler for the PaintWeb GUI script. This 
781    * method creates an instance of the GUI object that just loaded and starts 
782    * loading the GUI markup.
783    *
784    * @private
785    *
786    * @see PaintWeb.config.guiScript The interface script file.
787    * @see PaintWeb.config.guiMarkup The interface markup file.
788    * @see pwlib.gui The interface object.
789    * @see pwlib.xhrLoad The library function being used for creating the 
790    * XMLHttpRequest object.
791    */
792   this.guiScriptReady = function () {
793     var cfg    = _self.config,
794         gui    = _self.config.gui,
795         base   = cfg.interfacesFolder + '/' + gui + '/',
796         markup = base + cfg.guiMarkup;
797 
798     _self.gui = new pwlib.gui(_self);
799 
800     // Check if the interface markup is cached already.
801     if (markup in pwlib.fileCache) {
802       if (_self.gui.init(pwlib.fileCache[markup])) {
803         _self.initTools();
804       } else {
805         _self.initError(lang.errorInitGUI);
806       }
807 
808     } else {
809       pwlib.xhrLoad(PaintWeb.baseFolder + markup, _self.guiMarkupReady);
810     }
811   };
812 
813   /**
814    * The GUI markup reader. This is the event handler for the XMLHttpRequest 
815    * object, for the <code>onreadystatechange</code> event.
816    *
817    * @private
818    *
819    * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled.
820    *
821    * @see PaintWeb#guiScriptReady The method which issues the XMLHttpRequest 
822    * request for loading the interface markup file.
823    */
824   this.guiMarkupReady = function (xhr) {
825     if (!xhr || xhr.readyState !== 4) {
826       return;
827     }
828 
829     if (xhr.status !== 304 && xhr.status !== 200) {
830       _self.initError(lang.failedMarkupLoad);
831       return;
832     }
833 
834     var param;
835     if (xhr.responseXML && xhr.responseXML.documentElement) {
836       param = xhr.responseXML;
837     } else if (xhr.responseText) {
838       param = xhr.responseText;
839     } else {
840       _self.initError(lang.failedMarkupLoad);
841       return;
842     }
843 
844     if (_self.gui.init(param)) {
845       _self.initTools();
846     } else {
847       _self.initError(lang.errorInitGUI);
848     }
849   };
850 
851   /**
852    * Initialize the Canvas elements. This method creates the elements and 
853    * sets-up their dimensions.
854    *
855    * <p>The layer Canvas element will have the background rendered with the 
856    * color from {@link PaintWeb.config.backgroundColor}.
857    * 
858    * <p>If {@link PaintWeb.config.imageLoad} is defined, then the image element 
859    * is inserted into the Canvas image.
860    *
861    * <p>All the Canvas event listeners are also attached to the buffer Canvas 
862    * element.
863    *
864    * @private
865    * @returns {Boolean} True if the initialization was successful, or false if 
866    * not.
867    *
868    * @see PaintWeb#ev_canvas The global Canvas events handler.
869    */
870   this.initCanvas = function () {
871     var cfg           = this.config,
872         res           = this.resolution,
873         resInfo       = doc.getElementById(res.elemId),
874         layerCanvas   = doc.createElement('canvas'),
875         bufferCanvas  = doc.createElement('canvas'),
876         layerContext  = layerCanvas.getContext('2d'),
877         bufferContext = bufferCanvas.getContext('2d'),
878         width         = cfg.imageWidth,
879         height        = cfg.imageHeight,
880         imageLoad     = cfg.imageLoad;
881 
882     if (!resInfo) {
883       var style = doc.createElement('style');
884       style.type = 'text/css';
885       style.appendChild(doc.createTextNode(res.cssText));
886       _self.elems.head.appendChild(style);
887 
888       resInfo = doc.createElement('div');
889       resInfo.id = res.elemId;
890       doc.body.appendChild(resInfo);
891     }
892 
893     if (!resInfo) {
894       this.initError(lang.errorInitCanvas);
895       return false;
896     }
897     if (!layerCanvas || !bufferCanvas || !layerContext || !bufferContext) {
898       this.initError(lang.noCanvasSupport);
899       return false;
900     }
901 
902     if (!pwlib.isSameHost(imageLoad.src, win.location.host)) {
903       cfg.imageLoad = imageLoad = null;
904       alert(lang.imageLoadDifferentHost);
905     }
906 
907     if (imageLoad) {
908       width  = parseInt(imageLoad.width);
909       height = parseInt(imageLoad.height);
910     }
911 
912     res.elem = resInfo;
913 
914     this.image.width  = layerCanvas.width  = bufferCanvas.width  = width;
915     this.image.height = layerCanvas.height = bufferCanvas.height = height;
916 
917     this.layer.canvas   = layerCanvas;
918     this.layer.context  = layerContext;
919     this.buffer.canvas  = bufferCanvas;
920     this.buffer.context = bufferContext;
921 
922     if (imageLoad) {
923       layerContext.drawImage(imageLoad, 0, 0);
924     } else {
925       // Set the configured background color.
926       var fillStyle = layerContext.fillStyle;
927       layerContext.fillStyle = cfg.backgroundColor;
928       layerContext.fillRect(0, 0, width, height);
929       layerContext.fillStyle = fillStyle;
930     }
931 
932     /*
933      * Setup the event listeners for the canvas element.
934      *
935      * The event handler (ev_canvas) calls the event handlers associated with 
936      * the active tool (e.g. tool.mousemove).
937      */
938     var events = ['dblclick', 'click', 'mousedown', 'mouseup', 'mousemove', 
939         'contextmenu'],
940         n = events.length;
941 
942     for (var i = 0; i < n; i++) {
943       bufferCanvas.addEventListener(events[i], this.ev_canvas, false);
944     }
945 
946     return true;
947   };
948 
949   /**
950    * Initialize the Canvas buffer context. This method updates the context 
951    * properties to reflect the values defined in the PaintWeb configuration 
952    * file.
953    * 
954    * <p>Shadows support is also determined. The {@link PaintWeb#shadowSupported} 
955    * value is updated accordingly.
956    *
957    * @private
958    * @returns {Boolean} True if the initialization was successful, or false if 
959    * not.
960    */
961   this.initContext = function () {
962     var bufferContext = this.buffer.context;
963 
964     // Opera does not render shadows, at the moment.
965     if (!pwlib.browser.opera && bufferContext.shadowColor && 'shadowOffsetX' in 
966         bufferContext && 'shadowOffsetY' in bufferContext && 'shadowBlur' in 
967         bufferContext) {
968       this.shadowSupported = true;
969     } else {
970       this.shadowSupported = false;
971     }
972 
973     var cfg = this.config,
974         props = {
975           fillStyle:    cfg.fillStyle,
976           font:         cfg.text.fontSize + 'px ' + cfg.text.fontFamily,
977           lineCap:      cfg.line.lineCap,
978           lineJoin:     cfg.line.lineJoin,
979           lineWidth:    cfg.line.lineWidth,
980           miterLimit:   cfg.line.miterLimit,
981           strokeStyle:  cfg.strokeStyle,
982           textAlign:    cfg.text.textAlign,
983           textBaseline: cfg.text.textBaseline
984         };
985 
986     if (cfg.text.bold) {
987       props.font = 'bold ' + props.font;
988     }
989 
990     if (cfg.text.italic) {
991       props.font = 'italic ' + props.font;
992     }
993 
994     // Support Gecko 1.9.0
995     if (!bufferContext.fillText && 'mozTextStyle' in bufferContext) {
996       props.mozTextStyle = props.font;
997     }
998 
999     for (var prop in props) {
1000       bufferContext[prop] = props[prop];
1001     }
1002 
1003     // shadows are only for the layer context.
1004     if (cfg.shadow.enable && this.shadowSupported) {
1005       var layerContext = this.layer.context;
1006       layerContext.shadowColor   = cfg.shadow.shadowColor;
1007       layerContext.shadowBlur    = cfg.shadow.shadowBlur;
1008       layerContext.shadowOffsetX = cfg.shadow.shadowOffsetX;
1009       layerContext.shadowOffsetY = cfg.shadow.shadowOffsetY;
1010     }
1011 
1012     return true;
1013   };
1014 
1015   /**
1016    * Initialization procedure which runs after the configuration, language and 
1017    * GUI files have loaded.
1018    *
1019    * <p>This method dispatches the {@link pwlib.appEvent.appInit} event.
1020    *
1021    * @private
1022    *
1023    * @see pwlib.appEvent.appInit
1024    */
1025   this.initComplete = function () {
1026     if (!this.initCommands()) {
1027       this.initError(lang.errorInitCommands);
1028       return;
1029     }
1030 
1031     // The initial blank state of the image
1032     this.historyAdd();
1033     this.image.modified = false;
1034 
1035     // The global keyboard events handler implements everything needed for 
1036     // switching between tools and for accessing any other functionality of the 
1037     // Web application.
1038     kbListener_ = new pwlib.dom.KeyboardEventListener(this.config.guiPlaceholder,
1039         {keydown:  this.ev_keyboard,
1040          keypress: this.ev_keyboard,
1041          keyup:    this.ev_keyboard});
1042 
1043     this.updateCanvasScaling();
1044     this.win.addEventListener('resize', this.updateCanvasScaling, false);
1045 
1046     this.events.add('configChange',    this.configChangeHandler);
1047     this.events.add('imageSaveResult', this.imageSaveResultHandler);
1048 
1049     // Add the init event handler.
1050     if (typeof temp_.onInit === 'function') {
1051       _self.events.add('appInit', temp_.onInit);
1052       delete temp_.onInit;
1053     }
1054 
1055     this.initialized = PaintWeb.INIT_DONE;
1056 
1057     this.events.dispatch(new appEvent.appInit(this.initialized));
1058   };
1059 
1060   /**
1061    * Load all the configured drawing tools.
1062    * @private
1063    */
1064   this.initTools = function () {
1065     var id   = '',
1066         cfg  = this.config,
1067         n    = cfg.tools.length,
1068         base = PaintWeb.baseFolder + cfg.toolsFolder + '/';
1069 
1070     if (n < 1) {
1071       this.initError(lang.noToolConfigured);
1072       return;
1073     }
1074 
1075     temp_.toolsLoadQueue = n;
1076 
1077     for (var i = 0; i < n; i++) {
1078       id = cfg.tools[i];
1079       if (id in pwlib.tools) {
1080         this.toolLoaded();
1081       } else {
1082         this.scriptLoad(base + id + '.js' , this.toolLoaded);
1083       }
1084     }
1085   };
1086 
1087   /**
1088    * The <code>load</code> event handler for each tool script.
1089    * @private
1090    */
1091   this.toolLoaded = function () {
1092     temp_.toolsLoadQueue--;
1093 
1094     if (temp_.toolsLoadQueue === 0) {
1095       var t = _self.config.tools,
1096           n = t.length;
1097 
1098       for (var i = 0; i < n; i++) {
1099         if (!_self.toolRegister(t[i])) {
1100           _self.initError(pwlib.strf(lang.toolRegisterFailed, {id: t[i]}));
1101           return;
1102         }
1103       }
1104 
1105       _self.initExtensions();
1106     }
1107   };
1108 
1109   /**
1110    * Load all the extensions.
1111    * @private
1112    */
1113   this.initExtensions = function () {
1114     var id   = '',
1115         cfg  = this.config,
1116         n    = cfg.extensions.length,
1117         base = PaintWeb.baseFolder + cfg.extensionsFolder + '/';
1118 
1119     if (n < 1) {
1120       this.initComplete();
1121       return;
1122     }
1123 
1124     temp_.extensionsLoadQueue = n;
1125 
1126     for (var i = 0; i < n; i++) {
1127       id = cfg.extensions[i];
1128       if (id in pwlib.extensions) {
1129         this.extensionLoaded();
1130       } else {
1131         this.scriptLoad(base + id + '.js', this.extensionLoaded);
1132       }
1133     }
1134   };
1135 
1136   /**
1137    * The <code>load</code> event handler for each extension script.
1138    * @private
1139    */
1140   this.extensionLoaded = function () {
1141     temp_.extensionsLoadQueue--;
1142 
1143     if (temp_.extensionsLoadQueue === 0) {
1144       var e = _self.config.extensions,
1145           n = e.length;
1146 
1147       for (var i = 0; i < n; i++) {
1148         if (!_self.extensionRegister(e[i])) {
1149           _self.initError(pwlib.strf(lang.extensionRegisterFailed, {id: e[i]}));
1150           return;
1151         }
1152       }
1153 
1154       _self.initComplete();
1155     }
1156   };
1157 
1158   /**
1159    * Update the canvas scaling. This method determines the DPI and/or zoom level 
1160    * used by the browser to render the application. Based on these values, the 
1161    * canvas elements are scaled down to cancel any upscaling performed by the 
1162    * browser.
1163    *
1164    * <p>The {@link pwlib.appEvent.canvasSizeChange} application event is 
1165    * dispatched.
1166    */
1167   this.updateCanvasScaling = function () {
1168     var res         = _self.resolution,
1169         cs          = win.getComputedStyle(res.elem, null),
1170         image       = _self.image;
1171         bufferStyle = _self.buffer.canvas.style,
1172         layerStyle  = _self.layer.canvas.style,
1173         scaleNew    = 1,
1174         width       = parseInt(cs.width),
1175         height      = parseInt(cs.height);
1176 
1177     if (pwlib.browser.opera) {
1178       // Opera zoom level detection.
1179       // The scaling factor is sufficiently accurate for zoom levels between 
1180       // 100% and 200% (in steps of 10%).
1181 
1182       scaleNew = win.innerHeight / height;
1183       scaleNew = MathRound(scaleNew * 10) / 10;
1184 
1185     } else if (width && !isNaN(width) && width !== res.dpiOptimal) {
1186       // Page DPI detection. This only works in Gecko 1.9.1.
1187 
1188       res.dpiLocal = width;
1189 
1190       // The scaling factor is the same as in Gecko.
1191       scaleNew = MathFloor(res.dpiLocal / res.dpiOptimal);
1192 
1193     } else if (pwlib.browser.olpcxo && pwlib.browser.gecko) {
1194       // Support for the default Gecko included on the OLPC XO-1 system.
1195       //
1196       // See:
1197       // http://www.robodesign.ro/mihai/blog/paintweb-performance
1198       // http://mxr.mozilla.org/mozilla-central/source/gfx/src/thebes/nsThebesDeviceContext.cpp#725
1199       // dotsArePixels = false on the XO due to a hard-coded patch.
1200       // Thanks go to roc from Mozilla for his feedback on making this work.
1201 
1202       res.dpiLocal = 134; // hard-coded value, we cannot determine it
1203 
1204       var appUnitsPerCSSPixel  = 60, // hard-coded internally in Gecko
1205           devPixelsPerCSSPixel = res.dpiLocal / res.dpiOptimal; // 1.3958333333
1206           appUnitsPerDevPixel  = appUnitsPerCSSPixel / devPixelsPerCSSPixel; // 42.9850746278...
1207 
1208       scaleNew = appUnitsPerCSSPixel / MathFloor(appUnitsPerDevPixel); // 1.4285714285...
1209 
1210       // New in Gecko 1.9.2.
1211       if ('mozImageSmoothingEnabled' in layerStyle) {
1212         layerStyle.mozImageSmoothingEnabled 
1213           = bufferStyle.mozImageSmoothingEnabled = false;
1214       }
1215     }
1216 
1217     if (scaleNew === res.scale) {
1218       return;
1219     }
1220 
1221     res.scale = scaleNew;
1222 
1223     var styleWidth  = image.width  / res.scale * image.zoom,
1224         styleHeight = image.height / res.scale * image.zoom;
1225 
1226     image.canvasScale = styleWidth / image.width;
1227 
1228     // FIXME: MSIE 9 clears the Canvas element when you change the 
1229     // elem.style.width/height... *argh*
1230     bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
1231     bufferStyle.height = layerStyle.height = styleHeight + 'px';
1232 
1233     _self.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
1234           image.canvasScale));
1235   };
1236 
1237   /**
1238    * The Canvas events handler.
1239    * 
1240    * <p>This method determines the mouse position relative to the canvas 
1241    * element, after which it invokes the method of the currently active tool 
1242    * with the same name as the current event type. For example, for the 
1243    * <code>mousedown</code> event the <code><var>tool</var>.mousedown()</code> 
1244    * method is invoked.
1245    *
1246    * <p>The mouse coordinates are stored in the {@link PaintWeb#mouse} object.  
1247    * These properties take into account the current zoom level and the image 
1248    * scroll.
1249    *
1250    * @private
1251    *
1252    * @param {Event} ev The DOM Event object.
1253    *
1254    * @returns {Boolean} True if the tool event handler executed, or false 
1255    * otherwise.
1256    */
1257   this.ev_canvas = function (ev) {
1258     if (!_self.tool) {
1259       return false;
1260     }
1261 
1262     switch (ev.type) {
1263       case 'mousedown':
1264         /*
1265          * If the mouse is down already, skip the event.
1266          * This is needed to allow the user to go out of the drawing canvas, 
1267          * release the mouse button, then come back and click to end the drawing 
1268          * operation.
1269          * Additionally, this is needed to allow extensions like MouseKeys to 
1270          * perform their actions during a drawing operation, even when a real 
1271          * mouse is used. For example, allow the user to start drawing with the 
1272          * keyboard (press 0) then use the real mouse to move and click to end 
1273          * the drawing operation.
1274          */
1275         if (_self.mouse.buttonDown) {
1276           return false;
1277         }
1278         _self.mouse.buttonDown = true;
1279         break;
1280 
1281       case 'mouseup':
1282         // Skip the event if the mouse button was not down.
1283         if (!_self.mouse.buttonDown) {
1284           return false;
1285         }
1286         _self.mouse.buttonDown = false;
1287     }
1288 
1289     /*
1290      * Update the event, to include the mouse position, relative to the canvas 
1291      * element.
1292      */
1293     if ('layerX' in ev) {
1294       if (_self.image.canvasScale === 1) {
1295         _self.mouse.x = ev.layerX;
1296         _self.mouse.y = ev.layerY;
1297       } else {
1298         _self.mouse.x = MathRound(ev.layerX / _self.image.canvasScale);
1299         _self.mouse.y = MathRound(ev.layerY / _self.image.canvasScale);
1300       }
1301     } else if ('offsetX' in ev) {
1302       if (_self.image.canvasScale === 1) {
1303         _self.mouse.x = ev.offsetX;
1304         _self.mouse.y = ev.offsetY;
1305       } else {
1306         _self.mouse.x = MathRound(ev.offsetX / _self.image.canvasScale);
1307         _self.mouse.y = MathRound(ev.offsetY / _self.image.canvasScale);
1308       }
1309     }
1310 
1311     // The event handler of the current tool.
1312     if (ev.type in _self.tool && _self.tool[ev.type](ev)) {
1313       ev.preventDefault();
1314       return true;
1315     } else {
1316       return false;
1317     }
1318   };
1319 
1320   /**
1321    * The global keyboard events handler. This makes all the keyboard shortcuts 
1322    * work in the web application.
1323    *
1324    * <p>This method determines the key the user pressed, based on the 
1325    * <var>ev</var> DOM Event object, taking into consideration any browser 
1326    * differences. Two new properties are added to the <var>ev</var> object:
1327    *
1328    * <ul>
1329    *   <li><var>ev.kid_</var> is a string holding the key and the modifiers list 
1330    *   (<kbd>Control</kbd>, <kbd>Alt</kbd> and/or <kbd>Shift</kbd>). For 
1331    *   example, if the user would press the key <kbd>A</kbd> while holding down 
1332    *   <kbd>Control</kbd>, then <var>ev.kid_</var> would be "Control A". If the 
1333    *   user would press "9" while holding down <kbd>Shift</kbd>, then 
1334    *   <var>ev.kid_</var> would be "Shift 9".
1335    *
1336    *   <li><var>ev.kobj_</var> holds a reference to the keyboard shortcut 
1337    *   definition object from the configuration. This is useful for reuse, for 
1338    *   passing parameters from the keyboard shortcut configuration object to the 
1339    *   event handler.
1340    * </ul>
1341    *
1342    * <p>In {@link PaintWeb.config.keys} one can setup the keyboard shortcuts.  
1343    * If the keyboard combination is found in that list, then the associated tool 
1344    * is activated.
1345    *
1346    * <p>Note: this method includes some work-around for making the image zoom 
1347    * keys work well both in Opera and Firefox.
1348    *
1349    * @private
1350    *
1351    * @param {Event} ev The DOM Event object.
1352    *
1353    * @see PaintWeb.config.keys The keyboard shortcuts configuration.
1354    * @see pwlib.dom.KeyboardEventListener The class dealing with the 
1355    * cross-browser differences in the DOM keyboard events.
1356    */
1357   this.ev_keyboard = function (ev) {
1358     // Do not continue if the key was not recognized by the lib.
1359     if (!ev.key_) {
1360       return;
1361     }
1362 
1363     if (ev.target && ev.target.nodeName) {
1364       switch (ev.target.nodeName.toLowerCase()) {
1365         case 'input':
1366           if (ev.type === 'keypress' && (ev.key_ === 'Up' || ev.key_ === 'Down') 
1367               && ev.target.getAttribute('type') === 'number') {
1368             _self.ev_numberInput(ev);
1369           }
1370         case 'select':
1371         case 'textarea':
1372         case 'button':
1373           return;
1374       }
1375     }
1376 
1377     // Rather ugly, but the only way, at the moment, to detect these keys in 
1378     // Opera and Firefox.
1379     if (ev.type === 'keypress' && ev.char_) {
1380       var isZoomKey = true,
1381           imageZoomKeys = _self.config.imageZoomKeys;
1382 
1383       // Check if this is a zoom key and execute the commands as needed.
1384       switch (ev.char_) {
1385         case imageZoomKeys['in']:
1386           _self.imageZoomIn(ev);
1387           break;
1388 
1389         case imageZoomKeys['out']:
1390           _self.imageZoomOut(ev);
1391           break;
1392         case imageZoomKeys['reset']:
1393           _self.imageZoomReset(ev);
1394           break;
1395         default:
1396           isZoomKey = false;
1397       }
1398 
1399       if (isZoomKey) {
1400         ev.preventDefault();
1401         return;
1402       }
1403     }
1404 
1405     // Determine the key ID.
1406     ev.kid_ = '';
1407     var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'};
1408     for (i in kmods) {
1409       if (ev[i] && ev.key_ !== kmods[i]) {
1410         ev.kid_ += kmods[i] + ' ';
1411       }
1412     }
1413     ev.kid_ += ev.key_;
1414 
1415     // Send the keyboard event to the event handler of the active tool. If it 
1416     // returns true, we consider it recognized the keyboard shortcut.
1417     if (_self.tool && ev.type in _self.tool && _self.tool[ev.type](ev)) {
1418       return true;
1419     }
1420 
1421     // If there's no event handler within the active tool, or if the event 
1422     // handler does otherwise return false, then we continue with the global 
1423     // keyboard shortcuts.
1424 
1425     var gkey = _self.config.keys[ev.kid_];
1426     if (!gkey) {
1427       return false;
1428     }
1429 
1430     ev.kobj_ = gkey;
1431 
1432     // Check if the keyboard shortcut has some extension associated.
1433     if ('extension' in gkey) {
1434       var extension = _self.extensions[gkey.extension],
1435           method    = gkey.method || ev.type;
1436 
1437       // Call the extension method.
1438       if (method in extension) {
1439         extension[method].call(this, ev);
1440       }
1441 
1442     } else if ('command' in gkey && gkey.command in _self.commands) {
1443       // Invoke the command associated with the key.
1444       _self.commands[gkey.command].call(this, ev);
1445 
1446     } else if (ev.type === 'keydown' && 'toolActivate' in gkey) {
1447 
1448       // Active the tool associated to the key.
1449       _self.toolActivate(gkey.toolActivate, ev);
1450 
1451     }
1452 
1453     if (ev.type === 'keypress') {
1454       ev.preventDefault();
1455     }
1456   };
1457 
1458   /**
1459    * This is the <code>keypress</code> event handler for inputs of type=number.  
1460    * This function only handles cases when the key is <kbd>Up</kbd> or 
1461    * <kbd>Down</kbd>. For the <kbd>Up</kbd> key the input value is increased, 
1462    * and for the <kbd>Down</kbd> the value is decreased.
1463    *
1464    * @private
1465    * @param {Event} ev The DOM Event object.
1466    * @see PaintWeb#ev_keyboard
1467    */
1468   this.ev_numberInput = function (ev) {
1469     var target = ev.target;
1470 
1471     // Process the value.
1472     var val,
1473         max  = parseFloat(target.getAttribute('max')),
1474         min  = parseFloat(target.getAttribute('min')),
1475         step = parseFloat(target.getAttribute('step'));
1476 
1477     if (target.value === '' || target.value === null) {
1478       val = !isNaN(min) ? min : 0;
1479     } else {
1480       val = parseFloat(target.value.replace(/[,.]+/g, '.').
1481                                     replace(/[^0-9.\-]/g, ''));
1482     }
1483 
1484     // If target is not a number, then set the old value, or the minimum value. If all fails, set 0.
1485     if (isNaN(val)) {
1486       val = min || 0;
1487     }
1488 
1489     if (isNaN(step)) {
1490       step = 1;
1491     }
1492 
1493     if (ev.shiftKey) {
1494       step *= 2;
1495     }
1496 
1497     if (ev.key_ === 'Down') {
1498       step *= -1;
1499     }
1500 
1501     val += step;
1502 
1503     if (!isNaN(max) && val > max) {
1504       val = max;
1505     } else if (!isNaN(min) && val < min) {
1506       val = min;
1507     }
1508 
1509     if (val == target.value) {
1510       return;
1511     }
1512 
1513     target.value = val;
1514 
1515     // Dispatch the 'change' events to make sure that any associated event 
1516     // handlers pick up the changes.
1517     if (doc.createEvent && target.dispatchEvent) {
1518       var ev_change = doc.createEvent('HTMLEvents');
1519       ev_change.initEvent('change', true, true);
1520       target.dispatchEvent(ev_change);
1521     }
1522   };
1523 
1524   /**
1525    * Zoom into the image.
1526    *
1527    * @param {mixed} ev An event object which might have the <var>shiftKey</var> 
1528    * property. If the property evaluates to true, then the zoom level will 
1529    * increase twice more than normal.
1530    *
1531    * @returns {Boolean} True if the operation was successful, or false if not.
1532    *
1533    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
1534    * @see PaintWeb.config.zoomStep The value used for increasing the zoom level.
1535    */
1536   this.imageZoomIn = function (ev) {
1537     if (ev && ev.shiftKey) {
1538       _self.config.imageZoomStep *= 2;
1539     }
1540 
1541     var res = _self.imageZoomTo('+');
1542 
1543     if (ev && ev.shiftKey) {
1544       _self.config.imageZoomStep /= 2;
1545     }
1546 
1547     return res;
1548   };
1549 
1550   /**
1551    * Zoom out of the image.
1552    *
1553    * @param {mixed} ev An event object which might have the <var>shiftKey</var> 
1554    * property. If the property evaluates to true, then the zoom level will 
1555    * decrease twice more than normal.
1556    *
1557    * @returns {Boolean} True if the operation was successful, or false if not.
1558    *
1559    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
1560    * @see PaintWeb.config.zoomStep The value used for decreasing the zoom level.
1561    */
1562   this.imageZoomOut = function (ev) {
1563     if (ev && ev.shiftKey) {
1564       _self.config.imageZoomStep *= 2;
1565     }
1566 
1567     var res = _self.imageZoomTo('-');
1568 
1569     if (ev && ev.shiftKey) {
1570       _self.config.imageZoomStep /= 2;
1571     }
1572 
1573     return res;
1574   };
1575 
1576   /**
1577    * Reset the image zoom level to normal.
1578    *
1579    * @returns {Boolean} True if the operation was successful, or false if not.
1580    *
1581    * @see PaintWeb#imageZoomTo The method used for changing the zoom level.
1582    */
1583   this.imageZoomReset = function (ev) {
1584     return _self.imageZoomTo(1);
1585   };
1586 
1587   /**
1588    * Change the image zoom level.
1589    *
1590    * <p>This method dispatches the {@link pwlib.appEvent.imageZoom} application 
1591    * event before zooming the image. Once the image zoom is applied, the {@link 
1592    * pwlib.appEvent.canvasSizeChange} event is dispatched.
1593    *
1594    * @param {Number|String} level The level you want to zoom the image to.
1595    * 
1596    * <p>If the value is a number, it must be a floating point positive number, 
1597    * where 0.5 means 50%, 1 means 100% (normal) zoom, 4 means 400% and so on.
1598    *
1599    * <p>If the value is a string it must be "+" or "-". This means that the zoom 
1600    * level will increase/decrease using the configured {@link 
1601    * PaintWeb.config.zoomStep}.
1602    *
1603    * @returns {Boolean} True if the image zoom level changed successfully, or 
1604    * false if not.
1605    */
1606   this.imageZoomTo = function (level) {
1607     var image  = this.image,
1608         config = this.config,
1609         res    = this.resolution;
1610 
1611     if (!level) {
1612       return false;
1613     } else if (level === '+') {
1614       level = image.zoom + config.imageZoomStep;
1615     } else if (level === '-') {
1616       level = image.zoom - config.imageZoomStep;
1617     } else if (typeof level !== 'number') {
1618       return false;
1619     }
1620 
1621     if (level > config.imageZoomMax) {
1622       level = config.imageZoomMax;
1623     } else if (level < config.imageZoomMin) {
1624       level = config.imageZoomMin;
1625     }
1626 
1627     if (level === image.zoom) {
1628       return true;
1629     }
1630 
1631     var cancel = this.events.dispatch(new appEvent.imageZoom(level));
1632     if (cancel) {
1633       return false;
1634     }
1635 
1636     var styleWidth  = image.width  / res.scale * level,
1637         styleHeight = image.height / res.scale * level,
1638         bufferStyle = this.buffer.canvas.style,
1639         layerStyle  = this.layer.canvas.style;
1640 
1641     image.canvasScale = styleWidth / image.width;
1642 
1643     // FIXME: MSIE 9 clears the Canvas element when you change the 
1644     // elem.style.width/height... *argh*
1645     bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
1646     bufferStyle.height = layerStyle.height = styleHeight + 'px';
1647 
1648     image.zoom = level;
1649 
1650     this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
1651           image.canvasScale));
1652 
1653     return true;
1654   };
1655 
1656   /**
1657    * Crop the image.
1658    *
1659    * <p>The content of the image is retained only if the browser implements the 
1660    * <code>getImageData</code> and <code>putImageData</code> methods.
1661    *
1662    * <p>This method dispatches three application events: {@link 
1663    * pwlib.appEvent.imageSizeChange}, {@link pwlib.appEvent.canvasSizeChange} 
1664    * and {@link pwlib.appEvent.imageCrop}. The <code>imageCrop</code> event is 
1665    * dispatched before the image is cropped. The <code>imageSizeChange</code> 
1666    * and <code>canvasSizeChange</code> events are dispatched after the image is 
1667    * cropped.
1668    *
1669    * @param {Number} cropX Image cropping start position on the x-axis.
1670    * @param {Number} cropY Image cropping start position on the y-axis.
1671    * @param {Number} cropWidth Image crop width.
1672    * @param {Number} cropHeight Image crop height.
1673    *
1674    * @returns {Boolean} True if the image was cropped successfully, or false if 
1675    * not.
1676    */
1677   this.imageCrop = function (cropX, cropY, cropWidth, cropHeight) {
1678     var bufferCanvas  = this.buffer.canvas,
1679         bufferContext = this.buffer.context,
1680         image         = this.image,
1681         layerCanvas   = this.layer.canvas,
1682         layerContext  = this.layer.context;
1683 
1684     cropX      = parseInt(cropX);
1685     cropY      = parseInt(cropY);
1686     cropWidth  = parseInt(cropWidth);
1687     cropHeight = parseInt(cropHeight);
1688 
1689     if (!cropWidth || !cropHeight || isNaN(cropX) || isNaN(cropY) || 
1690         isNaN(cropWidth) || isNaN(cropHeight) || cropX >= image.width || cropY 
1691         >= image.height) {
1692       return false;
1693     }
1694 
1695     var cancel = this.events.dispatch(new appEvent.imageCrop(cropX, cropY, 
1696           cropWidth, cropHeight));
1697     if (cancel) {
1698       return false;
1699     }
1700 
1701     if (cropWidth > this.config.imageWidthMax) {
1702       cropWidth = this.config.imageWidthMax;
1703     }
1704 
1705     if (cropHeight > this.config.imageHeightMax) {
1706       cropHeight = this.config.imageHeightMax;
1707     }
1708 
1709     if (cropX === 0 && cropY === 0 && image.width === cropWidth && image.height 
1710         === cropHeight) {
1711       return true;
1712     }
1713 
1714     var layerData    = null,
1715         bufferData   = null,
1716         layerState   = this.stateSave(layerContext),
1717         bufferState  = this.stateSave(bufferContext),
1718         scaledWidth  = cropWidth  * image.canvasScale,
1719         scaledHeight = cropHeight * image.canvasScale,
1720         dataWidth    = MathMin(image.width,  cropWidth),
1721         dataHeight   = MathMin(image.height, cropHeight),
1722         sumX         = cropX + dataWidth,
1723         sumY         = cropY + dataHeight;
1724 
1725     if (sumX > image.width) {
1726       dataWidth -= sumX - image.width;
1727     }
1728     if (sumY > image.height) {
1729       dataHeight -= sumY - image.height;
1730     }
1731 
1732     if (layerContext.getImageData) {
1733       // TODO: handle "out of memory" errors.
1734       try {
1735         layerData = layerContext.getImageData(cropX, cropY, dataWidth, 
1736             dataHeight);
1737       } catch (err) { }
1738     }
1739 
1740     if (bufferContext.getImageData) {
1741       try {
1742         bufferData = bufferContext.getImageData(cropX, cropY, dataWidth, 
1743             dataHeight);
1744       } catch (err) { }
1745     }
1746 
1747     bufferCanvas.style.width  = layerCanvas.style.width  = scaledWidth  + 'px';
1748     bufferCanvas.style.height = layerCanvas.style.height = scaledHeight + 'px';
1749 
1750     layerCanvas.width  = cropWidth;
1751     layerCanvas.height = cropHeight;
1752 
1753     if (layerData && layerContext.putImageData) {
1754       layerContext.putImageData(layerData, 0, 0);
1755     }
1756 
1757     this.stateRestore(layerContext, layerState);
1758     state = this.stateSave(bufferContext);
1759 
1760     bufferCanvas.width  = cropWidth;
1761     bufferCanvas.height = cropHeight;
1762 
1763     if (bufferData && bufferContext.putImageData) {
1764       bufferContext.putImageData(bufferData, 0, 0);
1765     }
1766 
1767     this.stateRestore(bufferContext, bufferState);
1768 
1769     image.width  = cropWidth;
1770     image.height = cropHeight;
1771 
1772     bufferState = layerState = layerData = bufferData = null;
1773 
1774     this.events.dispatch(new appEvent.imageSizeChange(cropWidth, cropHeight));
1775     this.events.dispatch(new appEvent.canvasSizeChange(scaledWidth, 
1776           scaledHeight, image.canvasScale));
1777 
1778     return true;
1779   };
1780 
1781   /**
1782    * Save the state of a Canvas context.
1783    *
1784    * @param {CanvasRenderingContext2D} context The 2D context of the Canvas 
1785    * element you want to save the state.
1786    *
1787    * @returns {Object} The object has all the state properties and values.
1788    */
1789   this.stateSave = function (context) {
1790     if (!context || !context.canvas || !this.stateProperties) {
1791       return false;
1792     }
1793 
1794     var stateObj = {},
1795         prop = null,
1796         n = this.stateProperties.length;
1797 
1798     for (var i = 0; i < n; i++) {
1799       prop = this.stateProperties[i];
1800       stateObj[prop] = context[prop];
1801     }
1802 
1803     return stateObj;
1804   };
1805 
1806   /**
1807    * Restore the state of a Canvas context.
1808    *
1809    * @param {CanvasRenderingContext2D} context The 2D context where you want to 
1810    * restore the state.
1811    *
1812    * @param {Object} stateObj The state object saved by the {@link 
1813    * PaintWeb#stateSave} method.
1814    *
1815    * @returns {Boolean} True if the operation was successful, or false if not.
1816    */
1817   this.stateRestore = function (context, stateObj) {
1818     if (!context || !context.canvas) {
1819       return false;
1820     }
1821 
1822     for (var state in stateObj) {
1823       context[state] = stateObj[state];
1824     }
1825 
1826     return true;
1827   };
1828 
1829   /**
1830    * Allow shadows. This method re-enabled shadow rendering, if it was enabled 
1831    * before shadows were disallowed.
1832    *
1833    * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
1834    */
1835   this.shadowAllow = function () {
1836     if (this.shadowAllowed || !this.shadowSupported) {
1837       return;
1838     }
1839 
1840     // Note that some daily builds of Webkit in Chromium fail to render the 
1841     // shadow when context.drawImage() is used (see the this.layerUpdate()).
1842     var context = this.layer.context,
1843         cfg = this.config.shadow;
1844 
1845     if (cfg.enable) {
1846       context.shadowColor   = cfg.shadowColor;
1847       context.shadowOffsetX = cfg.shadowOffsetX;
1848       context.shadowOffsetY = cfg.shadowOffsetY;
1849       context.shadowBlur    = cfg.shadowBlur;
1850     }
1851 
1852     this.shadowAllowed = true;
1853 
1854     this.events.dispatch(new appEvent.shadowAllow(true));
1855   };
1856 
1857   /**
1858    * Disallow shadows. This method disables shadow rendering, if it is enabled.
1859    *
1860    * <p>The {@link pwlib.appEvent.shadowAllow} event is dispatched.
1861    */
1862   this.shadowDisallow = function () {
1863     if (!this.shadowAllowed || !this.shadowSupported) {
1864       return;
1865     }
1866 
1867     if (this.config.shadow.enable) {
1868       var context = this.layer.context;
1869       context.shadowColor   = 'rgba(0,0,0,0)';
1870       context.shadowOffsetX = 0;
1871       context.shadowOffsetY = 0;
1872       context.shadowBlur    = 0;
1873     }
1874 
1875     this.shadowAllowed = false;
1876 
1877     this.events.dispatch(new appEvent.shadowAllow(false));
1878   };
1879 
1880   /**
1881    * Update the current image layer by moving the pixels from the buffer onto 
1882    * the layer. This method also adds a point into the history.
1883    *
1884    * @returns {Boolean} True if the operation was successful, or false if not.
1885    */
1886   this.layerUpdate = function () {
1887     this.layer.context.drawImage(this.buffer.canvas, 0, 0);
1888     this.buffer.context.clearRect(0, 0, this.image.width, this.image.height);
1889     this.historyAdd();
1890 
1891     return true;
1892   };
1893 
1894   /**
1895    * Add the current image layer to the history.
1896    *
1897    * <p>Once the history state has been updated, this method dispatches the 
1898    * {@link pwlib.appEvent.historyUpdate} event.
1899    *
1900    * @returns {Boolean} True if the operation was successful, or false if not.
1901    */
1902   // TODO: some day it would be nice to implement a hybrid history system.
1903   this.historyAdd = function () {
1904     var layerContext = this.layer.context,
1905         history      = this.history,
1906         prevPos      = history.pos;
1907 
1908     if (!layerContext.getImageData) {
1909       return false;
1910     }
1911 
1912     // We are in an undo-step, trim until the end, eliminating any possible redo-steps.
1913     if (prevPos < history.states.length) {
1914       history.states.splice(prevPos, history.states.length);
1915     }
1916 
1917     // TODO: in case of "out of memory" errors... I should show up some error.
1918     try {
1919       history.states.push(layerContext.getImageData(0, 0, this.image.width, 
1920             this.image.height));
1921     } catch (err) {
1922       return false;
1923     }
1924 
1925     // If we have too many history ImageDatas, remove the oldest ones
1926     if ('historyLimit' in this.config &&
1927         history.states.length > this.config.historyLimit) {
1928 
1929       history.states.splice(0, history.states.length 
1930           - this.config.historyLimit);
1931     }
1932     history.pos = history.states.length;
1933 
1934     this.image.modified = true;
1935 
1936     this.events.dispatch(new appEvent.historyUpdate(history.pos, prevPos, 
1937           history.pos));
1938 
1939     return true;
1940   };
1941 
1942   /**
1943    * Jump to any ImageData/position in the history.
1944    *
1945    * <p>Once the history state has been updated, this method dispatches the 
1946    * {@link pwlib.appEvent.historyUpdate} event.
1947    *
1948    * @param {Number|String} pos The history position to jump to.
1949    * 
1950    * <p>If the value is a number, then it must point to an existing index in the  
1951    * <var>{@link PaintWeb#history}.states</var> array.
1952    *
1953    * <p>If the value is a string, it must be "undo" or "redo".
1954    *
1955    * @returns {Boolean} True if the operation was successful, or false if not.
1956    */
1957   this.historyGoto = function (pos) {
1958     var layerContext = this.layer.context,
1959         image        = this.image,
1960         history      = this.history;
1961 
1962     if (!history.states.length || !layerContext.putImageData) {
1963       return false;
1964     }
1965 
1966     var cpos = history.pos;
1967 
1968     if (pos === 'undo') {
1969       pos = cpos-1;
1970     } else if (pos === 'redo') {
1971       pos = cpos+1;
1972     }
1973 
1974     if (pos < 1 || pos > history.states.length) {
1975       return false;
1976     }
1977 
1978     var himg = history.states[pos-1];
1979     if (!himg) {
1980       return false;
1981     }
1982 
1983     // Each image in the history can have a different size. As such, the script 
1984     // must take this into consideration.
1985     var w = MathMin(image.width,  himg.width),
1986         h = MathMin(image.height, himg.height);
1987 
1988     layerContext.clearRect(0, 0, image.width, image.height);
1989 
1990     try {
1991       // Firefox 3 does not clip the image, if needed.
1992       layerContext.putImageData(himg, 0, 0, 0, 0, w, h);
1993 
1994     } catch (err) {
1995       // The workaround is to use a new canvas from which we can copy the 
1996       // history image without causing any exceptions.
1997       var tmp    = doc.createElement('canvas');
1998       tmp.width  = himg.width;
1999       tmp.height = himg.height;
2000 
2001       var tmp2 = tmp.getContext('2d');
2002       tmp2.putImageData(himg, 0, 0);
2003 
2004       layerContext.drawImage(tmp, 0, 0);
2005 
2006       tmp2 = tmp = null;
2007       delete tmp2, tmp;
2008     }
2009 
2010     history.pos = pos;
2011 
2012     this.events.dispatch(new appEvent.historyUpdate(pos, cpos, 
2013           history.states.length));
2014 
2015     return true;
2016   };
2017 
2018   /**
2019    * Clear the image history.
2020    *
2021    * <p>This method dispatches the {@link pwlib.appEvent.historyUpdate} event.
2022    *
2023    * @private
2024    */
2025   this.historyReset = function () {
2026     this.history.pos = 0;
2027     this.history.states = [];
2028 
2029     this.events.dispatch(new appEvent.historyUpdate(0, 0, 0));
2030   };
2031 
2032   /**
2033    * Perform horizontal/vertical line snapping. This method updates the mouse 
2034    * coordinates to "snap" with the given coordinates.
2035    *
2036    * @param {Number} x The x-axis location.
2037    * @param {Number} y The y-axis location.
2038    */
2039   this.toolSnapXY = function (x, y) {
2040     var diffx = MathAbs(_self.mouse.x - x),
2041         diffy = MathAbs(_self.mouse.y - y);
2042 
2043     if (diffx > diffy) {
2044       _self.mouse.y = y;
2045     } else {
2046       _self.mouse.x = x;
2047     }
2048   };
2049 
2050   /**
2051    * Activate a drawing tool by ID.
2052    *
2053    * <p>The <var>id</var> provided must be of an existing drawing tool, one that  
2054    * has been installed.
2055    *
2056    * <p>The <var>ev</var> argument is an optional DOM Event object which is 
2057    * useful when dealing with different types of tool activation, either by 
2058    * keyboard or by mouse events. Tool-specific code can implement different 
2059    * functionality based on events.
2060    *
2061    * <p>This method dispatches the {@link pwlib.appEvent.toolPreactivate} event 
2062    * before creating the new tool instance. Once the new tool is successfully 
2063    * activated, the {@link pwlib.appEvent.toolActivate} event is also 
2064    * dispatched.
2065    *
2066    * @param {String} id The ID of the drawing tool to be activated.
2067    * @param {Event} [ev] The DOM Event object.
2068    *
2069    * @returns {Boolean} True if the tool has been activated, or false if not.
2070    *
2071    * @see PaintWeb#toolRegister Register a new drawing tool.
2072    * @see PaintWeb#toolUnregister Unregister a drawing tool.
2073    *
2074    * @see pwlib.tools The object holding all the drawing tools.
2075    * @see pwlib.appEvent.toolPreactivate
2076    * @see pwlib.appEvent.toolActivate
2077    */
2078   this.toolActivate = function (id, ev) {
2079     if (!id || !(id in pwlib.tools) || typeof pwlib.tools[id] !== 'function') {
2080       return false;
2081     }
2082 
2083     var tool = pwlib.tools[id],
2084         prevId = this.tool ? this.tool._id : null;
2085 
2086     if (prevId && this.tool instanceof pwlib.tools[id]) {
2087       return true;
2088     }
2089 
2090     var cancel = this.events.dispatch(new appEvent.toolPreactivate(id, prevId));
2091     if (cancel) {
2092       return false;
2093     }
2094 
2095     var tool_obj = new tool(this, ev);
2096     if (!tool_obj) {
2097       return false;
2098     }
2099 
2100     /*
2101      * Each tool can implement its own mouse and keyboard events handler.
2102      * Additionally, tool objects can implement handlers for the deactivation 
2103      * and activation events.
2104      * Given tool1 is active and tool2 is going to be activated, then the 
2105      * following event handlers will be called:
2106      *
2107      * tool2.preActivate
2108      * tool1.deactivate
2109      * tool2.activate
2110      *
2111      * In the "preActivate" event handler you can cancel the tool activation by 
2112      * returning a value which evaluates to false.
2113      */
2114 
2115     if ('preActivate' in tool_obj && !tool_obj.preActivate(ev)) {
2116       tool_obj = null;
2117       return false;
2118     }
2119 
2120     // Deactivate the previously active tool
2121     if (this.tool && 'deactivate' in this.tool) {
2122       this.tool.deactivate(ev);
2123     }
2124 
2125     this.tool = tool_obj;
2126 
2127     this.mouse.buttonDown = false;
2128 
2129     // Besides the "constructor", each tool can also have code which is run 
2130     // after the deactivation of the previous tool.
2131     if ('activate' in this.tool) {
2132       this.tool.activate(ev);
2133     }
2134 
2135     this.events.dispatch(new appEvent.toolActivate(id, prevId));
2136 
2137     return true;
2138   };
2139 
2140   /**
2141    * Register a new drawing tool into PaintWeb.
2142    *
2143    * <p>This method dispatches the {@link pwlib.appEvent.toolRegister} 
2144    * application event.
2145    *
2146    * @param {String} id The ID of the new tool. The tool object must exist in 
2147    * {@link pwlib.tools}.
2148    *
2149    * @returns {Boolean} True if the tool was successfully registered, or false 
2150    * if not.
2151    *
2152    * @see PaintWeb#toolUnregister allows you to unregister tools.
2153    * @see pwlib.tools Holds all the drawing tools.
2154    * @see pwlib.appEvent.toolRegister
2155    */
2156   this.toolRegister = function (id) {
2157     if (typeof id !== 'string' || !id) {
2158       return false;
2159     }
2160 
2161     // TODO: it would be very nice to create the tool instance on register, for 
2162     // further extensibility.
2163 
2164     var tool = pwlib.tools[id];
2165     if (typeof tool !== 'function') {
2166       return false;
2167     }
2168 
2169     tool.prototype._id = id;
2170 
2171     this.events.dispatch(new appEvent.toolRegister(id));
2172 
2173     if (!this.tool && id === this.config.toolDefault) {
2174       return this.toolActivate(id);
2175     } else {
2176       return true;
2177     }
2178   };
2179 
2180   /**
2181    * Unregister a drawing tool from PaintWeb.
2182    *
2183    * <p>This method dispatches the {@link pwlib.appEvent.toolUnregister} 
2184    * application event.
2185    *
2186    * @param {String} id The ID of the tool you want to unregister.
2187    *
2188    * @returns {Boolean} True if the tool was unregistered, or false if it does 
2189    * not exist or some error occurred.
2190    *
2191    * @see PaintWeb#toolRegister allows you to register new drawing tools.
2192    * @see pwlib.tools Holds all the drawing tools.
2193    * @see pwlib.appEvent.toolUnregister
2194    */
2195   this.toolUnregister = function (id) {
2196     if (typeof id !== 'string' || !id || !(id in pwlib.tools)) {
2197       return false;
2198     }
2199 
2200     this.events.dispatch(new appEvent.toolUnregister(id));
2201 
2202     return true;
2203   };
2204 
2205   /**
2206    * Register a new extension into PaintWeb.
2207    *
2208    * <p>If the extension object being constructed has the 
2209    * <code>extensionRegister()</code> method, then it will be invoked, allowing 
2210    * any custom extension registration code to run. If the method returns false, 
2211    * then the extension will not be registered.
2212    *
2213    * <p>Once the extension is successfully registered, this method dispatches 
2214    * the {@link pwlib.appEvent.extensionRegister} application event.
2215    *
2216    * @param {String} id The ID of the new extension. The extension object 
2217    * constructor must exist in {@link pwlib.extensions}.
2218    *
2219    * @returns {Boolean} True if the extension was successfully registered, or 
2220    * false if not.
2221    *
2222    * @see PaintWeb#extensionUnregister allows you to unregister extensions.
2223    * @see PaintWeb#extensions Holds all the instances of registered extensions.
2224    * @see pwlib.extensions Holds all the extension classes.
2225    */
2226   this.extensionRegister = function (id) {
2227     if (typeof id !== 'string' || !id) {
2228       return false;
2229     }
2230 
2231     var func = pwlib.extensions[id];
2232     if (typeof func !== 'function') {
2233       return false;
2234     }
2235 
2236     func.prototype._id = id;
2237 
2238     var obj = new func(_self);
2239 
2240     if ('extensionRegister' in obj && !obj.extensionRegister()) {
2241       return false;
2242     }
2243 
2244     this.extensions[id] = obj;
2245     this.events.dispatch(new appEvent.extensionRegister(id));
2246 
2247     return true;
2248   };
2249 
2250   /**
2251    * Unregister an extension from PaintWeb.
2252    *
2253    * <p>If the extension object being destructed has the 
2254    * <code>extensionUnregister()</code> method, then it will be invoked, 
2255    * allowing any custom extension removal code to run.
2256    *
2257    * <p>Before the extension is unregistered, this method dispatches the {@link 
2258    * pwlib.appEvent.extensionUnregister} application event.
2259    *
2260    * @param {String} id The ID of the extension object you want to unregister.
2261    *
2262    * @returns {Boolean} True if the extension was removed, or false if it does 
2263    * not exist or some error occurred.
2264    *
2265    * @see PaintWeb#extensionRegister allows you to register new extensions.
2266    * @see PaintWeb#extensions Holds all the instances of registered extensions.
2267    * @see pwlib.extensions Holds all the extension classes.
2268    */
2269   this.extensionUnregister = function (id) {
2270     if (typeof id !== 'string' || !id || !(id in this.extensions)) {
2271       return false;
2272     }
2273 
2274     this.events.dispatch(new appEvent.extensionUnregister(id));
2275 
2276     if ('extensionUnregister' in this.extensions[id]) {
2277       this.extensions[id].extensionUnregister();
2278     }
2279     delete this.extensions[id];
2280 
2281     return true;
2282   };
2283 
2284   /**
2285    * Register a new command in PaintWeb. Commands are simple function objects 
2286    * which can be invoked by keyboard shortcuts or by GUI elements.
2287    *
2288    * <p>Once the command is successfully registered, this method dispatches the 
2289    * {@link pwlib.appEvent.commandRegister} application event.
2290    *
2291    * @param {String} id The ID of the new command.
2292    * @param {Function} func The command function.
2293    *
2294    * @returns {Boolean} True if the command was successfully registered, or 
2295    * false if not.
2296    *
2297    * @see PaintWeb#commandUnregister allows you to unregister commands.
2298    * @see PaintWeb#commands Holds all the registered commands.
2299    */
2300   this.commandRegister = function (id, func) {
2301     if (typeof id !== 'string' || !id || typeof func !== 'function' || id in 
2302         this.commands) {
2303       return false;
2304     }
2305 
2306     this.commands[id] = func;
2307     this.events.dispatch(new appEvent.commandRegister(id));
2308 
2309     return true;
2310   };
2311 
2312   /**
2313    * Unregister a command from PaintWeb.
2314    *
2315    * <p>Before the command is unregistered, this method dispatches the {@link 
2316    * pwlib.appEvent.commandUnregister} application event.
2317    *
2318    * @param {String} id The ID of the command you want to unregister.
2319    *
2320    * @returns {Boolean} True if the command was removed successfully, or false 
2321    * if not.
2322    *
2323    * @see PaintWeb#commandRegister allows you to register new commands.
2324    * @see PaintWeb#commands Holds all the registered commands.
2325    */
2326   this.commandUnregister = function (id) {
2327     if (typeof id !== 'string' || !id || !(id in this.commands)) {
2328       return false;
2329     }
2330 
2331     this.events.dispatch(new appEvent.commandUnregister(id));
2332 
2333     delete this.commands[id];
2334 
2335     return true;
2336   };
2337 
2338   /**
2339    * Load a script into the document.
2340    *
2341    * @param {String} url The script URL you want to insert.
2342    * @param {Function} [handler] The <code>load</code> event handler you want.
2343    */
2344   this.scriptLoad = function (url, handler) {
2345     if (!handler) {
2346       var elem = doc.createElement('script');
2347       elem.type = 'text/javascript';
2348       elem.src = url;
2349       this.elems.head.appendChild(elem);
2350       return;
2351     }
2352 
2353     // huh, use XHR then eval() the code.
2354     // browsers do not dispatch the 'load' event reliably for script elements.
2355 
2356     /** @ignore */
2357     var xhr = new XMLHttpRequest();
2358 
2359     /** @ignore */
2360     xhr.onreadystatechange = function () {
2361       if (!xhr || xhr.readyState !== 4) {
2362         return;
2363 
2364       } else if ((xhr.status !== 304 && xhr.status !== 200) || 
2365           !xhr.responseText) {
2366         handler(false, xhr);
2367 
2368       } else {
2369         try {
2370           eval.call(win, xhr.responseText);
2371         } catch (err) {
2372           eval(xhr.responseText, win);
2373         }
2374         handler(true, xhr);
2375       }
2376 
2377       xhr = null;
2378     };
2379 
2380     xhr.open('GET', url);
2381     xhr.send('');
2382   };
2383 
2384   /**
2385    * Insert a stylesheet into the document.
2386    *
2387    * @param {String} id The stylesheet ID. This is used to avoid inserting the 
2388    * same style in the document.
2389    * @param {String} url The URL of the stylesheet you want to insert.
2390    * @param {String} [media='screen, projection'] The media attribute.
2391    * @param {Function} [handler] The <code>load</code> event handler.
2392    */
2393   this.styleLoad = function (id, url, media, handler) {
2394     id = 'paintweb_style_' + id;
2395 
2396     var elem = doc.getElementById(id);
2397     if (elem) {
2398       return;
2399     }
2400 
2401     if (!media) {
2402       media = 'screen, projection';
2403     }
2404 
2405     elem = doc.createElement('link');
2406 
2407     if (handler) {
2408       elem.addEventListener('load', handler, false);
2409     }
2410 
2411     elem.id = id;
2412     elem.rel = 'stylesheet';
2413     elem.type = 'text/css';
2414     elem.media = media;
2415     elem.href = url;
2416 
2417     this.elems.head.appendChild(elem);
2418   };
2419 
2420   /**
2421    * Perform action undo.
2422    *
2423    * @returns {Boolean} True if the operation was successful, or false if not.
2424    *
2425    * @see PaintWeb#historyGoto The method invoked by this command.
2426    */
2427   this.historyUndo = function () {
2428     return _self.historyGoto('undo');
2429   };
2430 
2431   /**
2432    * Perform action redo.
2433    *
2434    * @returns {Boolean} True if the operation was successful, or false if not.
2435    *
2436    * @see PaintWeb#historyGoto The method invoked by this command.
2437    */
2438   this.historyRedo = function () {
2439     return _self.historyGoto('redo');
2440   };
2441 
2442   /**
2443    * Load an image. By loading an image the history is cleared and the Canvas 
2444    * dimensions are updated to fit the new image.
2445    *
2446    * <p>This method dispatches two application events: {@link 
2447    * pwlib.appEvent.imageSizeChange} and {@link 
2448    * pwlib.appEvent.canvasSizeChange}.
2449    *
2450    * @param {Element} importImage The image element you want to load into the 
2451    * Canvas.
2452    *
2453    * @returns {Boolean} True if the operation was successful, or false if not.
2454    */
2455   this.imageLoad = function (importImage) {
2456     if (!importImage || !importImage.width || !importImage.height || 
2457         importImage.nodeType !== this.ELEMENT_NODE || 
2458         !pwlib.isSameHost(importImage.src, win.location.host)) {
2459       return false;
2460     }
2461 
2462     this.historyReset();
2463 
2464     var layerContext = this.layer.context,
2465         layerCanvas  = this.layer.canvas,
2466         layerStyle   = layerCanvas.style,
2467         bufferCanvas = this.buffer.canvas,
2468         bufferStyle  = bufferCanvas.style,
2469         image        = this.image,
2470         styleWidth   = importImage.width  * image.canvasScale,
2471         styleHeight  = importImage.height * image.canvasScale,
2472         result       = true;
2473 
2474     bufferCanvas.width  = layerCanvas.width  = importImage.width;
2475     bufferCanvas.height = layerCanvas.height = importImage.height;
2476 
2477     try {
2478       layerContext.drawImage(importImage, 0, 0);
2479     } catch (err) {
2480       result = false;
2481       bufferCanvas.width  = layerCanvas.width  = image.width;
2482       bufferCanvas.height = layerCanvas.height = image.height;
2483     }
2484 
2485     if (result) {
2486       image.width  = importImage.width;
2487       image.height = importImage.height;
2488       // FIXME: MSIE 9 clears the Canvas element when you change the 
2489       // elem.style.width/height... *argh*
2490       bufferStyle.width  = layerStyle.width  = styleWidth  + 'px';
2491       bufferStyle.height = layerStyle.height = styleHeight + 'px';
2492       _self.config.imageLoad = importImage;
2493 
2494       this.events.dispatch(new appEvent.imageSizeChange(image.width, 
2495             image.height));
2496 
2497       this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, 
2498             image.canvasScale));
2499     }
2500 
2501     this.historyAdd();
2502     image.modified = false;
2503 
2504     return result;
2505   };
2506 
2507   /**
2508    * Clear the image.
2509    */
2510   this.imageClear = function (ev) {
2511     var layerContext = _self.layer.context,
2512         image = _self.image;
2513 
2514     layerContext.clearRect(0, 0, image.width, image.height);
2515 
2516     // Set the configured background color.
2517     var fillStyle = layerContext.fillStyle;
2518     layerContext.fillStyle = _self.config.backgroundColor;
2519     layerContext.fillRect(0, 0, image.width, image.height);
2520     layerContext.fillStyle = fillStyle;
2521 
2522     _self.historyAdd();
2523   };
2524 
2525   /**
2526    * Save the image.
2527    *
2528    * <p>This method dispatches the {@link pwlib.appEvent.imageSave} event.
2529    *
2530    * <p><strong>Note:</strong> the "Save image" operation relies on integration 
2531    * extensions. A vanilla configuration of PaintWeb will simply open the the 
2532    * image in a new tab using a data: URL. You must have some event listener for 
2533    * the <code>imageSave</code> event and you must prevent the default action.
2534    *
2535    * <p>If the default action for the <code>imageSave</code> application event 
2536    * is not prevented, then this method will also dispatch the {@link 
2537    * pwlib.appEvent.imageSaveResult} application event.
2538    *
2539    * <p>Your event handler for the <code>imageSave</code> event must dispatch 
2540    * the <code>imageSaveResult</code> event.
2541    *
2542    * @param {String} [type="auto"] Image MIME type. This tells the browser which 
2543    * format to use when saving the image. If the image format type is not 
2544    * supported, then the image is saved as PNG.
2545    *
2546    * <p>You can use the resulting data URL to check which is the actual image 
2547    * format.
2548    *
2549    * <p>When <var>type</var> is "auto" then PaintWeb checks the type of the 
2550    * image currently loaded ({@link PaintWeb.config.imageLoad}). If the format 
2551    * is recognized, then the same format is used to save the image.
2552    *
2553    * @returns {Boolean} True if the operation was successful, or false if not.
2554    */
2555   this.imageSave = function (type) {
2556     var canvas = _self.layer.canvas,
2557         cfg = _self.config,
2558         img = _self.image,
2559         imageLoad = _self.config.imageLoad,
2560         ext = 'png', idata = null, src = null, pos;
2561 
2562     if (!canvas.toDataURL) {
2563       return false;
2564     }
2565 
2566     var extMap = {'jpg' : 'image/jpeg', 'jpeg' : 'image/jpeg', 'png' 
2567       : 'image/png', 'gif' : 'image/gif'};
2568 
2569     // Detect the MIME type of the image currently loaded.
2570     if (typeof type !== 'string' || !type) {
2571       if (imageLoad && imageLoad.src && imageLoad.src.substr(0, 5) !== 'data:') {
2572         src = imageLoad.src;
2573         pos = src.indexOf('?');
2574         if (pos !== -1) {
2575           src = src.substr(0, pos);
2576         }
2577         ext = src.substr(src.lastIndexOf('.') + 1).toLowerCase();
2578       }
2579 
2580       type = extMap[ext] || 'image/png';
2581     }
2582 
2583     // We consider that other formats than PNG do not support transparencies.  
2584     // Thus, we create a new Canvas element for which we set the configured 
2585     // background color, and we render the image onto it.
2586     if (type !== 'image/png') {
2587       canvas = doc.createElement('canvas');
2588       var context = canvas.getContext('2d');
2589 
2590       canvas.width  = img.width;
2591       canvas.height = img.height;
2592 
2593       context.fillStyle = cfg.backgroundColor;
2594       context.fillRect(0, 0, img.width, img.height);
2595       context.drawImage(_self.layer.canvas, 0, 0);
2596 
2597       context = null;
2598     }
2599 
2600     try {
2601       // canvas.toDataURL('image/jpeg', quality) fails in Gecko due to security 
2602       // concerns, uh-oh.
2603       if (type === 'image/jpeg' && !pwlib.browser.gecko) {
2604         idata = canvas.toDataURL(type, cfg.jpegSaveQuality);
2605       } else {
2606         idata = canvas.toDataURL(type);
2607       }
2608     } catch (err) {
2609       alert(lang.errorImageSave + "\n" + err);
2610       return false;
2611     }
2612 
2613     canvas = null;
2614 
2615     if (!idata || idata === 'data:,') {
2616       return false;
2617     }
2618 
2619     var ev = new appEvent.imageSave(idata, img.width, img.height),
2620         cancel = _self.events.dispatch(ev);
2621 
2622     if (cancel) {
2623       return true;
2624     }
2625 
2626     var imgwin = _self.win.open();
2627     if (!imgwin) {
2628       return false;
2629     }
2630 
2631     imgwin.location = idata;
2632     idata = null;
2633 
2634     _self.events.dispatch(new appEvent.imageSaveResult(true));
2635 
2636     return true;
2637   };
2638 
2639   /**
2640    * The <code>imageSaveResult</code> application event handler. This method 
2641    * PaintWeb-related stuff: for example, the {@link PaintWeb.image.modified} 
2642    * flag is turned to false.
2643    *
2644    * @private
2645    *
2646    * @param {pwlib.appEvent.imageSaveResult} ev The application event object.
2647    *
2648    * @see {PaintWeb#imageSave} The method which allows you to save the image.
2649    */
2650   this.imageSaveResultHandler = function (ev) {
2651     if (ev.successful) {
2652       _self.image.modified = false;
2653     }
2654   };
2655 
2656   /**
2657    * Swap the fill and stroke styles. This is just like in Photoshop, if the 
2658    * user presses X, the fill/stroke colors are swapped.
2659    *
2660    * <p>This method dispatches the {@link pwlib.appEvent.configChange} event 
2661    * twice for each color (strokeStyle and fillStyle).
2662    */
2663   this.swapFillStroke = function () {
2664     var fillStyle     = _self.config.fillStyle,
2665         strokeStyle   = _self.config.strokeStyle;
2666 
2667     _self.config.fillStyle   = strokeStyle;
2668     _self.config.strokeStyle = fillStyle;
2669 
2670     var ev = new appEvent.configChange(strokeStyle, fillStyle, 'fillStyle', '', 
2671         _self.config);
2672 
2673     _self.events.dispatch(ev);
2674 
2675     ev = new appEvent.configChange(fillStyle, strokeStyle, 'strokeStyle', '', 
2676         _self.config);
2677 
2678     _self.events.dispatch(ev);
2679   };
2680 
2681   /**
2682    * Select all the pixels. This activates the selection tool, and selects the 
2683    * entire image.
2684    *
2685    * @param {Event} [ev] The DOM Event object which generated the request.
2686    * @returns {Boolean} True if the operation was successful, or false if not.
2687    *
2688    * @see {pwlib.tools.selection.selectAll} The command implementation.
2689    */
2690   this.selectAll = function (ev) {
2691     if (_self.toolActivate('selection', ev)) {
2692       return _self.tool.selectAll(ev);
2693     } else {
2694       return false;
2695     }
2696   };
2697 
2698   /**
2699    * Cut the available selection. This only works when the selection tool is 
2700    * active and when some selection is available.
2701    *
2702    * @param {Event} [ev] The DOM Event object which generated the request.
2703    * @returns {Boolean} True if the operation was successful, or false if not.
2704    *
2705    * @see {pwlib.tools.selection.selectionCut} The command implementation.
2706    */
2707   this.selectionCut = function (ev) {
2708     if (!_self.tool || _self.tool._id !== 'selection') {
2709       return false;
2710     } else {
2711       return _self.tool.selectionCut(ev);
2712     }
2713   };
2714 
2715   /**
2716    * Copy the available selection. This only works when the selection tool is 
2717    * active and when some selection is available.
2718    *
2719    * @param {Event} [ev] The DOM Event object which generated the request.
2720    * @returns {Boolean} True if the operation was successful, or false if not.
2721    *
2722    * @see {pwlib.tools.selection.selectionCopy} The command implementation.
2723    */
2724   this.selectionCopy = function (ev) {
2725     if (!_self.tool || _self.tool._id !== 'selection') {
2726       return false;
2727     } else {
2728       return _self.tool.selectionCopy(ev);
2729     }
2730   };
2731 
2732   /**
2733    * Paste the current clipboard image. This only works when some ImageData is 
2734    * available in {@link PaintWeb#clipboard}.
2735    *
2736    * @param {Event} [ev] The DOM Event object which generated the request.
2737    * @returns {Boolean} True if the operation was successful, or false if not.
2738    *
2739    * @see {pwlib.tools.selection.clipboardPaste} The command implementation.
2740    */
2741   this.clipboardPaste = function (ev) {
2742     if (!_self.clipboard || !_self.toolActivate('selection', ev)) {
2743       return false;
2744     } else {
2745       return _self.tool.clipboardPaste(ev);
2746     }
2747   };
2748 
2749   /**
2750    * The <code>configChange</code> application event handler. This method 
2751    * updates the Canvas context properties depending on which configuration 
2752    * property changed.
2753    *
2754    * @private
2755    * @param {pwlib.appEvent.configChange} ev The application event object.
2756    */
2757   this.configChangeHandler = function (ev) {
2758     if (ev.group === 'shadow' && _self.shadowSupported && _self.shadowAllowed) {
2759       var context = _self.layer.context,
2760           cfg = ev.groupRef;
2761 
2762       // Enable/disable shadows
2763       if (ev.config === 'enable') {
2764         if (ev.value) {
2765           context.shadowColor   = cfg.shadowColor;
2766           context.shadowOffsetX = cfg.shadowOffsetX;
2767           context.shadowOffsetY = cfg.shadowOffsetY;
2768           context.shadowBlur    = cfg.shadowBlur;
2769         } else {
2770           context.shadowColor   = 'rgba(0,0,0,0)';
2771           context.shadowOffsetX = 0;
2772           context.shadowOffsetY = 0;
2773           context.shadowBlur    = 0;
2774         }
2775         return;
2776       }
2777 
2778       // Do not update any context properties if shadows are not enabled.
2779       if (!cfg.enable) {
2780         return;
2781       }
2782 
2783       switch (ev.config) {
2784         case 'shadowBlur':
2785         case 'shadowOffsetX':
2786         case 'shadowOffsetY':
2787           ev.value = parseInt(ev.value);
2788         case 'shadowColor':
2789           context[ev.config] = ev.value;
2790       }
2791 
2792     } else if (ev.group === 'line') {
2793       switch (ev.config) {
2794         case 'lineWidth':
2795         case 'miterLimit':
2796           ev.value = parseInt(ev.value);
2797         case 'lineJoin':
2798         case 'lineCap':
2799           _self.buffer.context[ev.config] = ev.value;
2800       }
2801 
2802     } else if (ev.group === 'text') {
2803       switch (ev.config) {
2804         case 'textAlign':
2805         case 'textBaseline':
2806           _self.buffer.context[ev.config] = ev.value;
2807       }
2808 
2809     } else if (!ev.group) {
2810       switch (ev.config) {
2811         case 'fillStyle':
2812         case 'strokeStyle':
2813           _self.buffer.context[ev.config] = ev.value;
2814       }
2815     }
2816   };
2817 
2818   /**
2819    * Destroy a PaintWeb instance. This method allows you to unload a PaintWeb 
2820    * instance. Extensions, tools and commands are unregistered, and the GUI 
2821    * elements are removed.
2822    *
2823    * <p>The scripts and styles loaded are not removed, since they might be used 
2824    * by other PaintWeb instances.
2825    *
2826    * <p>The {@link pwlib.appEvent.appDestroy} application event is dispatched 
2827    * before the current instance is destroyed.
2828    */
2829   this.destroy = function () {
2830     this.events.dispatch(new appEvent.appDestroy());
2831 
2832     for (var cmd in this.commands) {
2833       this.commandUnregister(cmd);
2834     }
2835 
2836     for (var ext in this.extensions) {
2837       this.extensionUnregister(ext);
2838     }
2839 
2840     for (var tool in this.gui.tools) {
2841       this.toolUnregister(tool);
2842     }
2843 
2844     this.gui.destroy();
2845 
2846     this.initialized = PaintWeb.INIT_NOT_STARTED;
2847   };
2848 
2849   this.toString = function () {
2850     return 'PaintWeb v' + this.version + ' (build ' + this.build + ')';
2851   };
2852 
2853 
2854   preInit();
2855 };
2856 
2857 /**
2858  * Application initialization not started.
2859  * @constant
2860  */
2861 PaintWeb.INIT_NOT_STARTED = 0;
2862 
2863 /**
2864  * Application initialization started.
2865  * @constant
2866  */
2867 PaintWeb.INIT_STARTED = 1;
2868 
2869 /**
2870  * Application initialization completed successfully.
2871  * @constant
2872  */
2873 PaintWeb.INIT_DONE = 2;
2874 
2875 /**
2876  * Application initialization failed.
2877  * @constant
2878  */
2879 PaintWeb.INIT_ERROR = -1;
2880 
2881 /**
2882  * PaintWeb base folder. This is determined automatically when the PaintWeb 
2883  * script is added in a page.
2884  * @type String
2885  */
2886 PaintWeb.baseFolder = '';
2887 
2888 (function () {
2889   var scripts = document.getElementsByTagName('script'),
2890       n = scripts.length,
2891       pos, src;
2892 
2893   // Determine the baseFolder.
2894 
2895   for (var i = 0; i < n; i++) {
2896     src = scripts[i].src;
2897     if (!src || !/paintweb(\.dev|\.src)?\.js/.test(src)) {
2898       continue;
2899     }
2900 
2901     pos = src.lastIndexOf('/');
2902     if (pos !== -1) {
2903       PaintWeb.baseFolder = src.substr(0, pos + 1);
2904     }
2905 
2906     break;
2907   }
2908 })();
2909 
2910 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
2911 
2912