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