/*!
 * ngImgCrop v0.3.2
 * https://github.com/alexk111/ngImgCrop
 *
 * Copyright (c) 2014 Alex Kaul
 * License: MIT
 *
 * Generated at Wednesday, December 3rd, 2014, 3:54:12 PM
 */
(function() {
    'use strict';

    var crop = angular.module('ngImgCrop', []);

    crop.factory('cropAreaCircle', ['cropArea', function(CropArea) {
        var CropAreaCircle = function() {
            CropArea.apply(this, arguments);

            this._boxResizeBaseSize = 20;
            this._boxResizeNormalRatio = 0.9;
            this._boxResizeHoverRatio = 1.2;
            this._iconMoveNormalRatio = 0.9;
            this._iconMoveHoverRatio = 1.2;

            this._boxResizeNormalSize = this._boxResizeBaseSize * this._boxResizeNormalRatio;
            this._boxResizeHoverSize = this._boxResizeBaseSize * this._boxResizeHoverRatio;

            this._posDragStartX = 0;
            this._posDragStartY = 0;
            this._posResizeStartX = 0;
            this._posResizeStartY = 0;
            this._posResizeStartSize = 0;

            this._boxResizeIsHover = false;
            this._areaIsHover = false;
            this._boxResizeIsDragging = false;
            this._areaIsDragging = false;
        };

        CropAreaCircle.prototype = new CropArea();

        CropAreaCircle.prototype._calcCirclePerimeterCoords = function(angleDegrees) {
            var hSize = this._size / 2;
            var angleRadians = angleDegrees * (Math.PI / 180),
                circlePerimeterX = this._x + hSize * Math.cos(angleRadians),
                circlePerimeterY = this._y + hSize * Math.sin(angleRadians);
            return [circlePerimeterX, circlePerimeterY];
        };

        CropAreaCircle.prototype._calcResizeIconCenterCoords = function() {
            return this._calcCirclePerimeterCoords(-45);
        };

        CropAreaCircle.prototype._isCoordWithinArea = function(coord) {
            return Math.sqrt((coord[0] - this._x) * (coord[0] - this._x) + (coord[1] - this._y) * (coord[1] - this._y)) < this._size / 2;
        };
        CropAreaCircle.prototype._isCoordWithinBoxResize = function(coord) {
            var resizeIconCenterCoords = this._calcResizeIconCenterCoords();
            var hSize = this._boxResizeHoverSize / 2;
            return (coord[0] > resizeIconCenterCoords[0] - hSize && coord[0] < resizeIconCenterCoords[0] + hSize &&
                coord[1] > resizeIconCenterCoords[1] - hSize && coord[1] < resizeIconCenterCoords[1] + hSize);
        };

        CropAreaCircle.prototype._drawArea = function(ctx, centerCoords, size) {
            ctx.arc(centerCoords[0], centerCoords[1], size / 2, 0, 2 * Math.PI);
        };

        CropAreaCircle.prototype.draw = function() {
            CropArea.prototype.draw.apply(this, arguments);

            // draw move icon
            this._cropCanvas.drawIconMove([this._x, this._y], this._areaIsHover ? this._iconMoveHoverRatio : this._iconMoveNormalRatio);

            // draw resize cubes
            this._cropCanvas.drawIconResizeBoxNESW(this._calcResizeIconCenterCoords(), this._boxResizeBaseSize, this._boxResizeIsHover ? this._boxResizeHoverRatio : this._boxResizeNormalRatio);
        };

        CropAreaCircle.prototype.processMouseMove = function(mouseCurX, mouseCurY) {
            var cursor = 'default';
            var res = false;

            this._boxResizeIsHover = false;
            this._areaIsHover = false;

            if (this._areaIsDragging) {
                this._x = mouseCurX - this._posDragStartX;
                this._y = mouseCurY - this._posDragStartY;
                this._areaIsHover = true;
                cursor = 'move';
                res = true;
                this._events.trigger('area-move');
            } else if (this._boxResizeIsDragging) {
                cursor = 'nesw-resize';
                var iFR, iFX, iFY;
                iFX = mouseCurX - this._posResizeStartX;
                iFY = this._posResizeStartY - mouseCurY;
                if (iFX > iFY) {
                    iFR = this._posResizeStartSize + iFY * 2;
                } else {
                    iFR = this._posResizeStartSize + iFX * 2;
                }

                this._size = Math.max(this._minSize, iFR);
                this._boxResizeIsHover = true;
                res = true;
                this._events.trigger('area-resize');
            } else if (this._isCoordWithinBoxResize([mouseCurX, mouseCurY])) {
                cursor = 'nesw-resize';
                this._areaIsHover = false;
                this._boxResizeIsHover = true;
                res = true;
            } else if (this._isCoordWithinArea([mouseCurX, mouseCurY])) {
                cursor = 'move';
                this._areaIsHover = true;
                res = true;
            }

            this._dontDragOutside();
            angular.element(this._ctx.canvas).css({ 'cursor': cursor });

            return res;
        };

        CropAreaCircle.prototype.processMouseDown = function(mouseDownX, mouseDownY) {
            if (this._isCoordWithinBoxResize([mouseDownX, mouseDownY])) {
                this._areaIsDragging = false;
                this._areaIsHover = false;
                this._boxResizeIsDragging = true;
                this._boxResizeIsHover = true;
                this._posResizeStartX = mouseDownX;
                this._posResizeStartY = mouseDownY;
                this._posResizeStartSize = this._size;
                this._events.trigger('area-resize-start');
            } else if (this._isCoordWithinArea([mouseDownX, mouseDownY])) {
                this._areaIsDragging = true;
                this._areaIsHover = true;
                this._boxResizeIsDragging = false;
                this._boxResizeIsHover = false;
                this._posDragStartX = mouseDownX - this._x;
                this._posDragStartY = mouseDownY - this._y;
                this._events.trigger('area-move-start');
            }
        };

        CropAreaCircle.prototype.processMouseUp = function( /*mouseUpX, mouseUpY*/ ) {
            if (this._areaIsDragging) {
                this._areaIsDragging = false;
                this._events.trigger('area-move-end');
            }
            if (this._boxResizeIsDragging) {
                this._boxResizeIsDragging = false;
                this._events.trigger('area-resize-end');
            }
            this._areaIsHover = false;
            this._boxResizeIsHover = false;

            this._posDragStartX = 0;
            this._posDragStartY = 0;
        };

        return CropAreaCircle;
    }]);



    crop.factory('cropAreaSquare', ['cropArea', function(CropArea) {
        var CropAreaSquare = function() {
            CropArea.apply(this, arguments);
            this.resSizeScale=1;
            this._resizeCtrlBaseRadius = 10;
            this._resizeCtrlNormalRatio = 0.75;
            this._resizeCtrlHoverRatio = 1;
            this._iconMoveNormalRatio = 0.9;
            this._iconMoveHoverRatio = 1.2;

            this._resizeCtrlNormalRadius = this._resizeCtrlBaseRadius * this._resizeCtrlNormalRatio;
            this._resizeCtrlHoverRadius = this._resizeCtrlBaseRadius * this._resizeCtrlHoverRatio;

            this._posDragStartX = 0;
            this._posDragStartY = 0;
            this._posResizeStartX = 0;
            this._posResizeStartY = 0;
            this._posResizeStartSize = 0;

            this._resizeCtrlIsHover = -1;
            this._areaIsHover = false;
            this._resizeCtrlIsDragging = -1;
            this._areaIsDragging = false;
        };
        CropAreaSquare.prototype = new CropArea();

        CropAreaSquare.prototype._calcSquareCorners = function() {
            var hSize = this._size / 2;
            return [
                [this._x - hSize, this._y - hSize/this.resSizeScale],
                [this._x + hSize, this._y - hSize/this.resSizeScale],
                [this._x - hSize, this._y + hSize/this.resSizeScale],
                [this._x + hSize, this._y + hSize/this.resSizeScale]
            ];
        };

        CropAreaSquare.prototype._calcSquareDimensions = function() {
            var hSize = this._size / 2;
            return {
                left: this._x - hSize,
                top: this._y - hSize,
                right: this._x + hSize,
                bottom: this._y + hSize
            };
        };

        CropAreaSquare.prototype._isCoordWithinArea = function(coord) {
            var squareDimensions = this._calcSquareDimensions();
            return (coord[0] >= squareDimensions.left && coord[0] <= squareDimensions.right && coord[1] >= squareDimensions.top && coord[1] <= squareDimensions.bottom);
        };

        CropAreaSquare.prototype._isCoordWithinResizeCtrl = function(coord) {
            var resizeIconsCenterCoords = this._calcSquareCorners();
            var res = -1;
            for (var i = 0, len = resizeIconsCenterCoords.length; i < len; i++) {
                var resizeIconCenterCoords = resizeIconsCenterCoords[i];
                if (coord[0] > resizeIconCenterCoords[0] - this._resizeCtrlHoverRadius && coord[0] < resizeIconCenterCoords[0] + this._resizeCtrlHoverRadius &&
                    coord[1] > resizeIconCenterCoords[1] - this._resizeCtrlHoverRadius && coord[1] < resizeIconCenterCoords[1] + this._resizeCtrlHoverRadius) {
                    res = i;
                    break;
                }
            }
            return res;
        };

        CropAreaSquare.prototype._drawArea = function(ctx, centerCoords, size,scale) {
            var hSize = size / 2;
            scale=scale||1;
            ctx.rect(centerCoords[0] - hSize, centerCoords[1] - hSize/scale, size, size/scale);
        };

        CropAreaSquare.prototype.draw = function() {
          
            CropArea.prototype.draw.apply(this, arguments);

            // draw move icon
            this._cropCanvas.drawIconMove([this._x, this._y], this._areaIsHover ? this._iconMoveHoverRatio : this._iconMoveNormalRatio);

            // draw resize cubes
            var resizeIconsCenterCoords = this._calcSquareCorners();
            for (var i = 0, len = resizeIconsCenterCoords.length; i < len; i++) {
                var resizeIconCenterCoords = resizeIconsCenterCoords[i];
                this._cropCanvas.drawIconResizeCircle(resizeIconCenterCoords, this._resizeCtrlBaseRadius, this._resizeCtrlIsHover === i ? this._resizeCtrlHoverRatio : this._resizeCtrlNormalRatio);
            }
        };

        CropAreaSquare.prototype.processMouseMove = function(mouseCurX, mouseCurY) {
            var cursor = 'default';
            var res = false;

            this._resizeCtrlIsHover = -1;
            this._areaIsHover = false;

            if (this._areaIsDragging) {
                this._x = mouseCurX - this._posDragStartX;
                this._y = mouseCurY - this._posDragStartY;
                this._areaIsHover = true;
                cursor = 'move';
                res = true;
                this._events.trigger('area-move');
            } else if (this._resizeCtrlIsDragging > -1) {
                var xMulti, yMulti;
                switch (this._resizeCtrlIsDragging) {
                    case 0: // Top Left
                        xMulti = -1;
                        yMulti = -1;
                        cursor = 'nwse-resize';
                        break;
                    case 1: // Top Right
                        xMulti = 1;
                        yMulti = -1;
                        cursor = 'nesw-resize';
                        break;
                    case 2: // Bottom Left
                        xMulti = -1;
                        yMulti = 1;
                        cursor = 'nesw-resize';
                        break;
                    case 3: // Bottom Right
                        xMulti = 1;
                        yMulti = 1;
                        cursor = 'nwse-resize';
                        break;
                }
                var iFX = (mouseCurX - this._posResizeStartX) * xMulti;
                var iFY = (mouseCurY - this._posResizeStartY) * yMulti;
                var iFR;
                if (iFX > iFY) {
                    iFR = this._posResizeStartSize + iFY;
                } else {
                    iFR = this._posResizeStartSize + iFX;
                }
                var wasSize = this._size;
                this._size = Math.max(this._minSize, iFR);
                var posModifier = (this._size - wasSize) / 2;
                this._x += posModifier * xMulti;
                this._y += posModifier * yMulti;
                this._resizeCtrlIsHover = this._resizeCtrlIsDragging;
                res = true;
                this._events.trigger('area-resize');
            } else {
                var hoveredResizeBox = this._isCoordWithinResizeCtrl([mouseCurX, mouseCurY]);
                if (hoveredResizeBox > -1) {
                    switch (hoveredResizeBox) {
                        case 0:
                            cursor = 'nwse-resize';
                            break;
                        case 1:
                            cursor = 'nesw-resize';
                            break;
                        case 2:
                            cursor = 'nesw-resize';
                            break;
                        case 3:
                            cursor = 'nwse-resize';
                            break;
                    }
                    this._areaIsHover = false;
                    this._resizeCtrlIsHover = hoveredResizeBox;
                    res = true;
                } else if (this._isCoordWithinArea([mouseCurX, mouseCurY])) {
                    cursor = 'move';
                    this._areaIsHover = true;
                    res = true;
                }
            }

            this._dontDragOutside();
            angular.element(this._ctx.canvas).css({ 'cursor': cursor });

            return res;
        };

        CropAreaSquare.prototype.processMouseDown = function(mouseDownX, mouseDownY) {
            var isWithinResizeCtrl = this._isCoordWithinResizeCtrl([mouseDownX, mouseDownY]);
            if (isWithinResizeCtrl > -1) {
                this._areaIsDragging = false;
                this._areaIsHover = false;
                this._resizeCtrlIsDragging = isWithinResizeCtrl;
                this._resizeCtrlIsHover = isWithinResizeCtrl;
                this._posResizeStartX = mouseDownX;
                this._posResizeStartY = mouseDownY;
                this._posResizeStartSize = this._size;
                this._events.trigger('area-resize-start');
            } else if (this._isCoordWithinArea([mouseDownX, mouseDownY])) {
                this._areaIsDragging = true;
                this._areaIsHover = true;
                this._resizeCtrlIsDragging = -1;
                this._resizeCtrlIsHover = -1;
                this._posDragStartX = mouseDownX - this._x;
                this._posDragStartY = mouseDownY - this._y;
                this._events.trigger('area-move-start');
            }
        };

        CropAreaSquare.prototype.processMouseUp = function( /*mouseUpX, mouseUpY*/ ) {
            if (this._areaIsDragging) {
                this._areaIsDragging = false;
                this._events.trigger('area-move-end');
            }
            if (this._resizeCtrlIsDragging > -1) {
                this._resizeCtrlIsDragging = -1;
                this._events.trigger('area-resize-end');
            }
            this._areaIsHover = false;
            this._resizeCtrlIsHover = -1;

            this._posDragStartX = 0;
            this._posDragStartY = 0;
        };

        return CropAreaSquare;
    }]);

    crop.factory('cropArea', ['cropCanvas', function(CropCanvas) {
        var CropArea = function(ctx, events) {
            this._ctx = ctx;
            this._events = events;

            this._minSize = 80;
            this.resSizeScale = 1; //图片剪切比例
            this._cropCanvas = new CropCanvas(ctx);

            this._image = new Image();
            this._x = 0;
            this._y = 0;
            this._size = 200;
        };

        /* GETTERS/SETTERS */

        CropArea.prototype.getImage = function() {
            return this._image;
        };
        CropArea.prototype.setImage = function(image) {
            this._image = image;
        };

        CropArea.prototype.getX = function() {
            return this._x;
        };
        CropArea.prototype.setX = function(x) {
            this._x = x;
            this._dontDragOutside();
        };

        CropArea.prototype.getY = function() {
            return this._y;
        };
        CropArea.prototype.setY = function(y) {
            this._y = y;
            this._dontDragOutside();
        };

        CropArea.prototype.getSize = function() {
            return this._size;
        };
        CropArea.prototype.setSize = function(size) {
            this._size = Math.max(this._minSize, size);
            this._dontDragOutside();
        };

        CropArea.prototype.getMinSize = function() {
            return this._minSize;
        };
        CropArea.prototype.setMinSize = function(size) {
            this._minSize = size;
            this._size = Math.max(this._minSize, this._size);
            this._dontDragOutside();
        };

        /* FUNCTIONS */
        CropArea.prototype._dontDragOutside = function() {
            var h = this._ctx.canvas.height*this.resSizeScale,
                w = this._ctx.canvas.width;
            if (this._size > w) { this._size = w; }
            if (this._size > h) { this._size = h; }
            if (this._x < this._size / 2) { this._x = this._size / 2; }
            if (this._x > w - this._size / 2) { this._x = w - this._size / 2; }
            if (this._y < this._size / 2/this.resSizeScale) { this._y = this._size / 2/this.resSizeScale; }
            if (this._y > (h - this._size / 2)/this.resSizeScale) { this._y = (h - this._size / 2)/this.resSizeScale; }
        };

        CropArea.prototype._drawArea = function() {};

        CropArea.prototype.draw = function() {
            // draw crop area
            this._cropCanvas.drawCropArea(this._image, [this._x, this._y], this._size,this.resSizeScale, this._drawArea);
        };

        CropArea.prototype.processMouseMove = function() {};

        CropArea.prototype.processMouseDown = function() {};

        CropArea.prototype.processMouseUp = function() {};

        return CropArea;
    }]);

    crop.factory('cropCanvas', [function() {
        // Shape = Array of [x,y]; [0, 0] - center
        var shapeArrowNW = [
            [-0.5, -2],
            [-3, -4.5],
            [-0.5, -7],
            [-7, -7],
            [-7, -0.5],
            [-4.5, -3],
            [-2, -0.5]
        ];
        var shapeArrowNE = [
            [0.5, -2],
            [3, -4.5],
            [0.5, -7],
            [7, -7],
            [7, -0.5],
            [4.5, -3],
            [2, -0.5]
        ];
        var shapeArrowSW = [
            [-0.5, 2],
            [-3, 4.5],
            [-0.5, 7],
            [-7, 7],
            [-7, 0.5],
            [-4.5, 3],
            [-2, 0.5]
        ];
        var shapeArrowSE = [
            [0.5, 2],
            [3, 4.5],
            [0.5, 7],
            [7, 7],
            [7, 0.5],
            [4.5, 3],
            [2, 0.5]
        ];
        var shapeArrowN = [
            [-1.5, -2.5],
            [-1.5, -6],
            [-5, -6],
            [0, -11],
            [5, -6],
            [1.5, -6],
            [1.5, -2.5]
        ];
        var shapeArrowW = [
            [-2.5, -1.5],
            [-6, -1.5],
            [-6, -5],
            [-11, 0],
            [-6, 5],
            [-6, 1.5],
            [-2.5, 1.5]
        ];
        var shapeArrowS = [
            [-1.5, 2.5],
            [-1.5, 6],
            [-5, 6],
            [0, 11],
            [5, 6],
            [1.5, 6],
            [1.5, 2.5]
        ];
        var shapeArrowE = [
            [2.5, -1.5],
            [6, -1.5],
            [6, -5],
            [11, 0],
            [6, 5],
            [6, 1.5],
            [2.5, 1.5]
        ];

        // Colors
        var colors = {
            areaOutline: '#fff',
            resizeBoxStroke: '#fff',
            resizeBoxFill: '#444',
            resizeBoxArrowFill: '#fff',
            resizeCircleStroke: '#fff',
            resizeCircleFill: '#444',
            moveIconFill: '#fff'
        };

        return function(ctx) {

            /* Base functions */

            // Calculate Point
            var calcPoint = function(point, offset, scale) {
                return [scale * point[0] + offset[0], scale * point[1] + offset[1]];
            };

            // Draw Filled Polygon
            var drawFilledPolygon = function(shape, fillStyle, centerCoords, scale) {
                ctx.save();
                ctx.fillStyle = fillStyle;
                ctx.beginPath();
                var pc, pc0 = calcPoint(shape[0], centerCoords, scale);
                ctx.moveTo(pc0[0], pc0[1]);

                for (var p in shape) {
                    if (p > 0) {
                        pc = calcPoint(shape[p], centerCoords, scale);
                        ctx.lineTo(pc[0], pc[1]);
                    }
                }

                ctx.lineTo(pc0[0], pc0[1]);
                ctx.fill();
                ctx.closePath();
                ctx.restore();
            };

            /* Icons */

            this.drawIconMove = function(centerCoords, scale) {
                drawFilledPolygon(shapeArrowN, colors.moveIconFill, centerCoords, scale);
                drawFilledPolygon(shapeArrowW, colors.moveIconFill, centerCoords, scale);
                drawFilledPolygon(shapeArrowS, colors.moveIconFill, centerCoords, scale);
                drawFilledPolygon(shapeArrowE, colors.moveIconFill, centerCoords, scale);
            };

            this.drawIconResizeCircle = function(centerCoords, circleRadius, scale) {
                var scaledCircleRadius = circleRadius * scale;
                ctx.save();
                ctx.strokeStyle = colors.resizeCircleStroke;
                ctx.lineWidth = 2;
                ctx.fillStyle = colors.resizeCircleFill;
                ctx.beginPath();
                ctx.arc(centerCoords[0], centerCoords[1], scaledCircleRadius, 0, 2 * Math.PI);
                ctx.fill();
                ctx.stroke();
                ctx.closePath();
                ctx.restore();
            };

            this.drawIconResizeBoxBase = function(centerCoords, boxSize, scale) {
                var scaledBoxSize = boxSize * scale;
                ctx.save();
                ctx.strokeStyle = colors.resizeBoxStroke;
                ctx.lineWidth = 2;
                ctx.fillStyle = colors.resizeBoxFill;
                ctx.fillRect(centerCoords[0] - scaledBoxSize / 2, centerCoords[1] - scaledBoxSize / 2, scaledBoxSize, scaledBoxSize);
                ctx.strokeRect(centerCoords[0] - scaledBoxSize / 2, centerCoords[1] - scaledBoxSize / 2, scaledBoxSize, scaledBoxSize);
                ctx.restore();
            };
            this.drawIconResizeBoxNESW = function(centerCoords, boxSize, scale) {
                this.drawIconResizeBoxBase(centerCoords, boxSize, scale);
                drawFilledPolygon(shapeArrowNE, colors.resizeBoxArrowFill, centerCoords, scale);
                drawFilledPolygon(shapeArrowSW, colors.resizeBoxArrowFill, centerCoords, scale);
            };
            this.drawIconResizeBoxNWSE = function(centerCoords, boxSize, scale) {
                this.drawIconResizeBoxBase(centerCoords, boxSize, scale);
                drawFilledPolygon(shapeArrowNW, colors.resizeBoxArrowFill, centerCoords, scale);
                drawFilledPolygon(shapeArrowSE, colors.resizeBoxArrowFill, centerCoords, scale);
            };

            /* Crop Area */

            this.drawCropArea = function(image, centerCoords, size,scale, fnDrawClipPath) {
                var xRatio = image.width / ctx.canvas.width,
                    yRatio = image.height / ctx.canvas.height,
                    xLeft = centerCoords[0] - size / 2,
                    yTop = centerCoords[1] - size / 2;

                ctx.save();
                ctx.strokeStyle = colors.areaOutline;
                ctx.lineWidth = 2;
                ctx.beginPath();
                fnDrawClipPath(ctx, centerCoords, size,scale);
                ctx.stroke();
                ctx.clip();

                // draw part of original image
                if (size > 0) {
                    ctx.drawImage(image, xLeft * xRatio, yTop * yRatio, size * xRatio, size * yRatio, xLeft, yTop, size, size);
                }

                ctx.beginPath();
                fnDrawClipPath(ctx, centerCoords, size,scale);
                ctx.stroke();
                ctx.clip();

                ctx.restore();
            };

        };
    }]);

    /**
     * EXIF service is based on the exif-js library (https://github.com/jseidelin/exif-js)
     */

    crop.service('cropEXIF', [function() {
        var debug = false;

        var ExifTags = this.Tags = {

            // version tags
            0x9000: "ExifVersion", // EXIF version
            0xA000: "FlashpixVersion", // Flashpix format version

            // colorspace tags
            0xA001: "ColorSpace", // Color space information tag

            // image configuration
            0xA002: "PixelXDimension", // Valid width of meaningful image
            0xA003: "PixelYDimension", // Valid height of meaningful image
            0x9101: "ComponentsConfiguration", // Information about channels
            0x9102: "CompressedBitsPerPixel", // Compressed bits per pixel

            // user information
            0x927C: "MakerNote", // Any desired information written by the manufacturer
            0x9286: "UserComment", // Comments by user

            // related file
            0xA004: "RelatedSoundFile", // Name of related sound file

            // date and time
            0x9003: "DateTimeOriginal", // Date and time when the original image was generated
            0x9004: "DateTimeDigitized", // Date and time when the image was stored digitally
            0x9290: "SubsecTime", // Fractions of seconds for DateTime
            0x9291: "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal
            0x9292: "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized

            // picture-taking conditions
            0x829A: "ExposureTime", // Exposure time (in seconds)
            0x829D: "FNumber", // F number
            0x8822: "ExposureProgram", // Exposure program
            0x8824: "SpectralSensitivity", // Spectral sensitivity
            0x8827: "ISOSpeedRatings", // ISO speed rating
            0x8828: "OECF", // Optoelectric conversion factor
            0x9201: "ShutterSpeedValue", // Shutter speed
            0x9202: "ApertureValue", // Lens aperture
            0x9203: "BrightnessValue", // Value of brightness
            0x9204: "ExposureBias", // Exposure bias
            0x9205: "MaxApertureValue", // Smallest F number of lens
            0x9206: "SubjectDistance", // Distance to subject in meters
            0x9207: "MeteringMode", // Metering mode
            0x9208: "LightSource", // Kind of light source
            0x9209: "Flash", // Flash status
            0x9214: "SubjectArea", // Location and area of main subject
            0x920A: "FocalLength", // Focal length of the lens in mm
            0xA20B: "FlashEnergy", // Strobe energy in BCPS
            0xA20C: "SpatialFrequencyResponse", //
            0xA20E: "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit
            0xA20F: "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit
            0xA210: "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution
            0xA214: "SubjectLocation", // Location of subject in image
            0xA215: "ExposureIndex", // Exposure index selected on camera
            0xA217: "SensingMethod", // Image sensor type
            0xA300: "FileSource", // Image source (3 == DSC)
            0xA301: "SceneType", // Scene type (1 == directly photographed)
            0xA302: "CFAPattern", // Color filter array geometric pattern
            0xA401: "CustomRendered", // Special processing
            0xA402: "ExposureMode", // Exposure mode
            0xA403: "WhiteBalance", // 1 = auto white balance, 2 = manual
            0xA404: "DigitalZoomRation", // Digital zoom ratio
            0xA405: "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm)
            0xA406: "SceneCaptureType", // Type of scene
            0xA407: "GainControl", // Degree of overall image gain adjustment
            0xA408: "Contrast", // Direction of contrast processing applied by camera
            0xA409: "Saturation", // Direction of saturation processing applied by camera
            0xA40A: "Sharpness", // Direction of sharpness processing applied by camera
            0xA40B: "DeviceSettingDescription", //
            0xA40C: "SubjectDistanceRange", // Distance to subject

            // other tags
            0xA005: "InteroperabilityIFDPointer",
            0xA420: "ImageUniqueID" // Identifier assigned uniquely to each image
        };

        var TiffTags = this.TiffTags = {
            0x0100: "ImageWidth",
            0x0101: "ImageHeight",
            0x8769: "ExifIFDPointer",
            0x8825: "GPSInfoIFDPointer",
            0xA005: "InteroperabilityIFDPointer",
            0x0102: "BitsPerSample",
            0x0103: "Compression",
            0x0106: "PhotometricInterpretation",
            0x0112: "Orientation",
            0x0115: "SamplesPerPixel",
            0x011C: "PlanarConfiguration",
            0x0212: "YCbCrSubSampling",
            0x0213: "YCbCrPositioning",
            0x011A: "XResolution",
            0x011B: "YResolution",
            0x0128: "ResolutionUnit",
            0x0111: "StripOffsets",
            0x0116: "RowsPerStrip",
            0x0117: "StripByteCounts",
            0x0201: "JPEGInterchangeFormat",
            0x0202: "JPEGInterchangeFormatLength",
            0x012D: "TransferFunction",
            0x013E: "WhitePoint",
            0x013F: "PrimaryChromaticities",
            0x0211: "YCbCrCoefficients",
            0x0214: "ReferenceBlackWhite",
            0x0132: "DateTime",
            0x010E: "ImageDescription",
            0x010F: "Make",
            0x0110: "Model",
            0x0131: "Software",
            0x013B: "Artist",
            0x8298: "Copyright"
        };

        var GPSTags = this.GPSTags = {
            0x0000: "GPSVersionID",
            0x0001: "GPSLatitudeRef",
            0x0002: "GPSLatitude",
            0x0003: "GPSLongitudeRef",
            0x0004: "GPSLongitude",
            0x0005: "GPSAltitudeRef",
            0x0006: "GPSAltitude",
            0x0007: "GPSTimeStamp",
            0x0008: "GPSSatellites",
            0x0009: "GPSStatus",
            0x000A: "GPSMeasureMode",
            0x000B: "GPSDOP",
            0x000C: "GPSSpeedRef",
            0x000D: "GPSSpeed",
            0x000E: "GPSTrackRef",
            0x000F: "GPSTrack",
            0x0010: "GPSImgDirectionRef",
            0x0011: "GPSImgDirection",
            0x0012: "GPSMapDatum",
            0x0013: "GPSDestLatitudeRef",
            0x0014: "GPSDestLatitude",
            0x0015: "GPSDestLongitudeRef",
            0x0016: "GPSDestLongitude",
            0x0017: "GPSDestBearingRef",
            0x0018: "GPSDestBearing",
            0x0019: "GPSDestDistanceRef",
            0x001A: "GPSDestDistance",
            0x001B: "GPSProcessingMethod",
            0x001C: "GPSAreaInformation",
            0x001D: "GPSDateStamp",
            0x001E: "GPSDifferential"
        };

        var StringValues = this.StringValues = {
            ExposureProgram: {
                0: "Not defined",
                1: "Manual",
                2: "Normal program",
                3: "Aperture priority",
                4: "Shutter priority",
                5: "Creative program",
                6: "Action program",
                7: "Portrait mode",
                8: "Landscape mode"
            },
            MeteringMode: {
                0: "Unknown",
                1: "Average",
                2: "CenterWeightedAverage",
                3: "Spot",
                4: "MultiSpot",
                5: "Pattern",
                6: "Partial",
                255: "Other"
            },
            LightSource: {
                0: "Unknown",
                1: "Daylight",
                2: "Fluorescent",
                3: "Tungsten (incandescent light)",
                4: "Flash",
                9: "Fine weather",
                10: "Cloudy weather",
                11: "Shade",
                12: "Daylight fluorescent (D 5700 - 7100K)",
                13: "Day white fluorescent (N 4600 - 5400K)",
                14: "Cool white fluorescent (W 3900 - 4500K)",
                15: "White fluorescent (WW 3200 - 3700K)",
                17: "Standard light A",
                18: "Standard light B",
                19: "Standard light C",
                20: "D55",
                21: "D65",
                22: "D75",
                23: "D50",
                24: "ISO studio tungsten",
                255: "Other"
            },
            Flash: {
                0x0000: "Flash did not fire",
                0x0001: "Flash fired",
                0x0005: "Strobe return light not detected",
                0x0007: "Strobe return light detected",
                0x0009: "Flash fired, compulsory flash mode",
                0x000D: "Flash fired, compulsory flash mode, return light not detected",
                0x000F: "Flash fired, compulsory flash mode, return light detected",
                0x0010: "Flash did not fire, compulsory flash mode",
                0x0018: "Flash did not fire, auto mode",
                0x0019: "Flash fired, auto mode",
                0x001D: "Flash fired, auto mode, return light not detected",
                0x001F: "Flash fired, auto mode, return light detected",
                0x0020: "No flash function",
                0x0041: "Flash fired, red-eye reduction mode",
                0x0045: "Flash fired, red-eye reduction mode, return light not detected",
                0x0047: "Flash fired, red-eye reduction mode, return light detected",
                0x0049: "Flash fired, compulsory flash mode, red-eye reduction mode",
                0x004D: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
                0x004F: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
                0x0059: "Flash fired, auto mode, red-eye reduction mode",
                0x005D: "Flash fired, auto mode, return light not detected, red-eye reduction mode",
                0x005F: "Flash fired, auto mode, return light detected, red-eye reduction mode"
            },
            SensingMethod: {
                1: "Not defined",
                2: "One-chip color area sensor",
                3: "Two-chip color area sensor",
                4: "Three-chip color area sensor",
                5: "Color sequential area sensor",
                7: "Trilinear sensor",
                8: "Color sequential linear sensor"
            },
            SceneCaptureType: {
                0: "Standard",
                1: "Landscape",
                2: "Portrait",
                3: "Night scene"
            },
            SceneType: {
                1: "Directly photographed"
            },
            CustomRendered: {
                0: "Normal process",
                1: "Custom process"
            },
            WhiteBalance: {
                0: "Auto white balance",
                1: "Manual white balance"
            },
            GainControl: {
                0: "None",
                1: "Low gain up",
                2: "High gain up",
                3: "Low gain down",
                4: "High gain down"
            },
            Contrast: {
                0: "Normal",
                1: "Soft",
                2: "Hard"
            },
            Saturation: {
                0: "Normal",
                1: "Low saturation",
                2: "High saturation"
            },
            Sharpness: {
                0: "Normal",
                1: "Soft",
                2: "Hard"
            },
            SubjectDistanceRange: {
                0: "Unknown",
                1: "Macro",
                2: "Close view",
                3: "Distant view"
            },
            FileSource: {
                3: "DSC"
            },

            Components: {
                0: "",
                1: "Y",
                2: "Cb",
                3: "Cr",
                4: "R",
                5: "G",
                6: "B"
            }
        };

        function addEvent(element, event, handler) {
            if (element.addEventListener) {
                element.addEventListener(event, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent("on" + event, handler);
            }
        }

        function imageHasData(img) {
            return !!(img.exifdata);
        }

        function base64ToArrayBuffer(base64, contentType) {
            contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg'
            base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, '');
            var binary = atob(base64);
            var len = binary.length;
            var buffer = new ArrayBuffer(len);
            var view = new Uint8Array(buffer);
            for (var i = 0; i < len; i++) {
                view[i] = binary.charCodeAt(i);
            }
            return buffer;
        }

        function objectURLToBlob(url, callback) {
            var http = new XMLHttpRequest();
            http.open("GET", url, true);
            http.responseType = "blob";
            http.onload = function(e) {
                if (this.status == 200 || this.status === 0) {
                    callback(this.response);
                }
            };
            http.send();
        }

        function getImageData(img, callback) {
            function handleBinaryFile(binFile) {
                var data = findEXIFinJPEG(binFile);
                var iptcdata = findIPTCinJPEG(binFile);
                img.exifdata = data || {};
                img.iptcdata = iptcdata || {};
                if (callback) {
                    callback.call(img);
                }
            }

            if (img.src) {
                if (/^data\:/i.test(img.src)) { // Data URI
                    var arrayBuffer = base64ToArrayBuffer(img.src);
                    handleBinaryFile(arrayBuffer);

                } else if (/^blob\:/i.test(img.src)) { // Object URL
                    var fileReader = new FileReader();
                    fileReader.onload = function(e) {
                        handleBinaryFile(e.target.result);
                    };
                    objectURLToBlob(img.src, function(blob) {
                        fileReader.readAsArrayBuffer(blob);
                    });
                } else {
                    var http = new XMLHttpRequest();
                    http.onload = function() {
                        if (this.status == 200 || this.status === 0) {
                            handleBinaryFile(http.response);
                        } else {
                            throw "Could not load image";
                        }
                        http = null;
                    };
                    http.open("GET", img.src, true);
                    http.responseType = "arraybuffer";
                    http.send(null);
                }
            } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) {
                var fileReader = new FileReader();
                fileReader.onload = function(e) {
                    if (debug) console.log("Got file of length " + e.target.result.byteLength);
                    handleBinaryFile(e.target.result);
                };

                fileReader.readAsArrayBuffer(img);
            }
        }

        function findEXIFinJPEG(file) {
            var dataView = new DataView(file);

            if (debug) console.log("Got file of length " + file.byteLength);
            if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
                if (debug) console.log("Not a valid JPEG");
                return false; // not a valid jpeg
            }

            var offset = 2,
                length = file.byteLength,
                marker;

            while (offset < length) {
                if (dataView.getUint8(offset) != 0xFF) {
                    if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset));
                    return false; // not a valid marker, something is wrong
                }

                marker = dataView.getUint8(offset + 1);
                if (debug) console.log(marker);

                // we could implement handling for other markers here,
                // but we're only looking for 0xFFE1 for EXIF data

                if (marker == 225) {
                    if (debug) console.log("Found 0xFFE1 marker");

                    return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2);

                    // offset += 2 + file.getShortAt(offset+2, true);

                } else {
                    offset += 2 + dataView.getUint16(offset + 2);
                }

            }

        }

        function findIPTCinJPEG(file) {
            var dataView = new DataView(file);

            if (debug) console.log("Got file of length " + file.byteLength);
            if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
                if (debug) console.log("Not a valid JPEG");
                return false; // not a valid jpeg
            }

            var offset = 2,
                length = file.byteLength;

            var isFieldSegmentStart = function(dataView, offset) {
                return (
                    dataView.getUint8(offset) === 0x38 &&
                    dataView.getUint8(offset + 1) === 0x42 &&
                    dataView.getUint8(offset + 2) === 0x49 &&
                    dataView.getUint8(offset + 3) === 0x4D &&
                    dataView.getUint8(offset + 4) === 0x04 &&
                    dataView.getUint8(offset + 5) === 0x04
                );
            };

            while (offset < length) {

                if (isFieldSegmentStart(dataView, offset)) {

                    // Get the length of the name header (which is padded to an even number of bytes)
                    var nameHeaderLength = dataView.getUint8(offset + 7);
                    if (nameHeaderLength % 2 !== 0) nameHeaderLength += 1;
                    // Check for pre photoshop 6 format
                    if (nameHeaderLength === 0) {
                        // Always 4
                        nameHeaderLength = 4;
                    }

                    var startOffset = offset + 8 + nameHeaderLength;
                    var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength);

                    return readIPTCData(file, startOffset, sectionLength);

                    break;

                }

                // Not the marker, continue searching
                offset++;

            }

        }
        var IptcFieldMap = {
            0x78: 'caption',
            0x6E: 'credit',
            0x19: 'keywords',
            0x37: 'dateCreated',
            0x50: 'byline',
            0x55: 'bylineTitle',
            0x7A: 'captionWriter',
            0x69: 'headline',
            0x74: 'copyright',
            0x0F: 'category'
        };

        function readIPTCData(file, startOffset, sectionLength) {
            var dataView = new DataView(file);
            var data = {};
            var fieldValue, fieldName, dataSize, segmentType, segmentSize;
            var segmentStartPos = startOffset;
            while (segmentStartPos < startOffset + sectionLength) {
                if (dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos + 1) === 0x02) {
                    segmentType = dataView.getUint8(segmentStartPos + 2);
                    if (segmentType in IptcFieldMap) {
                        dataSize = dataView.getInt16(segmentStartPos + 3);
                        segmentSize = dataSize + 5;
                        fieldName = IptcFieldMap[segmentType];
                        fieldValue = getStringFromDB(dataView, segmentStartPos + 5, dataSize);
                        // Check if we already stored a value with this name
                        if (data.hasOwnProperty(fieldName)) {
                            // Value already stored with this name, create multivalue field
                            if (data[fieldName] instanceof Array) {
                                data[fieldName].push(fieldValue);
                            } else {
                                data[fieldName] = [data[fieldName], fieldValue];
                            }
                        } else {
                            data[fieldName] = fieldValue;
                        }
                    }

                }
                segmentStartPos++;
            }
            return data;
        }

        function readTags(file, tiffStart, dirStart, strings, bigEnd) {
            var entries = file.getUint16(dirStart, !bigEnd),
                tags = {},
                entryOffset, tag,
                i;

            for (i = 0; i < entries; i++) {
                entryOffset = dirStart + i * 12 + 2;
                tag = strings[file.getUint16(entryOffset, !bigEnd)];
                if (!tag && debug) console.log("Unknown tag: " + file.getUint16(entryOffset, !bigEnd));
                tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd);
            }
            return tags;
        }

        function readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) {
            var type = file.getUint16(entryOffset + 2, !bigEnd),
                numValues = file.getUint32(entryOffset + 4, !bigEnd),
                valueOffset = file.getUint32(entryOffset + 8, !bigEnd) + tiffStart,
                offset,
                vals, val, n,
                numerator, denominator;

            switch (type) {
                case 1: // byte, 8-bit unsigned int
                case 7: // undefined, 8-bit byte, value depending on field
                    if (numValues == 1) {
                        return file.getUint8(entryOffset + 8, !bigEnd);
                    } else {
                        offset = numValues > 4 ? valueOffset : (entryOffset + 8);
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            vals[n] = file.getUint8(offset + n);
                        }
                        return vals;
                    }

                case 2: // ascii, 8-bit byte
                    offset = numValues > 4 ? valueOffset : (entryOffset + 8);
                    return getStringFromDB(file, offset, numValues - 1);

                case 3: // short, 16 bit int
                    if (numValues == 1) {
                        return file.getUint16(entryOffset + 8, !bigEnd);
                    } else {
                        offset = numValues > 2 ? valueOffset : (entryOffset + 8);
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            vals[n] = file.getUint16(offset + 2 * n, !bigEnd);
                        }
                        return vals;
                    }

                case 4: // long, 32 bit int
                    if (numValues == 1) {
                        return file.getUint32(entryOffset + 8, !bigEnd);
                    } else {
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            vals[n] = file.getUint32(valueOffset + 4 * n, !bigEnd);
                        }
                        return vals;
                    }

                case 5: // rational = two long values, first is numerator, second is denominator
                    if (numValues == 1) {
                        numerator = file.getUint32(valueOffset, !bigEnd);
                        denominator = file.getUint32(valueOffset + 4, !bigEnd);
                        val = new Number(numerator / denominator);
                        val.numerator = numerator;
                        val.denominator = denominator;
                        return val;
                    } else {
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            numerator = file.getUint32(valueOffset + 8 * n, !bigEnd);
                            denominator = file.getUint32(valueOffset + 4 + 8 * n, !bigEnd);
                            vals[n] = new Number(numerator / denominator);
                            vals[n].numerator = numerator;
                            vals[n].denominator = denominator;
                        }
                        return vals;
                    }

                case 9: // slong, 32 bit signed int
                    if (numValues == 1) {
                        return file.getInt32(entryOffset + 8, !bigEnd);
                    } else {
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            vals[n] = file.getInt32(valueOffset + 4 * n, !bigEnd);
                        }
                        return vals;
                    }

                case 10: // signed rational, two slongs, first is numerator, second is denominator
                    if (numValues == 1) {
                        return file.getInt32(valueOffset, !bigEnd) / file.getInt32(valueOffset + 4, !bigEnd);
                    } else {
                        vals = [];
                        for (n = 0; n < numValues; n++) {
                            vals[n] = file.getInt32(valueOffset + 8 * n, !bigEnd) / file.getInt32(valueOffset + 4 + 8 * n, !bigEnd);
                        }
                        return vals;
                    }
            }
        }

        function getStringFromDB(buffer, start, length) {
            var outstr = "";
            for (var n = start; n < start + length; n++) {
                outstr += String.fromCharCode(buffer.getUint8(n));
            }
            return outstr;
        }

        function readEXIFData(file, start) {
            if (getStringFromDB(file, start, 4) != "Exif") {
                if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4));
                return false;
            }

            var bigEnd,
                tags, tag,
                exifData, gpsData,
                tiffOffset = start + 6;

            // test for TIFF validity and endianness
            if (file.getUint16(tiffOffset) == 0x4949) {
                bigEnd = false;
            } else if (file.getUint16(tiffOffset) == 0x4D4D) {
                bigEnd = true;
            } else {
                if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
                return false;
            }

            if (file.getUint16(tiffOffset + 2, !bigEnd) != 0x002A) {
                if (debug) console.log("Not valid TIFF data! (no 0x002A)");
                return false;
            }

            var firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd);

            if (firstIFDOffset < 0x00000008) {
                if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset + 4, !bigEnd));
                return false;
            }

            tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd);

            if (tags.ExifIFDPointer) {
                exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd);
                for (tag in exifData) {
                    switch (tag) {
                        case "LightSource":
                        case "Flash":
                        case "MeteringMode":
                        case "ExposureProgram":
                        case "SensingMethod":
                        case "SceneCaptureType":
                        case "SceneType":
                        case "CustomRendered":
                        case "WhiteBalance":
                        case "GainControl":
                        case "Contrast":
                        case "Saturation":
                        case "Sharpness":
                        case "SubjectDistanceRange":
                        case "FileSource":
                            exifData[tag] = StringValues[tag][exifData[tag]];
                            break;

                        case "ExifVersion":
                        case "FlashpixVersion":
                            exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]);
                            break;

                        case "ComponentsConfiguration":
                            exifData[tag] =
                                StringValues.Components[exifData[tag][0]] +
                                StringValues.Components[exifData[tag][1]] +
                                StringValues.Components[exifData[tag][2]] +
                                StringValues.Components[exifData[tag][3]];
                            break;
                    }
                    tags[tag] = exifData[tag];
                }
            }

            if (tags.GPSInfoIFDPointer) {
                gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd);
                for (tag in gpsData) {
                    switch (tag) {
                        case "GPSVersionID":
                            gpsData[tag] = gpsData[tag][0] +
                                "." + gpsData[tag][1] +
                                "." + gpsData[tag][2] +
                                "." + gpsData[tag][3];
                            break;
                    }
                    tags[tag] = gpsData[tag];
                }
            }

            return tags;
        }

        this.getData = function(img, callback) {
            if ((img instanceof Image || img instanceof HTMLImageElement) && !img.complete) return false;

            if (!imageHasData(img)) {
                getImageData(img, callback);
            } else {
                if (callback) {
                    callback.call(img);
                }
            }
            return true;
        }

        this.getTag = function(img, tag) {
            if (!imageHasData(img)) return;
            return img.exifdata[tag];
        }

        this.getAllTags = function(img) {
            if (!imageHasData(img)) return {};
            var a,
                data = img.exifdata,
                tags = {};
            for (a in data) {
                if (data.hasOwnProperty(a)) {
                    tags[a] = data[a];
                }
            }
            return tags;
        }

        this.pretty = function(img) {
            if (!imageHasData(img)) return "";
            var a,
                data = img.exifdata,
                strPretty = "";
            for (a in data) {
                if (data.hasOwnProperty(a)) {
                    if (typeof data[a] == "object") {
                        if (data[a] instanceof Number) {
                            strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n";
                        } else {
                            strPretty += a + " : [" + data[a].length + " values]\r\n";
                        }
                    } else {
                        strPretty += a + " : " + data[a] + "\r\n";
                    }
                }
            }
            return strPretty;
        }

        this.readFromBinaryFile = function(file) {
            return findEXIFinJPEG(file);
        }
    }]);

    crop.factory('cropHost', ['$document', 'cropAreaCircle', 'cropAreaSquare', 'cropEXIF', function($document, CropAreaCircle, CropAreaSquare, cropEXIF) {
        /* STATIC FUNCTIONS */

        // Get Element's Offset
        var getElementOffset = function(elem) {
            var box = elem.getBoundingClientRect();

            var body = document.body;
            var docElem = document.documentElement;

            var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
            var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

            var clientTop = docElem.clientTop || body.clientTop || 0;
            var clientLeft = docElem.clientLeft || body.clientLeft || 0;

            var top = box.top + scrollTop - clientTop;
            var left = box.left + scrollLeft - clientLeft;

            return { top: Math.round(top), left: Math.round(left) };
        };

        return function(elCanvas, opts, events) {
            /* PRIVATE VARIABLES */

            // Object Pointers
            var ctx = null,
                image = null,
                theArea = null;

            // Dimensions
            var minCanvasDims = [100, 100],
                maxCanvasDims = [300, 300];

            // Result Image size
            var resImgSize = 200;
            //图片剪切比例
            var resSizeScale = 1;
            // Result Image type
            var resImgFormat = 'image/png';

            // Result Image quality
            var resImgQuality = null;

            /* PRIVATE FUNCTIONS */

            // Draw Scene
            function drawScene() {
                // clear canvas
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

                if (image !== null) {
                    // draw source image
                    ctx.drawImage(image, 0, 0, ctx.canvas.width, ctx.canvas.height);

                    ctx.save();

                    // and make it darker
                    ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
                    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

                    ctx.restore();

                    // draw Area
                    theArea.draw();
                }
            }

            // Resets CropHost
            var resetCropHost = function() {
                if (image !== null) {
                    theArea.setImage(image);
                    var imageDims = [image.width, image.height],
                        imageRatio = image.width / image.height,
                        canvasDims = imageDims;

                    if (canvasDims[0] > maxCanvasDims[0]) {
                        canvasDims[0] = maxCanvasDims[0];
                        canvasDims[1] = canvasDims[0] / imageRatio;
                    } else if (canvasDims[0] < minCanvasDims[0]) {
                        canvasDims[0] = minCanvasDims[0];
                        canvasDims[1] = canvasDims[0] / imageRatio;
                    }
                    if (canvasDims[1] > maxCanvasDims[1]) {
                        canvasDims[1] = maxCanvasDims[1];
                        canvasDims[0] = canvasDims[1] * imageRatio;
                    } else if (canvasDims[1] < minCanvasDims[1]) {
                        canvasDims[1] = minCanvasDims[1];
                        canvasDims[0] = canvasDims[1] * imageRatio;
                    }
                    elCanvas.prop('width', canvasDims[0]).prop('height', canvasDims[1]);

                    theArea.setX(ctx.canvas.width / 2);
                    theArea.setY(ctx.canvas.height / 2);
                    theArea.setSize(Math.min(200, ctx.canvas.width / 2, ctx.canvas.height / 2));
                } else {
                    elCanvas.prop('width', 0).prop('height', 0).css({ 'margin-top': 0 });
                }

                drawScene();
            };

            /**
             * Returns event.changedTouches directly if event is a TouchEvent.
             * If event is a jQuery event, return changedTouches of event.originalEvent
             */
            var getChangedTouches = function(event) {
                if (angular.isDefined(event.changedTouches)) {
                    return event.changedTouches;
                } else {
                    return event.originalEvent.changedTouches;
                }
            };

            var onMouseMove = function(e) {
                if (image !== null) {
                    var offset = getElementOffset(ctx.canvas),
                        pageX, pageY;
                    if (e.type === 'touchmove') {
                        pageX = getChangedTouches(e)[0].pageX;
                        pageY = getChangedTouches(e)[0].pageY;
                    } else {
                        pageX = e.pageX;
                        pageY = e.pageY;
                    }
                    theArea.processMouseMove(pageX - offset.left, pageY - offset.top);
                    drawScene();
                }
            };

            var onMouseDown = function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (image !== null) {
                    var offset = getElementOffset(ctx.canvas),
                        pageX, pageY;
                    if (e.type === 'touchstart') {
                        pageX = getChangedTouches(e)[0].pageX;
                        pageY = getChangedTouches(e)[0].pageY;
                    } else {
                        pageX = e.pageX;
                        pageY = e.pageY;
                    }
                    theArea.processMouseDown(pageX - offset.left, pageY - offset.top);
                    drawScene();
                }
            };

            var onMouseUp = function(e) {
                if (image !== null) {
                    var offset = getElementOffset(ctx.canvas),
                        pageX, pageY;
                    if (e.type === 'touchend') {
                        pageX = getChangedTouches(e)[0].pageX;
                        pageY = getChangedTouches(e)[0].pageY;
                    } else {
                        pageX = e.pageX;
                        pageY = e.pageY;
                    }
                    theArea.processMouseUp(pageX - offset.left, pageY - offset.top);
                    drawScene();
                }
            };

            this.getResultImageDataURI = function() {
                var temp_ctx, temp_canvas;
                temp_canvas = angular.element('<canvas></canvas>')[0];
                temp_ctx = temp_canvas.getContext('2d');
                temp_canvas.width = resImgSize;
                temp_canvas.height = resImgSize / resSizeScale;
                if (image !== null) {
                    temp_ctx.drawImage(image, (theArea.getX() - theArea.getSize() / 2) * (image.width / ctx.canvas.width), (theArea.getY() - theArea.getSize() / 2 / resSizeScale) * (image.height / ctx.canvas.height), theArea.getSize() * (image.width / ctx.canvas.width), theArea.getSize() * (image.height / ctx.canvas.height) / resSizeScale, 0, 0, resImgSize, resImgSize / resSizeScale);
                }
                if (resImgQuality !== null) {
                    return temp_canvas.toDataURL(resImgFormat, resImgQuality);
                }
                return temp_canvas.toDataURL(resImgFormat);
            };

            this.setNewImageSource = function(imageSource) {
                image = null;
                resetCropHost();
                events.trigger('image-updated');
                if (!!imageSource) {
                    var newImage = new Image();
                    if (imageSource.substring(0, 4).toLowerCase() === 'http') {
                        newImage.crossOrigin = 'anonymous';
                    }
                    newImage.onload = function() {
                        events.trigger('load-done');

                        cropEXIF.getData(newImage, function() {
                            var orientation = cropEXIF.getTag(newImage, 'Orientation');

                            if ([3, 6, 8].indexOf(orientation) > -1) {
                                var canvas = document.createElement("canvas"),
                                    ctx = canvas.getContext("2d"),
                                    cw = newImage.width,
                                    ch = newImage.height,
                                    cx = 0,
                                    cy = 0,
                                    deg = 0;
                                switch (orientation) {
                                    case 3:
                                        cx = -newImage.width;
                                        cy = -newImage.height;
                                        deg = 180;
                                        break;
                                    case 6:
                                        cw = newImage.height;
                                        ch = newImage.width;
                                        cy = -newImage.height;
                                        deg = 90;
                                        break;
                                    case 8:
                                        cw = newImage.height;
                                        ch = newImage.width;
                                        cx = -newImage.width;
                                        deg = 270;
                                        break;
                                }

                                canvas.width = cw;
                                canvas.height = ch;
                                ctx.rotate(deg * Math.PI / 180);
                                ctx.drawImage(newImage, cx, cy);

                                image = new Image();
                                image.src = canvas.toDataURL("image/png");
                            } else {
                                image = newImage;
                            }
                            resetCropHost();
                            events.trigger('image-updated');
                        });
                    };
                    newImage.onerror = function() {
                        events.trigger('load-error');
                    };
                    events.trigger('load-start');
                    newImage.src = imageSource;
                }
            };

            this.setMaxDimensions = function(width, height) {
                maxCanvasDims = [width, height];

                if (image !== null) {
                    var curWidth = ctx.canvas.width,
                        curHeight = ctx.canvas.height;

                    var imageDims = [image.width, image.height],
                        imageRatio = image.width / image.height,
                        canvasDims = imageDims;

                    if (canvasDims[0] > maxCanvasDims[0]) {
                        canvasDims[0] = maxCanvasDims[0];
                        canvasDims[1] = canvasDims[0] / imageRatio;
                    } else if (canvasDims[0] < minCanvasDims[0]) {
                        canvasDims[0] = minCanvasDims[0];
                        canvasDims[1] = canvasDims[0] / imageRatio;
                    }
                    if (canvasDims[1] > maxCanvasDims[1]) {
                        canvasDims[1] = maxCanvasDims[1];
                        canvasDims[0] = canvasDims[1] * imageRatio;
                    } else if (canvasDims[1] < minCanvasDims[1]) {
                        canvasDims[1] = minCanvasDims[1];
                        canvasDims[0] = canvasDims[1] * imageRatio;
                    }
                    elCanvas.prop('width', canvasDims[0]).prop('height', canvasDims[1]);

                    var ratioNewCurWidth = ctx.canvas.width / curWidth,
                        ratioNewCurHeight = ctx.canvas.height / curHeight,
                        ratioMin = Math.min(ratioNewCurWidth, ratioNewCurHeight);

                    theArea.setX(theArea.getX() * ratioNewCurWidth);
                    theArea.setY(theArea.getY() * ratioNewCurHeight);
                    theArea.setSize(theArea.getSize() * ratioMin);
                } else {
                    elCanvas.prop('width', 0).prop('height', 0).css({ 'margin-top': 0 });
                }

                drawScene();

            };

            this.setAreaMinSize = function(size) {
                size = parseInt(size, 10);
                if (!isNaN(size)) {
                    theArea.setMinSize(size);
                    drawScene();
                }
            };

            this.setResultImageSize = function(size, scale) {
                size = parseInt(size, 10);
                resSizeScale = scale || 1;
                if (!isNaN(size)) {
                    resImgSize = size;
                }
            };

            this.setResultImageFormat = function(format) {
                resImgFormat = format;
            };

            this.setResultImageQuality = function(quality) {
                quality = parseFloat(quality);
                if (!isNaN(quality) && quality >= 0 && quality <= 1) {
                    resImgQuality = quality;
                }
            };

            this.setAreaType = function(type, scale) {
                var curSize = theArea.getSize(),
                    curMinSize = theArea.getMinSize(),
                    curX = theArea.getX(),
                    curY = theArea.getY();

                var AreaClass = CropAreaCircle;
                if (type === 'square') {
                    AreaClass = CropAreaSquare;
                }
                theArea = new AreaClass(ctx, events);
                theArea.setMinSize(curMinSize);
                theArea.resSizeScale = scale||1;
                theArea.setSize(curSize);
                theArea.setX(curX);
                theArea.setY(curY);

                // resetCropHost();
                if (image !== null) {
                    theArea.setImage(image);
                }

                drawScene();
            };

            /* Life Cycle begins */

            // Init Context var
            ctx = elCanvas[0].getContext('2d');

            // Init CropArea
            theArea = new CropAreaCircle(ctx, events);

            // Init Mouse Event Listeners
            $document.on('mousemove', onMouseMove);
            elCanvas.on('mousedown', onMouseDown);
            $document.on('mouseup', onMouseUp);

            // Init Touch Event Listeners
            $document.on('touchmove', onMouseMove);
            elCanvas.on('touchstart', onMouseDown);
            $document.on('touchend', onMouseUp);

            // CropHost Destructor
            this.destroy = function() {
                $document.off('mousemove', onMouseMove);
                elCanvas.off('mousedown', onMouseDown);
                $document.off('mouseup', onMouseMove);

                $document.off('touchmove', onMouseMove);
                elCanvas.off('touchstart', onMouseDown);
                $document.off('touchend', onMouseMove);

                elCanvas.remove();
            };
        };

    }]);


    crop.factory('cropPubSub', [function() {
        return function() {
            var events = {};
            // Subscribe
            this.on = function(names, handler) {
                names.split(' ').forEach(function(name) {
                    if (!events[name]) {
                        events[name] = [];
                    }
                    events[name].push(handler);
                });
                return this;
            };
            // Publish
            this.trigger = function(name, args) {
                angular.forEach(events[name], function(handler) {
                    handler.call(null, args);
                });
                return this;
            };
        };
    }]);

    crop.directive('imgCrop', ['$timeout', 'cropHost', 'cropPubSub', function($timeout, CropHost, CropPubSub) {
        return {
            restrict: 'E',
            scope: {
                image: '=',
                resultImage: '=',
                resultBlob: '=',

                changeOnFly: '=',
                areaType: '@',
                areaMinSize: '=',
                resultImageSize: '=',
                sizeScale: '=',
                resultImageFormat: '@',
                resultImageQuality: '=',

                onChange: '&',
                onLoadBegin: '&',
                onLoadDone: '&',
                onLoadError: '&'
            },
            template: '<canvas></canvas>',
            controller: ['$scope', function($scope) {
                $scope.events = new CropPubSub();
            }],
            link: function(scope, element /*, attrs*/ ) {
                // Init Events Manager
                var events = scope.events;

                // Init Crop Host
                var cropHost = new CropHost(element.find('canvas'), {}, events);

                // Store Result Image to check if it's changed
                var storedResultImage;

                function dataURItoBlob(dataURI) {
                    var byteString,
                        mimestring;

                    if (dataURI.split(',')[0].indexOf('base64') !== -1) {
                        byteString = atob(dataURI.split(',')[1]);
                    } else {
                        byteString = decodeURI(dataURI.split(',')[1]);
                    }

                    mimestring = dataURI.split(',')[0].split(':')[1].split(';')[0];

                    var content = new Array();
                    for (var i = 0; i < byteString.length; i++) {
                        content[i] = byteString.charCodeAt(i);
                    }

                    return new Blob([new Uint8Array(content)], { type: mimestring });
                }
                var updateResultImage = function(scope) {
                    var resultImage = cropHost.getResultImageDataURI();
                    if (storedResultImage !== resultImage) {
                        storedResultImage = resultImage;
                        if (angular.isDefined(scope.resultImage)) {
                            scope.resultBlob = dataURItoBlob(resultImage);
                            scope.resultImage = resultImage;
                        }
                        scope.onChange({ $dataURI: scope.resultImage });
                    }
                };

                // Wrapper to safely exec functions within $apply on a running $digest cycle
                var fnSafeApply = function(fn) {
                    return function() {
                        $timeout(function() {
                            scope.$apply(function(scope) {
                                fn(scope);
                            });
                        });
                    };
                };

                // Setup CropHost Event Handlers
                events
                    .on('load-start', fnSafeApply(function(scope) {
                        scope.onLoadBegin({});
                    }))
                    .on('load-done', fnSafeApply(function(scope) {
                        scope.onLoadDone({});
                    }))
                    .on('load-error', fnSafeApply(function(scope) {
                        scope.onLoadError({});
                    }))
                    .on('area-move area-resize', fnSafeApply(function(scope) {
                        if (!!scope.changeOnFly) {
                            updateResultImage(scope);
                        }
                    }))
                    .on('area-move-end area-resize-end image-updated', fnSafeApply(function(scope) {
                        updateResultImage(scope);
                    }));

                // Sync CropHost with Directive's options
                scope.$watch('image', function() {
                    cropHost.setNewImageSource(scope.image);
                });
                scope.$watch('areaType', function() {
                    cropHost.setAreaType(scope.areaType, scope.sizeScale);
                    updateResultImage(scope);
                });
                scope.$watch('areaMinSize', function() {
                    cropHost.setAreaMinSize(scope.areaMinSize);
                    updateResultImage(scope);
                });
                scope.$watch('resultImageSize', function() {
                    cropHost.setResultImageSize(scope.resultImageSize, scope.sizeScale);
                    updateResultImage(scope);
                });
                scope.$watch('resultImageFormat', function() {
                    cropHost.setResultImageFormat(scope.resultImageFormat);
                    updateResultImage(scope);
                });
                scope.$watch('resultImageQuality', function() {
                    cropHost.setResultImageQuality(scope.resultImageQuality);
                    updateResultImage(scope);
                });

                // Update CropHost dimensions when the directive element is resized
                $timeout(function() {
                        cropHost.setMaxDimensions(element[0].clientWidth, element[0].clientHeight - 4);
                        updateResultImage(scope);
                    }, true)
                    // scope.$watch(
                    //   function () {
                    //     return [element[0].clientWidth, element[0].clientHeight];
                    //   },
                    //   function (value) {
                    //     cropHost.setMaxDimensions(value[0],value[1]);
                    //     updateResultImage(scope);
                    //   },
                    //   true
                    // );

                // Destroy CropHost Instance when the directive is destroying
                scope.$on('$destroy', function() {
                    cropHost.destroy();
                });
            }
        };
    }]);
}());
