1 /*
  2  * Copyright (C) 2013 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
 27  * automatically handles incoming and outgoing Guacamole instructions via the
 28  * provided tunnel, updating the display using one or more canvas elements.
 29  * 
 30  * @constructor
 31  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
 32  *                                  Guacamole instructions.
 33  */
 34 Guacamole.Client = function(tunnel) {
 35 
 36     var guac_client = this;
 37 
 38     var STATE_IDLE          = 0;
 39     var STATE_CONNECTING    = 1;
 40     var STATE_WAITING       = 2;
 41     var STATE_CONNECTED     = 3;
 42     var STATE_DISCONNECTING = 4;
 43     var STATE_DISCONNECTED  = 5;
 44 
 45     var currentState = STATE_IDLE;
 46     
 47     var currentTimestamp = 0;
 48     var pingInterval = null;
 49 
 50     var displayWidth = 0;
 51     var displayHeight = 0;
 52     var displayScale = 1;
 53 
 54     /**
 55      * Translation from Guacamole protocol line caps to Layer line caps.
 56      * @private
 57      */
 58     var lineCap = {
 59         0: "butt",
 60         1: "round",
 61         2: "square"
 62     };
 63 
 64     /**
 65      * Translation from Guacamole protocol line caps to Layer line caps.
 66      * @private
 67      */
 68     var lineJoin = {
 69         0: "bevel",
 70         1: "miter",
 71         2: "round"
 72     };
 73 
 74     // Create bounding div 
 75     var bounds = document.createElement("div");
 76     bounds.style.position = "relative";
 77     bounds.style.width = (displayWidth*displayScale) + "px";
 78     bounds.style.height = (displayHeight*displayScale) + "px";
 79 
 80     // Create display
 81     var display = document.createElement("div");
 82     display.style.position = "relative";
 83     display.style.width = displayWidth + "px";
 84     display.style.height = displayHeight + "px";
 85 
 86     // Ensure transformations on display originate at 0,0
 87     display.style.transformOrigin =
 88     display.style.webkitTransformOrigin =
 89     display.style.MozTransformOrigin =
 90     display.style.OTransformOrigin =
 91     display.style.msTransformOrigin =
 92         "0 0";
 93 
 94     // Create default layer
 95     var default_layer_container = new Guacamole.Client.LayerContainer(0, displayWidth, displayHeight);
 96 
 97     // Position default layer
 98     var default_layer_container_element = default_layer_container.getElement();
 99     default_layer_container_element.style.position = "absolute";
100     default_layer_container_element.style.left = "0px";
101     default_layer_container_element.style.top  = "0px";
102     default_layer_container_element.style.overflow = "hidden";
103     default_layer_container_element.style.zIndex = "0";
104 
105     // Create cursor layer
106     var cursor = new Guacamole.Client.LayerContainer(null, 0, 0);
107     cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
108     cursor.getLayer().autoflush = true;
109 
110     // Position cursor layer
111     var cursor_element = cursor.getElement();
112     cursor_element.style.position = "absolute";
113     cursor_element.style.left = "0px";
114     cursor_element.style.top  = "0px";
115     cursor_element.style.zIndex = "1";
116 
117     // Add default layer and cursor to display
118     display.appendChild(default_layer_container.getElement());
119     display.appendChild(cursor.getElement());
120 
121     // Add display to bounds
122     bounds.appendChild(display);
123 
124     // Initially, only default layer exists
125     var layers =  [default_layer_container];
126 
127     // No initial buffers
128     var buffers = [];
129 
130     // No initial parsers
131     var parsers = [];
132 
133     // No initial audio channels 
134     var audio_channels = [];
135 
136     // No initial streams 
137     var streams = [];
138 
139     // Pool of available stream indices
140     var stream_indices = new Guacamole.IntegerPool();
141 
142     // Array of allocated output streams by index
143     var output_streams = [];
144 
145     function setState(state) {
146         if (state != currentState) {
147             currentState = state;
148             if (guac_client.onstatechange)
149                 guac_client.onstatechange(currentState);
150         }
151     }
152 
153     function isConnected() {
154         return currentState == STATE_CONNECTED
155             || currentState == STATE_WAITING;
156     }
157 
158     var cursorHotspotX = 0;
159     var cursorHotspotY = 0;
160 
161     var cursorX = 0;
162     var cursorY = 0;
163 
164     function moveCursor(x, y) {
165 
166         // Move cursor layer
167         cursor.translate(x - cursorHotspotX, y - cursorHotspotY);
168 
169         // Update stored position
170         cursorX = x;
171         cursorY = y;
172 
173     }
174 
175     /**
176      * Returns an element containing the display of this Guacamole.Client.
177      * Adding the element returned by this function to an element in the body
178      * of a document will cause the client's display to be visible.
179      * 
180      * @return {Element} An element containing ths display of this
181      *                   Guacamole.Client.
182      */
183     this.getDisplay = function() {
184         return bounds;
185     };
186 
187     /**
188      * Sends the current size of the screen.
189      * 
190      * @param {Number} width The width of the screen.
191      * @param {Number} height The height of the screen.
192      */
193     this.sendSize = function(width, height) {
194 
195         // Do not send requests if not connected
196         if (!isConnected())
197             return;
198 
199         tunnel.sendMessage("size", width, height);
200 
201     };
202 
203     /**
204      * Sends a key event having the given properties as if the user
205      * pressed or released a key.
206      * 
207      * @param {Boolean} pressed Whether the key is pressed (true) or released
208      *                          (false).
209      * @param {Number} keysym The keysym of the key being pressed or released.
210      */
211     this.sendKeyEvent = function(pressed, keysym) {
212         // Do not send requests if not connected
213         if (!isConnected())
214             return;
215 
216         tunnel.sendMessage("key", keysym, pressed);
217     };
218 
219     /**
220      * Sends a mouse event having the properties provided by the given mouse
221      * state.
222      * 
223      * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
224      *                                           in the mouse event.
225      */
226     this.sendMouseState = function(mouseState) {
227 
228         // Do not send requests if not connected
229         if (!isConnected())
230             return;
231 
232         // Update client-side cursor
233         moveCursor(
234             Math.floor(mouseState.x),
235             Math.floor(mouseState.y)
236         );
237 
238         // Build mask
239         var buttonMask = 0;
240         if (mouseState.left)   buttonMask |= 1;
241         if (mouseState.middle) buttonMask |= 2;
242         if (mouseState.right)  buttonMask |= 4;
243         if (mouseState.up)     buttonMask |= 8;
244         if (mouseState.down)   buttonMask |= 16;
245 
246         // Send message
247         tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask);
248     };
249 
250     /**
251      * Sets the clipboard of the remote client to the given text data.
252      * 
253      * @param {String} data The data to send as the clipboard contents.
254      */
255     this.setClipboard = function(data) {
256 
257         // Do not send requests if not connected
258         if (!isConnected())
259             return;
260 
261         tunnel.sendMessage("clipboard", data);
262     };
263 
264     /**
265      * Opens a new file for writing, having the given index, mimetype and
266      * filename. The stream to associate with this file must already exist.
267      * 
268      * @param {String} mimetype The mimetype of the file being sent.
269      * @param {String} filename The filename of the file being sent.
270      * @return {Guacamole.OutputStream} The created file stream.
271      */
272     this.createFileStream = function(mimetype, filename) {
273 
274         // Allocate index
275         var index = stream_indices.next();
276 
277         // Create new stream
278         tunnel.sendMessage("file", index, mimetype, filename);
279         var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
280 
281         // Override sendEnd() of stream to automatically free index
282         var old_end = stream.sendEnd;
283         stream.sendEnd = function() {
284             old_end();
285             stream_indices.free(index);
286             delete output_streams[index];
287         };
288 
289         // Return new, overridden stream
290         return stream;
291 
292     };
293 
294     /**
295      * Opens a new pipe for writing, having the given name and mimetype. The
296      * stream to associate with this pipe must already exist.
297      * 
298      * @param {String} mimetype The mimetype of the data being sent.
299      * @param {String} name The name of the pipe.
300      * @return {Guacamole.OutputStream} The created file stream.
301      */
302     this.createPipeStream = function(mimetype, name) {
303 
304         // Allocate index
305         var index = stream_indices.next();
306 
307         // Create new stream
308         tunnel.sendMessage("pipe", index, mimetype, name);
309         var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
310 
311         // Override sendEnd() of stream to automatically free index
312         var old_end = stream.sendEnd;
313         stream.sendEnd = function() {
314             old_end();
315             stream_indices.free(index);
316             delete output_streams[index];
317         };
318 
319         // Return new, overridden stream
320         return stream;
321 
322     };
323 
324     /**
325      * Acknowledge receipt of a blob on the stream with the given index.
326      * 
327      * @param {Number} index The index of the stream associated with the
328      *                       received blob.
329      * @param {String} message A human-readable message describing the error
330      *                         or status.
331      * @param {Number} code The error code, if any, or 0 for success.
332      */
333     this.sendAck = function(index, message, code) {
334 
335         // Do not send requests if not connected
336         if (!isConnected())
337             return;
338 
339         tunnel.sendMessage("ack", index, message, code);
340     };
341 
342     /**
343      * Given the index of a file, writes a blob of data to that file.
344      * 
345      * @param {Number} index The index of the file to write to.
346      * @param {String} data Base64-encoded data to write to the file.
347      */
348     this.sendBlob = function(index, data) {
349 
350         // Do not send requests if not connected
351         if (!isConnected())
352             return;
353 
354         tunnel.sendMessage("blob", index, data);
355     };
356 
357     /**
358      * Marks a currently-open stream as complete.
359      * 
360      * @param {Number} index The index of the stream to end.
361      */
362     this.endStream = function(index) {
363 
364         // Do not send requests if not connected
365         if (!isConnected())
366             return;
367 
368         tunnel.sendMessage("end", index);
369     };
370 
371     /**
372      * Fired whenever the state of this Guacamole.Client changes.
373      * 
374      * @event
375      * @param {Number} state The new state of the client.
376      */
377     this.onstatechange = null;
378 
379     /**
380      * Fired when the remote client sends a name update.
381      * 
382      * @event
383      * @param {String} name The new name of this client.
384      */
385     this.onname = null;
386 
387     /**
388      * Fired when an error is reported by the remote client, and the connection
389      * is being closed.
390      * 
391      * @event
392      * @param {Guacamole.Status} status A status object which describes the
393      *                                  error.
394      */
395     this.onerror = null;
396 
397     /**
398      * Fired when the clipboard of the remote client is changing.
399      * 
400      * @event
401      * @param {String} data The new text data of the remote clipboard.
402      */
403     this.onclipboard = null;
404 
405     /**
406      * Fired when the default layer (and thus the entire Guacamole display)
407      * is resized.
408      * 
409      * @event
410      * @param {Number} width The new width of the Guacamole display.
411      * @param {Number} height The new height of the Guacamole display.
412      */
413     this.onresize = null;
414 
415     /**
416      * Fired when a file stream is created. The stream provided to this event
417      * handler will contain its own event handlers for received data.
418      * 
419      * @event
420      * @param {Guacamole.InputStream} stream The stream that will receive data
421      *                                       from the server.
422      * @param {String} mimetype The mimetype of the file received.
423      * @param {String} filename The name of the file received.
424      */
425     this.onfile = null;
426 
427     /**
428      * Fired when a pipe stream is created. The stream provided to this event
429      * handler will contain its own event handlers for received data;
430      * 
431      * @event
432      * @param {Guacamole.InputStream} stream The stream that will receive data
433      *                                       from the server.
434      * @param {String} mimetype The mimetype of the data which will be received.
435      * @param {String} name The name of the pipe.
436      */
437     this.onpipe = null;
438 
439     /**
440      * Fired whenever a sync instruction is received from the server, indicating
441      * that the server is finished processing any input from the client and
442      * has sent any results.
443      * 
444      * @event
445      * @param {Number} timestamp The timestamp associated with the sync
446      *                           instruction.
447      */
448     this.onsync = null;
449 
450     // Layers
451     function getBufferLayer(index) {
452 
453         index = -1 - index;
454         var buffer = buffers[index];
455 
456         // Create buffer if necessary
457         if (buffer == null) {
458             buffer = new Guacamole.Layer(0, 0);
459             buffer.autoflush = 1;
460             buffer.autosize = 1;
461             buffers[index] = buffer;
462         }
463 
464         return buffer;
465 
466     }
467 
468     function getLayerContainer(index) {
469 
470         var layer = layers[index];
471         if (layer == null) {
472 
473             // Add new layer
474             layer = new Guacamole.Client.LayerContainer(index, displayWidth, displayHeight);
475             layers[index] = layer;
476 
477             // Get and position layer
478             var layer_element = layer.getElement();
479             layer_element.style.position = "absolute";
480             layer_element.style.left = "0px";
481             layer_element.style.top = "0px";
482             layer_element.style.overflow = "hidden";
483 
484             // Add to default layer container
485             layer.move(default_layer_container, 0, 0, 0);
486 
487         }
488 
489         return layer;
490 
491     }
492 
493     function getLayer(index) {
494        
495         // If buffer, just get layer
496         if (index < 0)
497             return getBufferLayer(index);
498 
499         // Otherwise, retrieve layer from layer container
500         return getLayerContainer(index).getLayer();
501 
502     }
503 
504     function getParser(index) {
505 
506         var parser = parsers[index];
507 
508         // If parser not yet created, create it, and tie to the
509         // oninstruction handler of the tunnel.
510         if (parser == null) {
511             parser = parsers[index] = new Guacamole.Parser();
512             parser.oninstruction = tunnel.oninstruction;
513         }
514 
515         return parser;
516 
517     }
518 
519     function getAudioChannel(index) {
520 
521         var audio_channel = audio_channels[index];
522 
523         // If audio channel not yet created, create it
524         if (audio_channel == null)
525             audio_channel = audio_channels[index] = new Guacamole.AudioChannel();
526 
527         return audio_channel;
528 
529     }
530 
531     /**
532      * Handlers for all defined layer properties.
533      * @private
534      */
535     var layerPropertyHandlers = {
536 
537         "miter-limit": function(layer, value) {
538             layer.setMiterLimit(parseFloat(value));
539         }
540 
541     };
542     
543     /**
544      * Handlers for all instruction opcodes receivable by a Guacamole protocol
545      * client.
546      * @private
547      */
548     var instructionHandlers = {
549 
550         "ack": function(parameters) {
551 
552             var stream_index = parseInt(parameters[0]);
553             var reason = parameters[1];
554             var code = parameters[2];
555 
556             // Get stream
557             var stream = output_streams[stream_index];
558             if (stream) {
559 
560                 // Signal ack if handler defined
561                 if (stream.onack)
562                     stream.onack(new Guacamole.Status(code, reason));
563 
564                 // If code is an error, invalidate stream
565                 if (code >= 0x0100) {
566                     stream_indices.free(stream_index);
567                     delete output_streams[stream_index];
568                 }
569 
570             }
571 
572         },
573 
574         "arc": function(parameters) {
575 
576             var layer = getLayer(parseInt(parameters[0]));
577             var x = parseInt(parameters[1]);
578             var y = parseInt(parameters[2]);
579             var radius = parseInt(parameters[3]);
580             var startAngle = parseFloat(parameters[4]);
581             var endAngle = parseFloat(parameters[5]);
582             var negative = parseInt(parameters[6]);
583 
584             layer.arc(x, y, radius, startAngle, endAngle, negative != 0);
585 
586         },
587 
588         "audio": function(parameters) {
589 
590             var stream_index = parseInt(parameters[0]);
591             var channel = getAudioChannel(parseInt(parameters[1]));
592             var mimetype = parameters[2];
593             var duration = parseFloat(parameters[3]);
594 
595             // Create stream 
596             var stream = streams[stream_index] =
597                     new Guacamole.InputStream(guac_client, stream_index);
598 
599             // Assemble entire stream as a blob
600             var blob_reader = new Guacamole.BlobReader(stream, mimetype);
601 
602             // Play blob as audio
603             blob_reader.onend = function() {
604                 channel.play(mimetype, duration, blob_reader.getBlob());
605             };
606 
607             // Send success response
608             guac_client.sendAck(stream_index, "OK", 0x0000);
609 
610         },
611 
612         "blob": function(parameters) {
613 
614             // Get stream 
615             var stream_index = parseInt(parameters[0]);
616             var data = parameters[1];
617             var stream = streams[stream_index];
618 
619             // Write data
620             stream.onblob(data);
621 
622         },
623 
624         "cfill": function(parameters) {
625 
626             var channelMask = parseInt(parameters[0]);
627             var layer = getLayer(parseInt(parameters[1]));
628             var r = parseInt(parameters[2]);
629             var g = parseInt(parameters[3]);
630             var b = parseInt(parameters[4]);
631             var a = parseInt(parameters[5]);
632 
633             layer.setChannelMask(channelMask);
634 
635             layer.fillColor(r, g, b, a);
636 
637         },
638 
639         "clip": function(parameters) {
640 
641             var layer = getLayer(parseInt(parameters[0]));
642 
643             layer.clip();
644 
645         },
646 
647         "clipboard": function(parameters) {
648             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
649         },
650 
651         "close": function(parameters) {
652 
653             var layer = getLayer(parseInt(parameters[0]));
654 
655             layer.close();
656 
657         },
658 
659         "copy": function(parameters) {
660 
661             var srcL = getLayer(parseInt(parameters[0]));
662             var srcX = parseInt(parameters[1]);
663             var srcY = parseInt(parameters[2]);
664             var srcWidth = parseInt(parameters[3]);
665             var srcHeight = parseInt(parameters[4]);
666             var channelMask = parseInt(parameters[5]);
667             var dstL = getLayer(parseInt(parameters[6]));
668             var dstX = parseInt(parameters[7]);
669             var dstY = parseInt(parameters[8]);
670 
671             dstL.setChannelMask(channelMask);
672 
673             dstL.copy(
674                 srcL,
675                 srcX,
676                 srcY,
677                 srcWidth, 
678                 srcHeight, 
679                 dstX,
680                 dstY 
681             );
682 
683         },
684 
685         "cstroke": function(parameters) {
686 
687             var channelMask = parseInt(parameters[0]);
688             var layer = getLayer(parseInt(parameters[1]));
689             var cap = lineCap[parseInt(parameters[2])];
690             var join = lineJoin[parseInt(parameters[3])];
691             var thickness = parseInt(parameters[4]);
692             var r = parseInt(parameters[5]);
693             var g = parseInt(parameters[6]);
694             var b = parseInt(parameters[7]);
695             var a = parseInt(parameters[8]);
696 
697             layer.setChannelMask(channelMask);
698 
699             layer.strokeColor(cap, join, thickness, r, g, b, a);
700 
701         },
702 
703         "cursor": function(parameters) {
704 
705             cursorHotspotX = parseInt(parameters[0]);
706             cursorHotspotY = parseInt(parameters[1]);
707             var srcL = getLayer(parseInt(parameters[2]));
708             var srcX = parseInt(parameters[3]);
709             var srcY = parseInt(parameters[4]);
710             var srcWidth = parseInt(parameters[5]);
711             var srcHeight = parseInt(parameters[6]);
712 
713             // Reset cursor size
714             cursor.resize(srcWidth, srcHeight);
715 
716             // Draw cursor to cursor layer
717             cursor.getLayer().copy(
718                 srcL,
719                 srcX,
720                 srcY,
721                 srcWidth, 
722                 srcHeight, 
723                 0,
724                 0 
725             );
726 
727             // Update cursor position (hotspot may have changed)
728             moveCursor(cursorX, cursorY);
729 
730         },
731 
732         "curve": function(parameters) {
733 
734             var layer = getLayer(parseInt(parameters[0]));
735             var cp1x = parseInt(parameters[1]);
736             var cp1y = parseInt(parameters[2]);
737             var cp2x = parseInt(parameters[3]);
738             var cp2y = parseInt(parameters[4]);
739             var x = parseInt(parameters[5]);
740             var y = parseInt(parameters[6]);
741 
742             layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
743 
744         },
745 
746         "dispose": function(parameters) {
747             
748             var layer_index = parseInt(parameters[0]);
749 
750             // If visible layer, remove from parent
751             if (layer_index > 0) {
752 
753                 // Remove from parent
754                 var layer_container = getLayerContainer(layer_index);
755                 layer_container.dispose();
756 
757                 // Delete reference
758                 delete layers[layer_index];
759 
760             }
761 
762             // If buffer, just delete reference
763             else if (layer_index < 0)
764                 delete buffers[-1 - layer_index];
765 
766             // Attempting to dispose the root layer currently has no effect.
767 
768         },
769 
770         "distort": function(parameters) {
771 
772             var layer_index = parseInt(parameters[0]);
773             var a = parseFloat(parameters[1]);
774             var b = parseFloat(parameters[2]);
775             var c = parseFloat(parameters[3]);
776             var d = parseFloat(parameters[4]);
777             var e = parseFloat(parameters[5]);
778             var f = parseFloat(parameters[6]);
779 
780             // Only valid for visible layers (not buffers)
781             if (layer_index >= 0) {
782 
783                 // Set layer transform 
784                 var layer_container = getLayerContainer(layer_index);
785                 layer_container.transform(a, b, c, d, e, f);
786 
787              }
788 
789         },
790  
791         "error": function(parameters) {
792 
793             var reason = parameters[0];
794             var code = parameters[1];
795 
796             // Call handler if defined
797             if (guac_client.onerror)
798                 guac_client.onerror(new Guacamole.Status(code, reason));
799 
800             guac_client.disconnect();
801 
802         },
803 
804         "end": function(parameters) {
805 
806             // Get stream
807             var stream_index = parseInt(parameters[0]);
808             var stream = streams[stream_index];
809 
810             // Signal end of stream
811             if (stream.onend)
812                 stream.onend();
813 
814         },
815 
816         "file": function(parameters) {
817 
818             var stream_index = parseInt(parameters[0]);
819             var mimetype = parameters[1];
820             var filename = parameters[2];
821 
822             // Create stream 
823             if (guac_client.onfile) {
824                 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
825                 guac_client.onfile(stream, mimetype, filename);
826             }
827 
828             // Otherwise, unsupported
829             else
830                 guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100);
831 
832         },
833 
834         "identity": function(parameters) {
835 
836             var layer = getLayer(parseInt(parameters[0]));
837 
838             layer.setTransform(1, 0, 0, 1, 0, 0);
839 
840         },
841 
842         "lfill": function(parameters) {
843 
844             var channelMask = parseInt(parameters[0]);
845             var layer = getLayer(parseInt(parameters[1]));
846             var srcLayer = getLayer(parseInt(parameters[2]));
847 
848             layer.setChannelMask(channelMask);
849 
850             layer.fillLayer(srcLayer);
851 
852         },
853 
854         "line": function(parameters) {
855 
856             var layer = getLayer(parseInt(parameters[0]));
857             var x = parseInt(parameters[1]);
858             var y = parseInt(parameters[2]);
859 
860             layer.lineTo(x, y);
861 
862         },
863 
864         "lstroke": function(parameters) {
865 
866             var channelMask = parseInt(parameters[0]);
867             var layer = getLayer(parseInt(parameters[1]));
868             var srcLayer = getLayer(parseInt(parameters[2]));
869 
870             layer.setChannelMask(channelMask);
871 
872             layer.strokeLayer(srcLayer);
873 
874         },
875 
876         "move": function(parameters) {
877             
878             var layer_index = parseInt(parameters[0]);
879             var parent_index = parseInt(parameters[1]);
880             var x = parseInt(parameters[2]);
881             var y = parseInt(parameters[3]);
882             var z = parseInt(parameters[4]);
883 
884             // Only valid for non-default layers
885             if (layer_index > 0 && parent_index >= 0) {
886 
887                 // Get container element
888                 var layer_container = getLayerContainer(layer_index);
889                 var parent = getLayerContainer(parent_index);
890 
891                 // Move layer
892                 layer_container.move(parent, x, y, z);
893 
894             }
895 
896         },
897 
898         "name": function(parameters) {
899             if (guac_client.onname) guac_client.onname(parameters[0]);
900         },
901 
902         "nest": function(parameters) {
903             var parser = getParser(parseInt(parameters[0]));
904             parser.receive(parameters[1]);
905         },
906 
907         "pipe": function(parameters) {
908 
909             var stream_index = parseInt(parameters[0]);
910             var mimetype = parameters[1];
911             var name = parameters[2];
912 
913             // Create stream 
914             if (guac_client.onpipe) {
915                 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
916                 guac_client.onpipe(stream, mimetype, name);
917             }
918 
919             // Otherwise, unsupported
920             else
921                 guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100);
922 
923         },
924 
925         "png": function(parameters) {
926 
927             var channelMask = parseInt(parameters[0]);
928             var layer = getLayer(parseInt(parameters[1]));
929             var x = parseInt(parameters[2]);
930             var y = parseInt(parameters[3]);
931             var data = parameters[4];
932 
933             layer.setChannelMask(channelMask);
934 
935             layer.draw(
936                 x,
937                 y,
938                 "data:image/png;base64," + data
939             );
940 
941         },
942 
943         "pop": function(parameters) {
944 
945             var layer = getLayer(parseInt(parameters[0]));
946 
947             layer.pop();
948 
949         },
950 
951         "push": function(parameters) {
952 
953             var layer = getLayer(parseInt(parameters[0]));
954 
955             layer.push();
956 
957         },
958  
959         "rect": function(parameters) {
960 
961             var layer = getLayer(parseInt(parameters[0]));
962             var x = parseInt(parameters[1]);
963             var y = parseInt(parameters[2]);
964             var w = parseInt(parameters[3]);
965             var h = parseInt(parameters[4]);
966 
967             layer.rect(x, y, w, h);
968 
969         },
970         
971         "reset": function(parameters) {
972 
973             var layer = getLayer(parseInt(parameters[0]));
974 
975             layer.reset();
976 
977         },
978         
979         "set": function(parameters) {
980 
981             var layer = getLayer(parseInt(parameters[0]));
982             var name = parameters[1];
983             var value = parameters[2];
984 
985             // Call property handler if defined
986             var handler = layerPropertyHandlers[name];
987             if (handler)
988                 handler(layer, value);
989 
990         },
991 
992         "shade": function(parameters) {
993             
994             var layer_index = parseInt(parameters[0]);
995             var a = parseInt(parameters[1]);
996 
997             // Only valid for visible layers (not buffers)
998             if (layer_index >= 0) {
999                 var layer_container = getLayerContainer(layer_index);
1000                 layer_container.shade(a);
1001             }
1002 
1003         },
1004 
1005         "size": function(parameters) {
1006 
1007             var layer_index = parseInt(parameters[0]);
1008             var width = parseInt(parameters[1]);
1009             var height = parseInt(parameters[2]);
1010 
1011             // If not buffer, resize layer and container
1012             if (layer_index >= 0) {
1013 
1014                 // Resize layer
1015                 var layer_container = getLayerContainer(layer_index);
1016                 layer_container.resize(width, height);
1017 
1018                 // If layer is default, resize display
1019                 if (layer_index == 0) {
1020 
1021                     displayWidth = width;
1022                     displayHeight = height;
1023 
1024                     // Update (set) display size
1025                     display.style.width = displayWidth + "px";
1026                     display.style.height = displayHeight + "px";
1027 
1028                     // Update bounds size
1029                     bounds.style.width = (displayWidth*displayScale) + "px";
1030                     bounds.style.height = (displayHeight*displayScale) + "px";
1031 
1032                     // Call resize event handler if defined
1033                     if (guac_client.onresize)
1034                         guac_client.onresize(width, height);
1035 
1036                 }
1037 
1038             }
1039 
1040             // If buffer, resize layer only
1041             else {
1042                 var layer = getBufferLayer(parseInt(parameters[0]));
1043                 layer.resize(width, height);
1044             }
1045 
1046         },
1047         
1048         "start": function(parameters) {
1049 
1050             var layer = getLayer(parseInt(parameters[0]));
1051             var x = parseInt(parameters[1]);
1052             var y = parseInt(parameters[2]);
1053 
1054             layer.moveTo(x, y);
1055 
1056         },
1057 
1058         "sync": function(parameters) {
1059 
1060             var timestamp = parameters[0];
1061 
1062             // When all layers have finished rendering all instructions
1063             // UP TO THIS POINT IN TIME, send sync response.
1064 
1065             var layersToSync = 0;
1066             function syncLayer() {
1067 
1068                 layersToSync--;
1069 
1070                 // Send sync response when layers are finished
1071                 if (layersToSync == 0) {
1072                     if (timestamp != currentTimestamp) {
1073                         tunnel.sendMessage("sync", timestamp);
1074                         currentTimestamp = timestamp;
1075                     }
1076                 }
1077 
1078             }
1079 
1080             // Count active, not-ready layers and install sync tracking hooks
1081             for (var i=0; i<layers.length; i++) {
1082 
1083                 var layer_container = layers[i];
1084                 if (layer_container) {
1085 
1086                     var layer = layer_container.getLayer();
1087                     if (layer) {
1088 
1089                         // Flush layer
1090                         layer.flush();
1091 
1092                         // If still not ready, sync later
1093                         if (!layer.isReady()) {
1094                             layersToSync++;
1095                             layer.sync(syncLayer);
1096                         }
1097 
1098                     }
1099 
1100                 } // end if layer exists
1101 
1102             } // end for each layer
1103 
1104             // If all layers are ready, then we didn't install any hooks.
1105             // Send sync message now,
1106             if (layersToSync == 0) {
1107                 if (timestamp != currentTimestamp) {
1108                     tunnel.sendMessage("sync", timestamp);
1109                     currentTimestamp = timestamp;
1110                 }
1111             }
1112 
1113             // If received first update, no longer waiting.
1114             if (currentState == STATE_WAITING)
1115                 setState(STATE_CONNECTED);
1116 
1117             // Call sync handler if defined
1118             if (guac_client.onsync)
1119                 guac_client.onsync(timestamp);
1120 
1121         },
1122 
1123         "transfer": function(parameters) {
1124 
1125             var srcL = getLayer(parseInt(parameters[0]));
1126             var srcX = parseInt(parameters[1]);
1127             var srcY = parseInt(parameters[2]);
1128             var srcWidth = parseInt(parameters[3]);
1129             var srcHeight = parseInt(parameters[4]);
1130             var function_index = parseInt(parameters[5]);
1131             var dstL = getLayer(parseInt(parameters[6]));
1132             var dstX = parseInt(parameters[7]);
1133             var dstY = parseInt(parameters[8]);
1134 
1135             /* SRC */
1136             if (function_index === 0x3)
1137                 dstL.put(
1138                     srcL,
1139                     srcX,
1140                     srcY,
1141                     srcWidth, 
1142                     srcHeight, 
1143                     dstX,
1144                     dstY
1145                 );
1146 
1147             /* Anything else that isn't a NO-OP */
1148             else if (function_index !== 0x5)
1149                 dstL.transfer(
1150                     srcL,
1151                     srcX,
1152                     srcY,
1153                     srcWidth, 
1154                     srcHeight, 
1155                     dstX,
1156                     dstY,
1157                     Guacamole.Client.DefaultTransferFunction[function_index]
1158                 );
1159 
1160         },
1161 
1162         "transform": function(parameters) {
1163 
1164             var layer = getLayer(parseInt(parameters[0]));
1165             var a = parseFloat(parameters[1]);
1166             var b = parseFloat(parameters[2]);
1167             var c = parseFloat(parameters[3]);
1168             var d = parseFloat(parameters[4]);
1169             var e = parseFloat(parameters[5]);
1170             var f = parseFloat(parameters[6]);
1171 
1172             layer.transform(a, b, c, d, e, f);
1173 
1174         },
1175 
1176         "video": function(parameters) {
1177 
1178             var stream_index = parseInt(parameters[0]);
1179             var layer = getLayer(parseInt(parameters[1]));
1180             var mimetype = parameters[2];
1181             var duration = parseFloat(parameters[3]);
1182 
1183             // Create stream 
1184             var stream = streams[stream_index] =
1185                     new Guacamole.InputStream(guac_client, stream_index);
1186 
1187             // Assemble entire stream as a blob
1188             var blob_reader = new Guacamole.BlobReader(stream, mimetype);
1189 
1190             // Play video once finished 
1191             blob_reader.onend = function() {
1192 
1193                 // Read data from blob from stream
1194                 var reader = new FileReader();
1195                 reader.onload = function() {
1196 
1197                     var binary = "";
1198                     var bytes = new Uint8Array(reader.result);
1199 
1200                     // Produce binary string from bytes in buffer
1201                     for (var i=0; i<bytes.byteLength; i++)
1202                         binary += String.fromCharCode(bytes[i]);
1203 
1204                     // Play video
1205                     layer.play(mimetype, duration, "data:" + mimetype + ";base64," + window.btoa(binary));
1206 
1207                 };
1208                 reader.readAsArrayBuffer(blob_reader.getBlob());
1209 
1210             };
1211 
1212             // Send success response
1213             tunnel.sendMessage("ack", stream_index, "OK", 0x0000);
1214 
1215         }
1216 
1217     };
1218 
1219     tunnel.oninstruction = function(opcode, parameters) {
1220 
1221         var handler = instructionHandlers[opcode];
1222         if (handler)
1223             handler(parameters);
1224 
1225     };
1226 
1227     /**
1228      * Sends a disconnect instruction to the server and closes the tunnel.
1229      */
1230     this.disconnect = function() {
1231 
1232         // Only attempt disconnection not disconnected.
1233         if (currentState != STATE_DISCONNECTED
1234                 && currentState != STATE_DISCONNECTING) {
1235 
1236             setState(STATE_DISCONNECTING);
1237 
1238             // Stop ping
1239             if (pingInterval)
1240                 window.clearInterval(pingInterval);
1241 
1242             // Send disconnect message and disconnect
1243             tunnel.sendMessage("disconnect");
1244             tunnel.disconnect();
1245             setState(STATE_DISCONNECTED);
1246 
1247         }
1248 
1249     };
1250     
1251     /**
1252      * Connects the underlying tunnel of this Guacamole.Client, passing the
1253      * given arbitrary data to the tunnel during the connection process.
1254      *
1255      * @param data Arbitrary connection data to be sent to the underlying
1256      *             tunnel during the connection process.
1257      * @throws {Guacamole.Status} If an error occurs during connection.
1258      */
1259     this.connect = function(data) {
1260 
1261         setState(STATE_CONNECTING);
1262 
1263         try {
1264             tunnel.connect(data);
1265         }
1266         catch (status) {
1267             setState(STATE_IDLE);
1268             throw status;
1269         }
1270 
1271         // Ping every 5 seconds (ensure connection alive)
1272         pingInterval = window.setInterval(function() {
1273             tunnel.sendMessage("sync", currentTimestamp);
1274         }, 5000);
1275 
1276         setState(STATE_WAITING);
1277     };
1278 
1279     /**
1280      * Sets the scale of the client display element such that it renders at
1281      * a relatively smaller or larger size, without affecting the true
1282      * resolution of the display.
1283      *
1284      * @param {Number} scale The scale to resize to, where 1.0 is normal
1285      *                       size (1:1 scale).
1286      */
1287     this.scale = function(scale) {
1288 
1289         display.style.transform =
1290         display.style.WebkitTransform =
1291         display.style.MozTransform =
1292         display.style.OTransform =
1293         display.style.msTransform =
1294 
1295             "scale(" + scale + "," + scale + ")";
1296 
1297         displayScale = scale;
1298 
1299         // Update bounds size
1300         bounds.style.width = (displayWidth*displayScale) + "px";
1301         bounds.style.height = (displayHeight*displayScale) + "px";
1302 
1303     };
1304 
1305     /**
1306      * Returns the width of the display.
1307      *
1308      * @return {Number} The width of the display.
1309      */
1310     this.getWidth = function() {
1311         return displayWidth;
1312     };
1313 
1314     /**
1315      * Returns the height of the display.
1316      *
1317      * @return {Number} The height of the display.
1318      */
1319     this.getHeight = function() {
1320         return displayHeight;
1321     };
1322 
1323     /**
1324      * Returns the scale of the display.
1325      *
1326      * @return {Number} The scale of the display.
1327      */
1328     this.getScale = function() {
1329         return displayScale;
1330     };
1331 
1332     /**
1333      * Returns a canvas element containing the entire display, with all child
1334      * layers composited within.
1335      *
1336      * @return {HTMLCanvasElement} A new canvas element containing a copy of
1337      *                             the display.
1338      */
1339     this.flatten = function() {
1340        
1341         // Get default layer
1342         var default_layer = getLayerContainer(0);
1343 
1344         // Get destination canvas
1345         var canvas = document.createElement("canvas");
1346         canvas.width = default_layer.width;
1347         canvas.height = default_layer.height;
1348 
1349         var context = canvas.getContext("2d");
1350 
1351         // Returns sorted array of children
1352         function get_children(layer) {
1353 
1354             // Build array of children
1355             var children = [];
1356             for (var index in layer.children)
1357                 children.push(layer.children[index]);
1358 
1359             // Sort
1360             children.sort(function children_comparator(a, b) {
1361 
1362                 // Compare based on Z order
1363                 var diff = a.z - b.z;
1364                 if (diff !== 0)
1365                     return diff;
1366 
1367                 // If Z order identical, use document order
1368                 var a_element = a.getElement();
1369                 var b_element = b.getElement();
1370                 var position = b_element.compareDocumentPosition(a_element);
1371 
1372                 if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1;
1373                 if (position & Node.DOCUMENT_POSITION_FOLLOWING) return  1;
1374 
1375                 // Otherwise, assume same
1376                 return 0;
1377 
1378             });
1379 
1380             // Done
1381             return children;
1382 
1383         }
1384 
1385         // Draws the contents of the given layer at the given coordinates
1386         function draw_layer(layer, x, y) {
1387 
1388             // Draw layer
1389             if (layer.width > 0 && layer.height > 0) {
1390 
1391                 // Save and update alpha
1392                 var initial_alpha = context.globalAlpha;
1393                 context.globalAlpha *= layer.alpha / 255.0;
1394 
1395                 // Copy data
1396                 context.drawImage(layer.getLayer().getCanvas(), x, y);
1397 
1398                 // Draw all children
1399                 var children = get_children(layer);
1400                 for (var i=0; i<children.length; i++) {
1401                     var child = children[i];
1402                     draw_layer(child, x + child.x, y + child.y);
1403                 }
1404 
1405                 // Restore alpha
1406                 context.globalAlpha = initial_alpha;
1407 
1408             }
1409 
1410         }
1411 
1412         // Draw default layer and all children
1413         draw_layer(default_layer, 0, 0);
1414 
1415         // Return new canvas copy
1416         return canvas;
1417         
1418     };
1419 
1420 };
1421 
1422 /**
1423  * Simple container for Guacamole.Layer, allowing layers to be easily
1424  * repositioned and nested. This allows certain operations to be accelerated
1425  * through DOM manipulation, rather than raster operations.
1426  * 
1427  * @constructor
1428  *
1429  * @param {Number} index The unique integer which identifies this layer.
1430  *
1431  * @param {Number} width The width of the Layer, in pixels. The canvas element
1432  *                       backing this Layer will be given this width.
1433  *                       
1434  * @param {Number} height The height of the Layer, in pixels. The canvas element
1435  *                        backing this Layer will be given this height.
1436  */
1437 Guacamole.Client.LayerContainer = function(index, width, height) {
1438 
1439     /**
1440      * Reference to this LayerContainer.
1441      * @private
1442      */
1443     var layer_container = this;
1444 
1445     /**
1446      * Unique integer identifying this layer container.
1447      * @type Number
1448      */
1449     this.index = index;
1450 
1451     /**
1452      * The opacity of the layer container, where 255 is fully opaque and 0 is
1453      * fully transparent.
1454      */
1455     this.alpha = 0xFF;
1456 
1457     /**
1458      * X coordinate of the upper-left corner of this layer container within
1459      * its parent, in pixels.
1460      * @type Number
1461      */
1462     this.x = 0;
1463 
1464     /**
1465      * Y coordinate of the upper-left corner of this layer container within
1466      * its parent, in pixels.
1467      * @type Number
1468      */
1469     this.y = 0;
1470 
1471     /**
1472      * Z stacking order of this layer relative to other sibling layers.
1473      * @type Number
1474      */
1475     this.z = 0;
1476 
1477     /**
1478      * The affine transformation applied to this layer container. Each element
1479      * corresponds to a value from the transformation matrix, with the first
1480      * three values being the first row, and the last three values being the
1481      * second row. There are six values total.
1482      * 
1483      * @type Number[]
1484      */
1485     this.matrix = [1, 0, 0, 1, 0, 0];
1486 
1487     /**
1488      * The width of this layer in pixels.
1489      * @type Number
1490      */
1491     this.width = width;
1492 
1493     /**
1494      * The height of this layer in pixels.
1495      * @type Number
1496      */
1497     this.height = height;
1498 
1499     /**
1500      * The parent layer container of this layer, if any.
1501      * @type Guacamole.Client.LayerContainer
1502      */
1503     this.parent = null;
1504 
1505     /**
1506      * Set of all children of this layer, indexed by layer index. This object
1507      * will have one property per child.
1508      */
1509     this.children = {};
1510 
1511     // Create layer with given size
1512     var layer = new Guacamole.Layer(width, height);
1513 
1514     // Set layer position
1515     var canvas = layer.getCanvas();
1516     canvas.style.position = "absolute";
1517     canvas.style.left = "0px";
1518     canvas.style.top = "0px";
1519 
1520     // Create div with given size
1521     var div = document.createElement("div");
1522     div.appendChild(canvas);
1523     div.style.width = width + "px";
1524     div.style.height = height + "px";
1525 
1526     /**
1527      * Changes the size of this LayerContainer and the contained Layer to the
1528      * given width and height.
1529      * 
1530      * @param {Number} width The new width to assign to this Layer.
1531      * @param {Number} height The new height to assign to this Layer.
1532      */
1533     this.resize = function(width, height) {
1534 
1535         layer_container.width = width;
1536         layer_container.height = height;
1537 
1538         // Resize layer
1539         layer.resize(width, height);
1540 
1541         // Resize containing div
1542         div.style.width = width + "px";
1543         div.style.height = height + "px";
1544 
1545     };
1546   
1547     /**
1548      * Returns the Layer contained within this LayerContainer.
1549      * @returns {Guacamole.Layer} The Layer contained within this
1550      *                            LayerContainer.
1551      */
1552     this.getLayer = function() {
1553         return layer;
1554     };
1555 
1556     /**
1557      * Returns the element containing the Layer within this LayerContainer.
1558      * @returns {Element} The element containing the Layer within this
1559      *                    LayerContainer.
1560      */
1561     this.getElement = function() {
1562         return div;
1563     };
1564 
1565     /**
1566      * The translation component of this LayerContainer's transform.
1567      * @private
1568      */
1569     var translate = "translate(0px, 0px)"; // (0, 0)
1570 
1571     /**
1572      * The arbitrary matrix component of this LayerContainer's transform.
1573      * @private
1574      */
1575     var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity
1576 
1577     /**
1578      * Moves the upper-left corner of this LayerContainer to the given X and Y
1579      * coordinate.
1580      * 
1581      * @param {Number} x The X coordinate to move to.
1582      * @param {Number} y The Y coordinate to move to.
1583      */
1584     this.translate = function(x, y) {
1585 
1586         layer_container.x = x;
1587         layer_container.y = y;
1588 
1589         // Generate translation
1590         translate = "translate("
1591                         + x + "px,"
1592                         + y + "px)";
1593 
1594         // Set layer transform 
1595         div.style.transform =
1596         div.style.WebkitTransform =
1597         div.style.MozTransform =
1598         div.style.OTransform =
1599         div.style.msTransform =
1600 
1601             translate + " " + matrix;
1602 
1603     };
1604 
1605     /**
1606      * Moves the upper-left corner of this LayerContainer to the given X and Y
1607      * coordinate, sets the Z stacking order, and reparents this LayerContainer
1608      * to the given LayerContainer.
1609      * 
1610      * @param {Guacamole.Client.LayerContainer} parent The parent to set.
1611      * @param {Number} x The X coordinate to move to.
1612      * @param {Number} y The Y coordinate to move to.
1613      * @param {Number} z The Z coordinate to move to.
1614      */
1615     this.move = function(parent, x, y, z) {
1616 
1617         // Set parent if necessary
1618         if (layer_container.parent !== parent) {
1619 
1620             // Maintain relationship
1621             if (layer_container.parent)
1622                 delete layer_container.parent.children[layer_container.index];
1623             layer_container.parent = parent;
1624             parent.children[layer_container.index] = layer_container;
1625 
1626             // Reparent element
1627             var parent_element = parent.getElement();
1628             parent_element.appendChild(div);
1629 
1630         }
1631 
1632         // Set location
1633         layer_container.translate(x, y);
1634         layer_container.z = z;
1635         div.style.zIndex = z;
1636 
1637     };
1638 
1639     /**
1640      * Sets the opacity of this layer to the given value, where 255 is fully
1641      * opaque and 0 is fully transparent.
1642      * 
1643      * @param {Number} a The opacity to set.
1644      */
1645     this.shade = function(a) {
1646         layer_container.alpha = a;
1647         div.style.opacity = a/255.0;
1648     };
1649 
1650     /**
1651      * Removes this layer container entirely, such that it is no longer
1652      * contained within its parent layer, if any.
1653      */
1654     this.dispose = function() {
1655 
1656         // Remove from parent container
1657         if (layer_container.parent) {
1658             delete layer_container.parent.children[layer_container.index];
1659             layer_container.parent = null;
1660         }
1661 
1662         // Remove from parent element
1663         if (div.parentNode)
1664             div.parentNode.removeChild(div);
1665         
1666     };
1667 
1668     /**
1669      * Applies the given affine transform (defined with six values from the
1670      * transform's matrix).
1671      * 
1672      * @param {Number} a The first value in the affine transform's matrix.
1673      * @param {Number} b The second value in the affine transform's matrix.
1674      * @param {Number} c The third value in the affine transform's matrix.
1675      * @param {Number} d The fourth value in the affine transform's matrix.
1676      * @param {Number} e The fifth value in the affine transform's matrix.
1677      * @param {Number} f The sixth value in the affine transform's matrix.
1678      */
1679     this.transform = function(a, b, c, d, e, f) {
1680 
1681         // Store matrix
1682         layer_container.matrix = [a, b, c, d, e, f];
1683 
1684         // Generate matrix transformation
1685         matrix =
1686 
1687             /* a c e
1688              * b d f
1689              * 0 0 1
1690              */
1691     
1692             "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
1693 
1694         // Set layer transform 
1695         div.style.transform =
1696         div.style.WebkitTransform =
1697         div.style.MozTransform =
1698         div.style.OTransform =
1699         div.style.msTransform =
1700 
1701             translate + " " + matrix;
1702 
1703     };
1704 
1705 };
1706 
1707 /**
1708  * Map of all Guacamole binary raster operations to transfer functions.
1709  * @private
1710  */
1711 Guacamole.Client.DefaultTransferFunction = {
1712 
1713     /* BLACK */
1714     0x0: function (src, dst) {
1715         dst.red = dst.green = dst.blue = 0x00;
1716     },
1717 
1718     /* WHITE */
1719     0xF: function (src, dst) {
1720         dst.red = dst.green = dst.blue = 0xFF;
1721     },
1722 
1723     /* SRC */
1724     0x3: function (src, dst) {
1725         dst.red   = src.red;
1726         dst.green = src.green;
1727         dst.blue  = src.blue;
1728         dst.alpha = src.alpha;
1729     },
1730 
1731     /* DEST (no-op) */
1732     0x5: function (src, dst) {
1733         // Do nothing
1734     },
1735 
1736     /* Invert SRC */
1737     0xC: function (src, dst) {
1738         dst.red   = 0xFF & ~src.red;
1739         dst.green = 0xFF & ~src.green;
1740         dst.blue  = 0xFF & ~src.blue;
1741         dst.alpha =  src.alpha;
1742     },
1743     
1744     /* Invert DEST */
1745     0xA: function (src, dst) {
1746         dst.red   = 0xFF & ~dst.red;
1747         dst.green = 0xFF & ~dst.green;
1748         dst.blue  = 0xFF & ~dst.blue;
1749     },
1750 
1751     /* AND */
1752     0x1: function (src, dst) {
1753         dst.red   =  ( src.red   &  dst.red);
1754         dst.green =  ( src.green &  dst.green);
1755         dst.blue  =  ( src.blue  &  dst.blue);
1756     },
1757 
1758     /* NAND */
1759     0xE: function (src, dst) {
1760         dst.red   = 0xFF & ~( src.red   &  dst.red);
1761         dst.green = 0xFF & ~( src.green &  dst.green);
1762         dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
1763     },
1764 
1765     /* OR */
1766     0x7: function (src, dst) {
1767         dst.red   =  ( src.red   |  dst.red);
1768         dst.green =  ( src.green |  dst.green);
1769         dst.blue  =  ( src.blue  |  dst.blue);
1770     },
1771 
1772     /* NOR */
1773     0x8: function (src, dst) {
1774         dst.red   = 0xFF & ~( src.red   |  dst.red);
1775         dst.green = 0xFF & ~( src.green |  dst.green);
1776         dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
1777     },
1778 
1779     /* XOR */
1780     0x6: function (src, dst) {
1781         dst.red   =  ( src.red   ^  dst.red);
1782         dst.green =  ( src.green ^  dst.green);
1783         dst.blue  =  ( src.blue  ^  dst.blue);
1784     },
1785 
1786     /* XNOR */
1787     0x9: function (src, dst) {
1788         dst.red   = 0xFF & ~( src.red   ^  dst.red);
1789         dst.green = 0xFF & ~( src.green ^  dst.green);
1790         dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
1791     },
1792 
1793     /* AND inverted source */
1794     0x4: function (src, dst) {
1795         dst.red   =  0xFF & (~src.red   &  dst.red);
1796         dst.green =  0xFF & (~src.green &  dst.green);
1797         dst.blue  =  0xFF & (~src.blue  &  dst.blue);
1798     },
1799 
1800     /* OR inverted source */
1801     0xD: function (src, dst) {
1802         dst.red   =  0xFF & (~src.red   |  dst.red);
1803         dst.green =  0xFF & (~src.green |  dst.green);
1804         dst.blue  =  0xFF & (~src.blue  |  dst.blue);
1805     },
1806 
1807     /* AND inverted destination */
1808     0x2: function (src, dst) {
1809         dst.red   =  0xFF & ( src.red   & ~dst.red);
1810         dst.green =  0xFF & ( src.green & ~dst.green);
1811         dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
1812     },
1813 
1814     /* OR inverted destination */
1815     0xB: function (src, dst) {
1816         dst.red   =  0xFF & ( src.red   | ~dst.red);
1817         dst.green =  0xFF & ( src.green | ~dst.green);
1818         dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
1819     }
1820 
1821 };
1822