/*
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * If you have purchased PXN8 for use on your own servers and want to change the
 * core functionality we strongly recommend that
 * You make a copy of this file and rename it to $YOURCOMPANY_pxn8core.js and use that
 * as a working copy.
 */

window.onerror = function(message,url,line)
{
    prompt("A Javascript error occurred!\n " + message + "\n at line " + line, url);
    return false;
};

/**************************************************************************

SECTION: VARIABLES
==================
Pixenate uses a number of javascript variables...

PXN8
====
PXN8 is the name of the global variable used by Pixenate to store variables and
functions used by the Pixenate javascript API. PXN8 acts as a global namespace for all such
variables and functions.

***/
var PXN8 = PXN8 || {};

PXN8.server = document.location.protocol + "/" + "/" + document.location.host;

/**************************************************************************

PXN8.root
=========
Specifies where Pixenate&trade; is located relative to the Web
Root. If you install Pixenate in a directory other than one named
'pixenate' in the webroot folder, *You must change this value
accordingly*. For example if your webroot is /var/www/html and you have installed PXN8
in /var/www/html/pixenate, then you should set PXN8.root = "/pixenate/".

Type
----
String

Default Value
-------------
    "/pixenate/"

***/
PXN8.root = "/pixenate/";

/**************************************************************************

PXN8.basename
=========
Specifies the basename Pixenate&trade; CGI script

Type
----
String

Default Value
-------------
    "pxn8.pl"

***/
PXN8.basename = "pxn8.pl";

/**************************************************************************

PXN8.replaceOnSave
==================
replaceOnSave specifies how PXN8 handles image URLs.
If set to true then PXN8 always assumes that the photo at the supplied URL has changed.
If set to false then PXN8 will assume that the photo at the supplied url hasn't changed since it was last retrieved.
If the photo URL maps to a filepath on the webserver and your photo-editing application
overwrites the original file when saved then you should set this to true.
By default, it's set to true to avoid potential caching problems when save operation overwrites the original image.

Type
----
boolean

Default Value
-------------
    true


***/
PXN8.replaceOnSave = true;

/***************************************************************************

PXN8.aspectRatio
================
The currently enforced aspect-ratio which is enforced when the user
selects an area of the photo. If the value is anything other than...

    {width: 0, height: 0}

...then that aspect ratio is enforced. E.g. to
enforce a 2x3 aspect ratio on selections...

    PXN8.selectByRatio("2x3");

Type
----
Object (with *width* and *height* properties). *READ ONLY*

Default Value
-------------
    {width: 0, height: 0}

***/
PXN8.aspectRatio =  {width:0 , height:0};

/**************************************************************************

PXN8.position
=============

The current mouse position on the photo. The PXN8.position property
takes into account the current magnification level so it is always the
position of the mouse relative to the top-left corner of the un-magnified photo.

Type
----
Object (with *x* and *y* properties). *READ ONLY*

Default Value
-------------
    {x: "-", y: "-"}

***/

PXN8.position = {x: "-", y: "-"};


/***************************************************************************

PXN8.style
==========
PXN8.style is a namespace used to define style-related variables used by Pixenate.

***/
PXN8.style = {};

/**************************************************************************

PXN8.style.notSelected
======================
This variable defines the opacity and color of the non-selected areas of the photo.

Type
----
Object (with *opacity* and *color* properties).

Default Value
-------------
    {opacity: 0.33,
     color:   "black"}

***/
PXN8.style.notSelected = {opacity: 0.33,color: "black"};

/***************************************************************************

PXN8.style.resizeHandles
========================
Defines the color, and size (in pixels) of the resize handles which appear at
the corners and sides of the selected area of the image.

Type
----
Object (with *color* and *size* properties).

Default Value
-------------
    {color: "white",
     size:  6};
***/
PXN8.style.resizeHandles ={color: "white",size: 6,smallsize: 4,oldsize: -1};

/***************************************************************************

PXN8.select.constrainToImageBounds
==================================
Controls the selection behaviour. By default users cannot select an area which clips the
bounds of the image. However, there are cases where the user might want to do this - for example when adding
clip-art to an image , part of the clip-art may be off-image (e.g. adding santa hats to photos - constraining
the overlay tool so that all of the overlay image must appear on top of the photo does not make sense).

Type
----
boolean

Default Value
-------------
true

***/

/***************************************************************************

PXN8.convertToJPEG
==================
By default Pixenate assumes it is working with photographic images which don't have an alpha channel.
If you would like to edit .GIF or .PNG images while keeping the alpha channel information then set this variable to false.

Type
----
boolean

Default Value
-------------
true

***/

/***************************************************************************

SECTION: CALLBACKS and Related Functions
========================================
Hooks can be added to Pixenate using the following pre-defined Pixenate event types.
For more information on adding hooks to Pixenate please refer to the *PXN8.listener* set
of functions.
***/

/**************************************************************************

PXN8.ON_IMAGE_LOAD
==================
This event fires whenever *a new photo* is loaded into the
web page as a result of an editing operation. It does not fire when
the user *Undoes* or *Redoes* an operation - to catch those events use *PXN8.ON_IMAGE_CHANGE*.

Examples
--------
    function myOnImageLoad(eventType){
        alert("A new image has been loaded");
    }
    PXN8.listener.add(PXN8.ON_IMAGE_LOAD,myOnImageLoad);

***/
PXN8.ON_IMAGE_LOAD = "ON_IMAGE_LOAD";

/***************************************************************************

PXN8.ON_IMAGE_CHANGE
====================
This event fires whenever the photo is changed as a result of an
editing operation (including the undo and redo family of operations).

Examples
--------
    function myOnImageChange(eventType){
        alert("The image has been modified");
    }
    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,myOnImageChange);
***/
PXN8.ON_IMAGE_CHANGE =  "ON_IMAGE_CHANGE";

/***************************************************************************

PXN8.BEFORE_IMAGE_CHANGE
====================
This event fires before the photo is changed as a result of an
editing operation (including the undo and redo family of operations).

Examples
--------
    function myBeforeImageChange(eventType){
        alert("The image is about to be modified");
    }
    PXN8.listener.add(PXN8.BEFORE_IMAGE_CHANGE,myBeforeImageChange);
***/
PXN8.BEFORE_IMAGE_CHANGE =  "BEFORE_IMAGE_CHANGE";

/****************************************************************************

PXN8.ON_ZOOM_CHANGE
===================
This event is fired whenever the magnification level of the photo has been changed
(when the user zooms in and out).

Examples
--------

    function myOnImageZoom(eventType){
        alert("You have zoomed the image to " + (PXN8.zoom.value() * 100) + "%");
    };
    PXN8.listener.add(PXN8.ON_IMAGE_ZOOM, myOnImageZoom);

***/
PXN8.ON_ZOOM_CHANGE =  "ON_ZOOM_CHANGE";

/*****************************************************************************

PXN8.ON_SELECTION_CHANGE
========================
This event fires whenever the selection area is modified. *Do
not do anything which requires User interaction (such as alert,
confirm, prompt etc) in your listener as this event can fire quite frequently* while
the user is resizing, moving or initializing the selection area using the mouse. If you
want your hook to be called after the user has finished selecting the area, then use
*PXN8.ON_SELECTION_COMPLETE* instead.

Examples
--------

Please see the <a href="example-selection-limit.html">Limiting Selection Size</a> example, which demonstrates how to modify the selection behaviour so that a minimum area of 400x200 pixels must be selected.
***/
PXN8.ON_SELECTION_CHANGE =  "ON_SELECTION_CHANGE";

/*****************************************************************************

PXN8.ON_SELECTION_COMPLETE
==========================
This event fires when the user mouseups after making a selection or when a selection
has been made programmatically.

Examples
--------

    function myOnSelectionComplete(eventType){
        var log = document.getElementById("my_log");
        var sel = PXN8.getSelection();
        alert("You selected: " + sel.top + "," + sel.left + "," +
              sel.width + "," + sel.height);
    }
    PXN8.listener.add(PXN8.ON_SELECTION_COMPLETE,myOnSelectionComplete);

***/
PXN8.ON_SELECTION_COMPLETE =  "ON_SELECTION_COMPLETE";

/*****************************************************************************

PXN8.ON_IMAGE_ERROR
===================
This event is fired when an image update fails or an image fails to load.

***/
PXN8.ON_IMAGE_ERROR =  "ON_IMAGE_ERROR";

/* ============================================================================
 *
 * Functions related to PXN8 listeners
 */
PXN8.listener = {
    /**
     * A map of listeners by event type
     */
    listenersByType : {}
};
PXN8.listener.listenersByType[PXN8.ON_ZOOM_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_SELECTION_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_IMAGE_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_IMAGE_ERROR] = [];

/****************************************************************************

PXN8.listener.add()
===================
Adds a new callback function to the list of functions to be called when a PXN8 event occurs.
Listeners can be added for the following event types...
* PXN8.ON_ZOOM_CHANGE : Fired when the image is zoomed in or out.
* PXN8.ON_SELECTION_CHANGE : Fired when the selection has changed (and during a manual selection operation).
* PXN8.ON_SELECTION_COMPLETE : Fired when the user has completed making a selection or when the selection has changed programattically.
* PXN8.ON_IMAGE_CHANGE : Fired when the image has been modified. This is fired *after* the changed image has been loaded into the browser.

Parameters
----------
* eventType : See above event types.
* callback : A function which will be called when an event of eventType fires. This should be a
javascript function reference or literal. The callback should take a single parameter called eventType which will be
one of the above defined event types.

Returns
-------
The supplied callback parameter.

Example
-------
The following snippet of code displays an alert message every time the image is modified.

    // add an anonymous function as a listener
    //
    var myListener = PXN8.listener.add(PXN8.ON_IMAGE_CHANGE, function(eventType){
        if (eventType == PXN8.ON_IMAGE_CHANGE){
           alert("The image has changed");
        }
    });

The code above is equivalent to...

    // define and name the function
    function myListener(eventType){
        if (eventType == PXN8.ON_IMAGE_CHANGE){
           alert("The image has changed");
        }
    }

    // add the named function
    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,myListener);

Related
-------
PXN8.listener.remove PXN8.listener.onceOnly

***/
PXN8.listener.add = function (eventType,callback)
{
    var self = PXN8.listener;

    var callbacks = self.listenersByType[eventType];
    var found = false;
    if (!callbacks){
        callbacks = [];
        self.listenersByType[eventType] = callbacks;
    }
    for (var i = 0;i < callbacks.length; i++){
        if (callbacks[i] == callback){
            found = true;
            break;
        }
    }
    if (!found){
        callbacks.push (callback);
    }
    return callback;

};
/***************************************************************************

PXN8.listener.remove()
======================
Removes a callback function from the list of functions to be called when a PXN8 event occurs.


Parameters
----------

* eventType : The type of event for which you want to remove the listener (A listener
can potentially listen for different types of events).
* callback : a function reference which will be removed from Pixenate's list of listeners
for that particular event type.

Example
-------

    PXN8.listener.remove(PXN8.ON_IMAGE_CHANGE,myListener);

Related
-------
PXN8.listener.add PXN8.listener.onceOnly

***/
PXN8.listener.remove = function (eventType, callback)
{
    var self = PXN8.listener;

    var callbacks = self.listenersByType[eventType];
    if (!callbacks) return;
    for (var i = 0;i < callbacks.length; i++){
        if (callbacks[i] == callback){
            callbacks.splice(i,1);
            i--;
        }
    }
};
/****************************************************************************

PXN8.listener.onceOnly()
========================
Add a special-case of listener that *will only be invoked once and once only*.

Parameters
----------
* eventType : The type of event for which you want to listen (once only).
* callback : The function to be called when the event occurs (only called once then removed from list).

Returns
-------
The newly added Listener.

Related
-------
PXN8.listener.add PXN8.listener.remove

***/
PXN8.listener.onceOnly = function (eventType,callback)
{
    var self = PXN8.listener;
    callback.onceOnly = true;
    return self.add(eventType, callback);
};


/*
 * What is the current operation number ?
 */
PXN8.opNumber =  0;

/**
 * what is the total number of operations performed ?
 */
PXN8.maxOpNumber =  0;


/**
 * The JSON response from the last image operation
 */
PXN8.response =  {
    status: "",
    image: "",
    errorCode: 0,
    errorMessage: ""
};

/**
 * If an operation is performed on an image then this is set to true
 * until the image update has completed
 */
PXN8.updating =  false;

/**
 * The upper bounds on image sizes
 */
PXN8.resizelimit = {
    width: 1600,
    height: 1200
};





/*
 * A hashtable of images with the image.src url as the key (value is 'true')
 * Need this for IE to force onload handler for images which
 * have already been loaded.
 */
PXN8.imagesBySrc =  {};

// the start of the selection along the X axis (from left)
PXN8.sx =  0;

// the start of the selection along the Y axis (from top)
PXN8.sy =  0;

// the end of the selection along the X axis
PXN8.ex =  0;

// the end of the selection along the Y axis
PXN8.ey =  0;

/***************************************************************************

SECTION: Core Pixenate Functions
================================
Pixenate's core javascript API relies on the following functions...

***/

/***************************************************************************

PXN8.initialize()
=================
Call this function to initialize the PXN8 editor.

Parameters
----------
* image_url : A string value which is an Image URL of any of the forms specified below.

The image_url parameter can be any of the following.

* full URL : E.g. "http://pixenate.com/pixenate/images/samples/hongkong.jpg"
* absolute path (relative to domain) : E.g. "/pixenate/images/samples/hongkong.jpg"
* relative path (relative to page) : E.g. "../../images/samples/hongkong.jpg"


Alternatively it can be an object with 2 attributes *url* (see above) and *filepath*.
The filepath should be a path relative to where Pixenate is installed and
which can be used by the Pixenate server CGI to access the image from the server's filesystem.

Example
-------
Pixenate can be initialized anywhere on the page *as long as the PXN8.initialize() function is called after
the 'pxn8_canvas' div has been parsed by the browser*. For example the following code won't work on many browsers...

*WRONG*

    <script type="text/javascript">PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");</script>
    <div id="pxn8_canvas"></div>

The correct approach is as follows...

    <div id="pxn8_canvas"></div>
    <!-- declare the pxn8_canvas BEFORE calling initialize -->
    <script type="text/javascript">PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");</script>

Another approach is to call PXN8.initialize() when the *window.onload* event fires...

    <!-- the following javascript block can appear anywhere on the page -->
    <script type="text/javascript">
      PXN8.dom.addLoadEvent(function(){
          // pxn8_canvas will have been parsed by the time this javascript is executed.
          PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");
      });
    </script>

Related
-------
PXN8.dom.addLoadEvent

***/
PXN8.initialize = function( param )
{

    PXN8.ready = false;

    var _ = PXN8.dom;

    var image_src;

    var paramType = typeof param;

    if (paramType == 'string'){
        image_src = param;
    }else{
        image_src = param.url;
    }

    PXN8.priv.createSelectionRect();

    var canvas = PXN8.initializeCanvas();

    //
    // create the pxn8_image_container element if not already present
    //
    var imgContainer = _.id("pxn8_image_container");
    if (!imgContainer){
        imgContainer = _.ac(canvas,_.ce("div",{id: "pxn8_image_container"}));
        //
        // FIX for IE's broken handling of Faded JPEGS (introduction of white-noise)...
        // IE interprets a completely black pixel in a JPEG as being transparent.
        // Because of this, in some dark areas there will be white pixels. This is
        // the background color showing through.
        // the solution is to change the background color of the pxn8_image_container
        // to black or change the #00000 pixels to #000001
        // see http://www.alexjudd.com/?p=5
        //
        if (document.all){
            //
            // wph 20080515 Only do this for IE. Firefox doesn't resize the pxn8_image_container
            // so a black area appears when the image is rotated 90 degrees.
            //
            imgContainer.style.backgroundColor = "black";
        }


    }

    if(navigator.userAgent.indexOf("Opera")!=-1){
        // opera (as of version 9.01) doesn't support opacity
        PXN8.style.notSelected.color = "transparent";
    }

    /**
     * It is VERY IMPORTANT that backgroundImageCache is enabled
     * in IE - otherwise there is an annoying flicker when the preview
     * pane is dragged.
     */
    try {
        document.execCommand('BackgroundImageCache', false, true);
    } catch(e) {}

    //
    // create and style the divs that will bound the selection area
    //
    var rects = ["pxn8_top_rect",
                 "pxn8_bottom_rect",
                 "pxn8_left_rect",
                 "pxn8_right_rect",
                 "pxn8_topleft_rect",
                 "pxn8_topright_rect",
                 "pxn8_bottomleft_rect",
                 "pxn8_bottomright_rect"];
    for (var i = 0;i < rects.length; i++)
	 {
        var rect = _.id(rects[i]);
        if (!rect){
            rect = _.ac(canvas,_.ce("div",{id: rects[i]}));
        }

        rect.style.fontSize = "0px";
        if (!rect.style.backgroundColor){
            rect.style.backgroundColor = PXN8.style.notSelected.color;
        }
        rect.style.position = "absolute";
        if (!rect.style.opacity){
            _.opacity(rect,PXN8.style.notSelected.opacity);
        }

        rect.style.top = "0px";
        rect.style.left = "0px";
        rect.style.width = "0px";
        rect.style.height = "0px";
        rect.style.display = "none";
        rect.style.zIndex = 1;

        var antshz = "url(" + PXN8.server + PXN8.root + "/images/ants_hr.gif)";
        var antsvt = "url(" + PXN8.server + PXN8.root + "/images/ants_vt.gif)";
        if (rects[i] == "pxn8_top_rect"){
            rect.style.backgroundImage = antshz;
            rect.style.backgroundPosition = "bottom left";
            rect.style.backgroundRepeat = "repeat-x";
        }
        if (rects[i] == "pxn8_bottom_rect"){
            rect.style.backgroundImage = antshz;
            rect.style.backgroundPosition = "top left";
            rect.style.backgroundRepeat = "repeat-x";
        }
        if (rects[i] == "pxn8_left_rect"){
            rect.style.backgroundImage = antsvt;
            rect.style.backgroundPosition = "top right";
            rect.style.backgroundRepeat = "repeat-y";
        }
        if (rects[i] == "pxn8_right_rect"){
            rect.style.backgroundImage = antsvt;
            rect.style.backgroundPosition = "top left";
            rect.style.backgroundRepeat = "repeat-y";
        }
    }


    PXN8.image.location = image_src;

    PXN8.opNumber = 0;
    PXN8.maxOpNumber = 0;

    PXN8.history = new Array();
    //
    // initialize offsets (for undo and redo of composite operations)
    //
    PXN8.offsets = [1];

    //
    // wph 20070123 : If the image URL passed to PXN8.initialize() is of the form...
    //
    // ../path/to/images/x.jpg
    // ../../gallery/x.jpg
    //
    // then

    var fetchOp = {
        operation: "fetch",
        url: PXN8.image.location
    };
    fetchOp.pxn8root = PXN8.root;
    if (paramType == 'object'){
        for (var i in param){
            fetchOp[i] = param[i];
        }
    }

    PXN8.history.push(fetchOp);


    if (PXN8.replaceOnSave){
        fetchOp.random = PXN8.randomHex();
    }


	 /**
	  * The following function insert's the photo's absolute URL into the
	  * 'fetch' operator. This is so that pxn8.pl always has the absolute URL
	  * (Pixenate can work as a CGI or via mod_perl so relative URLs are meaningless
	  *  to the server process)
	  */
    var gotAbsoluteImageSrc = false;

    var getAbsoluteImageSrc = function(){

        if (gotAbsoluteImageSrc){
            getAbsoluteImageSrc = function(){};
            return;
        }
        gotAbsoluteImageSrc = true;
        // wph 20070124
        // update the fetch.url attribute to reflect the canonical image location
        // e.g. ../../images/samples/greenleaves.jpg becomes...
        // http://mydomain/pixenate/images/samples/greenleaves.jpg
        // This is important when the image passed in is a relative path to the current page.

        var fetchOp = PXN8.getOperation(0);
        var theImage = document.getElementById("pxn8_image");

        //
        // wph 20070227 : must escape the URL to avoid the case where an image url contains an '&' character
        // in which case the 'script' parameter passed to pxn8.pl was being truncated at first '&' character
        //
        // wph 20070903 : Entire script is now escaped so no need to escape individual parts of script
        //fetchOp.url = escape(theImage.src);
        fetchOp.url = theImage.src;
        //
        // wph 20070201
        // Set the global PXN8.ready flag to true so that operations can be performed
        // on the image.
        //
        PXN8.ready = true;
    };

    var pxn8image = _.id("pxn8_image");

    /**
     * Safari doesn't load the image immediately
     * so setting the PXN8.image.width & height variables
     * makes no sense until the image has loaded.
     * the following function gets called directly from within
     * this function but also from within the img.onload function
     * if no <img id="pxn8_image".../> element appears in the body
     * (if pxn8_image is created dynamically as is the case with a
     * toolbar theme.
     *
     */

    var onImageLoad = function()
    {
        var _ = PXN8.dom;
        var pxn8image = _.id("pxn8_image");

        PXN8.image.width =  pxn8image.width;
        PXN8.image.height = pxn8image.height;

        PXN8.priv.addImageToHistory(pxn8image.src);
        PXN8.show.size();
        getAbsoluteImageSrc();
    };

    /**
     *  Initialize the image
     */
    if (!pxn8image)
	 {
        //
        // this won't work for Safari.
        // it is recommended that the <img> tag always appears
        // inside the pxn8_image_container tag.
        //

        // wph 20070117 : Use of innerHTML instead of DOM forces IE to load the image at the correct dimensions
        // Upload -> Crop -> save as same name -> upload same file : Image appears stretched to old dimensions
        //pxn8image = dom.ac(imgContainer,dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));

        var innerHTML = '<img id="pxn8_image" border="0" src="' + PXN8.image.location + '"/>';

        try {
            imgContainer.innerHTML = innerHTML;
        }catch (e){
            alert("An error occurred while adding the <img> tag to the page.\n" +
                  "This is most likely because the pxn8_canvas DIV has been added to an incorrect element (<table> or <p>).\n"  +
                  "The error message reported was: " + e.message);
            PXN8.dom.ac(imgContainer,PXN8.dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));
        }

        pxn8image = _.id("pxn8_image");

    }
	 else
	 {

        //
        //  The image is already present - re-add it to the DOM to ensure the
        //  correct dimensions are applied.
        //
        var imgContainer = _.cl("pxn8_image_container");
        //
        // wph 20060905 : Must change the image src attribute whenever PXN8.initialize is called
        // e.g. if there is a web-page with thumbnail images which change the current image for
        // editing, the .src attribute *MUST* be updated !


        // wph 20070117 : Use of innerHTML instead of DOM forces IE to load the image at the correct dimensions
        // Upload -> Crop -> save as same name -> upload same file : Image appears stretched to old dimensions
        //pxn8image = dom.ac(imgContainer,dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));

        var innerHTML = '<img id="pxn8_image" border="0" src="' + PXN8.image.location + '"/>';
        try {
            imgContainer.innerHTML = innerHTML;
        }catch(e){
            alert("An error occurred while adding the <img> tag to the page.\n" +
                  "This is most likely because the pxn8_canvas DIV has been added to an incorrect element (<table> or <p>).\n"  +
                  "The error message reported was: " + e.message);
            PXN8.dom.ac(imgContainer,PXN8.dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));
        }

        pxn8image = _.id("pxn8_image");
    }

    pxn8image.onload = onImageLoad;
    //
    // wph 20060714 notify ON_IMAGE_LOAD listeners
    //
    PXN8.event.removeListener(pxn8image,"load",PXN8.imageLoadNotifier);
    PXN8.event.addListener(pxn8image,"load",PXN8.imageLoadNotifier);

};


/***************************************************************************

PXN8.select()
=============
Selects an area of the image. Use this function to programmatically select
an area of the image.

Parameters
----------
* left : The start position of the selected area along the X axis (starts at left).
* top : The start position of the selected area along the Y axis (starts at top).
* width : The width of the selected area.
* height : The height of the selected area.

Alternatively you can provide a single object as a parameter (with the following properties)...

* left : The start position of the selected area along the X axis (starts at left).
* top : The start position of the selected area along the Y axis (starts at top).
* width : The width of the selected area.
* height : The height of the selected area.

Example
-------
The following example code will select the top half of the photo...

    var theImage = document.getElementById("pxn8_image");
    var zoomValue = PXN8.zoom.value();
    //
    // the image's width and height might not reflect the true width and height if the image
    // has been zoomed in or out.
    //
    var realWidth = theImage.width / zoomValue;
    var realHeight = theImage.height / zoomValue;

    PXN8.select(0, 0, realWidth, realHeight / 2);

For an example of limiting the selection area by size please <a href="example-selection-limit.html">click here</a>

Related
-------
PXN8.getSelection PXN8.selectByRatio

***/
PXN8.select = function (startX, startY, width, height)
{
    var self = PXN8;

    if (typeof startX == "object"){
        var sel = startX;
        startY = sel.top;
        width = sel.width;
        height = sel.height;
        startX = sel.left;
    }

    self.sx = startX;
    self.sy = startY;
    self.ex = self.sx + width;
    self.ey = self.sy + height;

	 if (PXN8.select.constrainToImageBounds == true)
	 {
		  if (self.sx < 0) self.sx = 0;

		  if (self.sy < 0) self.sy = 0;

		  if (self.ex > PXN8.image.width){
				self.ex = PXN8.image.width;
				self.sx = self.ex - width;
		  }

		  if (self.ey > PXN8.image.height){
				self.ey = PXN8.image.height;
				self.sy = self.ey - height;
		  }
	 }

    /*
     * update the field values
     */
    self.position.x = startX;
    self.position.y = startY;

    var selection = self.getSelection();
    self.listener.notify(PXN8.ON_SELECTION_CHANGE,selection);
};
PXN8.select.constrainToImageBounds = true;

PXN8.select.disabler = function(eventType,selection)
{
    if (selection.width > 0 || selection.height > 0){
        PXN8.unselect();
        return;
    }
};
/****************************************************************************

PXN8.select.enable()
===================
Enable selection of parts of the photo. No need to call this at startup because
Selection will be enabled by default.

Parameters
----------
* enabled : A boolean value, true to enable the selection. false to disable it.
* unselect (optional) : A boolean value indicating whether or not to discard the current selection.

Example
-------

Please see the <a href="example-selection-disable.html">Disabling Selection</a> example, which demonstrates how to disable and enable selection.

Returns
-------
null

Related
-------

***/
PXN8.select.enable = function(enabled,unselect)
{
	 var canvas = document.getElementById("pxn8_canvas");
    if (enabled){
        PXN8.listener.remove(PXN8.ON_SELECTION_CHANGE,PXN8.select.disabler);
    }
    else{
        if (unselect){
            PXN8.unselect();
        }
        PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.select.disabler);
    }

};

/**
 * listen to both ON_SELECTION_CHANGE and ON_ZOOM_CHANGE
 */
PXN8.select.defaultListener = function(eventType,selection)
{
    var _ = PXN8.dom;
    var self = PXN8;

    var theImg = _.id("pxn8_image");
    var selectRect = _.id("pxn8_select_rect");
    var leftRect = _.id("pxn8_left_rect");
    var rightRect = _.id("pxn8_right_rect");
    var topRect = _.id("pxn8_top_rect");
    var bottomRect = _.id("pxn8_bottom_rect");
    var topleftRect = _.id("pxn8_topleft_rect");
    var toprightRect = _.id("pxn8_topright_rect");
    var bottomleftRect = _.id("pxn8_bottomleft_rect");
    var bottomrightRect = _.id("pxn8_bottomright_rect");

    PXN8.show.position();
    PXN8.show.selection();

    if ((eventType == PXN8.ON_SELECTION_CHANGE && selection == null) ||
        eventType == PXN8.ON_ZOOM_CHANGE){
        selection = self.getSelection();
    }

    /*
     * has any selection been made yet ?
     */
    if (selection.width <=0 && selection.height <= 0 ){
        selectRect.style.display = "none";

        leftRect.style.display = "none";
        rightRect.style.display = "none";
        topRect.style.display = "none";
        bottomRect.style.display = "none";

        topleftRect.style.display = "none";
        toprightRect.style.display = "none";
        bottomleftRect.style.display = "none";
        bottomrightRect.style.display = "none";
        return;
    }

    var zoom = PXN8.zoom.value();

    var sel = {};
    for (var i in selection){
        // watch out for prototype !
        if (typeof selection[i] != "function"){
            sel[i] = Math.floor(selection[i] * zoom);
        }
    }

/*
 *  wph 20081121 - constraints take care of this
 *	 (commented out because when a selection area clips the right or bottom, the resize handles
 *  can be out of place.
 *
 if (sel.left + sel.width > theImg.width || sel.top + sel.height > theImg.height){
  return;
 }
*/


    var bh = theImg.height - (sel.top + sel.height);
	 if (bh < 0){
		  bh = 0;
	 }
	 bh = bh + "px";
    var bt = sel.top + sel.height + "px";
    var th = sel.top + "px";
    var ll = "0px";
    var lw = sel.left + "px";
    var rw = (theImg.width - (sel.left + sel.width));
	 if (rw < 0){
		  rw = 0;
	 }
	 rw = rw + "px";

    var rl = sel.left + sel.width + "px";

    topleftRect.style.display = "block";
    topleftRect.style.top = "0px";
    topleftRect.style.left = ll;
    topleftRect.style.width = lw;
    topleftRect.style.height = th;

    leftRect.style.display = "block";
    leftRect.style.top = sel.top + "px";
    leftRect.style.left = ll;
    leftRect.style.width =  lw;
    leftRect.style.height = sel.height + "px";

    bottomleftRect.style.display = "block";
    bottomleftRect.style.top = bt;
    bottomleftRect.style.left = ll;
    bottomleftRect.style.width =  lw;
    bottomleftRect.style.height = bh;

    topRect.style.display = "block";
    topRect.style.top = "0px";
    topRect.style.left = sel.left + "px";
    topRect.style.width = sel.width + "px";
    topRect.style.height = th;

    selectRect.style.top  = sel.top + "px";
    selectRect.style.left = sel.left + "px";
    selectRect.style.width = sel.width + "px";
    selectRect.style.height = sel.height + "px";
    selectRect.style.display = "block";
    selectRect.style.zIndex = 100;

    bottomRect.style.display = "block";
    bottomRect.style.top = bt;
    bottomRect.style.left = sel.left + "px";
    bottomRect.style.width = sel.width + "px";
    bottomRect.style.height = bh;

    toprightRect.style.display = "block";
    toprightRect.style.top = "0px";
    toprightRect.style.left = rl;
    toprightRect.style.width = rw;
    toprightRect.style.height = th;

    rightRect.style.display = "block";
    rightRect.style.top = sel.top + "px";
    rightRect.style.left = rl;
    rightRect.style.width = rw;
    rightRect.style.height = sel.height + "px";

    bottomrightRect.style.display = "block";
    bottomrightRect.style.top = bt;
    bottomrightRect.style.left = rl;
    bottomrightRect.style.width = rw;
    bottomrightRect.style.height = bh;


    /**
     * to enable marching ants in CSS
     *

#pxn8_top_rect { background: url(/ants.gif) bottom left repeat-x; }
#pxn8_bottom_rect { background: url(/ants.gif) top left repeat-x; }
#pxn8_left_rect { background: url(/antsvertical.gif) top right repeat-y; }
#pxn8_right_rect { background: url(/antsvertical.gif) top left repeat-y; }

    */
};
/**
 * PXN8.select.defaultListener must be the first listener added !
 */
PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.select.defaultListener);
PXN8.listener.add(PXN8.ON_ZOOM_CHANGE,PXN8.select.defaultListener);


/****************************************************************************

PXN8.getSelection()
===================
Return a Rect that represents the current selection.

Returns
-------
An object with the following properties...
* top : The topmost coordinate on the Y axis of the selected area.
* left: The leftmost coordinate on the X axis of the selected area.
* width: The width of the selected area.
* height: The height of the selected area.

Example
-------
    var selectedArea = PXN8.getSelection();
    alert ("You have selected an area " + selectedArea.width + "x" + selectedArea.height );
Related
-------
PXN8.select PXN8.selectByRatio

***/
PXN8.getSelection = function()
{
    var rect = {};
    var self = PXN8;

    rect.width = self.ex>self.sx?self.ex-self.sx:self.sx-self.ex;
    rect.height = self.ey>self.sy?self.ey-self.sy:self.sy-self.ey;
    rect.left = self.ex>self.sx?self.sx:self.ex;
    rect.top = self.ey>self.sy?self.sy:self.ey;
/*
 * wph 20081121 - why constrain here ?
 *
    rect.left = rect.left<0?0:rect.left;
    rect.top = rect.top<0?0:rect.top;
*/
    return rect;
};

/****************************************************************************

PXN8.selectByRatio()
====================
Selects an area using an aspect ratio of the form "WxH" where W is width and H is height.

Parameters
----------
* ratio : The ratio is expressed as a string e.g. "4x6".
* override (optional) : A boolean value indicating whether or not to ignore the images's dimensions (don't optimize selection size).
The default value if none is specified is *false*.

Example
-------

Assuming the user is working with a photo which is 300x225, calling
PXN8.selectByRatio("2x3") will result in the following selection...
    {width: 300, height: 200, left: 0, top: 12}
<table>
  <tr><td>Original</td><td>PXN8.selectByRatio("2x3")</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225sbr2x3.jpg"/></td>
  </tr>
  <tr><td>Original</td><td>PXN8.selectByRatio("2x3",true)</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225sbr2x3true.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectAll PXN8.unselect PXN8.getSelection

***/
PXN8.selectByRatio = function(ratio,override)
{
    var _ = PXN8.dom;
    var self = PXN8;

    if (typeof ratio != "string"){
        alert("Ratio must be expressed as a string e.g. '4x6'");
        return;
    }

	 if (!PXN8.ready)
	 {
		  // image hasn't loaded yet.
		  // put self on queue for execution when image has loaded.
		  //
        PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.selectByRatio(ratio,override);
		  });
		  return;
	 }

    var ih = PXN8.image.height;
    var iw = PXN8.image.width;


	 var sepIndex = ratio.indexOf("x");

	 if (sepIndex != -1)
	 {
		  var rw = parseFloat(ratio.substring(0,sepIndex));
		  var rh = parseFloat(ratio.substring(sepIndex+1));

        if (override){
            PXN8.aspectRatio.width = rw;
            PXN8.aspectRatio.height = rh;
        }else{
            if (iw > ih){
                if (rw > rh){
                    PXN8.aspectRatio.width = rw;
                    PXN8.aspectRatio.height = rh;
                }else{
                    PXN8.aspectRatio.width = rh;
                    PXN8.aspectRatio.height = rw;
                }
            }else{
                if (rw > rh){
                    PXN8.aspectRatio.width = rh;
                    PXN8.aspectRatio.height = rw;
                }else{
                    PXN8.aspectRatio.width = rw;
                    PXN8.aspectRatio.height = rh;
                }
            }
        }
        rw = PXN8.aspectRatio.width;
        rh = PXN8.aspectRatio.height;

    }else{
        PXN8.aspectRatio.width = 0;
        PXN8.aspectRatio.height = 0;

        PXN8.resize.enable(["n","s","e","w"],true);

        return;
    }
    PXN8.resize.enable(["n","s","e","w"],false);

    var left = 0;
    var top = 0;
    var width = 0;
    var height = 0;

    var fitWidth = function(){
        width = iw;
        height = Math.round(width / rw * rh);
        top = Math.round( (ih /2 ) - (height / 2));
    };
    var fitHeight = function(){
        height = ih;
        width = Math.round(height / rh * rw);
        left = Math.round((iw / 2) - (width /2));
    };

    if (iw > ih){
        if ((iw / ih) > (rw / rh)){
            fitHeight();
        }else{
            fitWidth();
        }
    }else{
        if ((ih / iw) > (rh / rw)){
            fitWidth();
        }else{
            fitHeight();
        }
    }
    self.select(left,top,width,height);
};



/****************************************************************************

PXN8.rotateSelection()
======================
Rotates the selection area by 90 degrees.

Example
-------
<table>
  <tr><td>Before</td><td>After PXN8.rotateSelection()</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225b4rotatesel.jpg"/></td>
     <td valign="top"><img src="pigeon300x225afterrotatesel.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectByRatio PXN8.selectAll PXN8.unselect PXN8.getSelection

***/
PXN8.rotateSelection = function()
{

	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.rotateSelection();
		  });
		  return;
	 }
    var sel = PXN8.getSelection();
	 var imgSize = PXN8.getImageSize();

    var cx = sel.left + (sel.width / 2);
    var cy = sel.top + (sel.height / 2);
	 //
	 // wph 20080624 Constrain selection to image size
	 //
    if (sel.width > imgSize.height){
        sel.height = sel.height * (imgSize.height / sel.width);
        sel.width = imgSize.height;
    }
    if (sel.height > imgSize.width){
        sel.width = sel.width * (imgSize.width / sel.height);
        sel.height = imgSize.width;
    }

    PXN8.select (cx - sel.height/2, cy - sel.width /2, sel.height, sel.width);

    //
    // swap width and height of aspectRatio
    //
    var temp = PXN8.aspectRatio.width;

    PXN8.aspectRatio.width = PXN8.aspectRatio.height;
    PXN8.aspectRatio.height = temp;
    //
    // snap to the current enforced aspect ratio
    //
    PXN8.snapToAspectRatio();

};

/****************************************************************************

PXN8.selectAll()
================
Selects the entire photo area.

Example
-------
<table>
  <tr><td>Before</td><td>After *PXN8.selectAll()*</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225selectall.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectByRatio PXN8.rotateSelection PXN8.unselect PXN8.getSelection

***/
PXN8.selectAll = function()
{
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.selectAll();
		  });
		  return;
	 }
    PXN8.select( 0, 0, PXN8.image.width, PXN8.image.height);
};
/****************************************************************************

PXN8.unselect()
===============
Unselect the entire photo. The selection will be discarded.

***/
PXN8.unselect = function ()
{
    PXN8.select( 0, 0, 0,0);
};

/* ============================================================================
 *
 * Miscellaneous top-level functions
 *
 */

/***************************************************************************

PXN8.getUncompressedImage()
===========================
Returns the relative URL to the uncompressed 100% full quality image.
This version of the image is not normally downloaded and displayed in the browser
during an editing session because it is typically much larger than the more bandwidth-friendly
lower resolution image (typically using 85% quality). Normally this
function be called from pxn8_save_image or some other function which
will save the image to the server.

Returns
-------
A path (relative the PXN8.root) to the uncompressed image if the image has changed or <em>false</em> if no changes have been made.

Examples
--------

    var uncompressed = PXN8.getUncompressedImage();

	 if (uncompressed != false)
	 {
		  // the image has been modified
		  // uncompressed = "cache/01_feabcdd1d0workingjpg.jpg";
        // view the image
        document.location = PXN8.server + PXN8.root + "/" +	 uncompressed;
    }

Related
-------
PXN8.save.toServer pxn8_save_image

***/
PXN8.getUncompressedImage = function()
{
    if (PXN8.responses[PXN8.opNumber]){
        return PXN8.responses[PXN8.opNumber].uncompressed;
    } else {
        return false;
    }
};


/**
 * -- function:    PXN8.listener.notify
 * -- description: Called by various methods to notify listeners
 * -- param eventType (ON_ZOOM_CHANGE, ON_IMAGE_CHANGE, ON_SELECTION_CHANGE etc)
 */
PXN8.listener.notify = function(eventType,source)
{
    var self = PXN8.listener;
    var listeners = self.listenersByType[eventType];
    if (listeners){
        for (var i = 0; i < listeners.length; i++){
            var listener = listeners[i];
            if (listener != null){
                listener(eventType,source);
                if (listener.onceOnly){
                    PXN8.listener.remove(eventType,listener);
                    i--;
                }

            }
        }
    }
};

/**
 * This function should be the first ON_IMAGE_LOAD function called.
 * It does all of the housekeeping necessary for PXN8.
 * All other ON_IMAGE_LOAD callbacks should be called after this !!!
 */
PXN8.imageLoadHousekeeping = function(eventType,theImage)
{
    var _ = PXN8.dom;

    theImage = _.id("pxn8_buffered_image");

    if (theImage == null){
        theImage = _.id("pxn8_image");
    }

    if (PXN8.log){PXN8.log.trace("image " + theImage.src + " has loaded");}

    PXN8.image.width = theImage.width;
    PXN8.image.height = theImage.height;

    var iw = PXN8.image.width;
    var ih = PXN8.image.height;

    /**
     * wph 20070630 : now we have the original size of the buffered image,
     * change it's width & height attributes to match the zoomed size before
     * copying the buffer to the display.
     */

    var zoomFactor = PXN8.zoom.value();

    theImage.width = iw * zoomFactor;
    theImage.height = ih * zoomFactor;

    //
    // now display the buffer contents
    //
    _.ac(_.cl("pxn8_image_container"),theImage);
    theImage.id = "pxn8_image";

    PXN8.show.size();

    PXN8.priv.addImageToHistory(theImage.src);

    var selection = PXN8.getSelection();
    if (selection.width > iw ||
        selection.left > iw ||
        selection.height > ih ||
        selection.top > ih)
    {
        PXN8.unselect();
    }else{
        //
        // might need to move the selection
        //
        var moved = false;

        if (selection.left + selection.width > iw){
            selection.left = iw - selection.width;
            moved = true;
        }
        if (selection.top + selection.height > ih){
            selection.top = ih - selection.height;
            moved = true;
        }
        if (moved){
            PXN8.select(selection);
        }
    }
    //
    // may need to reposition the bounding non-selected areas regardless
    // of whether the selection has changed or not
    //
    PXN8.listener.notify(PXN8.ON_SELECTION_CHANGE,selection);

    PXN8.imagesBySrc[theImage.src] = true;
    PXN8.listener.notify(PXN8.ON_IMAGE_CHANGE);

    var timer = _.id("pxn8_timer");
    if (timer){
        timer.style.display = "none";
    }

    return theImage;

};
/**
 * N.B. This should be the first call to PXN8.listener.add(PXN8.ON_IMAGE_LOAD,...)
 * Housekeeping should be done _BEFORE_ all other ON_IMAGE_LOAD handlers are called
 */
PXN8.listener.add(PXN8.ON_IMAGE_LOAD,PXN8.imageLoadHousekeeping);

/* ============================================================================
 * logging now uses log4javascript if present
 */
if (typeof log4javascript != "undefined"){
	 PXN8.log = log4javascript.getDefaultLogger();
}else{
	 PXN8.log = false;
}

/* ============================================================================ */



/**
 * wph 20070124
 * Return an operation based on the operation number.
 * Unlike PXN8.getScript() this returns a reference to the operation object - not a copy.
 * Changes to the returned object will be reflected the next time a server-call is made.
 */
PXN8.getOperation = function(i)
{
    if (i > PXN8.opNumber){
        return null;
    }
    return PXN8.history[i];
};
/**
 * Return an image operation where index is the user-op number
 *
 */
PXN8.getUserOperation = function(index)
{
    var self = PXN8;
    var result = null;
    var lastIndex = 0;
    for (var i = 0;i < index; i++){
        lastIndex += self.offsets[i];
    }
    return self.history[lastIndex];
};


/***************************************************************************

PXN8.getScript()
================
Return a list (a copy) of all the operations which have been performed (doesn't include undone operations).

Returns
-------
A array of objects each of which is a distinct operation which was performed on the image.

Examples
--------
The following code retrieves all of the operations performed in the current editing session and displays a series
of alerts telling the user what they have done.

    var whatYouDid = PXN8.getScript();
    for (var i = 0; i < whatYouDid.length; i++){
        alert("you performed a '" + whatYouDid[i].operation + "' operation");
    }

***/
PXN8.getScript = function()
{
    var self = PXN8;

    var result = new Array();

    //
    // WPH first get the real index of the last operation (this will not
    // necessarily be opNumber . E.g. if the user performs the following operations...
    //
    // [1] Rotate
    // [2] Enhance + Normalize (2 operations combined into one single user operation)
    // [3] Redeye
    // ... then the history, offsets and opNumber values will be as follows
    //
    // history  [fetch,rotate,enhance,normalize,redeye]
    // offsets  [1,1,2,1]
    // opNumber 3

    var lastIndex = 0;
    for (var i = 0;i <= self.opNumber; i++){
        lastIndex += self.offsets[i];
    }

    //for (var i = 0;i <= self.opNumber; i++){
    for (var i = 0;i < lastIndex; i++){

        var original = self.history[i];
        //
        // make a copy of the object
        //
        var duplicate = {};
        for (var j in original){
            duplicate[j] = original[j];
        }
        result.push(duplicate);
    }

    return result;
};
/***************************************************************************

PXN8.isUpdating()
=================
Is pixenate currently updating the photo ?

Returns
-------
*true* or *false* depending on whether the photo is currently being updated.

***/
PXN8.isUpdating = function()
{
    return PXN8.updating;
};

/**
 * -- function curry
 * -- description Currying is a way of 'baking-in' an object to a function
 * Its a way of permanently binding an object and a function together
 * in effect create a new distinct function with the object embedded in it.
 * It's one of the cool higher-order programming features of dynamic languages
 * like Javascript and Perl.
 * PXN8.curry is a functor - a function which returns a function
 * -- param object The object to be baked in to the function
 * -- param func The function into which the object will be baked.
 */
PXN8.curry = function(func,object)
{
    return function(){
        return func(object);
    };
};

/*
 * Update the UI to inform the user that the image is being updated
 * The msg param is optional - it contains text that will be displayed in
 * the *pxn8_timer* DIV. In most cases this is simply 'Updating image. Please wait...'
 * but it can be different - e.g. 'Saving image. Please wait...'
 */
PXN8.prepareForSubmit = function(msg)
{
    var _ = PXN8.dom;

    if (!msg){
        msg = PXN8.strings["UPDATING"];
    }

    var timer = _.id("pxn8_timer");
    if (!timer){
        timer = _.ce("div",{id: "pxn8_timer"});
        _.ac(timer,_.tx(msg));
	     var canvas = _.id("pxn8_canvas");
        _.ac(canvas,timer);
    }
    if (timer){
        _.ac(_.cl(timer),_.tx(msg));
        timer.style.display = 'block';
        var theImage = _.id("pxn8_image");
        var imagePos = _.ep(theImage);
        timer.style.width  = Math.max(200,theImage.width) + "px";
    }
    PXN8.updating = true;
};

/**
 * For a given point calculate it's real location when
 * the scroll area is taken into account.
 */
PXN8.scrolledPoint = function (x,y)
{
    var result = {"x":x,"y":y};

    var canvas = document.getElementById("pxn8_canvas");
    if (canvas.parentNode.id == "pxn8_scroller"){
        var scroller = document.getElementById("pxn8_scroller");
        result.x += scroller.scrollLeft;
        result.y += scroller.scrollTop;
    }
    return result;
};
/**
 *  Get the point to which the window is scrolled (useful for freehand drawing).
 */
PXN8.getWindowScrollPoint = function()
{
	 var scrOfX = 0, scrOfY = 0;
	 if( typeof( window.pageYOffset ) == 'number' ) {
		  //Netscape compliant
		  scrOfY = window.pageYOffset;
		  scrOfX = window.pageXOffset;
	 } else if( document.body && ( document.body.scrollLeft || document.body.scrollTop ) ) {
		  //DOM compliant
		  scrOfY = document.body.scrollTop;
		  scrOfX = document.body.scrollLeft;
	 } else if( document.documentElement && ( document.documentElement.scrollLeft || document.documentElement.scrollTop ) ) {
		  //IE6 standards compliant mode
		  scrOfY = document.documentElement.scrollTop;
		  scrOfX = document.documentElement.scrollLeft;
	 }
	 return {"x": scrOfX, "y": scrOfY };
};
/**
 * -- function PXN8.createPin
 * -- description Create a pin for placing on top of an image
 * -- param pinId The unique Id to be given to the created pin image
 * -- param imgSrc The image src attribute
 */
PXN8.createPin = function (pinId,imgSrc)
{
    var pinElement = document.createElement("img");
    pinElement.id = pinId;
    pinElement.className = "pin";
    pinElement.src = imgSrc;
    pinElement.style.position = "absolute";
    return pinElement;
};

/**
 * -- function mousePointToElementPoint
 * -- description Convert a mouse event point to a relative point for a given element
 * -- param mx The x value for the mouse event
 * -- param my The y value for the mouse event
 */
PXN8.mousePointToElementPoint = function(mx,my)
{
    var _ = PXN8.dom;
    var result = {};
    var canvas = _.id("pxn8_canvas");
    var imageBounds = _.eb(canvas);
    var scrolledPoint = PXN8.scrolledPoint(mx,my);
    var zoom = PXN8.zoom.value();

    result.x = Math.round((scrolledPoint.x - imageBounds.x)/zoom);
    result.y = Math.round((scrolledPoint.y - imageBounds.y)/zoom);

    if (canvas.style.borderWidth){

        var borderWidth = parseInt(canvas.style.borderWidth);
        result.x -= borderWidth;
        result.y -= borderWidth;
        if (result.x < 0){
            result.x = 0;
        }
        if (result.y < 0){
            result.y = 0;
        }
    }
    return result;
};

/***************************************************************************

PXN8.getImageSize()
=====================
Returns the real width and height of the image.

Returns
-------
Returns an object with <em>width</em> and <em>height</em> attributes - the real width and height of the image.

***/
PXN8.getImageSize = function()
{
    var imgElement = document.getElementById("pxn8_image");
    var zoomValue = PXN8.zoom.value();
    var realWidth = imgElement.width / zoomValue;
    var realHeight = imgElement.height / zoomValue;

    return {width: realWidth, height: realHeight};

};



/***************************************************************************

PXN8.objectToString()
=====================
Converts a given javascript object to a string which can be evaluated as a JSON
expression.

Parameters
----------
* object : The object to be converted into a string.

Returns
-------
A string which can later be evaluated as a JSON expression.
Boolean literals (*true* and *false*) are converted to strings.

Examples
--------

    var myObject = {
                    name: "Walter Higgins",
                    contacts: ["John Doe", "K DeLong"],
                    available: false
                    };

    var myObjectAsString = PXN8.objectToString(myObject);

    // myObjectAsString = '{"name":"Walter Higgins", "contacts":["John Doe","K DeLong"],"available":"false"}';


***/
PXN8.objectToString = function(obj)
{
    var s = "";

    var propToString = function(prop){return "\"" + prop + "\":";};

    var operationAlwaysFirst = function(a,b){
        if (a == "operation"){ return -1;}
        if (b == "operation"){ return 1;}
        return a > b ? 1 : b > a ? -1: 0;
    };

    var types = {array : {s:"[",e:"]",
                          indexer: function(o){ var result = new Array(); for (var i =0;i < o.length;i++){result.push(i);}return result;},
                          pusher: function(array,o,i){array.push(o);}},
                 object: {s:"{",e:"}",
                          indexer: function(o){ var result = new Array(); for (var i in o){ if (typeof o[i] != "function"){ result.push(i);}}return result.sort(operationAlwaysFirst);},
                          pusher: function(array,o,i){array.push(propToString(i) + o);}}
    };

    var type = "object";

    if (PXN8.isArray(obj)){
        type = "array";
    }

    s = types[type].s;

    var props = new Array();

    var pusher = types[type].pusher;

    var indexes = types[type].indexer(obj);

    for (var j = 0;j < indexes.length; j++){
        var i = indexes[j];
        if (typeof obj[i] == "function"){
            continue;
        }
        if (typeof obj[i] == "string"){
            pusher(props,"\"" +  obj[i] + "\"",i);
        }else if (typeof obj[i] == "object"){
            pusher(props, PXN8.objectToString(obj[i]),i);
        }else if (typeof obj[i] == "boolean"){
            pusher(props, "\"" + obj[i] + "\"",i);
        }else{
            pusher(props, obj[i],i);
        }
    }

    for (var i = 0;i < props.length; i++){
        s = s + props[i];
        if (i < props.length-1){
            s += ",";
        }
    }
    s += types[type].e;

    return s;
};

/**
 * Is an object an Array ?
 */
PXN8.isArray = function(o)
{
	return (o && typeof o == 'object') && o.constructor == Array;
};

/**
 * Return a random hexadecimal value in the range 0 - 65535 (0000 - FFFF)
 */
PXN8.randomHex = function()
{
    return (Math.round(Math.random()*65535)).toString(16)
};

PXN8.getImageBuffer = function()
{
    var _ = PXN8.dom;

    var buffer = _.id("pxn8_buffer");
    if (!buffer){
        buffer = _.ce("div",{id: "pxn8_buffer"});
        _.ac(document.body,buffer);
        buffer.style.width = "1px";
        buffer.style.height = "1px";
        buffer.style.overflow = "hidden";
    }
    return buffer;
};

/**
 * Replaces the current editing image with a new one
 */
PXN8.replaceImage = function(imageurl)
{
    var _ = PXN8.dom;

    var buffer = PXN8.getImageBuffer();

    // clear the buffer
    _.cl(buffer);

    //
    // create a new image element with an id of 'pxn8_buffered_image'
    //
    var theImage = _.ce("img",{id: "pxn8_buffered_image"});

    //
    // add the image to the buffer
    //
    _.ac(buffer,theImage);

    //
    // wph 20070630 : tell the user that the photo is loading
    // there is no visual clue now because the photo is first loaded into a
    // non-visible buffer.
    //
    var timer = _.id("pxn8_timer");
    if (timer){
        timer.style.display = "block";
        _.ac(_.cl(timer),_.tx("Loading photo. Please wait..."));
    }

    //
    // add the onload listener *BEFORE* setting the source
    // so that the listener will get notified in IE.
    //

    var notified = false;
    var closure = {
        onload: function(){
            if (!notified){
                PXN8.imageLoadNotifier();
                notified = true;
            }
        }
    };
    //
    // ensure that either the .onload handler or the preferred onload event
    // handler gets called but don't want them stepping on each other's toes
    //
    PXN8.event.addListener(theImage,"load",closure.onload);
    theImage.onload = closure.onload;

    //
    // set the image's src attribute
    //
    theImage.src = imageurl;

    PXN8.show.size();


};

/*
 * Called when the AJAX request has returned
 */
PXN8.imageUpdateDone = function (jsonResponse)
{
	 var errorMsg = null;
    var _ = PXN8.dom;
	 var status = null;

	 if (PXN8.log){PXN8.log.trace("PXN8.imageUpdateDone(" + jsonResponse + ")"); }

    var targetDiv = _.id("pxn8_image_container");
    PXN8.response = jsonResponse;

    if (jsonResponse && jsonResponse.status == "OK"){
        //
        // store the entire response object in the list of responses
        //
        PXN8.responses[PXN8.opNumber] = jsonResponse;
        //
        // wph 20060513: Workaround for IE's over-aggressive
        // image caching.
        // see IE bugs # 4
        // http://www.sourcelabs.com/blogs/ajb/2006/04/rocky_shoals_of_ajax_developme.html
	     //
        if (document.all){
            //
            // wph 20070226 : The passed in JSON response may have been
            // cached. If it was then don't force the image to reload.
            //
            /*
             * wph 20070226 : I can no longer see a need for this
             * as the PXN8.replaceOnSave flag should be set to true
             * if the image is to be replaced with a new one.
             * This workaround may have been needed during development
             * but should not be required for production as the server-side
             * Pixenate code ensures each image has a unique ID.
             * (the case of between session changes is covered by the use
             *  of the PXN8.replaceOnSave flage - see PXN8.initialize()'s use of the
             *  'random' attribute in the first 'fetch' operation).
             *
             if (typeof jsonResponse["cached"] == "undefined"){
             jsonResponse.image += "?rnd=" + PXN8.randomHex();
             jsonResponse["cached"] = true;
             }
            */
        }
        //
        // prepend the PXN8 root path to the returned path
        //
        var newImageSrc = PXN8.server + PXN8.root + "/" + jsonResponse.image;
        //
        // delete the old pxn8_image element and add a new pxn8_image element
        //
        PXN8.replaceImage(newImageSrc);
    }else{
        status = PXN8.response;
        if (PXN8.response && typeof PXN8.response == "object")
		  {
            status = PXN8.response.status;

				errorMsg = "An error occurred while updating the image.\n" +
                "status: " + status + "\n" +
                "errorMessage: " + PXN8.response.errorMessage;
				if (PXN8.log){ PXN8.log.error(errorMsg); }

            alert(errorMsg);
        }else{
				errorMsg = "An error occurred while updating the image.\nstatus:" + status ;

				if (PXN8.log){ PXN8.log.error(errorMsg); }
            alert(errorMsg);
        }

        PXN8.listener.notify(PXN8.ON_IMAGE_ERROR);
        /**
         * wph 20070530 : Set PXN8.updating = false so that other tasks can be performed
         */
        PXN8.updating = false;
        // decrement the PXN8.opNumber variable !!!
        PXN8.opNumber--;

        //
        // hide the timer !!!
        //
        var timer = _.id("pxn8_timer");
        if (timer){
            timer.style.display = "none";
        }
    }

    //
    // mark as done
    //
    // wph 20070223: What if the image is large, takes a while to load but the user clicks
    // undo while the new image is loading ?
    //  Imagine the following scenario...
    //  user uploads a large image - PXN8.opNumber is 0
    //  user rotates the image - PXN8.opNumber is 1
    //  the server completes the rotate op and returns a JSON response pointing to the new image
    //  pixenate starts loading the new image
    //  image is large so loads slowly - user clicks 'undo' - PXN8.opNumber is 0
    //  ...but unknown to the user, the new images' onload method will still get called !
    //     the onload method replaces PXN8.images[PXN8.opNumber] with the new image data
    //     (pxn8.opNumber has been reset to 0) so the image that the user sees and the image
    //     which the pixenate UNDO/REDO model sees are different. Since the undo/redo mechanism
    //     relies on the user NOT clicking 'undo' / 'redo' until after the new image is loaded.
    //     In order for the current undo/redo mechanism to work, the operation must be
    //     LOCKED between firing the initial AJAX request and the image loading
    //     so instead of setting PXN8.updating to false when the AJAX request returns
    //     ( as I do here ), PXN8.updating should be set to false ONLY WHEN THE NEW IMAGE
    //     HAS LOADED !
    // PXN8.updating = false;

    //
    // everything from here on used to be in PXN8.priv.postImageLoad()
    //
    var theImage = _.id("pxn8_image");
    theImage.onerror = function(){
        alert(PXN8.strings.IMAGE_ON_ERROR1 + theImage.src + PXN8.strings.IMAGE_ON_ERROR2);
        PXN8.listener.notify(PXN8.ON_IMAGE_ERROR);
    };

    //
    // IE Bug: If an image with the same URL has already been loaded
    // then the onload method is never called - need to explicitly call the
    // onloadFunc method so that listeners get notified etc.
    //
    /*
     * wph 20070508 : see PXN8.imageLoadHousekeeping !
     *
     *
    if (PXN8.imagesBySrc[theImage.src]){
        onloadFunc();
    }else{
        theImage.onload = onloadFunc;
    }

    PXN8.show.zoom();
    */
};

/* ============================================================================
 *
 * FUNCTIONS TO DISPLAY IMAGE INFORMATION
 */
PXN8.show = {};

/**
 * display selection info
 */
PXN8.show.selection = function()
{
    var _ = PXN8.dom;

    var selectionField = _.id("pxn8_selection_size");
    if (selectionField){
        var text = "N/A";
        if (PXN8.ex - PXN8.sx > 0){
            text = (PXN8.ex-PXN8.sx) + "," + (PXN8.ey-PXN8.sy);
        }
        _.ac(_.cl(selectionField),_.tx(text));
    }
};

/**
 * display position info
 */
PXN8.show.position = function()
{
    var _ = PXN8.dom;

    var posInfo = _.id("pxn8_mouse_pos");
    if (posInfo){
        var text = PXN8.position.x + "," + PXN8.position.y;
        _.ac(_.cl(posInfo),_.tx(text));
    }
};

/**
 * display position info
 */
PXN8.show.zoom = function(t,v)
{
    var _ = PXN8.dom;

    var zoomInfo = _.id("pxn8_zoom");
    if (zoomInfo){
        var text = Math.round((PXN8.zoom.value() * 100)) + "%";
        _.ac(_.cl(zoomInfo),_.tx(text));
    }
};

/**
 * display size info
 */
PXN8.show.size = function ()
{
    var _ = PXN8.dom;
    var sizeInfo = _.id("pxn8_image_size");
    if (sizeInfo){
        var text = PXN8.image.width + "x" + PXN8.image.height;
        _.ac(_.cl(sizeInfo),_.tx(text));
    }
};

/**
 * Display a soft alert that disappears after a short time
 */
PXN8.show.alert = function (message,duration)
{
    var _ = PXN8.dom;

    duration = duration || 1000;

    var warning = _.id("pxn8_warning");
    if (!warning){
        warning = _.ce("div",{id: "pxn8_warning",className: "warning"});

		  // looks better if appended to end of canvas - photo isn't bumped down
		  //_.id("pxn8_canvas").insertBefore(warning,_.id("pxn8_image_container"));

		  _.ac(_.id("pxn8_canvas"),warning);
    }else{
		  //
		  // make sure it's visible - it might have been made invisible by a previous fadeout()
		  //
        warning.style.display = "block";
	 }
	 _.ac(_.cl(warning),_.tx(message));
	 // wph 20081205 - reset opacity to 100% - (might have been set to 0 by last fadeout() )
    _.opacity(warning,90);

    warning.style.width  = (PXN8.image.width>200?PXN8.image.width:200) + "px";

    setTimeout("PXN8.fade.init();PXN8.fade.fadeout('pxn8_warning',false);",duration);
};



/* ============================================================================
 *
 * Fade functions - make a HTML element fade in and out
 */

PXN8.fade = {
	values: [0.99,0.85, 0.70, 0.55, 0.40, 0.25, 0.10, 0],
	times:      [75, 75,  75,  75,  75,  75,  75,  75],
	i: 0,
	stopfadeout: false
};

PXN8.fade.init = function(){ var self = PXN8.fade; self.i =0; self.stopfadeout = false;};

PXN8.fade.cancel = function(){ var self = PXN8.fade; self.stopfadeout = true; };

PXN8.fade.fadeout = function(eltid,destroyOnFade)
{
    var _ = PXN8.dom;
    var self = PXN8.fade;

    if (self.stopfadeout){
        return;
    }
    _.opacity(eltid,self.values[self.i]);
    if (self.i < self.values.length -1 ){
        self.i++;
        setTimeout("PXN8.fade.fadeout('" + eltid + "'," + destroyOnFade + ");",self.times[self.i]);
    }else{
        if (destroyOnFade){
            var node = _.id(eltid);
            // it's quite possible that the element has already been destroyed !
            if (!node){
                return;
            }else{
                var parent = node.parentNode;
                parent.removeChild(node);
            }
        }else{
				//
				// wph 20081205 - just make it invisible
				//
				_.id(eltid).style.display = "none";
		  }
    }
};

PXN8.fade.fadein = function(eltid)
{
    var _ = PXN8.dom;
    var self = PXN8.fade;
    try{
        if (self.i >= self.values.length){
            self.i = self.values.length - 1;
        }
        _.opacity(eltid,self.values[self.i]);
        if (self.i > 0){
            self.i--;
            setTimeout("PXN8.fade.fadein('" + eltid + "');",self.times[self.i]);
        }
    }catch(e){
        alert(e.message);
    }
};

/**
 *
 */
PXN8.offsets = [];


/**
 * Add a new operation to the PXN8.history !
 * (called via PXN8.tools.updateImage() - do not call directly !).
 */
PXN8.addOperations = function(operations)
{
    var self = PXN8;

    //
    // must call getUncompressedImage() _BEFORE_ opNumber is incremented !
    // to get the corrected cachedImage value.
    //
    var cachedImage = self.getUncompressedImage();

    // increment opNumber just once
    self.opNumber++;

    self.offsets[self.opNumber] = operations.length;

    var lastIndex = 0;
    for (var i = 0;i < self.opNumber; i++){
        lastIndex += self.offsets[i];
    }

    // add each operation to the history member
    for (var i = 0;i < operations.length; i++){
        self.history[lastIndex + i] = operations[i];
    }

    self.maxOpNumber = self.opNumber;

    var script = PXN8.getScript();

    self.prepareForSubmit();

    self.ajax.submitScript(script,self.imageUpdateDone);

};
/**
 * wph 20070105
 * Adjust the current selection to snap to the aspect ratio if one is enforced.
 */
PXN8.snapToAspectRatio = function()
{
    var sel = PXN8.getSelection();
    //
    // say ratio is 5x3 and current selection is 400x280
    // the selection should be shrunk to 400x240
    // new height = (400/5) * 3;
    //
    // say ratio is 5x3 and current selection is 280x500
    // the selection should be shrunk to 280x168
    // new height = (280/5) * 3;
    //
    // say ratio is 3x5 and current selectin is 400x280
    // the selection should be shrunk to 168x280
    // new width = (280/5) * 3;
    //
    // etc...
    //
    if (PXN8.aspectRatio.width != 0){
        //
        // an aspect ratio is enforced
        //
        if (PXN8.aspectRatio.width > PXN8.aspectRatio.height){
            sel.height = Math.round((sel.width / PXN8.aspectRatio.width ) * PXN8.aspectRatio.height);
        }else{
            sel.width = Math.round((sel.height / PXN8.aspectRatio.height) * PXN8.aspectRatio.width);
        }
        PXN8.select(sel);
    }
};

/***************************************************************************

SECTION: Zooming : Variables and Related Functions
==================================================
The following variables and functions are used for zooming in and out.
Zooming (or magnification) only changes the appearance of the photo in the
browser and does not change the photo's real size.

***/


/**************************************************************************

PXN8.zoom
=========
PXN8.zoom is a namespace used by all of the zoom-related variables and functions.

***/
PXN8.zoom = {};


/***************************************************************************

PXN8.zoom.values
================
Users can zoom in and out of a photo by cycling through an array of predefined zoom values.
PXN8.zoom.values specifies the levels of magnification a user can cycle through.

Type
----
Array

Default Value
-------------
    [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4 ];

Related
-------
PXN8.zoom.index
***/

PXN8.zoom.values = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4 ];

/****************************************************************************

PXN8.zoom.index
===============
PXN8.zoom.index is an index into the (zero-based) array of PXN8.zoom.values.

Type
----
number

Default Value
-------------
    3
***/
PXN8.zoom.index = 3;

PXN8.zoom.zoomedBy = PXN8.zoom.values[PXN8.zoom.index];

/***************************************************************************

PXN8.zoom.value()
=================
Get the current magnification value in use. This is expressed as a float. e.g. 200% magnification
returns a value of 2.0

Returns
-------
A float value - the current magnification factor.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.zoomByIndex PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.value = function()
{
    return PXN8.zoom.zoomedBy;
};
/***************************************************************************

PXN8.zoom.canZoomIn()
=====================
Indicates whether or not the image magnification can be increased any further.

Returns
-------
true or false.

Related
-------
PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.canZoomIn = function(){
    var self = PXN8.zoom;
    return self.zoomedBy < self.values[self.values.length-1];
};

/***************************************************************************

PXN8.zoom.canZoomOut()
======================
Indicates whether or not the image magnification can be decreased any further.

Returns
-------
true or false.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.canZoomOut = function(){
    var self = PXN8.zoom;
    return self.zoomedBy > self.values[0];
};

/***************************************************************************

PXN8.zoom.zoomByIndex()
=======================
Zoom the photo to a magnification level at the specified index (see the
PXN8.zoom.values array for a list of magnification levels.

Parameters
----------

* index : The index into the PXN8.zoom.values array. E.g. PXN8.zoom.values has this value

     [0.25, 0.5, 0.75, 1.0, 1.5, 2]

The PXN8.zoom.zoomByIndex(2) will zoom the image to 75% (0.75 is the value a PXN8.zoom.values[2]).

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.zoomByIndex = function(index)
{
    return PXN8.zoom.setIndex(index);
};

/***************************************************************************

PXN8.zoom.shrinkToWidth()
=======================
Zoom the photo to a magnification level such that the photo's width does not exceed the specified width.
The height will be adjusted accordingly. Note: This operation does not resize the photo permanently.
<b>If the photo's width is already less than the specified width then no action is taken.</b>

Parameters
----------

* width : The width to shrink the image to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue PXN8.zoom.toWidth PXN8.zoom.expandToWidth PXN8.zoom.toHeight PXN8.zoom.shrinkToHeight PXN8.zoom.expandToHeight

***/
PXN8.zoom.shrinkToWidth = function(width)
{
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.shrinkToWidth(width);
		  });
		  return;
	 }
	 var imgSize = PXN8.getImageSize();
	 if (imgSize.width > width){
		  PXN8.zoom.toWidth(width);
	 }
};
/***************************************************************************

PXN8.zoom.expandToWidth()
=======================
Zoom the photo to a magnification level such that the photo's width matches the specified width.
The height will be adjusted accordingly. Note: This operation does not resize the photo permanently.
<b>If the photo's width is already greater than the specified width then no action is taken.</b>

Parameters
----------

* width : The width to expand the image to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue PXN8.zoom.toWidth PXN8.zoom.expandToWidth PXN8.zoom.toHeight PXN8.zoom.shrinkToHeight PXN8.zoom.expandToHeight

***/
PXN8.zoom.expandToWidth = function(width)
{
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.expandToWidth(width);
		  });
		  return;
	 }
	 var imgSize = PXN8.getImageSize();
	 if (imgSize.width < width){
		  PXN8.zoom.toWidth(width);
	 }
};

/***************************************************************************

PXN8.zoom.expandToHeight()
=======================
Zoom the photo to a magnification level such that the photo's height matches the specified height.
The height will be adjusted accordingly. Note: This operation does not resize the photo permanently.
<b>If the photo's height is already greater than the specified height then no action is taken.</b>

Parameters
----------

* height : The height to expand the image to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue PXN8.zoom.toWidth PXN8.zoom.expandToWidth PXN8.zoom.toHeight PXN8.zoom.shrinkToHeight PXN8.zoom.expandToHeight

***/
PXN8.zoom.expandToHeight = function(height)
{
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.expandToHeight(height);
		  });
		  return;
	 }
	 var imgSize = PXN8.getImageSize();
	 if (imgSize.height < height){
		  PXN8.zoom.toHeight(height);
	 }
};

/***************************************************************************

PXN8.zoom.toWidth()
=======================
Zoom the photo to a magnification level such that the photo's width matches the specified width.
The height will be adjusted accordingly. Note: This operation does not resize the photo permanently.

Parameters
----------

* width : The width to set the image to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue PXN8.zoom.toWidth PXN8.zoom.expandToWidth PXN8.zoom.toHeight PXN8.zoom.shrinkToHeight PXN8.zoom.expandToHeight

***/
PXN8.zoom.toWidth = function(width){
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.toWidth(width);
		  });
		  return;
	 }
	 var imgSize = PXN8.getImageSize();
	 PXN8.zoom.setValue(width / imgSize.width);
};
/***************************************************************************

PXN8.zoom.toHeight()
=======================
Zoom the photo to a magnification level such that the photo's height matches the specified width.
The width will be adjusted accordingly. Note: This operation does not resize the photo permanently.

Parameters
----------

* height : The height to set the image to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue PXN8.zoom.toWidth PXN8.zoom.toWidth PXN8.zoom.expandToWidth PXN8.zoom.shrinkToHeight PXN8.zoom.expandToHeight

***/
PXN8.zoom.toHeight = function(height){
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.toHeight(height);
		  });
		  return;
	 }
	 var imgSize = PXN8.getImageSize();
	 PXN8.zoom.setValue(height / imgSize.height);

};
PXN8.zoom.setIndex = function(i)
{
    var self = PXN8.zoom;
    self.index = i;
    return self.setValue(self.values[i]);
};

PXN8.zoom.setValue = function(magnification)
{
    /**
     * wph 20070516 - zoom on a large image which hasn't yet fully loaded
     * will make the image disappear.
     */
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.setValue(magnification);
		  });
		  return;
	 }
    var self = PXN8.zoom;
    self.zoomedBy = magnification;

    //
    // update the width and height of the image
    //
    var theImg = document.getElementById("pxn8_image");
    theImg.width = PXN8.image.width * magnification;
    theImg.height = PXN8.image.height * magnification;
    PXN8.listener.notify(PXN8.ON_ZOOM_CHANGE,magnification);
    return magnification;
};




/***************************************************************************

PXN8.zoom.zoomIn()
==================
Zoom in (Increase the magnification level) so the photo appears bigger.
The amount by which the magnification level increases depends on the values in the
*PXN8.zoom.values* array.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.zoomIn = function()
{
    var self = PXN8.zoom;
    if (self.canZoomIn())
    {
        for (var i = 0; i < self.values.length;i++){
            if (self.values[i] > self.zoomedBy){
                self.setIndex(i);
                break;
            }
        }

    }else{
        PXN8.show.alert(PXN8.strings.NO_MORE_ZOOMIN,500);
    }
    // return false in case this is called from a link
    return false;
};

/***************************************************************************

PXN8.zoom.zoomOut()
===================
Zoom out (Decrease the magnification level) so the photo appears smaller.
The amount by which the magnification level decreases depends on the values in the
*PXN8.zoom.values* array.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.zoomOut = function()
{
    var self = PXN8.zoom;
    if (self.canZoomOut()){
        for (var i = self.values.length-1; i >= 0; i--){
            if (self.values[i] < self.zoomedBy){
                self.setIndex(i);
                break;
            }
        }
    }else{
        PXN8.show.alert(PXN8.strings.NO_MORE_ZOOMOUT,500);
    }
    return false;
};

/***************************************************************************

PXN8.zoom.toSize()
==================
Zoom the image to a fixed width and height.

Parameters
----------

* width : The width to zoom to.
* height: The height to zoom to.

Example
-------

To adjust the visible area of the photo (when it's first loaded) so that it's height is 500 Pixels high (and the width is also adjusted accordingly)

    PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD, function(){
       var img = document.getElementById("pxn8_image");
       var oh  = img.height;
       var ow = img.width;
       var nh = 800; // new height
       var ratio = oh / nh;
       var nw = ow / ratio;
       PXN8.zoom.toSize(nw,nh);
    });

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByValue

***/
PXN8.zoom.toSize = function(width, height)
{
	 if (!PXN8.ready){
		  PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){
				PXN8.zoom.toSize(width,height);
		  });
		  return;
	 }
    var hr = width / PXN8.image.width ;
    var vr = height / PXN8.image.height ;

    PXN8.zoom.setValue(Math.min(vr,hr));

    return false;
};

/***************************************************************************

PXN8.zoom.zoomByValue()
=======================
Zoom the photo to a magnification level.

Parameters
----------

* value : The magnification value

The PXN8.zoom.zoomByValue(2) will zoom the image to 200% .

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize PXN8.zoom.zoomByIndex

***/
PXN8.zoom.zoomByValue = function(magnification)
{
    PXN8.zoom.setValue(magnification);
};




/* ============================================================================
 *
 * PRIVATE FUNCTIONS and members internal to PXN8 only - do not call from client code
 */


PXN8.browser = {};
PXN8.browser.isIE6 = function()
{
    return window.navigator.userAgent.indexOf("MSIE 6") > -1;
};

/**
 * history stores all session operations
 */
PXN8.history =  [];

/**
 * An array of the response images returned from the server
 * This array contains relative file paths.
 * It is updated in the  imageUpdateDone() function.
 */
PXN8.responses =  [];

/**
 * images stores a list of all images indexed by opNumber
 * (used by PXN8.tools.history)
 */
PXN8.images =  [];

/**
 * A flag which is set when the image has fully loaded
 */
PXN8.ready = false;

/*
 * The current image - it's width; height and location (URL)
 */
PXN8.image =   {
    width: 0,
    height: 0,
    location: ""
};

PXN8.priv = {
};

PXN8.priv.addImageToHistory = function(imageLocation)
{
    var item = {"location": imageLocation,
                "width": PXN8.image.width,
                "height": PXN8.image.height
    };

    PXN8.images[PXN8.opNumber] = item;

    //
    // wph 20070223 : see comments in imageUpdateDone()
    //
    PXN8.updating = false;

};

/**
 * Create the selection area if it's not already defined.
 */
PXN8.priv.createSelectionRect = function()
{
    var _ = PXN8.dom;
    var selectRect = _.id("pxn8_select_rect");
    if (!selectRect){
        var canvas = _.id("pxn8_canvas");
        selectRect = _.ac(canvas, _.ce("div", {id: "pxn8_select_rect"}));
        selectRect.style.backgroundColor = "white";
        _.opacity(selectRect,0);
        selectRect.style.cursor = "move";
        selectRect.style.borderWidth  = "1px";
        selectRect.style.borderColor = "red";
        selectRect.style.borderStyle = "dotted";
        selectRect.style.position = "absolute";
        selectRect.style.zIndex = 1;
        selectRect.style.fontSize = "0px";
        selectRect.style.display = "block";
        selectRect.style.width = "0px";
        selectRect.style.height = "0px";
    }
    selectRect.onmousedown = function(event){
        if (!event) event = window.event;
        PXN8.drag.begin(selectRect,event,
                        PXN8.drag.moveSelectionBoxHandler,
                        PXN8.drag.upSelectionBoxHandler);
    };
    return selectRect;
};
/**
 * check every interval milliseconds for a condition
 * if it's true execute the callback, otherwise put itself back on the queue.
 */
PXN8.when = function (condition, callback, interval)
{
    if (!condition()){
        setTimeout(function(){PXN8.when(condition,callback,interval);},interval);
    }else{
        callback();
    }
};

PXN8.whenReady = function(callback)
{
	 var condition = function(){
		  return PXN8.ready;
	 };
	 PXN8.when(condition,callback,50);
};
/**
 * A private function called when the image has loaded.
 * This function in turn calls all of the PXN8.ON_IMAGE_LOAD listeners
 */
PXN8.imageLoadNotifier = function()
{
	 //
	 // wph 20080828 Don't notify listeners until PXN8.ready is true
	 //
    PXN8.when(

		  // when this condition is true...
		  function(){ return PXN8.ready; },

		  // .. do the following...
		  function(){ PXN8.listener.notify(PXN8.ON_IMAGE_LOAD); },

		  50
	 );
};

PXN8.onCanvasMouseDown = function(event){
    if (!event) event = window.event;
	 PXN8.drag.begin(document.getElementById("pxn8_canvas"),
		  event,
        PXN8.drag.moveCanvasHandler,
        PXN8.drag.upCanvasHandler);
};
/*
 * Sets up the mouse handlers for the canvas area
 * Some tools/operations might modify the canvas mouse behaviour
 * If they do so then they should call this method when the tool's
 * work is done or cancelled.
 */
PXN8.initializeCanvas = function()
{
    var _ = PXN8.dom;

    var canvas = _.id("pxn8_canvas");

    canvas.onmousemove = function (event){
        if (!event) event = window.event;
	     var cursorPos = _.cursorPos(event);
        var imagePoint = PXN8.mousePointToElementPoint(cursorPos.x, cursorPos.y);
        PXN8.position.x = imagePoint.x;
        PXN8.position.y = imagePoint.y;
        PXN8.show.position();
        return true;
    };

    canvas.onmouseout = function (event){
        if (!event) event = window.event;
        PXN8.position.x = "-";
        PXN8.position.y = "-";
        PXN8.show.position();
    };
    canvas.onmousedown = PXN8.onCanvasMouseDown;

    canvas.ondrag = function(){
        return false;
    };

    var computedCanvasStyle = _.computedStyle("pxn8_canvas");

    var canvasPosition = null;

    if (computedCanvasStyle.getPropertyValue){
        canvasPosition = computedCanvasStyle.getPropertyValue("position");
    }else{
        if (!computedCanvasStyle.position){
            // position may not be available if
            // computedStyle returns the inline style (on safari).
            //
            canvasPosition = "static";
        }else{
            canvasPosition = computedCanvasStyle.position;
        }
    }

    if (!canvasPosition || canvasPosition == "static"){
        // default the canvas position to relative
        canvas.style.position = "relative";
        canvas.style.top = "0px";
        canvas.style.left  = "0px";
    }
    //
    // the canvas should wrap tightly around the image
    // so that the canvas doesn't extend beyond the image,
    // set it's float css property if it hasn't already been set.
    //
    var floatProperty = "cssFloat";
    if (document.all){
        floatProperty = "styleFloat";
    }
    var floatValue = computedCanvasStyle[floatProperty];

    if (!floatValue || floatValue == "none"){
        canvas.style[floatProperty] = "left";
    }

    return canvas;
};

/*
 * END OF DECLARATIONS SECTION
 * ============================================================================
 */

PXN8.listener.add(PXN8.ON_IMAGE_CHANGE, PXN8.show.zoom);
PXN8.listener.add(PXN8.ON_ZOOM_CHANGE, PXN8.show.zoom);

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code which handles display of the Pixenate Toolbar.
 *
 */

var PXN8 = PXN8 || {};

PXN8.toolbar = {};

/**
 * You can override this value in your html
 */
PXN8.toolbar.crop_options = ["4x6","5x8"];


/**
 * -- function:    PXN8.toolbar.draw
 * -- description: Draw the toolbar
 * -- param: buttons. An array of strings. Can be any of the following...
 * --   "undo","redo","undoall", "redoall", "unselect", "selectall", "fillflash",
 * --   "crop", "rotate", "instantFix", "enhance", "normalize", "zoomout","zoomin"
 */
PXN8.toolbar.draw = function(buttons){

    var dom = PXN8.dom;

    if (!buttons){
        buttons = new Array();
        for (var i in PXN8.toolbar.buttons){
            buttons.push(i);
        }
    }

    document.writeln("<table cellspacing='0' cellpadding='0'><tbody><tr id='pxn8_toolbar_table'></tr></tbody></table>");

    for (var i in PXN8.toolbar.menu){
        document.writeln("<div id='pxn8_toolbar_" + i + "' class='pxn8_toolbar_dropdown' style='display:none;'></div>");
    }
    /**
     * wph 20060704: Need to move the drop-down menus up to the
     * document.body because the position will break inside a
     * relative div.
     * Create a closure that will move the dropdown menus to the body
     * when executed.
     */
    function moveMenusToBody(){
        for (var i in PXN8.toolbar.menu){
            var menuElement = document.getElementById("pxn8_toolbar_" + i);
            var menuParent = menuElement.parentNode;
            menuParent.removeChild(menuElement);
            document.body.appendChild(menuElement);
        }
    };
    /**
     * Delay running the closure until the document has loaded.
     */
    PXN8.dom.addLoadEvent(moveMenusToBody);

	 var toolbar = dom.id('pxn8_toolbar_table');

    for (var h =0; h < buttons.length; h++)
    {
        var i= buttons[h];

        var widgetModel = PXN8.toolbar.buttons[i];

        var cell = dom.ac(toolbar,dom.ce("td"));
        var widget = dom.ce("a",{className: "pxn8_toolbar_btn",
                                 href: "javascript:void(0);",
                                 onclick: widgetModel.onclick,
                                 title: widgetModel.tooltip,
                                 onmousedown: function(){this.className = 'pxn8_toolbar_btndown';},
                                 onmouseup: function(){this.className = 'pxn8_toolbar_btn';},
                                 onmouseout: function(){this.className = 'pxn8_toolbar_btn';}
        });



	     var arrowLink = dom.ce("a",{href: "javascript:void(0);",
                                    onclick: function(event,element){
                                        widgetModel.arrowClicked = widgetModel.arrowClicked==true?false:true;
                                    }
        });

        dom.ac(cell,widget);
        var widgetImage = dom.ce("img", {border: 0,
                                         alt: widgetModel.tooltip,
                                         src: PXN8.server + PXN8.root + widgetModel.image
        });
        dom.ac(widget,widgetImage);

    }

    dom.ac(dom.ac(dom.ac(toolbar,
                         dom.ce("td")),
                  dom.ce("a",{ href: "http:/" + "/pixenate.com/"})),
           dom.ce("img",{border: 0,
                         id: "pxn8_poweredby",
                         src: PXN8.server + PXN8.root + "/images/icons/powered_by_pxn8.gif"}));

};

/**
 * Hide the dropdown menu
 */
PXN8.toolbar.hidemenu = function(e)
{
    if (!e) var e = window.event;
    var tg = (window.event) ? e.srcElement : e.target;
    if (tg.nodeName != 'DIV') return;
    var reltg = (e.relatedTarget) ? e.relatedTarget : e.toElement;
    while (reltg != tg && reltg.nodeName != 'BODY'){
        reltg = reltg.parentNode;
    }
    if (reltg == tg) return;

    tg.style.display = "none";
};

/**
 * OnToolClick handles the special case of a toolbar button which
 * has a default action or displays a dropdown menu of operations if the
 * dropdown arrow is clicked.
 * menuDiv : the dropdown menu's div
 * button_offset: a numeric offset for where the dropdown arrow is
 * menuMap: A hash of menu text to functions
 * default_func: The default function to be called if the dropdown arrow
 * isn't clicked.
 */
PXN8.toolbar.ontoolclick = function(event, menuDiv, button_offset, menuMap, default_func)
{
    var dom = PXN8.dom;
    if (!event){
        event = window.event;
    }
    var button = (window.event) ? event.srcElement : event.target;

    menuDiv.onmouseout = function(event){
        PXN8.toolbar.hidemenu(event);
    };

    if (menuDiv.style.display == "block"){
        menuDiv.style.display = "none";
        return;
    }

    /**
     * Hide all other dropdowns
     */
    var dropdowns = PXN8.dom.clz("pxn8_toolbar_dropdown");
    for (var i = 0; i < dropdowns.length; i++){
        var dropdown = dropdowns[i];
        dropdown.style.display = "none";
    }


    var pos = dom.eb(button);
    var ox = event.clientX - pos.x ;
    var oy = event.clientY - pos.y ;


    if (ox > button_offset){
        dom.cl(menuDiv);

        for (var i in menuMap){

            var callback = function(f){
                return function(){
                    f();
                    var dropdowns = PXN8.dom.clz("pxn8_toolbar_dropdown");
                    for (var i = 0; i < dropdowns.length; i++){
                        var dropdown = dropdowns[i];
                        dropdown.style.display = "none";
                    }
                    return false;
                };
            };


            var link = dom.ce("a",{href: "javascript:void(0);",
                                   className: "pxn8_toolbar_option",
                                   onclick : callback(menuMap[i].onclick),
                                   title: menuMap[i].tooltip
                });
            if (menuMap[i].image){
                var linkImage = dom.ce("img", {border: 0, src: PXN8.server + PXN8.root + "/" +menuMap[i].image, alt: menuMap[i].tooltip});
                dom.ac(link,linkImage);
            }

            dom.ac(link,dom.tx(i));
            dom.ac(menuDiv,link);
        }
        menuDiv.style.display = "block";
        menuDiv.style.top = pos.y + pos.height + 4 + "px";
        menuDiv.style.left = (pos.x - 4) + "px";

    }else{
        default_func();
    }
    return false;

};
/*************************************************************************
 *
 * TOOLBAR and SUBMENU definitions start here
 *
 */

/**
 * Toolbar menu definitions go here
 */
PXN8.toolbar.menu = {
    crop: {},
    rotate: {},
    instantFix: {}
};

PXN8.toolbar.buttons = {};

PXN8.toolbar.buttons.zoomin = {
    onclick: function(){
        PXN8.zoom.zoomIn();return false;
    },
    image: "/images/icons/magnifier_zoom_in.gif",
    tooltip: "Zoom In"
};

PXN8.toolbar.buttons.zoomout = {
    onclick: function(){PXN8.zoom.zoomOut();return false;},
    image: "/images/icons/magnifier_zoom_out.gif",
    tooltip: "Zoom Out"
};

PXN8.toolbar.buttons.fof_redeye = {
    onclick: function(event){PXN8.tools.fixredeye();return false;},
    image: "/images/icons/fof_redeye.gif",
    tooltip: "Fixes red-eye - select an eye first"
};

PXN8.toolbar.buttons.fof_crop = {
    onclick: function(){
            var selection = PXN8.getSelection();
            if (selection.width > 0){
                PXN8.tools.crop(selection);
            }else{
                PXN8.selectByRatio("1x1");
            }
    },
    image: "/images/icons/fof_crop.gif",
    tooltip: "Crop the image"
};

PXN8.toolbar.buttons.fof_rotate = {
    onclick: function(){ PXN8.tools.rotate({angle: 90}); },
    image: "/images/icons/fof_rotate.gif",
    tooltip: "Rotate the image 90 degrees clockwise"
};

PXN8.toolbar.buttons.fof_undo = {
    onclick: function(){ PXN8.tools.undo();return false; },
    image: "/images/icons/fof_undo.gif",
    tooltip: "Undo the last operation"

};

PXN8.toolbar.buttons.fof_redo = {
    onclick: function(){ PXN8.tools.redo(); return false; },
    image: "/images/icons/fof_redo.gif",
    tooltip: "Redo the last operation"
};

PXN8.toolbar.buttons.fof_undoall = {
    onclick: function(){ PXN8.tools.undoall(); return false; },
    image: "/images/icons/fof_reset.gif",
    tooltip: "Revert to the original image"
};

PXN8.toolbar.buttons.fof_autofix = {
    onclick: function(event){ PXN8.tools.enhance(); },
    image: "/images/icons/fof_autofix.gif",
    tooltip: "A quick fix solution - gives better color balance and smooths lines"
};

PXN8.toolbar.buttons.fof_advanced = {
    onclick: function(event){
        var dropdown = document.getElementById("pxn8_toolbar_instantFix");
        var fixes = {
            "Enhance":   {
                onclick: function(){PXN8.tools.enhance(); },
                tooltip: "Enhance the photo (smooths facial lines)"
            },
            "Normalize": {
                onclick: function(){PXN8.tools.normalize();},
                tooltip: "Normalize gives better color balance"
            },
            "Fill-Flash": {
                onclick: function(){PXN8.tools.fill_flash();},
                tooltip: "Add Fill-Flash to brighten the image"
            }
        };
        PXN8.toolbar.ontoolclick(event,dropdown,0,fixes,PXN8.tools.fof_advanced);

        return false;
    },
    image: "/images/icons/fof_advanced.gif",
    tooltip: "A quick fix solution - gives better color balance and smooths lines"
};

PXN8.toolbar.buttons.rotate = {
    onclick: function(event){
        var dropdown = document.getElementById("pxn8_toolbar_rotate");
        var menuContents = {
            "Clockwise": {
                onclick: function(){PXN8.tools.rotate({angle: 90});},
                image: "/images/icons/rotate_clockwise.gif",
                tooltip: "Rotate the image 90 degrees clockwise"
            },

            "Anti-Clockwise": {
                onclick: function(){PXN8.tools.rotate({angle: 270});},
                image: "/images/icons/rotate_anticlockwise.gif",
                tooltip: "Rotate the image 90 degrees anti-clockwise"

            },
            "Flip Vertically": {
                onclick: function(){PXN8.tools.rotate({flipvt: "true"});},
                image: "/images/icons/shape_flip_vertical.gif",
                tooltip: "Flip the image on the Vertical axis"
            },
            "Flip Horizontally": {
                onclick: function(){PXN8.tools.rotate({fliphz: "true"});},
                image: "/images/icons/shape_flip_horizontal.gif",
                tooltip: "Flip the image on the Horizontal axis"
            }

        };
        PXN8.toolbar.ontoolclick(event,dropdown,40,menuContents,function(){PXN8.tools.rotate({angle:90});});
        return false;
    },
    image: "/images/icons/rotate.gif",
    tooltip: "Rotate the photo by 90 degrees clockwise"
};

PXN8.toolbar.buttons.add_text = {
    onclick: function(event){},
    image: "/images/icons/add_text.gif",
    tooltip: "Add Text to photo"
};

PXN8.toolbar.buttons.normalize = {
    onclick: function(){PXN8.tools.normalize();return false;},
    image: "/images/icons/normalize.gif",
    tooltip: "Gives better color balance"
};

PXN8.toolbar.buttons.enhance = {
    onclick: function(event){PXN8.tools.enhance();return false;},
    image: "/images/icons/enhance.gif",
    tooltip: "Smooths facial lines"
};

PXN8.toolbar.buttons.save = {
    onclick: function(event){return PXN8.save.toServer();},
    image: "/images/icons/save.gif",
    tooltip: "Save image to server"
};

PXN8.toolbar.buttons.instantFix = {
    onclick: function(event){
        var dropdown = document.getElementById("pxn8_toolbar_instantFix");
        var fixes = {
            "Enhance":   {
                onclick: function(){PXN8.tools.enhance(); },
                tooltip: "Enhance the photo (smooths facial lines)"
            },
            "Normalize": {
                onclick: function(){PXN8.tools.normalize();},
                tooltip: "Normalize gives better color balance"
            }
        };
        PXN8.toolbar.ontoolclick(event,dropdown,48,fixes,PXN8.tools.instantFix);

        return false;
    },
    image: "/images/icons/instant_fix.gif",
    tooltip: "A quick fix solution - gives better color balance and smooths lines"
};

PXN8.toolbar.buttons.crop = {
    onclick: function(event){

        var dropdown = document.getElementById("pxn8_toolbar_crop");

        var callback = function(opt){
            return function(){
                PXN8.selectByRatio(opt);
            };
        };

        var menuContents = {};

        for (var i = 0;i < PXN8.toolbar.crop_options.length; i++){
            var option = PXN8.toolbar.crop_options[i];
            menuContents[option] = {onclick: callback(option)};
        }

        PXN8.toolbar.ontoolclick(event,dropdown,40,menuContents,function(){
            var selection = PXN8.getSelection();
            if (selection.width > 0){
                PXN8.tools.crop(selection);
            }else{
                PXN8.show.alert("Select an area to crop");
            }
        });
        return false;
    },
    image: "/images/icons/cut_red.gif",
    tooltip: "Crop the image"
};

PXN8.toolbar.buttons.fillflash = {
    onclick: function(){PXN8.tools.fill_flash();return false;},
    image: "/images/icons/lightning_add.gif",
    tooltip: "Add Fill-Flash to brighten the image"

};

PXN8.toolbar.buttons.undo = {
    onclick: function(){PXN8.tools.undo();return false;},
    image: "/images/icons/undo.gif",
    tooltip: "Undo the last operation"

};

PXN8.toolbar.buttons.redo = {
    onclick: function(){PXN8.tools.redo();return false;},
    image: "/images/icons/redo.gif",
    tooltip: "Redo the last operation"
};

PXN8.toolbar.buttons.undoall = {
    onclick: function(){PXN8.tools.undoall();return false;},
    image: "/images/icons/undo_all.gif",
    tooltip: "Undo all operations"
};

PXN8.toolbar.buttons.redoall = {
    onclick: function(){PXN8.tools.redoall();return false;},
    image: "/images/icons/redo_all.gif",
    tooltip: "Redo all operations"
};

PXN8.toolbar.buttons.selectall = {
    onclick: PXN8.selectAll,
    image: "/images/icons/selectall.gif",
    tooltip: "Select entire photo"
};

PXN8.toolbar.buttons.unselect = {
    onclick: PXN8.unselect,
    image: "/images/icons/unselect.gif",
    tooltip: "Select entire photo"
};




/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code which handles event managment
 *
 */
var PXN8 = PXN8 || {};


/***************************************************************************

SECTION: DOM Event handling Wrapper functions
=============================================
The following section details some DOM-related event-handling functions used by
Pixenate.

***/

PXN8.event = {};
PXN8.event._added = [];

/**************************************************************************

PXN8.event.addListener()
========================
A cross-browser way to add event listeners - works with Safari, Internet Explorer and Firefox.

Parameters
----------
* element : The element (or the ID of the element) to which the event listener will be added.
* eventType : The event type string e.g. "mouseup","mousedown","click","keypress" etc.
* eventHandler :  The function which will be called when the event fires.

Returns
-------
The callback function which was supplied as an argument.

Examples
--------
    var onButtonClick = PXN8.event.addListener(document.getElement("myButton"),"click",function(event){
       event = event || window.event;
       alert("You clicked the button!");
    });

Related
-------
PXN8.event.removeListener()

***/
PXN8.event.addListener = function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el); }

    if (el.addEventListener){
        el.addEventListener(eventstr,func,true);
    }else if (el.attachEvent){
        el.attachEvent("on" + eventstr,func);
    }

    var record = {"element": el, "event": eventstr, "listener": func};
    PXN8.event._added.push(record);
    return func;
};

/*************************************************************************

PXN8.event.removeListener()
===========================
A cross-browser way to remove event listeners - works with Safari, Internet Explorer and Firefox.

Parameters
----------
* element : The element (or the ID of the element) from which the event listener will be removed.
* eventType : The event type string e.g. "mouseup","mousedown","click","keypress" etc.
* eventHandler : The callback to be removed (must be a named function or a function reference - see PXN8.event.addListener)

Examples
--------
To remove a named function from the list of listeners for an event on a particular element...

    PXN8.event.removeListener(document.getElementById("myButton"),"click",onButtonClick);

Related
-------
PXN8.event.addListener()

***/
PXN8.event.removeListener = function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el);}

    if (func){
        PXN8.event._removeListener(el,eventstr,func);
    } else {
        // if no func is supplied then remove ALL listeners of this type
        // from the element
        var original = PXN8.event._added;
        var removed = false;
        for (var i =0; i < original.length; i++){
            var record = original[i];
            if (record){
                if (record.eventstr == eventstr && record.element == el){
                    PXN8.event._removeListener(el,eventstr,record.listener);
                    original[i] = null;
                    removed = true;
                }
            }
        }
        if (!removed){ return ;}
        /**
         * Clear out any null elements from _added (not safe to do this in above loop).
         */
        var temp = [];
        for (var i = 0;i < original.length;i++){
            if (original[i] != null){
                temp.push(original[i]);
            }
        }
        PXN8.event._added = temp;
    }
};

PXN8.event._removeListener =  function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el); }

    if (el.removeEventListener){
        el.removeEventListener(eventstr,func,true);
    }else if (el.detachEvent){
        el.detachEvent("on" + eventstr,func);
    }
};


/**
 * PXN8.event.closure creates an event closure
 * Parameters: object The object to be baked into the event handler
 * func - The event handler (a function)
 * The closure returned will take 4 parameters...
 * object: The object that has been baked in.
 * source: The HTML element that triggered the event
 * event: The event which triggered the function call.
 * caller: The closure itself - this will not be the same as the function passed into PXN8.event.closure.
 */
PXN8.event.closure = function(object,func){
    return function(event){
        event = event || window.event;
        var source = (window.event) ? event.srcElement : event.target;
        func(event,object,source,arguments.callee);
    };
};

/**
 * Creates an event handler where the source, and event are guaranteed to be present and correct
 * It does things like normalizing the event (removing IE & firefox discrepancies)
 */
PXN8.event.normalize = function(func){
    return function(event){
        event = event || window.event;
        var source = (window.event) ? event.srcElement : event.target;
        func(event,source,arguments.callee);
    };
};


/**
 * Bind event-handling behaviour to all elements of a particular class
 */
PXN8.behaviour = {

    bind: function(className,behaviourObject){
        var elements = PXN8.dom.clz(className);
        for (var i = 0;i < elements.length; i++)
        {
            for (var j in behaviourObject){
                PXN8.event.addListener(elements[i],j,behaviourObject[j]);
            }
        }
    }
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code to draw and manage the selection box
 */
var PXN8 = PXN8 || {};

/* ============================================================================
 *
 * Drag - related functions and members
 */

PXN8.drag = {
    dx: 0,
    dy: 0,
    beginDragX: 0,
    beginDragY: 0,

    /* used when dragging selection */
    osx: 0,
    osy: 0,
    ow: 0,
    oh: 0
};

PXN8.drag.begin = function (elementToDrag, event, moveHandler, upHandler)
{
    var _ = PXN8.dom;

    var elementBounds = _.eb(elementToDrag);

    var cursorPos = _.cursorPos(event);

    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x,cursorPos.y);


    PXN8.drag.beginDragX = scrolledPoint.x;
    PXN8.drag.beginDragY = scrolledPoint.y;

    PXN8.drag.dx = scrolledPoint.x - elementBounds.x;
    PXN8.drag.dy = scrolledPoint.y - elementBounds.y;

    PXN8.drag.osx = PXN8.sx;
    PXN8.drag.osy = PXN8.sy;
    PXN8.drag.ow = PXN8.ex - PXN8.sx;
    PXN8.drag.oh = PXN8.ey - PXN8.sy;

    if (document.addEventListener){
        document.addEventListener("mousemove", moveHandler, true);
        document.addEventListener("mouseup", upHandler, true);
    }else if (document.attachEvent){
        document.attachEvent("onmousemove",moveHandler);
        document.attachEvent("onmouseup",upHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation();/* DOM Level 2 */
    }else{
        event.cancelBubble = true; /* IE */
    }


    if (event.preventDefault){
        event.preventDefault(); /* DOM Level 2 */
    }else {
        event.returnValue = false; /*  IE */
    }
};

PXN8.drag.moveCanvasHandler = function (event)
{
    var _ = PXN8.dom;

    if (!event) event = window.event; /* IE */

    var canvasBounds = _.eb("pxn8_canvas");

    var theImg = _.id("pxn8_image");

    var maxX = canvasBounds.x + theImg.width;
    var maxY = canvasBounds.y + theImg.height;

    var cursorPos = _.cursorPos(event);
    /*
     * prohibit move outside right and bottom
     */
    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x, cursorPos.y);

    var x2 = scrolledPoint.x>maxX?maxX:scrolledPoint.x;
    x2 = x2 < canvasBounds.x?canvasBounds.x:x2;
    var y2 = scrolledPoint.y>maxY?maxY:scrolledPoint.y;
    y2 = y2 < canvasBounds.y?canvasBounds.y:y2;

    var numerical = function(a,b){
        return a-b;
    };
    var xVals = [PXN8.drag.beginDragX-canvasBounds.x,x2-canvasBounds.x].sort(numerical);
    var yVals = [PXN8.drag.beginDragY-canvasBounds.y,y2-canvasBounds.y].sort(numerical);

    var pixelWidth = xVals[1] - xVals[0];
    var pixelHeight = yVals[1] - yVals[0];

    var width = Math.round(pixelWidth / PXN8.zoom.value());
    var height = Math.round(pixelHeight / PXN8.zoom.value());

    height = height > PXN8.image.height?PXN8.image.height:height;
    width = width > PXN8.image.width?PXN8.image.width:width;
    if (width > PXN8.aspectRatio.width && height > PXN8.aspectRatio.height && PXN8.aspectRatio.width > 0){

        if (PXN8.aspectRatio.width > PXN8.aspectRatio.height){
            height = Math.round(width/PXN8.aspectRatio.width *PXN8.aspectRatio.height);
        }else{
            width = Math.round(height/PXN8.aspectRatio.height *PXN8.aspectRatio.width);
        }
    }


    PXN8.sx = Math.round(xVals[0]/PXN8.zoom.value());
    PXN8.ex = PXN8.sx + width;

    PXN8.sy = Math.round(yVals[0]/PXN8.zoom.value());
    PXN8.ey = PXN8.sy + height;

    //
    // wph 20070105
    //
    PXN8.snapToAspectRatio();

    PXN8.listener.notify(PXN8.ON_SELECTION_CHANGE,PXN8.getSelection());

    if (event.stopPropogation){
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /*  IE */
    }

};

/*
 * Handler passed to beginDrag when the user is dragging on the canvas.
 * This handler will be invoked on a mouseup event
 */
PXN8.drag.upCanvasHandler = function (event)
{
    if (!event) event = window.event ; /* IE */

    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.drag.upCanvasHandler,true);
        document.removeEventListener("mousemove",PXN8.drag.moveCanvasHandler, true);
    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.drag.upCanvasHandler);
        document.detachEvent("onmousemove",PXN8.drag.moveCanvasHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation(); /*  DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }

    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);
};


PXN8.drag.moveSelectionBoxHandler = function (event)
{
    var _ = PXN8.dom;

    if (!event) event = window.event; /* IE  */

    var canvasBounds = _.eb("pxn8_canvas");
    var theImg = _.id("pxn8_image");

    var mx = canvasBounds.x + theImg.width;
    var my = canvasBounds.y + theImg.height;

    var cursorPos = _.cursorPos(event);
    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x, cursorPos.y);

    /* how much (in pixels) the cursor has moved */
    var rx = scrolledPoint.x - PXN8.drag.beginDragX;
    var ry = scrolledPoint.y - PXN8.drag.beginDragY;

    var zrx = rx / PXN8.zoom.value();
    var zry = ry / PXN8.zoom.value();

	 var sx = Math.round(PXN8.drag.osx + zrx);
	 var sy = Math.round(PXN8.drag.osy + zry);


	 // wph 20081121 No need to constrain here - constrained in PXN8.select()
    //sy = Math.round((sy+PXN8.drag.oh)>PXN8.image.height?(PXN8.image.height-PXN8.drag.oh):sy);

	 if (PXN8.select.constrainToImageBounds == true)
	 {
		  sx = Math.max(sx,0);
		  sx = Math.round((sx+PXN8.drag.ow)>PXN8.image.width?(PXN8.image.width-PXN8.drag.ow):sx);
		  sy = Math.max(sy,0);
		  sy = Math.round((sy+PXN8.drag.oh)>PXN8.image.height?(PXN8.image.height-PXN8.drag.oh):sy);
	 }

    var width = PXN8.drag.ow>0?PXN8.drag.ow:0;
    var height = PXN8.drag.oh>0?PXN8.drag.oh:0;

    //PXN8.ex = (PXN8.sx + PXN8.drag.ow)>0?(PXN8.sx+PXN8.drag.ow):0;
    //PXN8.ey = (PXN8.sy + PXN8.drag.oh)>0?(PXN8.sy+PXN8.drag.oh):0;

    if (event.stopPropogation) {
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }

    PXN8.select(sx,sy,width,height);

};


/*
 * Handler passed to beginDrag when the user is dragging the selection rect around.
 * This handler will be invoked on a mouseup event
 */
PXN8.drag.upSelectionBoxHandler = function (event)
{
    if (!event) event = window.event ; /* IE */
    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.drag.upSelectionBoxHandler,true);
        document.removeEventListener("mousemove",PXN8.drag.moveSelectionBoxHandler, true);
    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.drag.upSelectionBoxHandler);
        document.detachEvent("onmousemove",PXN8.drag.moveSelectionBoxHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }
    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code to draw and manage the resize handles that
 * appear on the selection box.
 *
 */
var PXN8 = PXN8 || {};

/**************************************************************************

SECTION: Selection Area Resizing
================================
The behaviour of the selection area can be modified using a single function
which turns on or off the resize handles which appear at the sides and corners of
the selected area.

***/

/* ============================================================================
 *
 * Resizing code makes extensive use of 'currying' (functions that return
 * functions with variables 'baked in'. If not for currying, this code would
 * be way too long and repetitive.
 * walter higgins
 * 3 February 2006
 *
 */
PXN8.resize = {
    dx: 0,
    dy: 0,
    start_width: 0,
    start_height: 0
};

PXN8.resize.canResizeNorth = function(yOffset){
    return (PXN8.sy + yOffset < (PXN8.ey-PXN8.style.resizeHandles.size)) && (PXN8.sy + yOffset > 0);
};

PXN8.resize.canResizeWest = function(xOffset){
    return (PXN8.sx + xOffset < (PXN8.ex-PXN8.style.resizeHandles.size)) && (PXN8.sx + xOffset > 0);
};


PXN8.resize.canResizeSouth = function(yOffset){
    return (PXN8.ey + yOffset > (PXN8.sy+PXN8.style.resizeHandles.size)) && (PXN8.ey + yOffset < PXN8.image.height);
};


PXN8.resize.canResizeEast = function(xOffset){
    return (PXN8.ex + xOffset > (PXN8.sx+PXN8.style.resizeHandles.size)) && (PXN8.ex + xOffset < PXN8.image.width);
};


PXN8.resize.nTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeNorth(yOffset) && PXN8.aspectRatio.width == 0)        // PXN8.sy > 0
    if (PXN8.resize.canResizeNorth(yOffset) )        // PXN8.sy > 0
    {
        PXN8.resize.dy = event.clientY;
        PXN8.sy = Math.round(PXN8.sy + yOffset);
        return true;

    }
    return false;
};


PXN8.resize.sTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeSouth(yOffset)  && PXN8.aspectRatio.width == 0)
    if (PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dy = event.clientY;
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.wTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeWest(xOffset)  && PXN8.aspectRatio.width == 0)        // PXN8.sx > 0
    if (PXN8.resize.canResizeWest(xOffset))        // PXN8.sx > 0
    {
        PXN8.resize.dx = event.clientX;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        return true;
    }
    return false;
};


PXN8.resize.eTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeEast(xOffset) && PXN8.aspectRatio.width == 0)
    if (PXN8.resize.canResizeEast(xOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        return true;
    }
    return false;
};


PXN8.resize.nwTest = function(xOffset,yOffset,event){
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;

    if (wr > hr){
        xOffset = yOffset * wr;
    }else if (wr < hr){
        yOffset = xOffset * hr;
    }else{
        yOffset = xOffset;
    }
    //
    // for NW corner
    // ensure both offsets are either negative or positive
    //
    if (xOffset > 0){
        // make Y positive if not already
        yOffset = Math.abs(yOffset);
    }else{
        // make y negative if not already
        yOffset = 0 - Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeWest(xOffset) && PXN8.resize.canResizeNorth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        PXN8.sy = Math.round(PXN8.sy + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.swTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;

    if (wr > hr){
        yOffset = xOffset * wr;
    }else{
        yOffset = xOffset;
    }

    //
    // for SW corner
    // ensure offset are +/-
    //
    if (xOffset > 0){
        // make Y negative if X is positive
        yOffset = 0 - Math.abs(yOffset);
    }else{
        // make y positive if X is negative
        yOffset = Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeWest(xOffset) && PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.neTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;

    if (wr > hr){
        xOffset = yOffset * wr;
    }else{
        xOffset = yOffset;
    }
    //
    // for NE corner
    // ensure offset are +/-
    //
    if (yOffset > 0){
        // make Y negative if X is positive
        xOffset = 0 - Math.abs(xOffset);
    }else{
        // make y positive if X is negative
        xOffset = Math.abs(xOffset);
    }
    if (PXN8.resize.canResizeEast(xOffset) && PXN8.resize.canResizeNorth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        PXN8.sy = Math.round(PXN8.sy + yOffset);

        return true;
    }
    return false;
};


PXN8.resize.seTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;

    if (wr > hr){
        xOffset = yOffset * wr;
    }else{
        yOffset = xOffset;
    }
    //
    // for SE corner
    // ensure offsets are both + or -
    //
    if (xOffset > 0){
        // make Y positive if X is positive
        yOffset = Math.abs(yOffset);
    }else{
        // make y negative if X is negative
        yOffset = 0 - Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeEast(xOffset) && PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.stopResizing = function(event){

    if (!event) event = window.event ; /* IE */

    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.resize.stopResizing,true);
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                document.removeEventListener("mousemove",PXN8.resize.handles[i].moveHandler, true);
            }
        }

    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.resize.stopResizing);
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                document.detachEvent("onmousemove",PXN8.resize.handles[i].moveHandler);
            }
        }
    }
    if (event.stopPropogation) event.stopPropogation(); /*  DOM Level 2 */
    else event.cancelBubble = true; /* IE */

    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);

};


    /**
     * Returns a handler that get's called when the user
     * mouses-down on one of the resize handlers
     */
PXN8.resize.startResizing = function(hdlr){
    var result = function(event){

        if (!event) event = window.event;

        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;

        var sel = PXN8.getSelection();

        PXN8.resize.start_height = sel.height;
        PXN8.resize.start_width = sel.width;

        if (document.addEventListener){
            document.addEventListener("mousemove", hdlr, true);
            document.addEventListener("mouseup", PXN8.resize.stopResizing, true);
        }else if (document.attachEvent){
            document.attachEvent("onmousemove",hdlr);
            document.attachEvent("onmouseup",PXN8.resize.stopResizing);
        }
        if (event.stopPropogation){
            event.stopPropogation();/* DOM Level 2 */
        }else {
            event.cancelBubble = true; /* IE */
        }

        if (event.preventDefault){
            event.preventDefault(); /* DOM Level 2 */
        }else {
            event.returnValue = false; /*  IE */
            }


    };
    return result;
};


PXN8.resize.createResizeHandle = function(direction,size,color) {
    var result = document.createElement("div");
    result.id = direction + "_handle";
    result.style.backgroundColor = color;
    result.style.position = "absolute";
    result.style.width = size + "px";
    result.style.height = size + "px";
    result.style.overflow = "hidden"; // fixes IE
    result.style.zIndex = 999;
    result.style.cursor = direction + "-resize";
    result.onmousedown = PXN8.resize.startResizing(PXN8.resize.handles[direction].moveHandler);
    result.ondrag = function(){return false;};
    return result;
};


PXN8.resize.positionResizeHandles = function() {
    var dom = PXN8.dom;

    var sel = PXN8.getSelection();

    if (sel.width == 0){
        PXN8.resize.hideResizeHandles();
        return;
    }
    var zoom = PXN8.zoom.value();
    var rhsz = PXN8.style.resizeHandles.size;
    var rhsm = PXN8.style.resizeHandles.smallsize;

    var canvas = dom.id("pxn8_canvas");

    for (var i in PXN8.resize.handles){

        if (typeof (PXN8.resize.handles[i]) == "function"){
            continue;
        }

        var handle = dom.id( i + "_handle");
        if (!handle){
            handle = PXN8.resize.createResizeHandle(i, rhsz, PXN8.style.resizeHandles.color);
            dom.ac(canvas,handle);
        }
        if (handle.style.display == "none"){
            handle.style.display = "block";
        }
        PXN8.resize.handles[i].position(handle,sel);
    }
};

PXN8.resize.hideResizeHandles = function(hdls) {
    var dom = PXN8.dom;

    if (hdls){
        for (var i =0; i < hdls.length;i++){
            var handle = dom.id( i + "_handle");
            if (handle){
                handle.style.display = "none";
            }
        }
    }else{
        // hide all
        for (var i in PXN8.resize.handles){
            if (typeof (PXN8.resize.handles[i]) == "function"){
                continue;
            }

            var handle = dom.id( i + "_handle");
            if (handle){
                handle.style.display = "none";
            }
        }
    }
};



PXN8.resize.resizer = function( testFunc )
{
    var result = function(event){

        if (!event) event = window.event;
        var rdy = event.clientY - PXN8.resize.dy;
        var rdx = event.clientX - PXN8.resize.dx;
        /*
         * sane resizing when zoomed
         */
        var prdy = Math.round(rdy / PXN8.zoom.value());
        var prdx = Math.round(rdx / PXN8.zoom.value());

        if (prdx == 0 && prdy == 0){
            // do nothing
        }else{
            //
            // testFunc will likely change the selection
            //
            if (testFunc(prdx,prdy,event) == true){
                //
                // snap to aspect ratio
                //
                PXN8.snapToAspectRatio();
                PXN8.listener.notify(PXN8.ON_SELECTION_CHANGE,PXN8.getSelection());
            }
        }

        if (event.stopPropogation) event.stopPropogation(); /* DOM Level 2 */
        else event.cancelBubble = true; /* IE */
    };
    return result;
};


/**
 * All of the resize handles are defined here
 */
PXN8.resize.handles = {
    "n":  { moveHandler: PXN8.resize.resizer(PXN8.resize.nTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = sel_rect.x + Math.ceil(sel_rect.width/2) - (PXN8.style.resizeHandles.size/2) + "px";
                handle.style.top = sel_rect.y + "px";
            }
    },
    "s":  { moveHandler: PXN8.resize.resizer(PXN8.resize.sTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = sel_rect.x + Math.ceil(sel_rect.width/2) - (PXN8.style.resizeHandles.size/2) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    },
    "e":  { moveHandler: PXN8.resize.resizer(PXN8.resize.eTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                //handle.style.left = (sel_rect.x + sel_rect.width - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = sel_rect.y + Math.ceil(sel_rect.height / 2) - (PXN8.style.resizeHandles.size / 2) + "px";
                //handle.style.top = Math.round((sel.top + (sel.height/2) - (PXN8.style.resizeHandles.size /2 )) * PXN8.zoom.value()) + "px";
            }
    },
    "w":  { moveHandler: PXN8.resize.resizer(PXN8.resize.wTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                //handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                //handle.style.top = Math.round((sel.top + (sel.height/2) - (PXN8.style.resizeHandles.size /2 )) * PXN8.zoom.value()) + "px";
                handle.style.top = sel_rect.y + Math.ceil(sel_rect.height / 2) - (PXN8.style.resizeHandles.size / 2) + "px";
                handle.style.left = sel_rect.x + "px";
            }
    },
    "nw": { moveHandler: PXN8.resize.resizer(PXN8.resize.nwTest),
            position: function(handle,sel){
                handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                handle.style.top = Math.round((sel.top * PXN8.zoom.value())) + "px";
            }
    },
    "sw": { moveHandler: PXN8.resize.resizer(PXN8.resize.swTest),
            position: function(handle,sel){
                handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    },
    "ne": { moveHandler: PXN8.resize.resizer(PXN8.resize.neTest),
            position: function(handle,sel){
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = Math.round((sel.top * PXN8.zoom.value())) + "px";
            }
    },
    "se": { moveHandler: PXN8.resize.resizer(PXN8.resize.seTest),
            position: function(handle,sel){
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    }
};

/**************************************************************************
PXN8.resize.enable()
====================
Enable or disable the resize handles which appear at the corners and sides of the selected
area.

Parameters
----------
* handles : An array of handles to enable or disable.
* enabled : A boolean specifying whether to enable or disable the supplied handles.

Examples
--------
To disable (hide) all of the handles...

    PXN8.resize.enable(["n","s","e","w","nw","ne","sw","se"],false);

To enable the side handles...

    PXN8.resize.enable(["n","s","e","w"],true);

<img src="pigeon300x225resizehandles.jpg"/>

***/
PXN8.resize.enable = function(handles, enable)
{
    var source = PXN8.resize.handles;
    var target = PXN8.resize.handle_store;
    var display = "none";
    var dom = PXN8.dom;


    if (enable){
        source = PXN8.resize.handle_store;
        target = PXN8.resize.handles;
        dispay = "block";
    }

    for (var i = 0; i < handles.length; i++){
        var handle = handles[i];

        var hdl = source[handle];
        delete source[handle];


        if (hdl){
            target[handle] = hdl;
            var handle_element = dom.id( handle + "_handle");

            if (handle_element){
                handle_element.parentNode.removeChild(handle_element);

                //handle_element.style.dispay = display;
            }
        }
    }

    PXN8.resize.positionResizeHandles();

};

PXN8.resize.handle_store = {};

/** ============================================================================
 *
 */
PXN8.listener.add(PXN8.ON_SELECTION_CHANGE, PXN8.resize.positionResizeHandles);
PXN8.listener.add(PXN8.ON_ZOOM_CHANGE, PXN8.resize.positionResizeHandles);
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code for manipulating the DOM (document object model)
 */
var PXN8 = PXN8 || {};
/**************************************************************************

SECTION: DOM manipulation utility Functions
===========================================
Pixenate uses a number of the following DOM manipulation functions which can
be used independently of Pixenate.
***/

PXN8.dom = {
    /**
     * Computing the style is expensive.
     * cache computation results here.
     */
    cachedComputedStyles: {}
};

/***************************************************************************

PXN8.dom.cl()
=============
Remove all child nodes from the specified element. This method repeated calls
Element.removeChild(Element.firstChild) until there are no children left.

Parameters
----------
* element : A HTML Element or the element's id attribute value.

Returns
-------
element. The element which was passed in.

Examples
--------
    var el = PXN8.dom.cl(document.getElementById("myElement"));
    // this is equivalent to...
    var el = PXN8.dom.cl("myElement");

Related
-------
PXN8.dom.ac PXN8.dom.ce PXN8.dom.id

***/
PXN8.dom.cl = function(elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt) return false;
    while (elt.firstChild){ elt.removeChild(elt.firstChild);}
    return elt;
};

/***************************************************************************

PXN8.dom.tx()
=============
A convenience function which is shorthand for document.createTextNode().

Parameters
----------
* string : The text from which to create a new TextNode.

Returns
-------
The newly created TextNode object.

Example
-------
     var tn = PXN8.dom.tx("Hello World");
     document.body.appendChild(tn);

Related
-------
PXN8.dom.ac PXN8.dom.ce

***/
PXN8.dom.tx = function(str){ return document.createTextNode(str);};

/***************************************************************************

PXN8.dom.id()
=============
A Convenience function which is shorthand for document.getElementById()

Parameters
----------
* string : The id attribute of the element to return.

Returns
-------

The DOM Element object which has the matching id attribute. If no match is found
then *null* is returned instead.

Example
-------
    example: var el = PXN8.dom.id("myElementId");

***/
PXN8.dom.id = function(str){ return document.getElementById(str);};

/**************************************************************************

PXN8.dom.ce()
=============

Description
-----------
A convenience function which is shorthand for document.createElement(). This function also
allows you to set attributes for the element.

Parameters
----------
* nodeType
The nodename of the type of element you want to create, for example,
if you want to create an *img* element then the nodename will be 'img'.

* attributes (optional)
An object whose properties are the attribute/value pairs for the new element.

Returns
-------
The newly created DOM Element object.

Example
-------
     // create an image with src attribute = "myimage.jpg" and a 4 pixel border.
     var img = PXN8.dom.ce("img", { src: "myimage.jpg" border: 4});

Related
-------
PXN8.dom.ac

***/
PXN8.dom.ce = function(nodeType,attrs)
{
    var el = document.createElement(nodeType);
    for (var i in attrs){ el[i] = attrs[i];}
    return el;
};

/***************************************************************************

PXN8.dom.css()
=============
A convenience function for specifying multiple CSS properties for an element

Parameters
----------
* elt
The element to which the css properties will be applied
* attrs
The attributes (CSS properties) to apply

Returns
-------
The element

Example
-------
    // make the #palette DIV's background blue and it's font size 18px
    PXN8.dom.css("palette", { backgroundColor: "blue", fontSize: "18px" });

Related
-------
PXN8.dom.ce

***/
PXN8.dom.css = function(elt,attrs)
{
	 var i;
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
	 for (i in attrs){
		  elt.style[i] = attrs[i];
	 }
	 return elt;
};
/***************************************************************************

PXN8.dom.ac()
=============
A convenience function which is shorthand for Element.appendChild().

Parameters
----------
* parent
The parent element to which the child element will be appended.
* child
The child element to append.

Returns
-------
The newly appended *child* DOM Element object.

Example
-------
    var address = document.getElementById("address");
    var user = document.getElementById("user");
    PXN8.dom.ac(user,address);
    // the 'address' is now a child element of the 'user' element

Related
-------
PXN8.dom.ce

***/
PXN8.dom.ac = function(parent,child)
{
    if (typeof parent == 'string'){ parent = PXN8.dom.id(parent); }
    if (typeof child == 'string'){ child = PXN8.dom.id(child); }
    if (!parent || !child){ return false; }

    parent.appendChild(child);
    return child;
};

/***************************************************************************

PXN8.dom.eb()
=============
Get the Bounds (size and coordinates) of an element on the page.

Parameters
----------
* element : An Element Object or the element's id attribute.

Returns
-------
The element bounds for an element. Bounds are in the form of an object with the following
properties... *x*, *y*, *width*, *height*

Example
-------
     var bounds = PXN8.dom.eb("myElementId");
     // bounds = {x: 48, y: 105, width: 200, height: 120}

Related
-------
PXN8.dom.ep PXN8.dom.cursorPos PXN8.dom.windowSize

***/
PXN8.dom.eb = function(elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt){ return false; }

    var x = null;
    var y = null;

    if(elt.style.position == "absolute")
    {
        x = parseInt(elt.style.left);
        y = parseInt(elt.style.top);
    } else {
        var pos = this.ep(elt);
        x = pos.x;
        y = pos.y;
    }
    return {x: x, y: y, width: elt.offsetWidth, height: elt.offsetHeight};
};
/***************************************************************************

PXN8.dom.ep()
=============
Given an element, calculate it's absolute position relative to the BODY element.

Parameters
----------
* element : An Element object or it's id attribute.

Returns
-------
An object with attributes x and y representing the
coordinates of the top left corner of the element

Example
-------
     var pos = PXN8.dom.ep("myElementId");
     // pos = { x: 48, y: 105 }
Related
-------
PXN8.dom.eb PXN8.dom.cursorPos

***/
PXN8.dom.ep = function (elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt) { return false; }

    var tmpElt = elt;
    var posX = parseInt(tmpElt["offsetLeft"]);
    var posY = parseInt(tmpElt["offsetTop"]);
    while(tmpElt.tagName.toUpperCase() != "BODY") {
        tmpElt = tmpElt.offsetParent;
        // IE7 bug ? - in one case the body tag was overlooked.
        if (tmpElt == null){
            break;
        }
        posX += parseInt(tmpElt["offsetLeft"]);
        posY += parseInt(tmpElt["offsetTop"]);
    }
    return {x: posX, y:posY};
};


/***************************************************************************

PXN8.dom.windowSize()
=====================
Calculate the size of the browser window (it's outer width & height).

Returns
-------
An object with 'height' and 'width' properties.

Example
-------
     var winSz = PXN8.dom.windowSize();
     // winSz = { height: 768, width: 1002}

Related
-------
PXN8.dom.eb PXN8.dom.cursorPos PXN8.dom.ep

***/
PXN8.dom.windowSize = function()
{
    if (document.all){
        return {width: document.body.clientWidth,
                height: document.body.clientHeight};
    }else{
        return {width: window.outerWidth,
                height: window.outerHeight};
    }
};
/***************************************************************************

PXN8.dom.opacity()
==================
Sets the opacity of a given element.

Parameters
----------
* element : The elment object or it's id attribute.
* value : The opacity expressed as a number between 0 and 1.0.

Example
-------
    PXN8.dom.opacity("myElementId",0.5);
    // sets opacity of the 'myElementId' element to 50%

***/
PXN8.dom.opacity = function(elt, value)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    /*
     * it's quite possible that the element has been deleted
     */
    if (!elt){
        return;
    }
    if (document.all){
        elt.style.filter = "alpha(opacity:" + (value*100) + ")";
    }else{
        elt.style.opacity = value;
        elt.style._moz_opacity = value;
    }
};

/***************************************************************************

PXN8.dom.clz()
==============
Return an array of elements with the supplied classname

Parameters
----------
* className : A string containing the name of the class.

Returns
-------
An array of Element objects with a matching *class* attribute.

Example
-------
    <span class="finance">Loan: $40,000</span>
    <span class="address">8092 Wolverdale drive</span>
    <span class="finance">Repayments: $800</span>
    <script type="text/javascript">
     var allFinanceElements = PXN8.dom.clz("finance");
     // allFinanceElements = [SpanElement{8092 Wolverdale drive}, SpanElement{Repayments: $800}]
    </script>

Related
-------
PXN8.dom.addClass PXN8.dom.removeClass

***/
PXN8.dom.clz = function(className)
{
    var links = document.getElementsByTagName("*");

    var result = new Array();
    for (var i = 0;i < links.length; i++){
        if (links[i].className.match(className)){
            result.push(links[i]);
        }
    }
    return result;
};

/***************************************************************************

PXN8.dom.removeClass()
======================
Removes a class from the element's className (or 'class' attribute).

Parameters
----------
* element : An element or it's attribute id, from which you want to remove the class.

* className : The name of the class to remove.

Example
-------

     // before
     // <span id="myElementId" class="finance private">Repayments: $800</span>
     PXN8.dom.removeClass("myElementId","private");
     // after
     // <span id="myElementId" class="finance">Repayments: $800</span>
Related
-------
PXN8.dom.addClass PXN8.dom.clz

***/
PXN8.dom.removeClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    var oldClassname = elt.className;
    var oldClasses = oldClassname.split(/ /);
    var newClasses = [];
    for (var i = 0;i < oldClasses.length; i++){
        if (oldClasses[i] != className){
            newClasses.push(oldClasses[i]);
        }
    }
    var newClassname = newClasses.join(' ');
    elt.className = newClassname;
};

/***************************************************************************

PXN8.dom.addClass()
===================
Add a new class to the element's className (or *class* attribute).

Parameters
----------

* element : The element or it's id attribute.
* className : The name of the class to add to this element.

Example
-------
Before...
    <div class="panel">...</div>
Javascript...
    PXN8.dom.addClass("myElementId","dragable");
After...
    <div class="panel dragable">...</div>

Related
-------
PXN8.dom.removeClass PXN8.dom.clz

***/
PXN8.dom.addClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    elt.className += ' ' + className;
};

/***************************************************************************

PXN8.dom.isClass()
==================
Does the element's className contain the supplied classname ?

Parameters
----------

* element : The element object or it's id attribute.
* className : The name of the class for which to check.

Returns
-------
*true* or *false* .

Related
-------
PXN8.dom.addClass PXN8.dom.removeClass

***/
PXN8.dom.isClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    var clzs = elt.className.split(/ /);
    for (var i = 0; i < clzs.length; i++){
        if (clzs[i] == className){
            return true;
        }
    }
    return false;
};

/***************************************************************************

PXN8.dom.computedStyle()
========================
Returns the style of an element based on it's external stylesheet, and any inline styles.

Parameters
----------
* elementId : The id of the element whose style must be computed

Returns
-------
A CSSStyleDeclaration object.

***/
PXN8.dom.computedStyle = function(elementId)
{
    var result = null;

    if (this.cachedComputedStyles[elementId]){
        result = this.cachedComputedStyles[elementId];
    }else{
        var element = this.id(elementId);
        if (document.all){

            result = element.currentStyle;

        }else{
            if (window.getComputedStyle){
                result = window.getComputedStyle(element,null);
            }else{
                /**
                 * Safari doesn't support getComputedStyle()
                 */
                result = element.style;
            }
        }
        this.cachedComputedStyles[elementId] = result;
    }
    return result;
};
/***************************************************************************

PXN8.dom.cursorPos()
====================
Return the current adjusted cursor position.

Parameters
-----------

* event : The event (a MouseEvent) from which to obtain the cursor position.

Returns
-------
An object with *height* and *width* parameters.

Related
-------
PXN8.dom.ep PXN8.dom.eb

***/
PXN8.dom.cursorPos = function (e)
{
    e = e || window.event;
    var cursor = {x:0, y:0};
    if (e.pageX || e.pageY) {
        cursor.x = e.pageX;
        cursor.y = e.pageY;
    }
    else {
        var sl = document.documentElement.scrollLeft || document.body.scrollLeft;
        var st = document.documentElement.scrollTop || document.body.scrollTop;
        cursor.x = e.clientX + sl - document.documentElement.clientLeft;
        cursor.y = e.clientY + st - document.documentElement.clientTop;
    }
    return cursor;
};

/***************************************************************************

PXN8.dom.addLoadEvent()
=======================
Add a callback to the window's onload event. This is a convenience function which enables
multiple functions to be called when the *window.onload* event is fired.

Parameters
----------
* function : The callback function to be called when the window has loaded.

Example
-------
    PXN8.dom.addLoadEvent(function(){
        alert("The window has loaded");
    });

***/
PXN8.dom.addLoadEvent = function(func)
{
    var oldonload = window.onload;
    if (typeof window.onload != 'function'){
        window.onload = func;
    } else {
        window.onload = function(){
            if (oldonload){
                oldonload();
            }
            func();
        };
    }
};

PXN8.dom.addClickEvent = function(elt,func)
{
    var oldonclick = elt.onclick;
    if (typeof oldonclick != 'function'){
        elt.onclick = func;
    } else {
        elt.onclick = function(){
            if (oldonclick){
                oldonclick(elt);
            }
            func(elt);
        };
    }
};
PXN8.dom.onceOnlyClickEvent = function(elt,func)
{
    var oldonclick = elt.onclick;

    PXN8.dom.addClickEvent(elt,function(){
        func();
        elt.onclick = oldonclick;
    });
};
/**
 * Make constructing tables easier
 *  param rows An array of arrays (2-dimensional)
 *    Each item in the inner arrays must be either a string or a DOM element.
 *
 *  e.g. PXN8.dom.table([[ "Row1Col1", document.getElementById("pxn8_image") ],
 *                         [ "Row2Col1", "Hello World" ],
 *                         [ "This spans two columns"  ]]);
 */
PXN8.dom.table = function(rows, attributes)
{
    var dom = PXN8.dom;

    var result = dom.ce("table",attributes);
    var tbody = dom.ce("tbody");
	 result.appendChild(tbody);
    /**
     * First scan the array to find find the widest row (most cells)
     */
    var mostCells = 0;
    for (var i = 0; i < rows.length; i++){
        var row = rows[i];
        if (row.length > mostCells){
           mostCells = row.length;
        }
    }

    for (var i = 0; i < rows.length; i++){
        var tr = dom.ce("tr");
	     tbody.appendChild(tr);
        var rowData = rows[i];
        var cellsInRow = rowData.length;
        for (var j = 0; j < rows[i].length; j++){
            var cellData = rows[i][j];

            var td = dom.ce("td");
	         tr.appendChild(td);
            if (j == rows[i].length -1 && cellsInRow < mostCells){
                td.colSpan = (mostCells - cellsInRow)+1;
            }
            if (typeof cellData == "string"){
	             td.appendChild(dom.tx(cellData));
            }else if (PXN8.isArray(cellData)){
                // it's an array
                for (var k = 0; k < cellData.length; k++){
                    if (typeof cellData[k] == "string"){
	                     td.appendChild(dom.tx(cellData[k]));
                    }else{
	                     td.appendChild(cellData[k]);
                    }
                }
            }else{
	             td.appendChild(cellData);
            }
        }
    }
    return result;
};

/**
 * Add a Flag icon to the page, placing it at x,y with ID of id
 * Returns the <IMG> flag element
 */
PXN8.dom.createFlag = function(x,y,id)
{
    var flag = PXN8.dom.ce("img",{"src": PXN8.root + "images/icons/flag.gif", "id": id});
    document.body.appendChild(flag);
    flag.style.position = "absolute";
    flag.style.top = y - 16 +  "px";
    flag.style.left = x - 11 + "px";
    return flag;
};

/**
 * (c) 2006-2008 Sxoop Technologies Ltd.
 *
 * This javascript file defines all of the image operations used by
 * pxn8_tools_ui.js
 *
 */

var PXN8 = PXN8 || {};

/*****************************************************************************

SECTION: Photo-Editing functions
================================
As well as the core selection, zoom and initialization functions, the Pixenate&trade;
javascript API is composed of a series of photo-editing functions which perform various
modifications to the photo by calling equivalent CGI functions in the Pixenate software running on
the web server.

PXN8.tools
==========
This variable defines a namespace within which all of the Pixenate photo-editing
functions are defined.
***/

PXN8.tools = {};

/*****************************************************************************

PXN8.tools.history()
====================
Go back in time 'offset' number of operations. Clients should not call this function
directly. Use the *PXN8.tools.undo()* and *PXN8.tools.redo()* functions instead.

Parameters
----------
* offset : A number (positive or negative) indicating how many operations to move forwards or backwards
in the user session stack.

Examples
--------
    PXN8.tools.history(-1);
    // same as PXN8.tools.undo();

    PXN8.tools.history(+1)
    // same sas PXN8.tools.redo();

Related
-------
PXN8.tools.undo PXN8.tools.redo

***/
PXN8.tools.history = function (offset)
{
    if (offset == 0){
        return;
    }
    if (PXN8.isUpdating()){
        alert (PXN8.strings.IMAGE_UPDATING);
        return;
    }
    var theImage = PXN8.dom.id("pxn8_image");
    if (!theImage){
        alert("Please wait for the image to load");
        return;
    }


    if (!offset) offset = -1;
    if (PXN8.opNumber == 0 && offset < 0){
        PXN8.show.alert(PXN8.strings.NO_MORE_UNDO);
        return;
    }
    if (PXN8.opNumber == PXN8.maxOpNumber && offset > 0){
        PXN8.show.alert(PXN8.strings.NO_MORE_REDO);
        return;
    }

    if (offset < 0){

        var userOp = PXN8.getUserOperation(PXN8.opNumber);

        PXN8.show.alert("- " + userOp.operation, 500);
    }else{

        var userOp = PXN8.getUserOperation(PXN8.opNumber+1);

        PXN8.show.alert("+ " + userOp.operation,500);
    }

    var index =  PXN8.opNumber + offset;

    var currentImageData = PXN8.images[index];

    if (!currentImageData){
        //
        // wph 20070223:
        // this could have potentially happenend
        // if the user clicked 'undo' before the new image loaded
        //
        alert("Error! PXN8.images[" + index + "] is undefined");
        return false;
    }

    PXN8.opNumber = index;

    PXN8.image.location = currentImageData.location;
    PXN8.image.width = currentImageData.width;
    PXN8.image.height = currentImageData.height;

    // point image at the array element was bad !
    // changes to PXN8.image were also reflected in
    // the array element leading to a long bug-tracking session
    // REMEMBER this !!!
    //PXN8.image = PXN8.images[PXN8.opNumber];

	 PXN8.listener.notify(PXN8.BEFORE_IMAGE_CHANGE,null);

    /**
     * wph 20070108 : don't unselect because some ON_IMAGE_CHANGE listeners
     * might want to automatically select the image whenever it's changed.
     * Better to unselect _before_ notifying listeners
     */
    PXN8.unselect();
    PXN8.replaceImage(PXN8.image.location);
    return false;
};

/**************************************************************************

PXN8.tools.undo
===============
Undo the last operation.

Related
-------
PXN8.tools.undoall PXN8.tools.redo PXN8.tools.redoall PXN8.tools.history

***/
PXN8.tools.undo = function()
{
    PXN8.tools.history(-1);
    return false;
};

/***************************************************************************

PXN8.tools.redo()
=================
Redo the last undone operation.

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history

***/
PXN8.tools.redo = function()
{
    PXN8.tools.history(+1);
    return false;
};
/***************************************************************************

PXN8.tools.undoall()
====================
Undo all changes that were made to the photo

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history PXN8.tools.redo

***/
PXN8.tools.undoall = function()
{
    PXN8.tools.history(0 - PXN8.opNumber);
    return false;
};

/**************************************************************************

PXN8.tools.redoall()
====================
Redo all changes made to the photo.

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history PXN8.tools.redo
***/
PXN8.tools.redoall = function()
{
    PXN8.tools.history(PXN8.maxOpNumber-PXN8.opNumber);
    return false;
};

/**************************************************************************

PXN8.tools.updateImage()
========================
This function takes an array of operations as a parameter and submits the
operations to the server to update the image. This function is called by all other PXN8.tools.*
functions (except PXN8.tools.undo(), PXN8.tools.redo(),  PXN8.tools.undoall(), PXN8.tools.redoall() ).


Parameters
----------
* operations : An array of 'operations' which will be performed on the image.

Example
-------
The following code will crop the photo and rotate it by 90&deg; ...

    PXN8.tools.updateImage([
                            {operation: "crop", top: 40, left: 40, width: 200, height: 200},
                            {operation: "rotate", angle: 90}
                           ]);

PXN8.tools.updateImage() can be used to combine multiple image-editing operations into a single
user operation (to the user it appears to be one operation even though the image goes through
two transformations, being first normalized and then enhanced).
When the user clicks *Undo*, both the *crop* and *rotate* operations will be undone.
PXN8.tools.updateImage() is really useful if you would like to create your own custom 'quick-fix'
operations which are combinations of existing operations.
Please see <a href="#OPERATIONS">API Operations</a> for a full list of operations

***/
PXN8.tools.updateImage = function(ops)
{
    var theImage = PXN8.dom.id("pxn8_image");
    if (!PXN8.ready){
        alert("Please wait for the image to load");
        return;
    }

    /**
     * wph 20060909 : Don't increment PXN8.opNumber unless the
     * last operation has completed.
     */
    if (PXN8.isUpdating()){
        alert (PXN8.strings.IMAGE_UPDATING);
        return;
    }

	 PXN8.listener.notify(PXN8.BEFORE_IMAGE_CHANGE,ops);

    /**
     * increment PXN8.opNumber & add op to the history
     */
    PXN8.addOperations(ops);

};

/**************************************************************************

PXN8.tools.startTransaction()
====================
If you want to combine multiple image-editing operations into a single atomic user operation, you can do so
by bracketing the operations with PXN8.tools.startTransaction() and PXN8.tools.endTransaction().

Examples
--------

    PXN8.tools.startTransaction();

    PXN8.tools.snow();
    PXN8.tools.rotate({angle: 90});
    PXN8.tools.resize(40,40);

    // commit changes to the server
    PXN8.tools.endTransaction();


In the above example, the PXN8.tools.snow(), PXN8.tools.rotate() and PXN8.tools.resize() set of statements will be treated as a single user operation. If the user clicks 'Undo' all 3 operations will be undone. Please see <a href="example-transaction.html">Combining many operations in a single click.</a>

Related
-------
PXN8.tools.endTransaction()

***/

PXN8.tools.startTransaction = function()
{
    PXN8.tools.transactionCache = new Array();
    PXN8.tools.commit = PXN8.tools.updateImage;
    PXN8.tools.updateImage = PXN8.tools.cacheUpdates;
};

PXN8.tools.cacheUpdates = function(ops)
{
    for (var i =0; i < ops.length; i++){
        PXN8.tools.transactionCache.push(ops[i]);
    }

};
/**************************************************************************

PXN8.tools.endTransaction()
====================
If you want to combine multiple image-editing operations into a single atomic user operation, you can do so
by bracketing the operations with PXN8.tools.startTransaction() and PXN8.tools.endTransaction().
PXN8.tools.endTransaction() commits changes to the server.

Examples
--------

    PXN8.tools.startTransaction();

    PXN8.tools.snow();
    PXN8.tools.rotate({angle: 90});
    PXN8.tools.resize(40,40);

    // commit changes to the server
    PXN8.tools.endTransaction();


In the above example, the PXN8.tools.snow(), PXN8.tools.rotate() and PXN8.tools.resize() set of statements will be treated as a single user operation. If the user clicks 'Undo' all 3 operations will be undone.

Related
-------
PXN8.tools.startTransaction()

***/
PXN8.tools.endTransaction = function()
{
    PXN8.tools.updateImage = PXN8.tools.commit;
    PXN8.tools.updateImage(PXN8.tools.transactionCache);
};

/**************************************************************************

PXN8.tools.enhance()
====================
Apply a digital filter to enhance a noisy photo. This is useful for smoothing facial lines.

***/
PXN8.tools.enhance = function()
{
    PXN8.tools.updateImage([{operation: "enhance"}]);
};

/**************************************************************************

PXN8.tools.normalize()
======================
Transform photo to span the full range of color values. This results in a more
colorful, better balanced image.

***/
PXN8.tools.normalize = function()
{
    PXN8.tools.updateImage([{operation: "normalize"}]);
};

/**************************************************************************

PXN8.tools.instantFix()
=======================
instant_fix performs both 'enhance' and 'normalize'

Related
-------
PXN8.tools.enhance PXN8.tools.normalize

***/
PXN8.tools.instantFix = function()
{
    PXN8.tools.updateImage([ {operation: "normalize"},
                             {operation: "enhance"}
                           ]);
};

/**************************************************************************

PXN8.tools.spiritlevel()
========================
Fix the horizon on a photo: Uses two points (left and right) to ascertain
what the correct angle of the photo should be.
This function is a wrapper for PXN8.tools.rotate().

Parameters
----------
* x1 : the X coordinate of the first point
* y1 : The Y coordinate of the first point
* x2 : The X coordinate of the second point
* y2 : The Y coordinate of the second point

Related
-------
PXN8.tools.rotate

***/
PXN8.tools.spiritlevel = function(x1,y1,x2,y2)
{

    var opposite = y1 > y2 ? y1 - y2 : y2 - y1;
    var adjacent = x1 > x2 ? x1 - x2 : x2 - x1;
    var hypotenuse = Math.sqrt((opposite * opposite) + (adjacent * adjacent));
    var sineratio = opposite / hypotenuse;
    var RAD2DEG = 57.2957795;
    var rads = Math.atan2(sineratio,Math.sqrt(1 - (sineratio * sineratio)));
    var degrees = rads * RAD2DEG;
    if (y1 < y2){
        degrees = 360 - degrees;
    }
    PXN8.tools.rotate ({angle: degrees});
    return degrees;

};
/**
 * -- TODO: document PXN8.tools.spiritlevel_mode for API reference
 * wph 20070426 : A new mode which simplifies the UI interaction for using the spirit-level tool.
 */

PXN8.tools.spiritlevel_mode = {};
PXN8.tools.spiritlevel_mode.clicks = [];
PXN8.tools.spiritlevel_mode.callback = null;
PXN8.tools.spiritlevel_mode.start = function(callback)
{
    var _ = PXN8.dom;
    var img = _.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();
    var sel = _.id("pxn8_select_rect");

    sel.style.cursor = "pointer";
    PXN8.crosshairs.setEnabled(false);
    PXN8.resize.enable(["n","s","e","w","ne","se","nw","sw"],false);

    PXN8.select({top:0,left:0,width: iw/2,height: ih});
    PXN8.event.addListener(sel,"click",this.onclick);

    this.callback = callback;
};

PXN8.tools.spiritlevel_mode.end = function()
{
    var _ = PXN8.dom;

    var pin1 = _.id('pxn8_flag_0');
    if (pin1){
        document.body.removeChild(pin1);
    }
    var pin2 = _.id('pxn8_flag_1');
    if (pin2){
        document.body.removeChild(pin2);
    }
    PXN8.unselect();
    var sel = _.id("pxn8_select_rect");
    sel.style.cursor = "move";
    PXN8.crosshairs.setEnabled(true);

    PXN8.resize.enable(["n","s","e","w","ne","se","nw","sw"],true);

    PXN8.event.removeListener(sel,"click",this.onclick);
    this.clicks = [];
};


PXN8.tools.spiritlevel_mode.onclick = function(event)
{
    var _ = PXN8.dom;
    var self = PXN8.tools.spiritlevel_mode;

    var img = _.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();
    var sel = _.id("pxn8_select_rect");
    var pos = _.cursorPos(event);

    var flag = PXN8.dom.createFlag(pos.x,pos.y,"pxn8_flag_" + self.clicks.length);

    self.clicks.push(pos);

    PXN8.select({top:0,left:iw/2,width: iw/2,height: ih});

    if (self.clicks.length == 2){
        PXN8.event.removeListener(sel,"click",self.onclick);
        PXN8.tools.spiritlevel(self.clicks[0].x,self.clicks[0].y,self.clicks[1].x,self.clicks[1].y);

        var oldSize = PXN8.dom.eb("pxn8_image");

        PXN8.listener.onceOnly(PXN8.ON_IMAGE_CHANGE,function(){
                self.end();
                var newSize = PXN8.dom.id("pxn8_image");
                var hdiff = newSize.height - oldSize.height;
                var wdiff = newSize.width - oldSize.width;
                PXN8.select(wdiff,hdiff,oldSize.width-wdiff,oldSize.height-hdiff);

                self.callback();
            });

    }
};

/**************************************************************************

PXN8.tools.rotate()
===================
Rotate a photo or flip it.

Parameters
----------
* params : An object which must have at least one of the following properties...
  * angle : A number specifing the degrees through which the photo should be rotated.
  * flipvt : A boolean indicating whether or not the photo should be flipped vertically.
  * fliphz : A boolean indicating whether or not the photo should be flipped horizontally.

If no parameters are supplied, the default is to rotate the photo through 90 degrees clockwise.

Examples
--------
To rotate a photo 90 degrees clockwise...

    PXN8.tools.rotate({angle: 90});

To flip a photo along the horizontal pane (mirror photo)...

    PXN8.tools.rotate({fliphz: true});

***/
PXN8.tools.rotate = function(params)
{
    var operation = {"operation" : "rotate", angle: 0, "fliphz": false, "flipvt": false };
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    if (!params){
        operation.angle = 90;
    }

    if (operation.angle > 0 || operation.flipvt || operation.fliphz){
        PXN8.tools.updateImage([operation]);
    }
};

/**************************************************************************

PXN8.tools.blur()
=================
Blur an area of the photo (or the entire photo).

Examples
--------
To blur the entire photo with a radius of 2x2...

    PXN8.tools.blur({radius: 2});

To blur an area of the photo...

    PXN8.tools.blur({radius: 2, top: 4, left: 40, width: 400, height: 200});

***/
PXN8.tools.blur = function (params)
{
    params.operation = "blur";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.colors()
===================
Change the brightness, saturation ,hue and contrast of a photo.

Examples
--------
To increase saturation by 20%...

    PXN8.tools.colors({saturation: 120});

To increase contrast & reduce brightness by 20 %

    PXN8.tools.colors({contrast: 1, brightness: 80});

To increase saturation, brightness, hue and contrast...

    PXN8.tools.colors ({brightness: 110, saturation: 110, hue: 180, contrast: 2});

Contrast must be in the range -5 to +5.
All other parameters must be in the range 0 - 200

***/
PXN8.tools.colors = function(param)
{
    if (!param.saturation) param.saturation = 100;
    if (!param.brightness) param.brightness = 100;
    if (!param.hue) param.hue = 100;
    if (!param.contrast) param.contrast = 0;
    param.operation = "colors";
    PXN8.tools.updateImage([param]);
};

/**************************************************************************

PXN8.tools.crop()
=================
Crop a photo to the dimensions provided. The most common way to call this is
as follows...

    PXN8.tools.crop();

... which will simply crop the photo to the currently selected area. This is
equivalent to ...
    PXN8.tools.crop(PXN8.getSelection());

Parameters
----------
* geometry : An object with *width*, *height*, *top* and *left* properties.

Examples
--------
Crop the photo begining at 10 pixels from left, 200 pixels from the top and
extending 40 pixels to the right and 80 pixels to the bottom...

    PXN8.tools.crop({top: 10, left: 200, width: 40, height: 80});

***/
PXN8.tools.crop = function (params)
{
    if (!params){
        params = PXN8.getSelection();
    }

    var operation = {operation: "crop"};

    for (var i in params){
        operation[i] = params[i];
    }
    /**
     * wph 20070526 - use current selection if no params provided
     */

    if (operation.width <= 0 || operation.height <= 0){
        PXN8.show.alert(PXN8.strings.CROP_SELECT_AREA);
        return;
    }
    PXN8.tools.updateImage([operation]);
};

/**************************************************************************

PXN8.tools.previewCrop()
========================
A utility function to allow the user to preview what a crop based on the current
selection would look like.

Parameters
----------
* timeoutMillis : Exit the preview mode after the timeoutMillis milliseconds.

***/
PXN8.tools.previewCrop = function (timeout)
{
    timeout = timeout || 3500;

    var preview = function(borderColor,borderOpacity,handleOpacity){
        var _ = PXN8.dom;
        var rects = ["left","right","top","bottom","topleft","topright","bottomleft","bottomright"];
        for (var i  = 0;i < rects.length; i++){
            var rect = _.id("pxn8_" + rects[i] + "_rect");
            rect.style.backgroundColor = borderColor;
            _.opacity(rect,borderOpacity);
        }
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                var handle = _.id( i + "_handle");
                _.opacity(handle,handleOpacity);
            }
        }
    };
    preview("white",1.00,0);

    setTimeout(function(){
            var _ = PXN8.style.notSelected;
            preview(_.color,_.opacity,1.00);
        },timeout);
};
// for backward compatibility with older non-camelized naming scheme
PXN8.tools.preview_crop = PXN8.tools.previewCrop;

/**************************************************************************

PXN8.tools.filter()
===================
Apply a 'lens-filter' effect to the photo.This mimics the effect
of using those tinted lens filters on SLR to create more dramatic
skies.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* params : An object with *top*, *color* and *opacity* properties. (see below).

Examples
--------
The following javascript code will produce the image on the right...

    PXN8.tools.filter({top: 125, color: '#ffa500', opacity: 80});

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225filter.jpg"/></td></tr>
</table>

The *top* property specifies where the filter should trail off completely. The filter
is always applied starting at the top of the photo. If you would like the filter to begin
at the bottom of the photo you should use the following code instead...

    PXN8.tools.updateImage([
                           {operation: "rotate", flipvt: true},
                           {operation: "filter", top: 125, color: '#ffa500', opacity: 80},
                           {operation: "rotate", flipvt: true},
                           ]);

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225filterflip.jpg"/></td></tr>
</table>

***/
PXN8.tools.filter = function (params)
{
    params.color = params.color;
    params.operation = "filter";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.interlace()
======================
Adds TV-like scan-lines overlaying the photo to make it appear like it is a
screen-grab from broadcast TV.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* params : An object with *color* and *opacity* properties.

Examples
--------

    PXN8.tools.interlace({color: '#ffffff', opacity: 66 });

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225interlace.jpg"/></td></tr>
</table>

***/
PXN8.tools.interlace = function(params)
{
    params.color = params.color;
    params.operation = "interlace";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.lomo()
=================
This function adds a 'lomo' effect to the photo. This is an atmospheric and artistic
effect that darkens the corners and (optionally) saturates the colors so that the photo
appears to have been taken using a Russian LOMO&trade; camera.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* params : An object with *opacity* (numeric) and *saturate* (boolean) properties.

Examples
--------

    PXN8.tools.lomo({opacity: 30, saturate: true});

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225lomo.jpg"/></td></tr>
</table>

***/
PXN8.tools.lomo = function(params)
{
    params.operation = "lomo";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.fillFlash()
=======================
Adds a fill-flash effect to the photo to brighten it. This is more
subtle than using the PXN8.tools.colors() function as it composites a
duplicate layer on top of the existing image using the 'SCREEN'
compositing operation.

Parameters
----------
* luminosity : A value between 1 and 100 (default value is 50 if parameter not passed).

Examples
--------

    PXN8.tools.fillFlash();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225fillflash.jpg"/></td></tr>
</table>

***/
PXN8.tools.fillFlash = function(opacity)
{
    var operation = {operation: "fill_flash"};
    if (opacity){
        operation.opacity = Math.max(0,Math.min(100,opacity));
    }else{
        operation.opacity = 50;
    }

    PXN8.tools.updateImage([operation]);
};
// for compatibility with older non-camelized naming scheme
PXN8.tools.fill_flash = PXN8.tools.fillFlash;

/**************************************************************************

PXN8.tools.snow()
=================
Adds snowflakes to the photo. This is basically a wrapper around PXN8.tools.overlay().
<em>This tool is only available in Pixenate Premium edition.</em>

Examples
--------

    PXN8.tools.snow();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225snow.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.overlay()

***/
PXN8.tools.snow = function ()
{
    PXN8.tools.overlay({filepath: "images/overlays/snowflakes.png", tile: "true"});
};

/**************************************************************************

PXN8.tools.overlay()
==================
Overlays an image on top of the photo.
The overlay tool is useful for superimposing clip-art and borders on top of photos.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------

* params : An object with the following properties...
  * filepath : The image property should be a relative path to the overlay image (relative to where pixenate is installed *not* the webroot). This is the image which will be overlaid in front of (or behind) the user's photo. You can provide either a <em>filepath</em> or <em>url</em> property. filepath will always be used if both url and filepath are provided. You must provide either the <em>filepath</em> or <em>url</em> parameter!
  * url : A url to the image to use as the overlay. You can use this instead of the filepath parameter.
  * image : (deprecated - use 'filepath' or 'url' instead) The image property should be a relative path to the overlay image (relative to where pixenate is installed *not* the webroot). This is the image which will be overlaid in front of (or behind) the user's photo.
  * tile  : (optional) A boolean indicating whether the image should be tiled (repeated in the x and y direction).
  * left : (optional) The X coordinate where the left-most side of the overlay will appear (ignored if *tile* is true).
  * top : (optional) The Y coordinate where the top-most side of the overlay will appear (ignored if *tile* is true).
  * width: (optional) The width to which the overlay will be resized. If the width property is not present then the overlay will not be resized.
  * height: (optional) The height to which the overlay will be resized.
  * position: (optional) The position to place the overlay relative to the photo. Possible values are *"front"* and *"back"* (default value is *"front"*).
  * extend: (optional) A boolean indicating whether or not the photo canvas should be extended (not scaled) to match the size of the overlay image (used for Overlays which have an Alpha Channel).
  * opacity: (optional) A value between 1 and 100 specifying the opacity of the overlay (default is 100).
Examples
--------

Let's say your users have been working on a photo and would now like to stick it on to a background image to give it a border.

<img align="center" src="border1.jpg"/>

The border is 300 x 225 pixels in size and is a plain JPEG image (no transparency). What we are going to do is position the above image at the *back* of the photo.

<img align="center" src="pigeon200x150.jpg"/>

(a 200 x 150 photo) which will result in the following completed image

<img align="center" src="overlay1.jpg"/>

This can be achieved with the following code...

    PXN8.initialize("/pixenate/docs/pigeon200x150.jpg");
    // more code..
    PXN8.tools.overlay({image: "docs/border1.jpg", position: "back", left: 50, top: 37});

Please see <a href="example-borders.html">Adding Borders</a> and <a href="example-speech-bubbles.html">Speech bubbles</a> for more Examples.

Related
-------
PXN8.tools.snow() PXN8.tools.addText()

***/
PXN8.tools.overlay = function(params)
{
    params.operation="overlay";
    PXN8.tools.updateImage([params]);
};


/**************************************************************************

PXN8.tools.addText()
=====================
Adds text to a photo.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------

* params : An object with the following properties...
  * text : (this is the only compulsory property) The text to add.
  * gravity : A string specifying where the text will appear. Possible values are "North", "South", "East", "West", "NorthWest", "SouthWest", "NorhtEast", "SouthEast", "Center".
  * y : The Y coordinate where the text will appear (this is an offset from the default position specified by gravity)
  * x : The X coordinate where the text will appear (this is an offset from the default position specified by gravity)
  * font : The font family to use when adding text (default is Arial or Courier depending on what fonts are installed on the server).
  * fill : The font color (expressed as a &hash; prefixed hexadecimal string).
  * pointsize: The size of the font.
  * style: (optional) Values can be "Normal", "Italic", "Oblique", "Any".
  * stretch: (optional) Values can be "Normal", "UltraCondensed", "ExtraCondensed","Condensed","SemiCondensed", "SemiExpanded","Expanded","ExtraExpanded","UltraExpanded".
  * stroke: (optional) The stroke color (expressed as a &hash; prefixed hexadecimal string).
  * strokewidth: (optional) The stroke width (for outlining text).
  * antialias: (optional) values can be <em>true</em> or <em>false</em>.
  * rotate: (optional) The number of degrees to rotate the text.

Examples
--------
To position text on the photo using the *gravity* property...

    PXN8.tools.addText({fill : '#ffffff', font: 'Arial', pointsize: 20, text: 'Hello World', gravity: 'NorthEast'});

<img src="pigeon300x225textgravity.jpg"/>

The following table illustrates how the *gravity* and *x* and *y* parameters are used in conjunction with each other.
The red arrow in each diagram points to the adjusted position used for *gravity*...

<table>
  <tr>
    <td><img src="pigeonNorthWest.jpg"/></td>
    <td><img src="pigeonNorth.jpg"/></td>
    <td><img src="pigeonNorthEast.jpg"/></td>
  </tr>
  <tr>
    <td><img src="pigeonWest.jpg"/></td>
    <td><img src="pigeonCenter.jpg"/></td>
    <td><img src="pigeonEast.jpg"/></td>
  </tr>
  <tr>
    <td><img src="pigeonSouthWest.jpg"/></td>
    <td><img src="pigeonSouth.jpg"/></td>
    <td><img src="pigeonSouthEast.jpg"/></td>
  </tr>
</table>
Please see <a href="example-text.html">Text Example</a> for an example of how to position text on the photo.

Related
-------
PXN8.tools.overlay()

***/
PXN8.tools.addText = function(params)
{
   params.operation = "add_text";
   params.fill = params.fill;

   //
   // wph 20070309 : allow doublequotes inside text string
   //
   params.text = params.text.replace(/\"/g,"\\\"");

   PXN8.tools.updateImage([params]);
};
// for backward compatibility with older non-camelized naming scheme
PXN8.tools.add_text = PXN8.tools.addText;

/**************************************************************************

PXN8.tools.whiten()
===================
Whitens off-color teeth.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* geometry : An object containing *top*, *left*, *width* and *height* coordinates. (usually obtained from the current user selection). If no parameter is supplied, then the current selected area will be used.

Related
-------
PXN8.tools.fixredeye()

***/
PXN8.tools.whiten = function (params)
{
    if (!params){
        params = PXN8.getSelection();
    }
    if (params.width == 0 || params.height == 0){
        return;
    }
    params.operation = "whiten";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.fixredeye()
======================
Removes 'red-eye' from the specified area.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* geometry : An object containing *top*, *left*, *width* and *height* coordinates. (usually obtained from the current user selection)


Examples
--------

    PXN8.tools.fixredeye({top: 40, left: 60, width: 75, height: 75});

Alternatively the *geometry* parameter can instead be an Array of geometry objects.

    PXN8.tools.fixredeye([{top: 40, left: 60, width: 75, height: 75},
                         {top: 50, left: 200, width: 80, height: 94}]);

You must supply one or more rectangles to which the redeye fix will be applied.
The rectangles should be approximately centered on the eye.

Related
-------
PXN8.tools.whiten()

***/
PXN8.tools.fixredeye = function(params)
{
    if (!params){
        params = PXN8.getSelection();
    }
    if (PXN8.isArray(params)){
        for (var i = 0;i < params.length; i++){
            params[i].operation = "fixredeye";
        }
        PXN8.tools.updateImage(params);
    }else{
        params.operation = "fixredeye";
        PXN8.tools.updateImage([params]);
    }
};

/**************************************************************************

PXN8.tools.resize()
===================
Resize an image to the specified width and height.

Parameters
----------
* width : The new desired width of the image.
* height: The new desired height of the image.

***/
PXN8.tools.resize = function(width, height)
{
    PXN8.tools.updateImage([{"operation": "resize", "width": width, "height": height}]);
};

/**************************************************************************

PXN8.tools.roundedCorners()
===========================
Add rounded corners to the image.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* color : The color of the corners.
* radius : The radius of the rounded corners.

Examples
--------

    PXN8.tools.roundedCorners('#ffffff',32);

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225rounded.jpg"/></td></tr>
</table>

***/
PXN8.tools.roundedCorners = function(color, radius)
{
    PXN8.tools.updateImage([{"operation":"roundcorners",
                             "color": color,
                             "radius":radius}]);
};
// for compatibility with older non-camelized naming scheme
PXN8.tools.roundedcorners = PXN8.tools.roundedCorners;
/**************************************************************************

PXN8.tools.sepia()
==================
Add a sepia-tone effect to the image.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* color : the color of the 'tint' to use when applying the effect. ('#a28a65' seems to be a good sepia color).

Examples
--------

    PXN8.tools.sepia('#a28a65');

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225sepia.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.grayscale()

***/
PXN8.tools.sepia = function(color)
{
    PXN8.tools.updateImage([{"operation":"sepia",
                             "color": color}]);
};

/**************************************************************************

PXN8.tools.grayscale()
======================
Make the image grayscale (black & white).
<em>This tool is only available in Pixenate Premium edition.</em>

Examples
--------
    PXN8.tools.grayscale();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225grayscale.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.sepia()

***/
PXN8.tools.grayscale = function()
{
    PXN8.tools.updateImage([{"operation":"grayscale"}]);
};

/**************************************************************************

PXN8.tools.charcoal()
=====================
Create a charcoal drawing from an image.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* radius : (A value between 1 and 8). Defines how acute the effect will be.

Examples
--------

<table>
<tr><td>Before</td><td>radius = 2</td><td>radius = 5</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225ch2.jpg"/></td><td><img src="pigeon300x225ch5.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.oilpaint()
***/
PXN8.tools.charcoal = function(radius)
{
    PXN8.tools.updateImage([{"operation" : "charcoal", "radius" : radius}]);
};

/**************************************************************************

PXN8.tools.oilpaint()
=====================
Create an oil-painting from a photo.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* radius : (A value between 1 and 8). Defines how acute the effect will be.

Examples
--------

<table>
<tr><td>Before</td><td>radius = 2</td><td>radius = 5</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225oil2.jpg"/></td><td><img src="pigeon300x225oil5.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.charcoal()

***/
PXN8.tools.oilpaint = function(radius)
{
    PXN8.tools.updateImage([{"operation" : "oilpaint", "radius" : radius}]);
};

/*========================================================================

PXN8.tools.unsharpmask()
========================
Uses the unsharpmask algorithm to sharpen an image.

*/
PXN8.tools.unsharpmask = function(params)
{
    var operation = {"operation": "unsharpmask"};
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    PXN8.tools.updateImage([operation]);
};

/**************************************************************************

PXN8.tools.fetch()
==================
Fetches an image either from a remote server or the server's own filesystem.
This is not normally called directly by client code.

***/
PXN8.tools.fetch = function(params)
{
    var operation = {"operation" : "fetch"};
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    PXN8.tools.updateImage([operation]);
};


/**************************************************************************

PXN8.tools.hi_res()
=====================
Apply all changes to the photo which were performed in the current editing session
to a high-resolution version of the image. Please note that as with all editing operations, this does not overwrite the original hi-res photo.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* hires_image_details : An object which must have at least one of the following attributes...
  * filepath : The path (relative to the pixenate directory) to the hi-res image. filepath will always be used if both url and filepath are provided. You must provide either the <em>filepath</em> or <em>url</em> parameter!
  * url : A url to the hi-res image. You can use this instead of the filepath parameter.
* callback : A function which will be called when changes to the hi-res image have been complete. The callback takes a single parameter <b>jsonResponse</b>. <b>jsonResponse.image</b> contains the url (relative to the pixenate directory) where the hi-res edited photo resides.

Examples
--------
To save a hi-res version of your changes to the local client storage...

    // A url for the hi-res version of the photo (this will never be displayed in the browser)
    var hires_image = "http://pixenate.com/images/samples/hires/garinish.jpg";

    //
    // this function will be called instead of the default PXN8.save.toDisk() function
    //
    function hires_save(){
        PXN8.tools.hi_res({url: hires_image}, on_hires_edit_complete);
    }
    //
    // the following function will be called when all changes have been
    // applied to the high-resolution photo
    //
    function on_hires_edit_complete(jsonResponse)
    {

        // grab the location of the hi-res photo
        var hires_edited_image = jsonResponse.image;

        // change the location to the new image
        var newURL = PXN8.root + "/save.pl?image=" + hires_edited_image;

        // open URL
        document.location = newURL;
    }

    // change PXN8.save.toDisk to point at our new function
    PXN8.save.toDisk = hires_save;

***/

PXN8.tools.hi_res = function(hires_params,callback)
{
    var script = PXN8.getScript();
    var fetchOp = script[0];
    var hires_script = [fetchOp];
    var proxyOp = {operation: "proxy"};
    for (var i in fetchOp){
        if (i != "operation"){
            proxyOp[i] = fetchOp[i];
        }
    }
    //
    // wph 20080417 : Ensure that each new call to hi_res will force the
    // server to reevaluate the entire script.
    //
    proxyOp.timestamp = new Date().getTime();

    hires_script.push(proxyOp);

    for (var i in hires_params){
        fetchOp[i] = hires_params[i];
    }
    for (var i = 1; i < script.length; i++){
        hires_script.push(script[i]);
    }

    // inform user while hi-res operation happens.
    PXN8.prepareForSubmit(PXN8.strings.SAVING_HI_RES);

        // submit the script to the server for processing
    PXN8.ajax.submitScript(hires_script,function(jsonResponse){

        // hide the timer
        var timer = document.getElementById("pxn8_timer");
        if (timer){
            timer.style.display = "none";
        }
        //
        // wph 20070403 - must reset PXN8.updating or the user
        // won't be able to do any further operations after they've saved the
        // photo to their disk.
        //
        PXN8.updating = false;

        callback(jsonResponse);

    });
};

/**************************************************************************

PXN8.tools.mask()
=====================
The 'mask' tool lets you apply a alpha-channel Mask to a photo. A mask is a grayscale image
composed solely of colors, black, white and shades of gray. Dark areas of the mask will
result in equvalent transparent areas in the photo, while white areas of the mask will result
in opaque areas of the photo.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* filepath : The path (relative to the pixenate directory) to the mask image.
* background_color : The background color to apply where the mask is not fully opaque. The default value is #00000000 (transparent).

Examples
--------
Please see <a href="example-mask-1.html">example-mask-1</a> for a simple example of using the Mask tool.
Refer to <a href="example-crop-face.html">Face copy-and-paste</a> for an example of how the <b>mask</b> tool can
be used to copy and paste parts of a photo on top of itself.

Related
-------
PXN8.tools.crop PXN8.overlay.start PXN8.overlay.stop PXN8.ajax.submitScript

***/

PXN8.tools.mask = function(params)
{
    var operation = {"operation" : "mask"};
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    PXN8.tools.updateImage([operation]);
};

/**************************************************************************

PXN8.tools.rearrange()
=====================
The Rearrange tool lets you move many parts of a photo around.
<em>This tool is only available in Pixenate Premium edition.</em>

Parameters
----------
* pieces: An array of objects. Each object specifies coordinates for the part of the photo and the displacement.
          Each object must have the following properties...
  * x : The leftmost point of the area to be moved
  * y : The topmost point of the area to be moved
  * width: The width of the area to be moved
  * height: The height of the area to be moved
  * dx: The difference along the X axis by which the area should be moved (- values are to the left, + values are to the right)
  * dy: The difference along the Y axis by which teh area should be moved (- values are to the top, + values are to the bottom)

Examples
--------
Please see <a href="example-rearrange.html">example-rearrange</a> for a simple example of using the Rearrange tool.

Related
-------
PXN8.tools.crop PXN8.tools.mask

***/
PXN8.tools.rearrange = function(params)
{
    var operation = {"operation" : "rearrange"};
    if (!params)
    {
        alert(PXN8.strings.INVALID_PARAMETER + " (null) ");
        return;
    }
    operation.pieces = params;
    PXN8.tools.updateImage([operation]);

};
/**************************************************************************

PXN8.tools.transparent()
=====================
Make a color within a photo transparent. PXN8.tools.transparent() can be called in 2 ways.
You can provide x and y coordinates in which case the color at that point will be chosen and
all pixels in the photo with the same color will be set to transparent.
Or you can provide a color parameter.

Parameters
----------
* o: An object with one or more of the following attributes...
  * x : The x coordinate of the point at which the color will be chosen (optional)
  * y : The y coordinate of the point at which the color will be chosen (optional)
  * color: The color to set as transparent (optional)
  * reduce: If this attribute is present, the image will be reduced to 256 colors

Examples
--------
Please see <a href="example-transparent.html">example-transparent</a> for a simple example of using the Transparent tool.

***/
PXN8.tools.transparent = function (params){
	 params.__extension = ".png";
	 params.operation = "transparent";
	 PXN8.tools.updateImage([params]);
};

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code for handling hi-res images
 *
 */

var PXN8 = PXN8 || {};
// called when the hi-res update is complete
PXN8.ON_HIRES_COMPLETE = "ON_HIRES_COMPLETE";
PXN8.ON_HIRES_BEGIN = "ON_HIRES_BEGIN";


PXN8.hires = {
    originalURL: "",
    responses : [],
    jsonCallback : function(jsonResponse){
        PXN8.listener.notify(PXN8.ON_HIRES_COMPLETE,jsonResponse);
    }
};
/**
 * Given a series of commands, scale each of the commands to a certain ratio
 * Only certain commands need to be scaled
 * Any command with 'top','left','width','height' or 'radius' parameters needs
 * to be scaled.
 */
PXN8.hires.scaleScript = function(script,ratio)
{
    var paramsToScale = ["left","width","top","height","radius"];

    for (var i = 0;i < script.length; i++){
        var op = script[i];
        for (var j = 0; j < paramsToScale.length; j++){
            var attr = paramsToScale[j];

            if (op[attr]){
                op[attr] = op[attr] * ratio;
            }
        }
    }
};

/**
 * Called whenever the image is updated by the user.
 */
PXN8.hires.doImageChange = function(eventType)
{
    var loRes = PXN8.images[0];
    var ratio = PXN8.hires.responses[0].height / loRes.height;

    var script = PXN8.getScript();

    PXN8.hires.interpolate(script,PXN8.hires.originalURL,ratio);

};
/**
 * Apply a script which was performed on a lo-res version of the image
 * to the high-res version of the same image.
 * Parameters : originalScript - the Original Script that was used on the
 * the lo-res version (see PXN8.getScript() )
 *  hiresImageURL - the URL to the hi-res version of the same image
 *  ratio  - the hires height divided by the lo-res height
 *   (e.g. if the hires image is 3000x2000 and the lo-res image is 600x400
 *   the ratio will be 5 )
 */
PXN8.hires.interpolate = function(originalScript,hiresImageURL,ratio)
{
    originalScript[0].image = hiresImageURL;

    PXN8.hires.scaleScript(originalScript,ratio);

    PXN8.listener.notify(PXN8.ON_HIRES_BEGIN);

    var opNumberWas = PXN8.opNumber;

    PXN8.ajax.submitScript(originalScript,function(jsonResponse){
        //
        // wph 20070228 : curry so the callback will know which opNumber
        // it was called for (may not be what the current value of PXN8.opNumber is
        // when this is eventually invoked!)
        //
        jsonResponse.initOpNumber = opNumberWas;
        PXN8.hires.jsonCallback(jsonResponse);

    });
};


/**
 * Initialize the Hi-Res Ajax Requestor
 * This will kick off a listener which will request an updated version of the hi-res image
 * whenever the user changes the lo-res version.
 */
PXN8.hires.init = function(imageUrl)
{
    PXN8.listener.add(PXN8.ON_HIRES_COMPLETE,function(eventType,jsonResponse){
        PXN8.hires.responses[jsonResponse.initOpNumber] = jsonResponse;
    });
    // set so that later calls will use same URL
    PXN8.hires.originalURL = imageUrl;

    var fetch = {operation: "fetch",
                 image: imageUrl,
                 pxn8root: PXN8.root,
                 random: Math.random()
    };
    PXN8.listener.notify(PXN8.ON_HIRES_BEGIN);

    var opNumberWas = PXN8.opNumber;

    PXN8.ajax.submitScript([fetch],function(jsonResponse){
        //
        // wph 20070228 : curry so the callback will know which opNumber
        // it was called for (may not be what the current value of PXN8.opNumber is !)
        //
        jsonResponse.initOpNumber = opNumberWas;
        PXN8.hires.jsonCallback(jsonResponse);
    });


    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,PXN8.hires.doImageChange);
    /**
     * over-ride the default PXN8.getUncompressedImage if in hi-res mode
     */
    PXN8.getUncompressedImage = PXN8.hires.getUncompressedImage;
};

/**
 * Get the path to the uncompressed hi-res edited image
 */
PXN8.hires.getUncompressedImage = function()
{
    var result = false;
    if (PXN8.hires.responses[PXN8.opNumber]){
        result = PXN8.hires.responses[PXN8.opNumber].uncompressed;
    }
    return result;

};

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains most of the string literals used by PXN8's UI
 *
 */

var PXN8 = PXN8 || {};

PXN8.strings = {};

// alert when no more redo
PXN8.strings.NO_MORE_REDO = "No more operations left to redo!";

// alert when no more undo
PXN8.strings.NO_MORE_UNDO     = "No operations left to undo!";


// alert when fully zoomed in
PXN8.strings.NO_MORE_ZOOMIN  = "Cannot zoom-in any further!";

    // alert when fully zoomed out
PXN8.strings.NO_MORE_ZOOMOUT      = "Cannot zoom-out any further!";

    // alert when using old IE (pre 6.0)
PXN8.strings.MUST_UPGRADE_IE      = "You must upgrade to Internet Explorer 6.0 to use PXN8";

    // alert when AJAX request fails due to server error
PXN8.strings.WEB_SERVER_ERROR     = "Web server error:";

    // alert when an image fails to load due to bad URL
PXN8.strings.IMAGE_ON_ERROR1      = "An error occured while attempting to load ";

PXN8.strings.IMAGE_ON_ERROR2       = "\nPlease check the URL is correct and try again";

    // alert when no pxn8_config_content div has been defined
PXN8.strings.NO_CONFIG_CONTENT    = "ERROR: no config_content element is defined in your html template";

PXN8.strings.CONFIG_BLUR_TOOL     = "Configure Blur tool";

    // appears at the bottom of the blur config tool
PXN8.strings.BLUR_PROMPT          = "Enter a value in the range 1 to 8 for blur radius. A larger radius results in a more blurred image.";

    // alert when blur out of range
PXN8.strings.BLUR_RANGE           = "Blur radius must be in the range 1 - 8";

PXN8.strings.RADIUS_LABEL         = "Radius:";

    // alert when brightness out of range
PXN8.strings.BRIGHTNESS_RANGE     = "Enter a percentage value for brightness";

    // alert when hue out of range
PXN8.strings.HUE_RANGE            = "Hue must be in the range 0-200";

    // alert when saturation out of range
PXN8.strings.SATURATION_RANGE     = "Enter a percentage value for saturation";

    // appears at the top of the crop tool panel
PXN8.strings.CONFIG_CROP_TOOL     = "Configure Crop Tool";

PXN8.strings.CONFIG_COLOR_TOOL    = "Change colors ";

    // appears at the top of the lens filter tool panel
PXN8.strings.CONFIG_FILTER_TOOL   = "Configure Lens Filter";

    // appears at the bottom of the blur config tool.
PXN8.strings.FILTER_PROMPT        = "Click on the image and a graduated filter of the selected color (and opacity) will be overlayed on top of the image";

    // appears at the top of the interlace tool panel
PXN8.strings.CONFIG_INTERLACE_TOOL= "Configure Interlace Effect";

PXN8.strings.INTERLACE_PROMPT      = "Creates an interlaced overlay above the image making it appear like a grab from a TV screen.";

PXN8.strings.INVALID_HEX_VALUE     = "You must enter a hexadecimal color value or choose one from the color palette";

PXN8.strings.CONFIG_LOMO_TOOL      = "Configure Lomo Effect";

PXN8.strings.OPACITY_PROMPT        = "Low opacity means darker corners. High opacity means lighter corners.";

PXN8.strings.OPACITY_RANGE         = "Opacity must be in the range 0 - 100";

PXN8.strings.WHITEN_SELECT_AREA    = "You must first select the area of the image you wish to whiten";

PXN8.strings.CONFIG_WHITEN_TOOL    = "Configure Teeth Whitening";

PXN8.strings.CROP_SELECT_AREA      = "You must first select the area of the image you wish to crop";

PXN8.strings.RESIZE_SELECT_AREA    = "You must first select an area to resize to.";

PXN8.strings.RESIZE_SELECT_LABEL   = "Resize to selected area.";

PXN8.strings.SELECT_SMALLER_AREA   = "Please select a smaller area";

PXN8.strings.REDEYE_SELECT_AREA    = "You must first select the area you wish to fix";

PXN8.strings.REDEYE_SMALLER_AREA   = "Please select a smaller area to fix (less than 100x100)";

PXN8.strings.CONFIG_REDEYE_TOOL    = "Fix Red Eye";

PXN8.strings.REDEYE_PROMPT         = "To fix red-eye, select the affected area (begining at the top left corner and centering the cross-hairs on the iris) and click the 'Apply' button or press 'Enter'.";

PXN8.strings.NUMERIC_WIDTH_HEIGHT  = "You must specify a numeric value for new width and height";

PXN8.strings.LIMIT_SIZE            = "You can't resize larger than ";

PXN8.strings.ASPECT_LABEL          = "Preserve aspect ratio: ";

PXN8.strings.ASPECT_CROP_LABEL     = "Aspect ratio: ";

PXN8.strings.CROP_FREE             = "free select";

PXN8.strings.CROP_SQUARE           = "(square)";

PXN8.strings.WIDTH_LABEL           = "Width: ";

PXN8.strings.HEIGHT_LABEL          = "Height: ";

PXN8.strings.FLIPVT_LABEL          = "Flip vertically: ";

PXN8.strings.FLIPHZ_LABEL          = "Flip horizontally: ";

PXN8.strings.ANGLE_LABEL           = "Angle: ";

PXN8.strings.OPACITY_LABEL         = "Opacity: ";

PXN8.strings.CONTRAST_NORMAL       = "Normal ";

PXN8.strings.COLOR_LABEL           = "Color: ";

PXN8.strings.SEPIA_LABEL           = "Sepia";

PXN8.strings.SATURATE_LABEL        = "Saturate :";

PXN8.strings.GRAYSCALE_LABEL       = "Grayscale";

PXN8.strings.ORIENTATION_LABEL     = "Orientation: ";

PXN8.strings.CONFIG_RESIZE_TOOL    = "Resize Image";

PXN8.strings.CONFIG_ROTATE_TOOL    = "Rotate or Flip Image";

PXN8.strings.SPIRIT_LEVEL_PROMPT1  = "Please click on the left half of the crooked horizon.";

PXN8.strings.SPIRIT_LEVEL_PROMPT2  = "OK. Now click on the right half of the crooked horizon.";

PXN8.strings.CONFIG_SPIRITLVL_TOOL = "Spirit-level Mode";

PXN8.strings.CONFIG_ROUNDED_TOOL   = "Configure Rounded Corners";

PXN8.strings.CONFIG_BW_TOOL        = "Configure Sepia or Black & White";

PXN8.strings.ORIENTATION_PORTRAIT  = "Portrait";

PXN8.strings.ORIENTATION_LANDSCAPE = "Landscape";

PXN8.strings.PROMPT_ROTATE_CHOICE  = "Please specify an angle of rotation or flip orientation";

PXN8.strings.BW_PROMPT             = "Turn your photograph into black & white or add a sepia tone.";

PXN8.strings.IMAGE_UPDATING        = "The image is currently updating.\nPlease wait for the current operation to complete.";

PXN8.strings.BRIGHTNESS_LABEL      = "brightness";

PXN8.strings.SATURATION_LABEL      = "saturation";

PXN8.strings.CONTRAST_LABEL        = "contrast";

PXN8.strings.HUE_LABEL             = "hue";

PXN8.strings.UPDATING              = "Updating image. Please wait...";

PXN8.strings.CONFIG_OILPAINT_TOOL  = "Apply Oil-Painting Filter";

PXN8.strings.CONFIG_CHARCOAL_TOOL  = "Apply Charcoal Filter";

PXN8.strings.SAVING_HI_RES         = "Saving High-Resolution image. Please wait a moment...";

PXN8.strings.INVALID_PARAMETER     = "Invalid parameter passed to function.";

PXN8.strings.RESET                 = "Reset";
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code for displaying cross-hairs on the selection
 *
 */
var PXN8 = PXN8 || {};

PXN8.crosshairs = {};
/*************************************************************************

SECTION: Cross-Hairs functions
==============================
The following functions are related to setting and getting the cross-hairs image
(by default a white cross which is displayed in the center of the selection area),
and also enabling and disabling the cross-hairs.

***/

PXN8.crosshairs.enabled = true;
PXN8.crosshairs.image = null;


/**************************************************************************

PXN8.crosshairs.setEnabled()
============================
Enables or disables the display of the cross-hairs image at the center of the
selected area.

Parameters
----------

* enabled : A boolean (true or false) indicating whether or not cross-hairs should be displayed.

Related
-------
PXN8.crosshairs.isEnabled()

***/
PXN8.crosshairs.setEnabled = function(enabled){
    PXN8.crosshairs.enabled = enabled;
    PXN8.crosshairs.refresh();
};

/**************************************************************************

PXN8.crosshairs.isEnabled()
===========================
Returns a boolean value indicating whether or not the crosshairs are displayed in the selected area.

Returns
-------
*true* if the crosshairs are displayed in selections. *false* otherwise.

Related
-------
PXN8.crosshairs.setEnabled()

***/
PXN8.crosshairs.isEnabled = function(){
    return PXN8.crosshairs.enabled;
};



/**************************************************************************

PXN8.crosshairs.getImage()
==========================
Get the Image URL currently used for displaying the cross-hairs at the center
of the selected area.

Returns
-------
A URL to the image used as the cross-hairs image. The URL returned will be an
absolute URL including the domain name and full URL path.

Related
-------
PXN8.crosshairs.setImage()

***/
PXN8.crosshairs.getImage = function()
{
    if (PXN8.crosshairs.image == null){
        PXN8.crosshairs.image = PXN8.server + PXN8.root + "/images/pxn8_xhairs_white.gif";
    }
    return PXN8.crosshairs.image;
};

/***************************************************************************

PXN8.crosshairs.setImage()
==========================
You can change the cross-hairs image to suit your own tastes.

Parameters
----------
* imageURL : A URL to the image which should be displayed in the center of the selection area.

Related
-------
PXN8.crosshairs.getImage()

***/
PXN8.crosshairs.setImage = function(imageURL)
{
    PXN8.crosshairs.image = imageURL;
    PXN8.crosshairs.refresh();
};

/**
 * Called when the selection changes
 */
PXN8.crosshairs.refresh = function()
{
    var _ = PXN8.dom;
    var xhairs = _.id("pxn8_crosshairs");

    if (!PXN8.crosshairs.isEnabled()){
        if (xhairs){
            xhairs.style.display = "none";
        }
        return;
    }
    var sel = PXN8.getSelection();
    var _ = PXN8.dom;
    var selBounds = _.eb("pxn8_select_rect");
    var canvas = _.id("pxn8_canvas");
    if (!xhairs){
        xhairs = _.ac(canvas,_.ce("img",{id: "pxn8_crosshairs", src: PXN8.crosshairs.getImage()}));
    }
    if (selBounds.width <= 0){
        xhairs.style.display = "none";
        return;
    }
    //
    // center the crosshairs on the selection area
    //
    var xhcx = xhairs.width/2;
    var xhcy = xhairs.height/2;

    xhairs.style.display = "inline";
    xhairs.style.position = "absolute";
    //
    // wph 20070613 : must use Math.floor or the xhairs will not be perfectly centered
    // (off by up to 2 pixels to the right and bottom)
    //
    xhairs.style.left = Math.floor((selBounds.x + (selBounds.width / 2)) - xhcx) + "px";
    xhairs.style.top = Math.floor((selBounds.y + (selBounds.height / 2)) - xhcy) + "px";
};

PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.crosshairs.refresh);
PXN8.listener.add(PXN8.ON_ZOOM_CHANGE,PXN8.crosshairs.refresh);

/*
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 */
PXN8 = PXN8 || {};
/**************************************************************************

SECTION: Preview Functions
==========================
Pixenate allows you to display a "preview pane" on your page where you can see
a preview of what the currently active tool (lomo, colors, etc) will look like.
Ideally, the preview pane should only show a small section of the entire image.
The user can click and drag inside the preview pane to show obscured parts of the image.

***/

PXN8.preview = {};
PXN8.preview._div_to_intervalid = {};


/**************************************************************************
PXN8.preview.initialize()
=========================
Multiple Preview panes can be displayed at the same time so each distinct preview
pane must be associated with it's own preview state object. The preview state object
just contains information about the state of the preview pane : the relative position of
the background image and other information specific to that particular preview pane.
When you initialize a preview pane, you must provide an Element Id.

Parameters
----------
* ElementId : The ID attribute of the HTML Element (a DIV ideally) in which the preview will be displayed.

Returns
-------
An object which contains the state information for preview-related data for the supplied Element.
This object should be later passed to the PXN8.preview.show() and PXN8.preview.hide() functions.

Examples
--------
The following code will create a preview pane on the page. And will initialize the Preview pane
with a 'colors' operation...

    <div id="colors_preview" style="width: 120px; height: 120px;"></div>
    <script type="text/javascript">
       var colors_peek = PXN8.preview.initialize("colors_preview");
       PXN8.preview.show(colors_peek, {operation: "colors", brightness: 150, saturation: 150});
    </script>

<img src="pigeon300x225preview.jpg"/>

Related
-------
PXN8.preview.show() PXN8.preview.hide() OPERATIONS

***/
PXN8.preview.initialize = function(elementId,preview_method)
{
    if (!preview_method){
        preview_method = "crop";
    }
    if (preview_method == "crop" || preview_method == "resize"){

    }else{
        alert("Invalid preview method provided - use either 'crop' or 'resize'");
        return;
    }

    /**
     * It is VERY IMPORTANT that backgroundImageCache is enabled
     * in IE - otherwise there is an annoying flicker when the preview
     * pane is dragged.
     */
    try {
        document.execCommand('BackgroundImageCache', false, true);
    } catch(e) {}


    /**
     * Firstly, clear any existing event listeners and timers for the element.
     */
    PXN8.event.removeListener(elementId,"mousedown");
    PXN8.event.removeListener(elementId,"mouseup");
    PXN8.event.removeListener(elementId,"mousemove");
    var oldIntervalId = PXN8.preview._div_to_intervalid[elementId];
    if (oldIntervalId){
        clearInterval(oldIntervalId);
    }

    var result = {};

    var element = PXN8.dom.id(elementId);
    if (!element){return false;}

    // wph 20080401 preview_method can be either "resize" or "crop" - "crop" is default
    result._preview_method = preview_method;
    result._element = element;
    result._sizeX = parseInt(element.style.width);
    result._sizeY = parseInt(element.style.height);
    result._offset = {x: 0, y: 0};
    result._op = false;
    result._opQueue = [];
    result._intervalId = 0;
    result._beginDrag = {x: 0, y: 0};
    result._endDrag = {x: 0, y: 0};
    result._mouseDownCoords = {};
    result._mouseUpCoords = {};

    result._mouseDown = PXN8.event.closure(result,PXN8.preview._innerMouseDown);
    result._mouseUp = PXN8.event.closure(result,PXN8.preview._innerMouseUp);
    result._mouseMove = PXN8.event.closure(result,PXN8.preview._innerMouseMove);



    var img = PXN8.dom.id("pxn8_image");
    if (img){
        //
        // pxn8_image might not yet be present when PXN8.preview.initialize() is called
        //
        var real_image_width = img.width / PXN8.zoom.value();
        var real_image_height = img.height / PXN8.zoom.value();
        result._fullsize_ratio = Math.ceil(Math.max(real_image_height/result._sizeY, real_image_width/result._sizeX));
        /**
         * Center the preview pane on the image
         */
        PXN8.preview.centerOffset(result);

    }else{
        result._fullsize_ratio = -1;
    }

    result._intervalId = setInterval(function(){PXN8.preview._manageQueue(result);},750);

    PXN8.preview._div_to_intervalid[elementId] = result._intervalId;

    PXN8.event.addListener(elementId,"mousedown",result._mouseDown);


    return result;
};

/**
 * Show a part of the photo in the preview pane
 */
PXN8.preview.setOffset = function(object,x,y)
{
    var img = PXN8.dom.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();

    var maxY = ih - object._sizeY;
    var maxX = iw - object._sizeX;
    if (x < 0){ x = 0; }
    if (y < 0){ y = 0; }
    if (y > maxY) { y = maxY;}
    if (x > maxX) { x = maxX;}

    object._offset.x = x;
    object._offset.y = y;
    PXN8.preview._refresh(object);
};


/**
 * Make the preview pane show the center of the photo
 */
PXN8.preview.centerOffset = function(object)
{
    var _ = PXN8.dom;
    var image = _.id("pxn8_image");
    var iw = image.width / PXN8.zoom.value();
    var ih = image.height / PXN8.zoom.value();
    var halfW = object._sizeX / 2;
    var halfH = object._sizeY / 2;
    PXN8.preview.setOffset(object,Math.round((iw/2) - halfW), Math.round((ih/2) - halfH));
};




/**************************************************************************

PXN8.preview.show()
===================
Shows the preview pane.

Parameters
----------
* previewStateObject : The object which was returned from PXN8.preview.initialize()
* operation : An operation state object. This parameter is *optional* .

The operation parameter can be a single *operation* object (as described in <a href="#OPERATIONS">OPERATIONS</a>
or an array of operation objects.

Example
-------
The following code creates two buttons to increase and decrease the saturation of the photo
and show the change in a preview pane...


    <div id="colors_preview" style="width: 120px; height: 120px;"></div>
    <script type="text/javascript">
       //
       // declare a global variable 'colors_peek' to be used by buttons too.
       //
       colors_peek = PXN8.preview.initialize("colors_preview");
       saturation = 100;

       PXN8.preview.show(colors_peek);

       function increase_saturation(){
          saturation += 20;
          //
          // Update the preview pane.
          //
          PXN8.preview.show(colors_peek,{"operation": "colors", "saturation": saturation});
       }

       function decrease_saturation(){
          saturation -= 20;
          //
          // Update the preview pane.
          //
          PXN8.preview.show(colors_peek,{"operation": "colors", "saturation": saturation});
       }
    </script>

    <button onclick="increase_saturation()">+</button>
    <button onclick="decrease_saturation()">-</button>

<img src="pigeon300x225previewshow.jpg"/>

The following code demonstrates previewing a combined operation (Saturation and Interlace combined)...

    PXN8.preview.show(colors_peek,
		                [{"operation": "colors", "saturation": saturation},
                       {"operation": "interlace", opacity: 50,color: '#ffffff'}
 		                ]);

Related
-------
PXN8.preview.initialize() PXN8.preview.show() OPERATIONS

***/
PXN8.preview.show = function(object,op)
{
    var _ = PXN8.dom;

    if (!object._element){
        return;
    }
    if (object._element.style.display == "none"){
        object._element.style.display = "block";
    }
    if (op){ object._op = op; }

    if (op){
        PXN8.preview._enqueue(object);
    }
};

/**************************************************************************

PXN8.preview.hide()
===================
Hide (and clear) the preview pane. When you're finished with Preview Pane,
you should call this method to clean up.

Parameters
----------
* previewStateObject : The object which was returned from the call to PXN8.preview.initialize()

Examples
--------
Please refer to the <a href="example-preview.html">Preview Example</a>.

Related
-------
PXN8.preview.show() PXN8.preview.initialize() OPERATIONS

***/
PXN8.preview.hide = function(object)
{
    if (object._element){
        object._element.innerHTML = "";
        object._element.style.display = "none";
    }
    object._op = false;
    clearInterval(object._intervalId);

    PXN8.event.removeListener(object._element,"mousedown",object._mouseDown);
    PXN8.event.removeListener(object._element,"mousemove",object._mouseMove);
    PXN8.event.removeListener(object._element,"mouseup",object._mouseUp);
};


PXN8.preview._enqueue = function(object)
{
    object._opQueue.push(object._op);
};
/**
 * A global used by all preview panes.
 * don't throttle the server with too many spurious previews
 * If the last preview op hasn't completed then wait until it's complete before
 * submitting the next preview op.
 */
PXN8.preview._complete = true;

/**
 * description: Which part of the photo is the preview pane showing ?
 */
PXN8.preview._getOffset = function(object)
{
    return {x: object._offset.x, y: object._offset.y};
};

PXN8.preview._manageQueue = function(object)
{
    //
    // a last-in first-out queue.
    // all but the last op are ignored.
    // (this is to avoid excessive calls to the server).
    //
    var lastIndex = object._opQueue.length -1;
    if (lastIndex < 0){
        // nothing on the queue
        return;
    }

    if (!PXN8.preview._complete){
        return;
    }
    //
    // The pxn8_preview div uses the current image's src as it's backgroundImage anyway
    // so no need to go to the server unless there's an op to perform
    //
    var _ = PXN8.dom;
    var script = PXN8.getScript();
    var image = _.id("pxn8_image");
    if (!image){
        return;
    }

    //
    // set the _complete flag now
    //
    PXN8.preview._complete = false;

    var op = object._opQueue[lastIndex];

    object._opQueue.length = 0;


    var width = image.width / PXN8.zoom.value();
    var height = image.height / PXN8.zoom.value();

    var top = object._offset.y;
    var left = object._offset.x;
    //
    // round the x & y offsets to the nearest 100 pixels
    // to avoid too much server interaction
    // (snap-to-grid for less granular calls to server )
    //
    var gridSize = 100;

    var offsetTop = top % gridSize;
    var offsetLeft = left % gridSize;

    top = top - offsetTop;
    left = left - offsetLeft;

    var right = Math.min(width,left + (object._sizeX + gridSize));
    var bottom = Math.min(height, top + (object._sizeY + gridSize));

    var addedOps = [];

    var prepareOp = null;

    if (object._preview_method == "crop"){
        prepareOp = {"operation":"crop", "top":top, "left":left, "width":right-left, "height":bottom-top, "__quality":100, "__uncompressed":0};
    }else{
        prepareOp = {"operation":"resize", "width":right-left, "height":bottom-top};
    }


    if (width > object._sizeX && height > object._sizeY){
        addedOps.push(prepareOp);
    }
    var opsFromQueue = [];
    if (PXN8.isArray(op)){
        opsFromQueue = op;
    }else{
        opsFromQueue.push(op);
    }

    for (var i = 0;i < opsFromQueue.length; i++){
        var opFromQueue = opsFromQueue[i];

        /**
         * For the unsharpmask operation, the quality is very important
         * Only set the quality to 65 if a __quality attribute is not already present
         */
        if (opFromQueue["__quality"] == null){
            opFromQueue.__quality = 65;
        }
        opFromQueue.__uncompressed = 0;

        addedOps.push(opFromQueue);
    }


    object._element.innerHTML = '<span class="pxn8_preview_update">Please wait...</span>';


    var cachedImage = PXN8.getUncompressedImage();
    if (cachedImage){
        //
        // truncate script so it can be used for GET requests
        //
        script = [{"operation": "cache", "image":cachedImage}];
    }
    for (var i = 0;i < addedOps.length; i++){
        script.push(addedOps[i]);
    }

    PXN8.ajax.submitScript(script, function(jsonResponse){

        PXN8.preview._complete = true;
        _.cl(object._element);

        //
        // wph 20080218: Did the server return an error?
        //
        if (jsonResponse.status == "ERROR")
        {
            alert(jsonResponse.errorMessage);
            return;
        }
        if (!jsonResponse){
            return;
        }
        var prevImgURL = PXN8.server + PXN8.root + "/" + jsonResponse.image;
        object._element.style.backgroundImage = "url(" + prevImgURL + ")";
        var position = (offsetLeft * -1) + "px " + (offsetTop * -1) + "px";
        object._element.style.backgroundPosition = position;
    });
};

/**
 * Update the preview pane to reflect the current part of the photo showing.
 */
PXN8.preview._refresh = function(object)
{
    var _ = PXN8.dom;

    if (!object._element){
        return;
    }

    var position = (object._offset.x * -1) + "px " + (object._offset.y * -1) + "px";

    var image = _.id("pxn8_image");
    object._element.style.backgroundPosition = position;
    object._element.style.backgroundImage = "url(" + image.src + ")";
    object._element.style.cursor = "move";
    object._element.style.backgroundRepeat = "no-repeat";
};

PXN8.preview._innerMouseMove = function(event,object,source)
{
    var _ = PXN8.dom;

    object._endDrag = _.cursorPos(event);
    var offset = PXN8.preview._getOffset(object);
    var xdiff = object._beginDrag.x - object._endDrag.x;
    var ydiff = object._beginDrag.y - object._endDrag.y;

    if (object._fullsize_ratio == -1){
        var img = PXN8.dom.id("pxn8_image");
        if (img){
            var real_image_width = img.width / PXN8.zoom.value();
            var real_image_height = img.height / PXN8.zoom.value();
            object._fullsize_ratio = Math.ceil(Math.max(real_image_height/object._sizeY, real_image_width/object._sizeX));
        }
    }

    offset.x += xdiff * object._fullsize_ratio;
    offset.y += ydiff * object._fullsize_ratio;

    PXN8.preview.setOffset(object,offset.x,offset.y);
    object._beginDrag = object._endDrag;

};


PXN8.preview._innerMouseUp = function(event,object,source)
{
    PXN8.event.removeListener(object._element,"mouseup",object._mouseUp);
    PXN8.event.removeListener(object._element,"mouseout",object._mouseUp);
    PXN8.event.removeListener(object._element,"mousemove",object._mouseMove);
    PXN8.preview.show(object,object._op);
};


PXN8.preview._innerMouseDown = function(event,object,source)
{
    var _ = PXN8.dom;

    var img = _.id("pxn8_image");
    object._element.style.backgroundImage = "url(" + img.src + ")";


    PXN8.preview._refresh(object);

    object._beginDrag = _.cursorPos(event);
    object._mouseDownCoords = _.cursorPos(event);


    PXN8.event.addListener(object._element,"mouseup",object._mouseUp);
    PXN8.event.addListener(object._element,"mouseout",object._mouseUp);
    PXN8.event.addListener(object._element,"mousemove",object._mouseMove);
};


/**
 * (c) Copyright Sxoop Technologies Ltd. 2005 - 2009
 * For handling overlay image movement and resizing
 */

var PXN8 = PXN8 || {};
/***************************************************************************

SECTION: Overlay Helper Functions
=================================
Pixenate provides 2 functions to simplify UI programming for overlaying photos.
PXN8.overlay.start() puts the editor in *overlay* mode. In this mode, every time
the user makes or changes a selected area of the image, an overlay image will
be placed on top of the selected area. The overlay image will be resized to fit the selected
area. This function provides a handy way for users to *preview* what the the overlay will
look like when it is applied.
PXN8.overlay.stop() makes the editor exit *overlay* mode.

***/
PXN8.overlay = {};
/**************************************************************************

PXN8.overlay.start()
====================
PXN8.overlay.start() puts the editor into *overlay* mode. In this mode, an overlay image
appears superimposed on top of the photo whenever the selection is changed. This function
is provided as a convenience function to let users preview  the effects of the
<a href="#pxn8_tools_overlay">PXN8.tools.overlay()</a> function before applying it.
Please note that PXN8.overlay.start() does not change the photo. You still need to call PXN8.tools.overlay()
to *apply* the overlay.

Parameters
----------
* overlayURL : A url to the image to use as an overlay. This URL will be used to construct a new image so it should be an Image URL.
* dimensions : An object with *top*, *left*, *width* and *height* properties used to position the overlay image. The dimensions are relative to the top left corner of the <a href="#pxn8_canvas">pxn8_canvas</a> div.

Examples
--------
Please see the <a href="example-speech-bubbles.html">Speech bubbles</a> Example.

Related
-------
PXN8.overlay.stop()

***/
PXN8.overlay.start = function(overlayURL,dimensions)
{
    PXN8.overlay.stop();

    if (dimensions == undefined){
        dimensions = PXN8.getSelection();
    }


    var rects = ["top","bottom","left","right","topright","topleft","bottomleft","bottomright"];

    for (var i = 0;i < rects.length; i++){
        PXN8.dom.opacity("pxn8_" + rects[i] + "_rect",0);
    }

    var img = document.getElementById("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();

    if (dimensions.top == undefined && dimensions.left == undefined){
        dimensions.top = (ih/2) - (dimensions.height / 2);
        dimensions.left = (iw/2) - (dimensions.width / 2);
    }

    PXN8.select(dimensions);

    /**
     * create the overlay div if it's not already present
     */
    var canvas = PXN8.dom.id("pxn8_canvas");
    var overlayEl = PXN8.dom.id("pxn8_overlay");

    // work with PNGs in IE
    if (overlayURL.indexOf(".png") > -1 && PXN8.browser.isIE6())
    {
        canvas.removeChild(overlayEl);
        overlayEl = null;
    }

    if (overlayEl == null)
    {
        overlayEl = PXN8.dom.ce("img", {id: "pxn8_overlay"});
        overlayEl.style.position = "absolute";
        canvas.appendChild(overlayEl);

    }

	 if (overlayURL.indexOf(".png") > -1 && PXN8.browser.isIE6())
	 {
        overlayEl.onload = function(){PXN8.overlay.fixPNG(overlayEl);};
	 }
    overlayEl.src = overlayURL;

    PXN8.overlay.show();

    PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.overlay.show);
	 //
	 // wph 20080831 : Change overlay position when the image is zoomed too.
	 //
	 PXN8.listener.add(PXN8.ON_ZOOM_CHANGE, PXN8.overlay.show);

    PXN8.crosshairs.setEnabled(false);

};





/**************************************************************************

PXN8.overlay.stop()
===================
Makes the editor exit *overlay* mode. You should call this function when the user applies or cancels
an overlay operation.

Examples
--------
Please see the <a href="example-speech-bubbles.html">Speech bubbles</a> Example.

Related
-------
PXN8.overlay.start()

***/
PXN8.overlay.stop = function()
{

    var rects = ["top","bottom","left","right","topright","topleft","bottomleft","bottomright"];

    for (var i = 0;i < rects.length; i++){
        PXN8.dom.opacity("pxn8_" + rects[i]+ "_rect",PXN8.style.notSelected.opacity);
    }
    PXN8.listener.remove(PXN8.ON_SELECTION_CHANGE,PXN8.overlay.show);

	 PXN8.listener.remove(PXN8.ON_ZOOM_CHANGE, PXN8.overlay.show);

    var overlay = document.getElementById("pxn8_overlay");
    /* it's possible the overlay element doesn't exist yet - if called from PXN8.overlay.start */
    if (overlay){
        overlay.style.display = "none";
    }
    PXN8.selectByRatio("free");

    PXN8.crosshairs.setEnabled(true);
    /*
     * don't unselect
    PXN8.unselect();
     */

};

PXN8.overlay.show = function (){

    var overlay = document.getElementById("pxn8_overlay");

    var sel = PXN8.getSelection();
    var zoom = PXN8.zoom.value();

    overlay.style.display = "block";

    overlay.style.top = (sel.top * zoom) + "px";
    overlay.style.left = (sel.left * zoom) + "px";

    overlay.width = sel.width * zoom;
    overlay.height = sel.height * zoom;
    //
    // if the image is no longer an <IMG/> tag but a <SPAN/> tag
    // (a PNG fixed for IE6) then set the style width and height attributes
    //
    overlay.style.width = sel.width * zoom + "px";
    overlay.style.height = sel.height * zoom + "px";

};
/**
 * Fix it so that PNGs display correctly in IE 6
 */
PXN8.overlay.fixPNG = function(img)
{
    if (!PXN8.browser.isIE6()){
        return;
    }
    if (img.src.indexOf(".png") == -1){
        return;
    }

    var imgID = (img.id) ? "id='" + img.id + "' " : "";

    var imgClass = (img.className) ? "class='" + img.className + "' " : "";

    var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' ";

    var imgStyle = "display:inline-block;" + img.style.cssText ;

    if (img.align == "left") imgStyle = "float:left;" + imgStyle;

    if (img.align == "right") imgStyle = "float:right;" + imgStyle;

    if (img.parentElement.href) imgStyle = "cursor:hand;" + imgStyle;

    var strNewHTML = "<span " + imgID + imgClass + imgTitle + " style=\"" + "width:" + img.width + "px; height:" + img.height + "px;" + imgStyle + ";";

    strNewHTML = strNewHTML + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader";

    strNewHTML = strNewHTML + "(src=\'" + img.src + "\', sizingMethod='scale');\"></span>" ;
    img.outerHTML = strNewHTML;

};
/* ============================================================================
 *
 * (c) 2005-2009 Sxoop Technologies Ltd. All rights reserved.
 *
 * For support contact support@sxoop.com
 *
 * These function handle sliders as used by some of the
 * tool configuration panels.
 */
var PXN8 = PXN8 || {};
PXN8.slide = {};
/**
 * Turn a regular HTML div element into a slide
 * slideElement = The empty DIV that will be turned into a slide
 * inputElementId = The INPUT field which will be updated when the user moves the slider
 * startRange = The smallest valid value
 * rangeSize = The size of the range (range = startSize + rangeSize)
 * so for example if you want to create a slider with range 80 ... 130 startRange is 80, and rangeSize is 50
 * initValue = The start / default value for the slider & input
 * optionalCallback = An optional function which gets called whenever the user moves the slider or
 * the user changes a value in the INPUT field.
 */
PXN8.slide.bind = function(slideElement,inputElementId,startRange,rangeSize,initValue,increment,optionalCallback)
{
    if (!increment){
        increment = 1;
    }

    if (typeof slideElement == 'string'){
        slideElement = PXN8.dom.id(slideElement);
    }

    slideElement.className = "pxn8_slide";
    slideElement.onmousedown = function(event){
        if (!event) event = window.event;
        PXN8.slide.onmousedown(slideElement,event,inputElementId,startRange,rangeSize,increment);
    };
    var slider = document.createElement("span");
    slider.className = "pxn8_slider";
    slideElement.appendChild(slider);

    PXN8.slide.refresh_slider(slider,initValue,startRange,rangeSize);

    var inputElement = PXN8.dom.id(inputElementId);
    if (!inputElement){
        alert("ERROR: NO <input/> element was found with id=\"" + inputElementId + "\"");
        return false;
    }

    inputElement.value = initValue;
    inputElement.onblur = function(){
        if (isNaN(this.value)){
            this.value = startRange;
        }
        if (this.value > startRange + rangeSize){
            this.value = startRange + rangeSize;
        }
        if (this.value < startRange){
            this.value = startRange;
        }
        PXN8.slide.refresh_slider(slider,this.value,startRange,rangeSize);
    };
    if (typeof optionalCallback == 'function'){
        PXN8.event.addListener(slideElement,"mouseup",optionalCallback);
        PXN8.event.addListener(inputElement,"change",optionalCallback);
    }
};

PXN8.slide.refresh_slider = function(slider,value,startRange,rangeSize)
{
    slider.style.left = (3 + (((value-startRange) / rangeSize) * 117)) + "px";
};



/**
 * This method is called when the user mousedowns on a div of class 'pxn8_slide'
 * Every div of class pxn8_slide should have a child div of class 'pxn8_slider'
 * The slide is the horizontal area through which the slider moves. The slider is the
 * bar indicator which indicates where the current position is in the slide.
 *
 * |--------------------| slide
 *                 ^      slider
 *
 * -- param slide The slide div
 * -- param event The mouse event which triggered this call (need to obtain position)
 * -- param inputId An input element whose value must be updated whenever the slider is moved
 * -- param start The start value (lowest possible value that can appear in the input element
 *          (basically the lowest in the range)
 * -- param size The range of values that can appear in the input element.
 */
PXN8.slide.onmousedown = function(slide,event,inputId,start,size,increment)
{
    var kids = slide.getElementsByTagName("*");
    var slider = undefined;
    for (var i = 0; i < kids.length; i++){
        if (kids[i].className == "pxn8_slider"){
            slider = kids[i];
            break;
        }
    }
    slider.onmousemove = null;
    var inputElement = document.getElementById(inputId);

    slide.onmousemove = function(evt){
        return PXN8.slide.update(slider,inputElement,slide,evt,start,size,increment);
    };
    slide.onmouseup = function(){
        slide.onmousemove = null;
    };

    PXN8.slide.update(slider,inputElement,slide,event,start,size,increment);
};
/**
 *
 */
PXN8.slide.update = function(slider,inputElement,slide, evt,start,size,increment)
{
    evt = (evt)?evt:window.event;
    var px = PXN8.slide.position(slide);
    var nx = evt.clientX - px;
    if (nx <= 120 && nx >= 3){
        //slider.style.left = (nx-3) + "px";
        var iv = start + (((nx-3) / 117 ) * size);

        // to the nearest increment
        iv = iv - (iv % increment);
        PXN8.slide.refresh_slider(slider,iv,start,size);
        inputElement.value = Math.round(iv,2);
    }
};
/**
 * get the X position of an element relative to it's parent
 */
PXN8.slide.position = function (obj)
{
    var curleft = 0;
    if (obj.offsetParent)
    {
        while (obj.offsetParent)
        {
            curleft += obj.offsetLeft;
            obj = obj.offsetParent;
        }
    }else if (obj.x){
        curleft += obj.x;
    }

    return curleft;
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code which handles color selection
 *
 */
var PXN8 = PXN8 || {};

PXN8.colors = {};

PXN8.colors.values = [
	 "#000000","#000033","#000066","#000099","#0000CC","#0000FF","#330000","#330033","#330066","#330099","#3300CC",
    "#3300FF","#660000","#660033","#660066","#660099","#6600CC","#6600FF","#990000","#990033","#990066","#990099",
    "#9900CC","#9900FF","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#FF0000","#FF0033","#FF0066",
    "#FF0099","#FF00CC","#FF00FF","#003300","#003333","#003366","#003399","#0033CC","#0033FF","#333300","#333333",
    "#333366","#333399","#3333CC","#3333FF","#663300","#663333","#663366","#663399","#6633CC","#6633FF","#993300",
    "#993333","#993366","#993399","#9933CC","#9933FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF",
    "#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#006600","#006633","#006666","#006699","#0066CC",
	 "#0066FF","#336600","#336633","#336666","#336699","#3366CC","#3366FF","#666600","#666633","#666666","#666699",
    "#6666CC","#6666FF","#996600","#996633","#996666","#996699","#9966CC","#9966FF","#CC6600","#CC6633","#CC6666",
    "#CC6699","#CC66CC","#CC66FF","#FF6600","#FF6633","#FF6666","#FF6699","#FF66CC","#FF66FF","#009900","#009933",
    "#009966","#009999","#0099CC","#0099FF","#339900","#339933","#339966","#339999","#3399CC","#3399FF","#669900",
    "#669933","#669966","#669999","#6699CC","#6699FF","#999900","#999933","#999966","#999999","#9999CC","#9999FF",
    "#CC9900","#CC9933","#CC9966","#CC9999","#CC99CC","#CC99FF","#FF9900","#FF9933","#FF9966","#FF9999","#FF99CC",
    "#FF99FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#33CC00","#33CC33","#33CC66","#33CC99",
    "#33CCCC","#33CCFF","#66CC00","#66CC33","#66CC66","#66CC99","#66CCCC","#66CCFF","#99CC00","#99CC33","#99CC66",
    "#99CC99","#99CCCC","#99CCFF","#CCCC00","#CCCC33","#CCCC66","#CCCC99","#CCCCCC","#CCCCFF","#FFCC00","#FFCC33",
    "#FFCC66","#FFCC99","#FFCCCC","#FFCCFF","#00FF00","#00FF33","#00FF66","#00FF99","#00FFCC","#00FFFF","#33FF00",
    "#33FF33","#33FF66","#33FF99","#33FFCC","#33FFFF","#66FF00","#66FF33","#66FF66","#66FF99","#66FFCC","#66FFFF",
    "#99FF00","#99FF33","#99FF66","#99FF99","#99FFCC","#99FFFF","#CCFF00","#CCFF33","#CCFF66","#CCFF99","#CCFFCC",
    "#CCFFFF","#FFFF00","#FFFF33","#FFFF66","#FFFF99","#FFFFCC","#FFFFFF"
];

/*
 * Return the HTML to show a color picker
 */
PXN8.colors.picker = function(initColor,callback)
{
    var _ = PXN8.dom;

    var cols = 18;
    var rows = Math.ceil(PXN8.colors.values.length/cols);

	 var well = _.ce("div", {className : "pxn8_color_well"});
    well.style.backgroundColor = initColor;

	 var input = _.ce("input",
	 {
		  className : "pxn8_color_value",
		  type:  "text",
		  value: initColor
	 });

	 input.onchange = function()
	 {
		  var v = input.value;
		  if (v.match(/#[0-9A-Fa-f]{6}/))
		  {
				well.style.backgroundColor = v;
				callback(v);
		  }
		  else
		  {
				alert(PXN8.strings.INVALID_HEX_VALUE);
		  }
	 };
	 var reset = _.ce("button",{ className : "pxn8_color_reset" });
	 _.ac(reset,_.tx(PXN8.strings.RESET));
	 reset.onclick = function()
	 {
		  well.style.backgroundColor = initColor;
		  input.value = initColor;
		  callback(initColor);
	 };

    var closure = function(color,func)
	 {
        return function(){
				well.style.backgroundColor=color;
				input.value = color;
				func(color);
		  };
    };

    var table = _.ce("table", {className: "pxn8_color_table"});
    table.setAttribute("cellpadding","0");
    table.setAttribute("cellspacing","0");
    table.cellPadding = 0;
    table.cellSpacing = 0; // IE
	 var row;

    var tbody = _.ac(table,_.ce("tbody"));
    for (var i = 0;i < rows;i++)
    {
        row = _.ac(tbody,_.ce("tr"));
        for (var j = 0; j < cols; j++)
		  {
            var cell = _.ac(row,_.ce("td"));
            var index = (i*cols)+j;
            if (index < PXN8.colors.values.length){
                var color = PXN8.colors.values[index];
                var link = _.ce("a",
					 {
						  href: "#",
						  title: color,
						  onclick: closure(color,callback)
					 });
                _.ac(link,_.tx(" "));
                link.style.backgroundColor = color;
                _.ac(cell,link);
            }
        }
    }

	 var container = _.ce("div",{className: "pxn8_color_container"});
	 var picker = _.ce("div",{className: "pxn8_color_picker"});

	 _.ac(picker, well);
	 _.ac(picker, input);
	 _.ac(picker, reset);

	 _.ac(container,table);
	 _.ac(container,picker);

    return container;

};
/**
 * (c) 2006-2008 Sxoop Technologies Ltd.
 *
 * A bridge between javascript and server-side ImageMagick.
 *
 */

var PXN8 = PXN8 || {};

/*************************************************************************

SECTION: ImageMagick Bridge functions
=====================================
Using the ImageMagick plugin, it is possible to call imagemagick functions from
javascript.
This can prove useful if you want to extend the features available in Pixenate without
writing server-side Pixenate plugins in Perl.
For complex functions, you should probably consider coding a server-side Perl plugin but for basic
functions, you can now code a client-side javascript plugin.
The following table shows how you might write the Fill-Flash plugin using only Javascript. On the left hand side is
listed the javascript code to perform a fill-flash. This code uses the PXN8.ImageMagick API to call ImageMagick manipulation routines via javascript.
On the right hand side is shown the equivalent code implemented as a perl server-side plugin.

<table>
  <tr>
    <th>Javascript (client side)</th>
    <th>Perl (server side) Equivalent</th>
  </tr>
 <tr>
 <td valign="top"><pre>



function fill_flash(opacity)
{
   var image = PXN8.ImageMagick.start();

   var brighter = image.Clone();

   if (!opacity){
      opacity = 50;
   }

   image.Method("Composite",{"image": brighter,
                             "compose": "Screen",
                             "opacity": opacity + "%"});

   PXN8.ImageMagick.end(image);
}
</pre></td>

<td valign="top"><pre>
use strict;
use Sxoop::PXN8 ':all';

sub fill_flash
{
  my ($image, $params) = @_;

  my $brighter = $image->Clone();

  my $opacity = 50;
  if (exists $params->{opacity}){
      $opacity = $params->{opacity};
  }
  my $imrc = $image->Composite(image   => $brighter,
                               compose => "Screen",
                               opacity => $opacity . "%");
  if (is_imagick_error($imrc))
  {
      die "FillFlash failed: $imrc";
  }
  return $image;
}
AddOperation('fill_flash', \&fill_flash);
1;
</pre></td>
</tr>
</table>

***/

/****************************************************************************

PXN8.ImageMagick
================
PXN8.ImageMagick is the object used as a proxy for the server-side Image::Magick class.
PXN8.ImageMagick is a <a href="http://en.wikipedia.org/wiki/Mock_object">Mock Object</a>. Methods invoked on this object are not implemented
in the client in realtime - the methods are invoked on the server when the PXN8.ImageMagick.end() method is called.

You construct a new PXN8.ImageMagick object like so...

var myImage = new PXN8.ImageMagick();

... However, it is more common to simply use the default PXN8.ImageMagick() object returned by the global PXN8.ImageMagick.start() method.

The PXN8.ImageMagick object has just 4 methods.

* Method : Use this to invoke image manipulation routines on the image. For a full list of methods available you should consult the ImageMagick website. <a href="http://imagemagick.org/script/perl-magick.php#manipulate">http://imagemagick.org/script/perl-magick.php#manipulate</a>.
** Method takes 2 parameters, the name of the Method (e.g. "Crop", "Rotate", "Draw" etc) and the parameters for the method. Please bear in mind that PXN8.ImageMagick is a mock object - the <em>Method</em> method lets you use the full array of ImageMagick manipulation routines.

* Clone : This works exactly like the Clone function in ImageMagick. It creates a deep copy of an existing image.

* Append : This will append all images side by side.

* push : pushes an image on to the image stack. (equivalent to perl code : push (@$image1, $image2) ;)

In addition, there are 2 static functions belonging to the PXN8.ImageMagick class.

* PXN8.ImageMagick.start() : This returns an object which is a reference to the current image. (see example of use above)

* PXN8.ImageMagick.end(image, updateFlag) : This commits all of the ImageMagick operations to the server and indicates which of the images to send to the editor. There is an optional <em>updateFlag</em> which indicates whether or not the operation should be committed or simply returned. It might be useful to simply return the operation if you would like to include it as one of a batch of operations (see PXN8.tools.updateImage() ).

Example
-------

The following snippet of code will create a reflection at the bottom of the current image.

    //
    // start an editing session using PXN8.ImageMagick
    //
    var image = PXN8.ImageMagick.start();

    var reflectionHeight = 100;
    var imageWidth = PXN8.getImageSize().width;

    //
    // clone the image and flip the clone
    //
    var flipped = image.Clone();
    flipped.Method("Flip");

    //
    // create a white background for the reflection to fade to.
    //
    var white = new PXN8.ImageMagick();
    white.Method("Set",{size: imageWidth + "x" + reflectionHeight});
    white.Method("Read", "xc:white");

    //
    // create a gradient mask to apply to the flipped image
    //
    var mask = new PXN8.ImageMagick();
    mask.Method("Set", {size: imageWidth + "x" + reflectionHeight});
    mask.Method("Read", "gradient:#000000-#FFFFFF");
    mask.Method("Set", {matte: false});

	 //
    // apply the mask
    //
    white.Method("Composite",{image: mask, compose: "CopyOpacity"});

    //
    // superimpose the faded white on top of the flipped image
    //
    flipped.Method("Composite",{image: white, compose: "Over", x:0, y:0});

    //
    // crop the flipped image
    //
    flipped.Method("Crop",{x:0,y:0,width: imageWidth, height: reflectionHeight});

    //
    // push the flipped image on to the original
    //
    image.push(flipped);

    //
    // append the images
    //
    var reflection = image.Append(false);

    //
    // Commit the operations to the server
    //
    PXN8.ImageMagick.end(reflection);

You can see the example code in action <a href="example-imagemagick.html">here</a>.

An example of how to create a fake-polaroid effect using Pixenate's ImageMagick bridge is <a href="example-imagemagick2.html">available here</a>.

An example of adding a grey border to make a rectangular image square is in <a href="example-imagemagick3.html">Example #3</a>.

A more advanced example of using PXN8.ImageMagick to create a clone tool that uses free-hand selection can be found <a href="example-clone.html">here</a>.

***/

PXN8.ImageMagick = function()
{
    var id = PXN8.ImageMagick.objectCount++;
    if (id == 0){
        this.handle = "_";
    }else{
        this.handle = "im" + id;
    }
    return this;
};

PXN8.ImageMagick.curry = function(methodName){
	 return function(a) {
		  var statement = new Array();
		  statement.push(this.handle);
		  statement.push(methodName);
        for (var i = 0; i < arguments.length;i++){
            statement.push(arguments[i]);
        }
        PXN8.ImageMagick.statements.push(statement);
	 };
};
PXN8.ImageMagick.prototype.Method = function()
{
	 var methodName = arguments[0];
	 if (!PXN8.ImageMagick.prototype[methodName])
	 {
		  PXN8.ImageMagick.prototype[methodName] = PXN8.ImageMagick.curry(methodName);
	 }
    var statement = new Array();
    statement.push(this.handle);
    for (var i = 0; i < arguments.length;i++){
        statement.push(arguments[i]);
    }
    PXN8.ImageMagick.statements.push(statement);
};

PXN8.ImageMagick.prototype.Crop = function(coords){
	 var _coords = {x: (coords.left || coords.x),
						 y: (coords.top || coords.y),
						 width: coords.width,
						 height: coords.height};
	 this.Method("Crop",_coords);
};

PXN8.ImageMagick.prototype._CropToText = function ( textParams){
	 this.Method("_CropToText", textParams) ;
};
PXN8.ImageMagick.prototype._ScaleTo = function (scaleParams){
	 this.Method("_ScaleTo", scaleParams) ;
};
PXN8.ImageMagick.prototype.Clone = function()
{
    var clone = new PXN8.ImageMagick();
    var statement = new Array();
    statement.push(clone.handle);
    statement.push("clone_from");
    statement.push(this.handle);
    PXN8.ImageMagick.statements.push(statement);
    return clone;
};

PXN8.ImageMagick.prototype.Append = function(stack)
{
    var appended = new PXN8.ImageMagick();
    var statement = new Array();
    statement.push(appended.handle);
    statement.push("append");
    statement.push([this.handle,stack]);
    PXN8.ImageMagick.statements.push(statement);
    return appended;
};

PXN8.ImageMagick.prototype.push = function(image){
	 var statement = new Array();
	 statement.push(this.handle);
	 statement.push("push");
	 statement.push(image.handle);
	 PXN8.ImageMagick.statements.push(statement);
	 return this;
};

PXN8.ImageMagick.start = function()
{
    PXN8.ImageMagick.objectCount = 0;
    var image = new PXN8.ImageMagick();
    PXN8.ImageMagick.statements = new Array();
    return image;
};

PXN8.ImageMagick.end = function(image,callback)
{
    var op = {operation: "imagemagick"};
    op.script = PXN8.ImageMagick.statements;
    op.script.push([image.handle,"return"]);

	 if (typeof callback == "undefined")
	 {
		  PXN8.tools.updateImage([op]);
	 }else if (typeof callback == "boolean"){
		  if (callback == true){
				PXN8.tools.updateImage([op]);
		  }
	 }else if (typeof callback == "function"){
		  PXN8.ajax.submitScript([op],callback);
	 }
	 return op;
};
/****************************************************************************

PXN8.ImageMagick.maskFromPaths
==============================
This utility function will return a PXN8.ImageMagick() image object which is a
mask constructed (white on black) from the supplied paths. If you want to reverse the mask
then call the ImageMagick "Negate" method on the returned object.
This function can be used in conjunction with PXN8.freehand.* functions to create a mask from a path.
This can be very useful for creating freehand masks, e.g. for more fine-grained application of tools such as
Red-Eye and teeth-whitening.

Parameters
----------

* size  : An object with 'width' and 'height' properties (for example the object returned by PXN8.getImageSize() )
* paths : An array of PXN8.freehand.Path objects

Returns
-------
A PXN8.ImageMagick() object which is a white-on-black mask constructed from the supplied paths.

Examples
--------
See <a href="example-advanced-redeye.html">Advanced Red-Eye Removal</a>.

Related
-------
PXN8.freehand.Path() PXN8.freehand.start() PXN8.freehand.end() PXN8.ImageMagick.start() PXN8.ImageMagick.end()

***/
PXN8.ImageMagick.maskFromPaths = function(size, paths)
{
	 var result = new PXN8.ImageMagick();
	 result.Set({size: size.width + "x" + size.height});
	 result.Read("xc:#000000");
	 for (var i = 0; i < paths.length; i++){
       var path = paths[i];
       var ps = PXN8.freehand.getPoints(path);
       result.Draw({stroke: "#ffffff", fill: "#00000000", primitive: "path", points: ps, strokewidth: path.width});
    }
    // must reset mask's matte to false _after_ Draw because Draw
    // resets it to true
	 result.Set({matte: false});
	 return result;
};
// some common methods
PXN8.ImageMagick.methods = ["AdaptiveBlur","AdaptivelyResize","AdaptiveSharpen","AdaptiveThreshold",
                            "AddNoise","AffineTransform","Annotate","AutoOrient",
                            "Blur","Border","BlackThreshold",
                            "Charcoal","Colorize","Contrast","Composite",
                            "Draw",
                            "Edge","Emboss","Enhance","Extent",
                            "Flip","Flop","Frame","Gamma","GaussianBlur","Implode","Label",
                            "Level","Magnify","Mask","Minify","Modulate","MotionBlur",
                            "Negate","Normalize","OilPaint","Opaque","Posterize","Quantize","Raise",
                            "ReduceNoise","Resample","Read",,"Rotate","Resize","Roll","Set","SetPixel",
                            "Shadow","Sharpen","Shear","Sketch","Solarize","Spread","Stegano","Stereo","Strip","Swirl",
                            "Texture","Thumbnail","Threshold","Tint","Transparent","Transpose","Transverse",
                            "Trim","UnsharpMask","Vignette","Wave","WhiteThreshold"];

for (var i = 0; i < PXN8.ImageMagick.methods.length;i++)
{
    var method = PXN8.ImageMagick.methods[i];
    PXN8.ImageMagick.prototype[method] = PXN8.ImageMagick.curry(method);
}

/*
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 */
PXN8 = PXN8 || {};

/**************************************************************************

SECTION: Freehand drawing Functions
===================================
Pixenate allows you to do freehand drawing on top of an image. This is
achieved by first calling PXN8.freehand.start() to put the editor in a
state ready for drawing. Once the editor is in this state, the user
can draw freehand lines of any color and width by moving the pointer
while the mouse button is held down.
The color can be set using the PXN8.freehand.color property. The width
of the line can be set using the PXN8.freehand.width property.

<blockquote>N.B. PXN8.freehand is a relatively new feature of
Pixneate. It depends on Raphael javascript libraries.</blockquote>

***/

PXN8.freehand = {};

/**************************************************************************

PXN8.freehand.color
===================
Set this property to change the default color that a new PXN8.freehand.Path object will use,
when created in freehand mode. Color should be a hex string. Default value is "red"

Examples
--------
   PXN8.freehand.color = "#00ff00"; // green

***/
PXN8.freehand.color = "#ff0000";

/**************************************************************************

PXN8.freehand.opacity
=====================
Set this property to change the default opacity that a new PXN8.freehand.Path object will use,
when created in freehand mode. Default value is 0.

Examples
--------
   PXN8.freehand.opacity = ; // green

***/
PXN8.freehand.opacity = 0;

/**************************************************************************

PXN8.freehand.width
===================
Set this property to change the default brush width that a new PXN8.freehand.Path object will use,
when created in freehand mode. Width should be numeric.

Examples
--------
   PXN8.freehand.width = 12; // a brush 12x12 pixels.

***/
PXN8.freehand.width = 1;

// private
PXN8.freehand.started = false;

/**************************************************************************

PXN8.freehand.start()
=========================
Line drawing in Pixenate is achieved by passing an array of
PXN8.freehand.Path objects to the PXN8.tools.freehand() function. The
easiest way to construct an array of Path objects is using the
PXN8.freehand.start() / PXN8.freehand.end() pair of functions. What
these functions do is place the editor in a special mode where the
user can draw freehand lines using the mouse (Each line in a freehand
drawing is equivalent to 1 PXN8.freehand.Path object so a freehand
drawing can be thought of as an array of PXN8.freehand.Path objects).

Related
-------
PXN8.freehand.end PXN8.tools.freehand

Examples
--------
<a href="example-freehand.html">Freehand Drawing Example</a>

***/
PXN8.freehand.start = function()
{
	 if (PXN8.log){ PXN8.log.trace("PXN8.freehand.start()"); }

	 var _ = PXN8.dom;

	 if (!Raphael){
		  alert("Warning: PXN8.freehand requires the Raphael Javascript Library");
		  return;
	 }
    if (PXN8.freehand.started){
        alert("Warning: PXN8.freehand has already started");
        return;
    }
	 var self = PXN8.freehand;

    self.started = true;

	 PXN8.unselect();
	 var pos = _.ep("pxn8_canvas");

	 var theImage = _.id("pxn8_image");

	 var glassPane = _.ce("div",{id: "pxn8_freehand_glasspane"});

    _.css(glassPane,
	 {
		  top             : pos.y + "px",
		  left            : pos.x + "px",
		  position        : "absolute" ,
		  width           : theImage.width,
		  height          : theImage.height,
		  backgroundColor : "white",
		  opacity         : 0.01,
		  filter          : "alpha(opacity:1)",
		  cursor          : "crosshair"
	 });
	 _.ac(document.body,glassPane);

	 var raphaelContainer = _.ce("div",{id: "pxn8_raphael_container"});
	 _.css(raphaelContainer,
	 {
		  position : "absolute",
		  top      : pos.y + "px",
		  left     : pos.x + "px"
	 });
	 _.ac(document.body, raphaelContainer);

	 self.paper = Raphael("pxn8_raphael_container",theImage.width,theImage.height);

	 self.oldmousemove = document.body.onmousemove;
	 document.body.onmousemove = self.onmousemove;

    self.oldmousedown = document.body.onmousedown;
    self.oldmouseup = document.body.onmouseup;

    document.body.onmousedown = function(event){
        event = event || window.event;
        event.cancelBubble = true;
        self.mousedown = true;
		  _.css("pxn8_raphael_container",{cursor: "crosshair"});
    };
    document.body.onmouseup = function(event){
        event = event || window.event;
        self.mousedown = false;
		  _.css("pxn8_raphael_container",{cursor: "default"});
    };

	 PXN8.listener.add(PXN8.ON_ZOOM_CHANGE,self.onZoomChange);
};

PXN8.freehand.onZoomChange = function(eventType,zoom)
{
	 var self = PXN8.freehand;

	 var sz = PXN8.getImageSize();

    PXN8.dom.id("pxn8_raphael_container").innerHTML = "";
	 self.paper = Raphael("pxn8_raphael_container",sz.width * zoom,sz.height * zoom);

	 //
	 // redraw all the canvas contents
	 //

	 self.refresh();
};
/**
 * Refresh the Raphael canvas
 */
PXN8.freehand.refresh = function()
{
	 var self = PXN8.freehand;
	 var i = 0;
	 self.paper.clear();
	 for (i = 0;i < self.paths.length; i++){
		  self.paths[i].refresh();
	 }
};
/**************************************************************************
PXN8.freehand.end()
=========================
This function takes the editor out of freehand drawing mode and back
to its standard editing mode. All previous settings are restored.

Related
-------
PXN8.freehand.start PXN8.tools.freehand

Examples
--------
<a href="example-freehand.html">Freehand Drawing Example</a>

***/
PXN8.freehand.end = function()
{
	 if (PXN8.log){ PXN8.log.trace("PXN8.freehand.end()"); }

    var self = PXN8.freehand;
    if (!self.started){
        return;
    }
    self.started = false;
	 self.activePath = null;
	 self.paths = [];

	 document.body.removeChild(document.getElementById("pxn8_freehand_glasspane"));
	 document.body.removeChild(document.getElementById("pxn8_raphael_container"));
    document.body.onmousemove = self.oldmousemove;
    document.body.onmousedown = self.oldmousedown;
    document.body.onmouseup = self.oldmouseup;

	 PXN8.listener.remove(PXN8.ON_ZOOM_CHANGE,self.onZoomChange);
};
/**************************************************************************
PXN8.freehand.undo()
=========================
A function for fine-grained undo of individual paths within a freehand drawing.
This function will remove the last path created in freehand mode.

***/

PXN8.freehand.undo = function()
{
	 var self = PXN8.freehand;
	 var lastPath = self.paths.pop();
	 if (!lastPath){
		  return;
	 }
	 lastPath.view.remove();
};


PXN8.freehand.count = 0;
PXN8.freehand.mousedown = false;

PXN8.freehand.isDrawing = function(event){
	 return PXN8.freehand.mousedown || event.button;
};
PXN8.freehand.onmousemove = function(event)
{
	 event = event || window.event;
	 var self = PXN8.freehand;

    if (!self.isDrawing(event))
    {
        // the mouse has move and the shift key isn't pressed.
        // this means a new path must be created the next time the mouse is
        // moved and the shift key is held down.
        self.count = 0;
        return;
    }
    var canvasPos = PXN8.dom.ep("pxn8_canvas");
	 var windowOffset = PXN8.getWindowScrollPoint();
    var cx = Math.floor(event.clientX - canvasPos.x + windowOffset.x);
    var cy = Math.floor(event.clientY - canvasPos.y + windowOffset.y);

   var theImage = document.getElementById("pxn8_image");

   if (cx < 0 || cx > theImage.width
       || cy < 0 || cy > theImage.height){
       self.count = 0;
       return;
   }


	if (self.count != 0)
   {
		 PXN8.freehand.activePath.lineTo(cx,cy);
   }
	else
   {
       self.count = 2;
       PXN8.freehand.activePath = new PXN8.freehand.Path(
			  PXN8.freehand.width,
			  PXN8.freehand.color,
			  cx, cy, []);
   }
};
PXN8.freehand.activePath = null;
PXN8.freehand.paths = [];

PXN8.freehand.getPaths = function()
{
	 var result = [];
	 var i = 0;
	 var path = null;
	 for (i = 0;i < PXN8.freehand.paths.length; i++){
		  path = PXN8.freehand.paths[i];
		  //
		  // check that path isn't just a M x, y with no accompanying L or
		  // you will end up with a line going from top left corner of image to x, y
		  //
		  if (path.d.length > 2){
				result.push(path);
		  }
	 }
	 return result;
};
/**
 * Given a PXN8.freehand.Path object, return it's points as a string for use in ImageMagick operations.
 *
 */
PXN8.freehand.getPoints = function(path)
{
	 var result = null;
	 if (path.d.length > 2){
		  result = "M " + path.d[0] + " " + path.d[1] + " L";
		  for (j = 2; j < path.d.length; j = j + 2){
				result += " " + path.d[j] + " " + path.d[j+1];
		  }
	 }
	 return result;
};
/**************************************************************************

PXN8.freehand.Path()
=========================
A PXN8.freehand.Path() object contains all of the information and
methods for drawing a Path ( a series of lines ) both on the client
(using Raphael JS library) and on the server (using Pixenate /
ImageMagick).

Paramaters
----------
* width : The brush width (should be numeric) in pixels.
* color : The stroke color (should be hex strig - e.g. "#ff0000" for red).
* x     : The x coordinate for the start of the path
* y     : The y coordinate for the start of the path

Returns
-------
A new PXN8.freehand.Path object.

Examples
--------

    //
    // create a square 50 x 50 starting at point 100, 10 (100 from
    // left, 10 from top)
    //
    var myPath = new PXN8.freehand.Path(2,"#ff0000", 100, 10);
    myPath.lineTo(150, 10);
    myPath.lineTo(150, 60);
    myPath.lineTo(100, 60);
    myPath.lineTo(100, 10);

	 //
    // bake the path into the image
	 //
    PXN8.tools.freehand([myPath]);


***/
PXN8.freehand.Path = function(width, color, x, y)
{
	 var zoom = PXN8.zoom.value();

    this.d = [x / zoom, y / zoom];
    this.color = color;
    this.width = width;
	 this.opacity = PXN8.freehand.opacity;

    this.view = PXN8.freehand.paper.path(
		  {
				stroke: color,
				"fill-opacity": this.opacity,
				"stroke-width":width * zoom
		  });
    this.view.moveTo(x,y);
    this.view.lineTo(x,y);
	 PXN8.freehand.paths.push(this);
    return this;

};

/**************************************************************************

PXN8.freehand.Path.lineTo()
===========================

The lineTo() method is used to plot a new point on the path to which a
line will be drawn.

Parameters
----------
* x     : The x coordinate for the next point of the path
* y     : The y coordinate for the next point of the path

***/
PXN8.freehand.Path.prototype.lineTo = function(x,y)
{
    this.view.lineTo(x,y);
	 var zoom = PXN8.zoom.value();
    this.d.push(x / zoom,y / zoom);
};
PXN8.freehand.Path.prototype.refresh = function(){
	 var x,y = 0;
	 var zoom = PXN8.zoom.value();

	 x = this.d[0] * zoom;
	 y = this.d[1] * zoom;
	 var i = 0;
	 this.view = PXN8.freehand.paper.path(
		  {
				stroke: this.color,
				"fill-opacity":this.opacity,
				"stroke-width":this.width * zoom
		  });
    this.view.moveTo(x,y);
    this.view.lineTo(x,y);
	 for (i = 2;i < this.d.length; i+=2){
		  x = this.d[i] * zoom;
		  y = this.d[i+1] * zoom;
		  this.view.lineTo(x,y);
	 }
};

/**************************************************************************

PXN8.tools.freehand()
=====================

Allows freehand drawing on top of an image.

Parameters
----------
* paths : An array of PXN8.freehand.Path objects.

Examples
--------

    //
    // create a square 50 x 50 starting at point 100, 10 (100 from
    // left, 10 from top)
    //
    var myPath = new PXN8.freehand.Path(2,"#ff0000", 100, 10);
    myPath.lineTo(150, 10);
    myPath.lineTo(150, 60);
    myPath.lineTo(100, 60);
    myPath.lineTo(100, 10);

    //
    // bake the path into the image
    //
    PXN8.tools.freehand([myPath]);

See also: <a href="example-freehand.html">Freehand Drawing Example</a>


Related
-------
PXN8.freehand.Path PXN8.freehand.start PXN8.freehand.end

***/
PXN8.tools.freehand = function(paths)
{
	 if (typeof paths == "undefined"){
		  paths = PXN8.freehand.paths;
	 }
    var i =0,
		  j = 0,
		  path = null,
		  ps = "",
		  p = null,
		  image = null,
		  end = 0;

	 var ops = [];

	 if (PXN8.ajax.useXHR)
	 {
        var image = PXN8.ImageMagick.start();
		  var paths = PXN8.freehand.getPaths();
        for (;i < paths.length;i++)
        {
            path = paths[i];
				ps = PXN8.freehand.getPoints(path);
				image.Draw({stroke: path.color, fill: "#00000000", primitive: "path", points: ps, strokewidth: path.width});

        }
		  PXN8.ImageMagick.end(image);
	 }
    else
    {
        var chunkedPaths = [];
        //
        // if not using XHR, there is an upper limit of about 2000 chars per request
        // break long paths into chunks of 100 because no single Pixenate operation should exceed about 1000 characters.
        //
        for (i = 0; i < paths.length;i++){
            p = paths[i];
            j = p.d.length;
            var start = 0;
            while (j > 0){
                end = Math.min(start+j,start+100);
                var tp = {width: p.width, color: p.color, d:[]};
                tp.d = p.d.slice(start,end);
                chunkedPaths.push(tp);
                j = j - (end - start);
                start = end;
            }
        }

        for (i = 0;i < chunkedPaths.length;i++)
        {
            path = chunkedPaths[i];
				if (path.d.length > 2)
				{
					 image = PXN8.ImageMagick.start();
					 ps = PXN8.freehand.getPoints(path);
					 image.Draw({stroke: path.color, fill: "#00000000", primitive: "path", points: ps, strokewidth: path.width});
					 ops.push(PXN8.ImageMagick.end(image,false));
				}
        }
        PXN8.tools.updateImage(ops);
	 }
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code which handles AJAX / JSON requests
 *
 */

var PXN8 = PXN8 || {};

/**************************************************************************

SECTION: Pixenate AJAX Functions
===========================================
Pixenate provides functions to create XMLHttpRequest Objects and to submit photo editing operations using custom JSON response handlers.
***/

PXN8.ajax = {};
    // 
    // ======================================================================
PXN8.ajax.useXHR = true;
    //


/***************************************************************************

PXN8.ajax.createRequest
=======================
Create a XMLHttpRequest Object.

Returns
-------
A new XMLHttpRequest Object.

***/
PXN8.ajax.createRequest = function(){

	if (typeof XMLHttpRequest != 'undefined') {
   	 return new XMLHttpRequest();
   }
   try 	{
       return new ActiveXObject("Msxml2.XMLHTTP");
   } catch (e) {
       try {
           return new ActiveXObject("Microsoft.XMLHTTP");
       } catch (e) { }
   }
   return false;
};

/***************************************************************************

PXN8.ajax.submitScript
======================
Submit a series of image-manipulation commands to the server. This is the end-point
through which all image manipulation commands are passed to the server.
Normally you would not call this directly. However if you would like to make changes to
a photo without changing the current working displayed photo, you can do so by calling this
function and providing your own callback function. If you do so, then the current displayed image
will not be updated.

Parameters
----------
* script : An array containing a series of operations to be performed on the photo.
* callback
A function which will be called when the server has completed the supplied series of operations and returns with *JSON* response.
The callback function should accept a single parameter of type object.
The object supplied to the callback will have the following important properties...
    * status : A string value that can be eiterh "OK" or "ERROR" (if an error occurred while the server was processing the script.
    * errorMessage : A string value. Blank if no error occurred.
    * image : A relative (to PXN8.root) path to the compressed (bandwidth-friendly) image. This will be empty if an error has occurred.
    * uncompressed : A relative (to PXN8.root) path to the uncompressed (100% quality) image. This will be empty if an error has occurred.

Example
-------

    var script = PXN8.getScript();
    PXN8.ajax.submitScript(script,function(jsonResponse){
        if (jsonResponse.status == "OK"){
           var image = jsonResponse.image;
           // do something with the image
        }else{
           alert(jsonResponse.errorMessage);
        }
    });
***/
PXN8.ajax.submitScript = function(script, callback)
{
	 if (PXN8.log)
	 {
		  PXN8.log.trace("PXN8.ajax.submitScript() ----- START -----");
		  var s = PXN8.objectToString(script);
		  PXN8.log.trace(s);
		  PXN8.log.trace("PXN8.ajax.submitScript() ------ END ------");
	 }
	 var path, suffix, i, op, scriptToText, cachedJSON;
	 var urlWithoutScript, url, scriptSizeLimit, bite, c;

	 // wph 20080611
	 // although Pixenate is primarily for editing Photos (JPEG) some customers
	 // use it for editing non-photo files (e.g. PNGs).
	 // sometimes it is not desirable to convert a PNG to JPEG - for example if the PNG
	 // has an alpha channel.
	 //
    if (typeof (PXN8.convertToJPEG) == "boolean" && PXN8.convertToJPEG == false)
	 {
        //
        // ensure that the file suffix is preserved for all operations
        //
        // Need to first determine what type of image the script is using
        // (can do this by looking at either the 'fetch' or 'cache' operation's
        // image, url or filepath parameter.
        suffix = '.jpg';
		  for (i =0;i < script.length; i++)
		  {
				op = script[i];
            if (op.operation == "fetch")
				{
                path = op.url || op.filepath;
					 c = path.indexOf('?');
					 if (c == -1){
						  c = path.length;
					 }
                suffix = path.substring(path.lastIndexOf('.'),c);
					 break;
            }
				if (op.operation == "cache")
				{
                path = op.image;
					 c = path.indexOf('?');
					 if (c == -1){
						  c = path.length;
					 }
                suffix = path.substring(path.lastIndexOf('.'),c);
					 break;
				}
        }
		  // we have the suffix - now force all ops to use that suffix
		  for (i = 0;i < script.length;i++)
		  {
				op = script[i];
				if (!op.__extension)
				{
					 op.__extension = suffix;
				}else{
					 // wph 20090107 if the op *has* an extension ensure all subsequent ops have same extension
					 // (e.g. transparent + rotate should result in a .png file)
					 suffix = op.__extension;
				}

		  }
    }
	 // wph 20090107 Ensure that if an extension has been set for an operation, then all subsequent operations have
	 // the same extension
	 suffix = '';
	 for (i = 0;i < script.length; i++){
		  op = script[i];
		  if (op.__extension){
				suffix = op.__extension;
		  }else{
				if (suffix != ''){
					 op.__extension = suffix;
				}
		  }
	 }

    // wph 20070226:
    // optimize the script.
    //
    // [1] if there are 2 or more sequential resizes, then only use the last resize.
    // [2] if the user is rotating (without flipping ) then aggregate each rotation into
    // a single operation (modulus 360).
    //
    script = PXN8.optimizeScript(script);

    scriptToText = escape(PXN8.objectToString(script));

    cachedJSON = PXN8.json.getResponseForScript(scriptToText);

    if (cachedJSON){
        //
        // call immediately without going to the server
        //
        callback(cachedJSON);
        return;
    }


	 if (PXN8.ajax.useXHR == false)
    {
		  urlWithoutScript = PXN8.server + PXN8.root + "/" + PXN8.basename + "?callback=pxn8cb&script=";

		  url = urlWithoutScript + scriptToText;

		  //
		  // Internet Explorer imposes a limit of 2048 (2083 but 2048 is recommended to be on the safe side)
		  // characters on any GET url.
		  //
		  scriptSizeLimit = 2048 - urlWithoutScript.length;
		  if (scriptToText.length > scriptSizeLimit)
		  {
				//
            // divide and conquer
            //
            // take a manageable bite and leave the remainder for the next request.
            // Think of it like pacman...
            // when each manageable request is complete we have a token pointing to the end
            // of that request chain.
            // we then start a new request chain using that token as a starting point.
            //
            bite = "[";
            c = 0;
            i = 0;
            for (;i < script.length; i++)
            {
                var optext = escape(PXN8.objectToString(script[i]));
                if (bite.length + optext.length  < scriptSizeLimit)
                {
                    bite = bite + (i>0?"," : "");
                    bite = bite + optext;
                }else{
                    break;
                }
            }
            bite = bite + "]";

            var remainder = script.slice(i);

            pxn8cb = function(json) {
                var script = [{"operation": "cache", "image": json.image}];
                for (var i = 0;i < remainder.length;i++){
                    script.push(remainder[i]);
                }
                PXN8.ajax.submitScript(script,callback);
            };
            url = urlWithoutScript + bite;

        }
        else
        {
            pxn8cb = function(json){
					 PXN8.json.setResponseForScript(scriptToText,json);
                callback(json);
            };
        }
		  var insertScriptTag = function()
		  {
				var scriptTag = document.createElement("script");
				scriptTag.setAttribute("type", "text/javascript");
				scriptTag.setAttribute("src", url);
				document.getElementsByTagName("head")[0].appendChild(scriptTag);
		  };
		  if (PXN8.browser.isIE6())
		  {
				// wph 20080729
				// IE6: for long script URLs (e.g. PXN8.ImageMagick funcs) the script sometimes never loads.
				// only workaround is to postpone insertion.
				setTimeout(insertScriptTag,50);
		  }
		  else
		  {
				insertScriptTag();
		  }

	 }
    else // PXN8.ajax.useXHR = true
    {
        var req = PXN8.ajax.createRequest();

        var onJSONerror = function(r)
        {
            alert(unescape(PXN8.strings.WEB_SERVER_ERROR) + "\n" + r.statusText + "\n" + r.responseText) ;
            var timer = document.getElementById("pxn8_timer");
            if (timer){
                timer.style.display = "none";
            }
            PXN8.updating = false;
        };

        PXN8.json.bindScriptToResponse(req,callback,scriptToText,onJSONerror);

        req.open("POST", PXN8.root + "/" + PXN8.basename, true);
        req.setRequestHeader('Content-Type',
                             'application/x-www-form-urlencoded');

        var submission = "script=" + scriptToText;

        req.send(submission);

	 }
};

/**
 * Perform a series of optimizations on the script
 */
PXN8.optimizeScript = function(script)
{
    var self = PXN8;

    for (var i = 0; i < self.optimizations.length; i++){
        var optimize = self.optimizations[i];
        script = optimize(script);
    }
    return script;
};

PXN8.optimizations = [
function(script) {
    /**
     * flatten resize ops
     */
    var result = [];

    for (var i = 0;i < script.length; i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && op.operation == 'resize' && nextop.operation == 'resize'){
            //
            // do nothing - skip this operation
            //
        }else{
            result.push(op);
        }
    }
    return result;
},
function(script) {
    /**
     * remove 2nd + more consecutive normalize operations.
     * (normalize - unlike enhance is not progressive)
     */
    var result = [];
    for (var i =0; i < script.length;i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && (nextop.operation == 'normalize') && (op.operation == 'normalize')){
        }else{
            result.push(op);
        }
    }
    return result;
},
function(script) {
    var result = [];

    var colorsOp = null;
    //
    // optimizations for consecutive 'colors' operations.
    //
    for (var i = 0; i < script.length; i++){
        var op = script[i];
        if (op.operation != "colors"){
            if (colorsOp != null){
                result.push(colorsOp);
            }
            result.push(op);
            colorsOp = null;
        }else{
            if (colorsOp != null){
                //
                // saturation, brightness and hue are multiplicative
                //
                colorsOp.saturation = ((colorsOp.saturation / 100) * (op.saturation /100)) * 100;
                colorsOp.brightness = ((colorsOp.brightness / 100) * (op.brightness /100)) * 100;
                colorsOp.hue = ((colorsOp.hue / 100) * (op.hue /100)) * 100;
                //
                // contrast is additive
                //
                colorsOp.contrast = colorsOp.contrast  + op.contrast ;
            }else{
                colorsOp = op;
            }
        }
    }
    if (colorsOp != null){
        result.push(colorsOp);
    }
    return result;
},

function(script) {
    /**
     * modulus 360 all consecutive rotate ops
     */
    var result = [];
    for (var i = 0;i < script.length; i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && (nextop.operation == 'rotate') && (op.operation == 'rotate')){
            //
            //
            //
            var flipping = (op.flipvt || op.fliphz || nextop.flipvt || nextop.fliphz);
            if (!flipping) {
                nextop.angle = (op.angle + nextop.angle) % 360;
            }else{
                //
                // it's a flip
                // is it the same type of flip as next op and are angles 0 in both cases ?
                if ((op.angle == 0 && nextop.angle == 0) && ((op.flipvt == nextop.flipvt) && (op.fliphz == nextop.fliphz))){
                    //
                    // it's two flipvts in a row or two fliphzs in a row
                    //
                    i += 1;
                }else{
                    result.push(op);
                }

            }
        }else{
            if (op.operation == 'rotate'){
                var flipping = (op.flipvt || op.fliphz || nextop.flipvt || nextop.fliphz);
                if (!flipping && op.angle == 0){
                    // skip operation - it's effectively a NOP
                }else{
                    // it's a straight rotation with an angle > 0
                    result.push(op);
                }
            }else{
                result.push(op);
            }
        }
    }
    return result;
}
];


PXN8.json = function()
{
	 var that = { };
    /**
     * An associative array of scriptText / responseText pairings.
     */
	 var scriptCache = { };
	 function getResponseForScript(script){ return scriptCache[script];};
	 function setResponseForScript(script,json){ scriptCache[script] = json;};
    /**
     * wph 20070131
     * Store scriptText/responseText pairings in a cache
     * avoid unnecessary calls to the server
     */
	 function bindScriptToResponse (request,callback,scriptAsString,onerror) {
		  request.onreadystatechange = function(){
				if (request.readyState == 4) {

					 if (request.status == 200) {
						  var json ;
						  try{
								json  = eval('('+ request.responseText + ')');
								//
								// store request/response pairing in the cache
								//
								setResponseForScript(scriptAsString,json);
                    }catch (e){
                        alert("An exception occured tring to evaluate server response:\n" +
                              request.responseText + "\nException: " + e);
                        //
                        // wph 20070905
                        // also output the error text to a window so the user can copy and paste it
                        // for sending bug reports
                        //
                        var wnd = window.open("","","height=200, width=640, scrollbars=1, resizable=1, location=0, menubar=0, toolbar=0");
                        var doc = wnd.document;
                        doc.open("text/plain");
                        doc.writeln("If contacting the site administrator, please copy and paste the following...");
                        doc.writeln("============================================================================");
                        doc.writeln("An exception (" + e + ") occured trying to evaluate server response:");
                        doc.writeln(request.responseText);
                        doc.close();
                        return;

                    }
                    callback(json);
                } else {
                    if (onerror){
                        onerror(request);
                    }else{
                        alert(unescape(PXN8.strings.WEB_SERVER_ERROR) + "\n" + request.statusText + "\n" + request.responseText) ;
                    }
                }
            }
        };
    };


    that.bindScriptToResponse = bindScriptToResponse;
    that.getResponseForScript = getResponseForScript;
    that.setResponseForScript = setResponseForScript;

    return that;
}();


    /* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2009
 * All rights reserved.
 *
 * This file contains code which handles saving of images
 *
 */
var PXN8 = PXN8 || {};

/*************************************************************************

SECTION: Saving edited Photos
=============================
The following functions are used for saving the edited photo to the user's
client-side storage or to the server's own storage on the web.

***/

PXN8.save = {};

/**************************************************************************

PXN8.save.toDisk()
==================
Save the photo to the user's client-side storage.

***/
PXN8.save.toDisk = function()
{
    var uncompressedImage = PXN8.getUncompressedImage();
    if (uncompressedImage){
        var newURL = PXN8.server + PXN8.root + "/save.pl?";

        if (typeof pxn8_original_filename == "string"){
            newURL += "originalFilename=" + pxn8_original_filename + "&";
        }

        newURL += "image=" + uncompressedImage;

        document.location = newURL;

    }else{
        document.location = "#";
        PXN8.show.alert("You have not changed the image !");
    }
};

/**************************************************************************

PXN8.save.toServer()
====================
Save to server is a wrapper function. It in turn will call *pxn8_save_image()*
which is a function which must be implemented by the customer.

Related
-------
PXN8.getUncompressedImage pxn8_save_image

***/
PXN8.save.toServer = function()
{

    var relativeFilePathToUncompressedImage = PXN8.getUncompressedImage();

    /**
     * wph 20070102 : Don't prohibit the user from saving just because they haven't changed
     * the image.
     * Let the custom pxn8_save_image() function handle that case if needed.
     *
     if (!relativeFilePathToUncompressedImage){
     alert("The image has not been modified.");
     return false;
     }
    */

    if (typeof pxn8_save_image == 'function'){
        return pxn8_save_image(relativeFilePathToUncompressedImage);
    } else {

        alert("This feature is not available by default.\n" +
              "To enable this feature you must create a PHP,ASP or JSP page to save the image to your own server.\n" +
              "You must also create a javascript function called 'pxn8_save_image()' - it's first parameter is the URL of the changed image.\n" +
              "The path to the changed image (relative to the directory where PXN8 is installed) is " + PXN8.getUncompressedImage());
        return false;
    }

};

/**************************************************************************

pxn8_save_image()
=================
This function is not provided by Pixenate but must be implemented by the customer
if you want to be able to save edited photos to your own webserver.

Parameters
----------

* imagePath : A path to the image which should be saved or <em>false</em> if the image has not been changed.

The imagePath parameter will be a path relative to the directory where pixenate is installed.

For example the imagePath parameter might be *cache/03_04fbcedaf099feded02working.jpg*.
If Pixenate is installed at /var/www/html/pixenate then the actual path to the file will be...

    /var/www/html/pixenate/cache/03_04fbcedaf099feded02working.jpg

... so to save the image to your webserver's filesystem or database you need to copy the image at the
above path to your own permanent storage.
You should provide a .PHP, .JSP, .ASP or CGI program to do just this.
Your pxn8_save_image() function should call this CGI passing the *imagePath* value as a parameter
to the server program.

Examples
--------

    function pxn8_save_image( newImagePath ){
       //
       // newImagePath parameter will be something like...
       //
       // cache/03_04fbcedaf099feded02working.jpg
       //
       // ... this path is relative to where pixenate was installed

       if (newImagePath != false){
          document.location = "save.php?replacementImagePath=" + newImagePath;
       }else{
          alert("The image has not been modified");
       }
    }

Related
-------
PXN8.save.toServer PXN8.getUncompressedImage

***/

// 