<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link href="//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css" rel="stylesheet">
        <link href="//cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
        <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/ui-cropper@1.0.9/compile/unminified/ui-cropper.min.css" />
        <link href="//cdnjs.cloudflare.com/ajax/libs/angularjs-toaster/3.0.0/toaster.min.css" rel="stylesheet" />
        <link href="lib/additional_style.css" rel="stylesheet">
        <link href="lib/style.css" rel="stylesheet">

        <script src="//cdn.jsdelivr.net/npm/jquery@2.2.4/dist/jquery.min.js"></script>
        <script src="//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
        <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.14/angular.js"></script>
        <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.14/angular-animate.js"></script>
        <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.14/angular-sanitize.js"></script>
        <script src="//cdn.jsdelivr.net/npm/angular-ui-bootstrap@2.5.6/dist/ui-bootstrap-tpls.js"></script>
        <script src="//cdn.jsdelivr.net/npm/ng-file-upload@12.2.13/dist/ng-file-upload.js"></script>
        <script src="lib/ui-cropper.js"></script> <!-- Is patched !!! -->
        <script src="//cdnjs.cloudflare.com/ajax/libs/angularjs-toaster/3.0.0/toaster.min.js"></script>
        <script src="lib/script.js"></script>
    </head>
    <body ng-app="angularjs.modalform"
          ng-controller="MainCtrl">
        <div class="app-container">
            <div class="view-container container-fluid">
                <ng-view class="app-view"></ng-view>
                <toaster-container 
                    toaster-options="{
                        'toaster-id': 'main',
                        'position-class': 'toast-center',
                        'tap-to-dismiss': false,
                        'close-button': true,
                        'body-output-type': 'trustedHtml'
                    }">
                </toaster-container>
                <toaster-container 
                    toaster-options="{
                        'toaster-id': 'main-tiny',
                        'position-class': 'toast-center tiny',
                        'time-out': 3000,
                        'tap-to-dismiss': false,
                        'close-button': true,
                        'body-output-type': 'trustedHtml'
                    }">
                </toaster-container>
                <button type="button" class="btn btn-default" 
                    ng-click="openModal()">Afbeelding opladen</button>
            </div>
        </div>
    </body>
</html>
<div class="modal-header">
    <h3 class="form-horizontal-header">Domein&nbsp;
        <span ng-if="!vm.appform.$pristine"
            class="fa fa-pencil"></span>
    </h3>
</div>

<div class="modal-body">
    <form name="vm.appform"
          role="form"
          class="form-horizontal"
          novalidate>
        <div class="form-group form-group-sm">
            <label class="col-sm-4 control-label">Systeem ID</label>
            <div class="col-sm-4">
                <div class="form-control-static"
                     ng-bind="vm.domain.id">
                </div>
            </div>
        </div>

        <div class="form-group form-group-sm">
            <label class="col-sm-4 control-label">Naam</label>
            <div class="col-sm-8">
                <input class="form-control"
                       type="text"
                       ng-maxlength="8"
                       ng-model="vm.domain.name"
                       ng-model-options="{allowInvalid: true}"
                       name="name"
                       required=""
                       tooltip-form-validation=""
                       ng-virtual-keyboard/>
            </div>
        </div>

        <div class="form-group form-group-sm">
            <label class="col-sm-4 control-label">Range</label>
            <div class="col-sm-4">
                <input class="form-control"
                       type="number" ng-step="1" 
                       ng-min="0" ng-max="vm.domain.max || 4294967295"
                       ng-model="vm.domain.min"
                       ng-model-options="{allowInvalid: true}"
                       name="min"
                       tooltip-form-validation=""
                       ng-virtual-keyboard/>
            </div>
            <div class="col-sm-4">
                <input class="form-control"
                       type="number" ng-step="1" 
                       ng-min="vm.domain.min || 0" ng-max="4294967295"
                       ng-model="vm.domain.max"
                       ng-model-options="{allowInvalid: true}"
                       name="max"
                       tooltip-form-validation=""
                       ng-virtual-keyboard/>
            </div>
        </div>
    </form>
</div>

<div class="modal-footer">
    <button ng-if="!vm.appform.$pristine"
            class="btn" ng-click="vm.ok()">Bewaren
    </button>
    <button class="btn" ng-click="vm.cancel()">Annuleren</button>
</div>
:root {
    --main-bg-color-r: 88;
    --main-bg-color-g: 160;
    --main-bg-color-b: 160;
    --main-bg-color: rgb(var(--main-bg-color-r), var(--main-bg-color-g), var(--main-bg-color-b));
    --off-white-r: 255;
    --off-white-g: 255;
    --off-white-b: 231;
    --off-white: rgb(var(--off-white-r), var(--off-white-g), var(--off-white-b));
    --off-black-r: 64;
    --off-black-g: 32;
    --off-black-b: 32;
    --off-black: rgb(var(--off-black-r), var(--off-black-g), var(--off-black-b));
    --soft-yellow-r: 255;
    --soft-yellow-g: 255;
    --soft-yellow-b: 127;
    --soft-yellow: rgb(var(--soft-yellow-r), var(--soft-yellow-g), var(--soft-yellow-b));
    --soft-green-r: 208;
    --soft-green-g: 255;
    --soft-green-b: 192;
    --soft-green: rgb(var(--soft-green-r), var(--soft-green-g), var(--soft-green-b));
    --soft-red-r: 255;
    --soft-red-g: 192;
    --soft-red-b: 160;
    --soft-red: rgb(var(--soft-red-r), var(--soft-red-g), var(--soft-red-b));
    --button-bg-color-r: 181;
    --button-bg-color-g: 116;
    --button-bg-color-b: 35;
    --button-bg-color: rgb(var(--button-bg-color-r), var(--button-bg-color-g), var(--button-bg-color-b));
    --input-bg-color-r: 181;
    --input-bg-color-g: 116;
    --input-bg-color-b: 35;
    --input-bg-color: rgb(var(--input-bg-color-r), var(--input-bg-color-g), var(--input-bg-color-b));
    --padding: 2px;
    --border-width: 1px;
    --actionbar-height: 128px;
    --cage-remark-color: #c0392b;
    --text-shadow-light: .1em .1em .2em #ffffff;
    --text-shadow-dark: .1em .1em .2em #000000;
    --toolbar-height: 32px;
    --scale: 1;
}

body {
    width: 100vw;
    height: 100vh;
    /*padding-bottom: 128px;*/
    /*overflow: auto;*/
    opacity: .99999; /* For z-index purposes */
}

.app-container {
    width: 100%;
    height: 100%;
}

.view-container {
    width: 100%;
    height: calc(100% - var(--actionbar-height));
    overflow: auto;
    display: flex;
    flex-direction: column;
}

.app-view {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
}

.app-content {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
}

.app-content-horizontal {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: row;
}

.app-content-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

.text-shadow-dark {
    text-shadow: var(--text-shadow-dark);
}

.text-shadow-light {
    text-shadow: var(--text-shadow-light);
}

html, body, input, .popover {
    font-family: 'Laila';
    font-size: 12px;
    font-weight: 400;
    background-color: var(--main-bg-color);
    color: var(--off-white);
}

h1, .h1, h2, .h2, h3, .h3 {
    margin-top: 8px;
    margin-bottom: 8px;
    text-shadow: var(--text-shadow-dark);
}

h4, .h4, h5, .h5, h6, .h6 {
    margin-top: 6px;
    margin-bottom: 6px;
    text-shadow: var(--text-shadow-dark);
}

h4, .h4 {
    font-size: 18px;
}

h5, .h5 {
    font-size: 14px;
}

h6, .h6 {
    font-size: 12px;
}


.form-horizontal {
    background: rgba(0, 0, 0, .1);
    border: 1px solid rgba(255, 255, 255, .3);
    border-radius: 6px;
    padding: 16px 16px 0 16px;
    /*display: table;*/
    margin: auto;
    min-width: 75%;
    max-width: 95%;
    text-align: left;
}

.form-horizontal.full-width {
    max-width: 100%;
}

.form-horizontal input[type='button'] {
    text-align: left;
}

.modal-header, .modal-footer,
.form-horizontal-header, .form-horizontal-footer {
    text-align: center;
}

.form-horizontal-header {
    display: block;
}

.form-horizontal-footer {
    padding-top: 16px;
}

.form-control {
    padding: 6px;
    height: auto;
    font-size: inherit;
}

.input-group,
.form-group-sm .form-control {
    border-radius: 4px;
}

.form-horizontal .form-group-sm .control-label {
    padding: 7px;
}

.form-horizontal .form-group-sm .control-label.fa {
    padding: 5px;
}

.form-horizontal .form-group-sm .control-label.fa::before {
    font-size: 1.35em;
}

.form-horizontal .radio, .form-horizontal .checkbox {
    white-space: nowrap;
    border-radius: 3px;
    padding: 5px 5px 2px 5px;
}

.modal-content {
    background-color: var(--main-bg-color);
    color: var(--off-white);
}

.horizontal-flex-container {
    display: flex;
    flex-direction: row;
    /*justify-content: center;*/
    /*justify-content: space-around;*/
    justify-content: space-between;
    align-items: center;
    /*margin-left: 1px;*/
    /*margin-right: 1px;*/
}

.horizontal-flex-container-start-aligned {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: start;
}

.horizontal-flex-container2 {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
}

.horizontal-flex-container2-start-aligned {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    align-items: start;
}

.horizontal-flex-container-center {
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
}

/*******************************************************************************
 * Flex box with variable items supporting overflow
 * https://stackoverflow.com/questions/33454533/
 *     cant-scroll-to-top-of-flex-item-that-is-overflowing-container
 * https://stackoverflow.com/a/33856609/3597276
 */

.horizontal-flex-container-overflow,
.horizontal-flex-container-center-overflow,
.horizontal-flex-container-center-top-overflow {
    display: flex;
    flex-direction: row;
    overflow-x: auto;
}

.horizontal-flex-container-overflow > * {
    margin: auto;
}

.horizontal-flex-container-center-overflow > * {
    margin-top: auto;
    margin-bottom: auto;
}

.horizontal-flex-container-center-top-overflow > * {
    margin-bottom: auto;
}

.horizontal-flex-container-center-overflow > *:first-child,
.horizontal-flex-container-center-top-overflow > *:first-child {
    margin-left: auto;
}

.horizontal-flex-container-center-overflow > *:last-child,
.horizontal-flex-container-center-top-overflow > *:last-child {
    margin-right: auto;
}

/*******************************************************************************
 */

.vertical-flex-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.vertical-flex-container2 {
    display: flex;
    flex-direction: column;
    justify-content: space-around;
}

.vertical-flex-container-center {
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.vertical-flex-container .checkbox label,
.vertical-flex-container2 .checkbox label,
.vertical-flex-container-center .checkbox label {
    padding-left: 0;
}

.vertical-flex-container .checkbox label div,
.vertical-flex-container2 .checkbox label div,
.vertical-flex-container-center .checkbox label div {
    display: inherit;
    /*background: unset;*/
    border: 1px solid;
    margin-bottom: -.56em;
    padding: .1em;
    border-radius: 2px;
}

.vertical-inline-flex {
    display: inline-flex;
    flex-direction: column;
    /*margin: -1px;*/
}

.clickable .fa,
.clickable .el,
.clickable .ion {
    border-radius: 3px;
    padding: 2px;
    border: 1px solid transparent;
}

:not([disabled]) .clickable:not([disabled]) > .fa,
:not([disabled]) .clickable:not([disabled]) > .el,
:not([disabled]) .clickable:not([disabled]) > .ion {
    background: rgba(255, 255, 255, .1);
    border-color: rgba(255, 255, 255, .2);
}

:not([disabled]) .clickable:not([disabled]):hover > .fa,
:not([disabled]) .clickable:not([disabled]):focus > .fa,
:not([disabled]) .clickable:not([disabled]):active > .fa,
:not([disabled]) .clickable:not([disabled]):hover > .el,
:not([disabled]) .clickable:not([disabled]):focus > .el,
:not([disabled]) .clickable:not([disabled]):active > .el,
:not([disabled]) .clickable:not([disabled]):hover > .ion,
:not([disabled]) .clickable:not([disabled]):focus > .ion,
:not([disabled]) .clickable:not([disabled]):active > .ion {
    /*.clickable:not([disabled]):hover > .fa,*/
    /*.clickable:not([disabled]):focus > .fa,*/
    /*.clickable:not([disabled]):active > .fa {*/
    background: rgba(0, 0, 0, .1);
    border-color: rgba(0, 0, 0, .2);
}

.clickable.img-thumbnail:not([disabled]):hover,
.clickable.img-thumbnail:not([disabled]):focus,
.clickable.img-thumbnail:not([disabled]):active,
.clickable:not([disabled]):hover > .action-image,
.clickable:not([disabled]):focus > .action-image,
.clickable:not([disabled]):active > .action-image {
    background: rgba(255, 255, 255, .2);
    /*text-decoration: none;*/
}

.clickable:not([disabled]):hover {
    cursor: pointer;
}

[disabled].clickable:hover,
[disabled] > .clickable:hover,
[disabled] > * > .clickable:hover {
    cursor: not-allowed;
}

.clickable:hover > * {
    cursor: inherit;
}

.form-control, .input-group-addon {
    background-color: var(--input-bg-color);
    border-color: var(--off-black);
    color: var(--off-white);
}

.input-group-addon {
    padding: 0 6px;
}

.btn.disabled, .btn[disabled], fieldset[disabled] .btn {
    color: var(--off-black);
}

.btn-info, .btn-default .btn-danger, .btn-success {
    color: var(--off-black);
    background-color: var(--off-white);
    border-color: rgba(0, 0, 0, .2);
}

.open > .dropdown-toggle.btn-default,
.btn, .btn-info, .btn-success, .btn-danger {
    background-color: var(--button-bg-color);
    border-color: var(--off-black);
    color: var(--off-white);
}

.btn-default:active, .btn-default.active,
.btn-info:active, .btn-info.active {
    background-color: var(--soft-green);
    border-color: var(--off-black);
    color: var(--off-black);
}

.btn-info:active:hover, .btn-info.active:hover,
.open > .dropdown-toggle.btn-info:hover,
.btn-info:active:focus, .btn-info.active:focus,
.open > .dropdown-toggle.btn-info:focus,
.btn-info.focus:active, .btn-info.active.focus,
.open > .dropdown-toggle.btn-info.focus,
.btn-default:active:hover, .btn-default.active:hover,
.open > .dropdown-toggle.btn-default:hover,
.btn-default:active:focus, .btn-default.active:focus,
.open > .dropdown-toggle.btn-default:focus,
.btn-default.focus:active, .btn-default.active.focus,
.open > .dropdown-toggle.btn-default.focus,
.btn-info:hover, .btn-default:hover, .btn-danger:hover, .btn-success:hover,
.btn:hover, .btn:focus, .btn.focus {
    color: var(--off-black);
/*     color: inherit; */
}

:not([disabled]).btn:hover,
:not([disabled]).btn:focus {
    background-color: var(--button-bg-color);
    /*border-color: var(--off-black);*/
    /*box-shadow: inset 0 0 2px rgba(0, 0, 0, .5), 0 0 8px rgba(247, 209, 50, .9);*/
    border-color: rgba(0, 0, 0, .75);
    /*border-radius: 4px;*/
    box-shadow: 0 0 4px rgba(255, 255, 255, .5) inset, 0 0 8px rgba(0, 0, 0, .5); /*color: var(--off-white);*/
    /*-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(247, 209, 50, .6);*/
}

:not([disabled]).btn:active,
:not([disabled]).btn:active:hover,
:not([disabled]).btn.active:hover,
:not([disabled]).btn:active:focus,
:not([disabled]).btn.active:focus,
:not([disabled]).btn.focus:active,
:not([disabled]).btn.active.focus {
    background-color: var(--soft-green);
    border-color: rgba(0, 0, 0, .75);
    box-shadow: 0 0 4px rgba(255, 255, 255, .35) inset, 0 0 8px rgba(0, 0, 0, .35);
}

:not([disabled]).text-input:focus,
:not([disabled]).form-control:focus {
    border-color: #66AFE9;
}

.col-vertical-span {
    position: absolute;
    left: -15px;
}

/*.square-vertical-span > * {*/
/*width: 100%;*/
/*padding-bottom: 100%;*/
/*}*/
/*.square-vertical-span > * > * {*/
/*position: absolute;*/
/*}*/
.col-container {
    padding-left: 0;
    padding-right: 0;
}

.flex-container {
    align-items: center;
    display: flex;
}

.flex-container-start {
    align-items: flex-start;
    display: flex;
}

.flex-grow {
    flex-grow: 1;
}

/*******************************************************************************
 * Number spinner
 */
input[numbers-only] {
    min-width: 4em;
}

.number-spinner.btn,
.number-spinner.btn:focus,
.number-spinner.btn:active,
.number-spinner.btn:focus:active,
.number-spinner {
    padding: 0 2px;
    margin-left: -2.5em;
    font-size: 7px;
    font-weight: 900;
    border: 1px solid var(--off-black);
    background-color: rgba(0, 0, 0, .25);
    box-shadow: none;
    z-index: 3;
}

.number-spinner.btn:first-child,
.number-spinner.btn:first-child:focus,
.number-spinner.btn:first-child:active,
.number-spinner.btn:first-child:focus:active {
    /*margin-bottom: -1px;*/
    /*padding-bottom: 1px;*/
    /*border-radius: 4px 4px 0 0;*/
    margin-bottom: -1px;
    padding: 1px 0;
    border-radius: 4px 4px 0 0;
}

.number-spinner.btn:last-child,
.number-spinner.btn:last-child:focus,
.number-spinner.btn:last-child:active,
.number-spinner.btn:last-child:focus:active {
    /*padding-top: 1px;*/
    padding: 1px 0;
    border-radius: 0 0 4px 4px;
}

.number-spinner.btn > .fa,
.number-spinner.btn > .el,
.number-spinner.btn > .ion {
    padding: 1px;
}

.number-spinner.btn:hover,
.number-spinner.btn:hover:focus,
.number-spinner.btn:hover:active,
.number-spinner.btn:hover:focus:active {
    background-color: rgba(0, 0, 0, .25);
}

:not([disabled]) > :not([disabled]) > .number-spinner.btn:not([disabled]):hover,
:not([disabled]) > :not([disabled]) > .number-spinner.btn:not([disabled]):hover:focus,
:not([disabled]) > :not([disabled]) > .number-spinner.btn:not([disabled]):hover:active {
    background-color: rgba(0, 0, 0, .45);
    box-shadow: inherit;
}

:not([disabled]) > :not([disabled]) > .number-spinner.btn:not([disabled]):hover:focus:active {
    background-color: rgba(0, 0, 0, .55);
    box-shadow: inherit;
}

.clickable.number-spinner.btn > .fa,
.clickable.number-spinner.btn:hover > .fa,
.clickable.number-spinner.btn:focus > .fa,
.clickable.number-spinner.btn:active > .fa,
.clickable.number-spinner.btn > .el,
.clickable.number-spinner.btn:hover > .el,
.clickable.number-spinner.btn:focus > .el,
.clickable.number-spinner.btn:active > .el,
.clickable.number-spinner.btn > .ion,
.clickable.number-spinner.btn:hover > .ion,
.clickable.number-spinner.btn:focus > .ion,
.clickable.number-spinner.btn:active > .ion {
    background: transparent;
    border-color: transparent;
}

.input-group .form-control.last-child:first-child,
.input-group .form-control.last-child:not(:first-child) {
    border-top-right-radius: 4px;
    border-bottom-right-radius: 4px;
}

.modal {
    text-align: center;
    bottom: calc(var(--actionbar-height) + var(--padding) + var(--border-width));
}

.modal::before {
    content: '';
    display: inline-block;
    height: 100%;
    vertical-align: middle;
}

.modal-dialog {
    display: inline-block;
    vertical-align: middle;
}

/******************************************************************************/
.has-error .input-group-addon,
.has-error.form-control,
.has-error .form-control {
    border-color: #dd0000;
    box-shadow: 0 0 1px #dd0000 inset, 0 0 1px #000;
    /*background-color: rgba(255, 128, 0, .65);*/
    background-color: rgba(255, 160, 40, .85);
    /*B57423*/
}

.has-error .date-picker .input-placeholder.text-muted,
.has-error.form-control .text-muted,
.has-error .form-control .text-muted {
    color: rgba(0, 0, 0, .55);
}

.has-error .input-group-addon {
    color: inherit;
}

/*******************************************************************************
 *******************************************************************************
 */
:root {
    --error-red-r: 221; /* #DD */
    --error-red-g: 0;
    --error-red-b: 0;
    --error-red: rgb(var(--error-red-r), var(--error-red-g), var(--error-red-b));
    --error-bg-color-r: 255;
    --error-bg-color-g: 160;
    --error-bg-color-b: 40;
    --error-bg-color: rgba(var(--error-bg-color-r), var(--error-bg-color-g), var(--error-bg-color-b), .75);
 }

/*******************************************************************************
 * Number spinner
 * => To be updated: input[numbers-only] --> input[number-spinners-addon] !!!
 */
input[number-spinners-addon] {
    min-width: 4em;
}

/*******************************************************************************
 * Form errors
 * => To be added to:
 * .has-error .input-group-addon,
 * .has-error.form-control,
 * .has-error .form-control {
 *  ...
 * and add: color: var(--off-black);
 */
form.ng-submitted input.ng-invalid {
    border-color: var(--error-red);
    box-shadow: 0 0 1px var(--error-red) inset, 0 0 1px #000;
    /*background-color: rgba(255, 128, 0, .65);*/
    background-color: var(--error-bg-color);
    color: black;
}

/*******************************************************************************
 * Form validation tooltip
 */
.tooltip.form-validation {
    margin-top: -8px;
    margin-left: 4px;
}

 .tooltip.form-validation .tooltip-inner {
    color: var(--error-red);
    /* background-color: rgb(255, 184, 102); */
    background-color: var(--off-white);
    box-shadow: 0 6px 12px rgba(0,0,0,.175);
    border-style: solid;
    border-width: 1px;
    border-color: var(--off-black);
}

.tooltip.form-validation .tooltip-arrow {
    border-bottom-color: var(--off-black);
}
var app = angular.module('angularjs.modalform', 
    ['ngAnimate', 'ngSanitize', 'ui.bootstrap', 'ngFileUpload',
     'uiCropper', 'toaster']);

app.service(
    'formValidationErrorService',
    function ($log) {
        var log = $log;
        var self = this;

        // A tiny internal database with form validation errors.
        // Simply based on key (constraint) and value (error description) pairs.
        var validationErrors = {
            unknown: 'Onbekende fout',
            required: 'Verplicht veld',
            number: 'Ongeldig getal',
            min: 'Getal is te klein',
            max: 'Getal is te groot',
            email: 'Ongeldige email',
            url: 'Ongeldige URL',
            date: 'Ongeldige datum',
            time: 'Ongeldige tijd',
            datetimelocal: 'Ongeldige lokale datum-tijd',
            week: 'Ongeldige week',
            month: 'Ongeldige maand',
            pattern: 'Ongeldige waarde',
            minlength: 'Waarde is te kort',
            maxlength: 'Waarde is te lang'
        };

        // Register and/or update one or more new/existing validation errors.
        // The errors argument is an object with one or more properties
        // (constraints) with their corresponding values (descriptions).
        // It can be used e.g. for a form-control directive that installs a
        // custom validator to register it's corresponding validation errors.
        this.register = function (errors, force = false) {
            log.debug('formValidationErrorService.register:', errors, force);
            if (force)
                angular.extend(validationErrors, errors);
            else {
                // Not forced so, only add item if it doesn't exist yet
                Object.keys(errors).forEach(function(item, index) {
                    if (!validationErrors.hasOwnProperty(item)) {
                        validationErrors[item] = errors[item];
                    };
                });
            }
        };

        // Get one or more existing validation errors. The argument is an
        // array (list) of constraints. The return value is an object with the
        // requested properties (constraints) and their corresponding values
        // (descriptions).
        this.get = function (constraints, strict = false) {
            log.debug('formValidationErrorService.get:', constraints, strict);
            var key, suffix = '', errors = {};
            constraints.forEach(function(item, index) {
                if (validationErrors.hasOwnProperty(item)) {
                    key = item;
                } else if (!strict) {
                    key = 'unknown';
                    suffix = ', waarde voldoet niet aan eis \'' + item + '\'';
                } else {
                    // Do nothing, go to next
                    return;
                };
                errors[item] = validationErrors[key] + suffix;
            });
            return errors;
        };

        // Show form validation error on mouse-over or always.
        // TODO: showOnMouseOver as preference
        this.showOnMouseOver = true;
    }
);

app.directive(
    'tooltipFormValidation',
    function ($log, $compile, $interval, formValidationErrorService) {
        var log = $log;
        return {
            restrict: 'A',
            require: '?ngModel',
            //scope: true,
            link: function (scope, element, attrs, modelCtrl) {
                // At this linking phase, automatic compiling has already been
                // performed. Due to manual compilation below, this link
                // function gets called again. To prevent execution twice, add
                // an "already-linked" flag to the element. If the flag was
                // already set, just return.
                const alreadyLinkedAttr =
                    'tooltip-form-validation-already-linked';
                if (attrs[attrs.$normalize(alreadyLinkedAttr)] === 'true')
                    return;
                attrs.$set(alreadyLinkedAttr, 'true');

                log.debug('directive tooltipFormValidation link:', arguments);
                attrs.$set('tooltip-placement', 'bottom-left');
                attrs.$set('tooltip-class', 'form-validation');
                attrs.$set('tooltip-trigger', '"none"');

                const nameAttr = attrs.name;
                if (!modelCtrl.$isEmpty(nameAttr)) {
                    const formFieldStr = 'vm.appform["' + nameAttr + '"]';
                    attrs.$set('tooltip-is-open', formFieldStr + '.isOpen');
                    attrs.$set('uib-tooltip',
                        '{{' + formFieldStr + '.errorMsg}}');
                    attrs.$set('ng-mouseover',
                        formFieldStr + '.mouseOver = true'); 
                    attrs.$set('ng-mouseout',
                        formFieldStr + '.mouseOver = false');

                    // Remove ng-virtual-keyboard and tooltip-form-validation
                    // directives before compiling to avoid multiple/infinite
                    // compile loops.
                    attrs.$set('ng-virtual-keyboard');
                    attrs.$set('tooltip-form-validation');
                    $compile(element)(scope);
                    log.debug('directive tooltipFormValidation link compiled:',
                        element[0].outerHTML);

                    // A watch on either modelCtrl.$error, modelCtrl.$invalid
                    // or scope.vm.appform.$submitted doesn't seem to work, so
                    // let's implement an old fashioned polling mechanism
                    // using a timer.
                    var oldError, newError;
                    var timer = $interval(function () {
                        newError = modelCtrl.$error;
                        if (!angular.equals(newError, oldError)) {
                            log.debug('directive tooltipFormValidation link',
                              'watch modelCtrl.$error:', oldError, newError);
                            // Get the error messages for only those keys who's
                            // values are set to 'true'.
                            const errors = formValidationErrorService.get(
                                Object.keys(newError).filter(
                                    (error) => newError[error])
                            );
                            log.debug('Errors:', errors);
                            // For now, simply get the 1st error message and
                            // drop the rest. Another possibility would be to
                            // concatenate all messages.
                            const errorMsg = 
                                Object.values(errors).length  > 0 ?
                                Object.values(errors)[0] : ''; 
                            scope.vm.appform[nameAttr].errorMsg = errorMsg;
                            oldError = angular.copy(newError);
                        };
                        // Set tooltip isOpen depending on the form submission,
                        // the field validity, the mouse-over state and the
                        // setting 'show-on-mouse-over'.
                        if (scope.vm.appform[nameAttr]) // not destroyed yet
                            scope.vm.appform[nameAttr].isOpen = 
                                scope.vm.appform.$submitted &&
                                modelCtrl.$invalid &&
                                ( scope.vm.appform[nameAttr].mouseOver ||
                                !formValidationErrorService.showOnMouseOver);
                    }, 200);

                    // On DOM element destroy (removal), stop the timer to
                    // prevent updating the DOM element after it was removed.
                    element.on('$destroy', function () {
                        $interval.cancel(timer);
                    });

                };
            },
        };
    }
);


app.controller('MainCtrl', function ($scope, $uibModal, $log) {
    //var vm = this;
    var log = $log;
    $scope.domain = {
        id: 456,
        name: 'my domain',
        min: null,
        max: null
    };
    $scope.guest = {
        "id": 2168,
        "uuid": "3554d780-c200-11e6-9892-d346b6a39a5c",
        "external_ref": null,
        "entrance": "2016-12-13T23:00:00.000Z",
        "animal": 816,
        "quantity": 1,
        "cage": 66,
        "exit": null,
        "exit_reason": null,
        "staff_only": 0,
        "dangerous": 0,
        "medication": null,
        "comment": null,
        "for_adoption": 0,
        "adoption_from": null,
        "origin": null,
        "id_number": "K 011 77 A 5022",
        "given_name": null,
        "date_of_birth": null,
        "exit_comment": null,
        "just_comment": "IBN 13/12 6320 17/5",
        "reserved": 0,
        "menu_percentage": 100,
        "reserved_for": null,
        "male_quantity": 0,
        "female_quantity": 0,
        "entrance_reason": 0,
        "domain": 4,
        "origin_country": "--",
        "contact": null,
        "animal_name": "Steller zeearend",
        "cage_name": "VK 8",
        "color": "rgb(204, 0, 0)",
        "unspecifiedSexQuantity": 1
    };

    $scope.openModal = function () {
        var animal = {
            uuid: angular.copy($scope.guest.uuid),
            name: angular.copy($scope.guest.animal_name),
            id: angular.copy($scope.guest.id)
        };
        var modalInstance = $uibModal.open({
            // templateUrl: 'modal_form.html',
            templateUrl: 'upload-image-modal.html',
            controller: 'UploadImageCtrl',
            controllerAs: 'vm',
            resolve: {
                animal: animal
            },
            backdrop: 'static'
        });

        modalInstance.result.then(function (filename) {
            log.debug('Modal instance result:', filename);
        }, function (reason) {
            log.debug('Modal dismissed reason:', reason);
        });
    };
});

// Please note that $uibModalInstance represents a modal window (instance)
// dependency. It is not the same as the $uibModal service used above.

app.controller(
    'UploadImageCtrl', 
    function ($uibModalInstance, $log, Upload, $timeout, toaster, animal) {
        var vm = this;
        var log = $log;
        vm.animal = animal;

        vm.upload = {
            imgFormat: 'image/jpeg',
            fileExtension: 'jpg',
            serverUrl: 'images/uploads/' + 
                (vm.animal.id ? 'hospitalizations' : 'animals')
        };
        vm.upload.filename = vm.animal.uuid + '.' + vm.upload.fileExtension;

        // TODO: Image minimum dimensions width/height as system 
        // fine-tuning/setting
        const minDim = 480;
        vm.imgDimensions = {
            minHeight: minDim,
            minWidth: minDim
        };

        vm.onFileChange = function (files, file, newFiles, duplicateFiles,
                invalidFiles, event) {
            console.log(arguments, vm);
            if (files.length !== 1 || !file || invalidFiles.length > 0) {
                // Get the 1st form error, that is not 'required'
                const err = 
                    Object.keys(vm.imageCropUploadForm.$error)
                    .filter((val) => val !== 'required')[0];
                if (err) {
                    // Determine the corresponding error-message
                    var errMsg;
                    switch(err) {
                        case 'pattern':
                            errMsg = 'Bestand is geen afbeelding.';
                            break;
                        case 'minWidth':
                        case 'minHeight':
                            errMsg = 'Afbeelding heeft een te lage resolutie.';
                            break;
                        default:
                            errMsg = 'Onbekende fout: "' + err + '".';
                    }
                    // Show the error-message
                    toaster.error({
                        toasterId: 'main',
                        title: 'Afbeelding selecteren is mislukt!',
                        body: errMsg
                    });
                }
                // Reset the data-URL and crop-info
                vm.croppedDataUrl = '';
                vm.cropper = undefined;
            } 
            // Reset the upload info
            vm.upload.progress = undefined;
            // Reset the form
            vm.imageCropUploadForm.$setPristine();
        }

        vm.uploadImg = function (dataUrl, filename, cb) {
            //console.log(dataUrl, filename, vm);
            if (!dataUrl || !filename) return;

            // const fileUrl = vm.upload.serverUrl + '/' + filename;
            // console.log(fileUrl);
            vm.uploader = Upload.upload({
                url: 'https://angular-file-upload-cors-srv.appspot.com/upload',
                // url: 'http://192.168.1.3:3003/img-upload',
                // url: vm.upload.serverUrl,
                data: {
                    // The key (i.e. 'imagefile') becomes the field 'name'
                    // and the filename becomes the field 'filename' in
                    // the Content-Disposition form-data. The blob is the
                    // actual image.
                    imagefile: Upload.dataUrltoBlob(dataUrl, filename)
                },
            });
            vm.uploader.then(function (response) {
                // Handle successful upload
                $timeout(function () {
                    cb(null, response.data);
                });
            }, function (response) {
                // Handle error
                if (response.status > 0) {
                    cb(response.status + ' ' + 
                        response.statusText + ': ' + 
                        response.data);
                }
            }, function (evt) {
                // Handle progress notification
                vm.upload.progress = parseInt(100.0 * evt.loaded / evt.total);
            });
        }

        // Stop the click-event to prevent AngularJS from
        // interpreting it as a form submission.
        vm.stopClick = function (event) {
            event.preventDefault();
            event.stopPropagation();
        };

        vm.ok = () => {
            vm.imageCropUploadForm.$setSubmitted();
            if (vm.imageCropUploadForm.$valid) {
                vm.uploadImg(vm.croppedDataUrl, vm.upload.filename,
                    (err, result) => {
                        if (err) {
                            console.log('Upload failed:', err);
                            toaster.error({
                                toasterId: 'main',
                                title: 'Afbeelding opladen is mislukt!',
                                body: 'Error ' + err
                            });

                        } else {
                            console.log('Upload succeeded:', result);
                            $uibModalInstance.close(vm.upload.filename);
                            toaster.success({
                                toasterId: 'main',
                                title: 'Afbeelding opladen is gelukt!',
                                body: 'Bestandsnaam: ' + vm.upload.filename
                            });
                        }
                    });
            } else
                log.debug('Form validation errors:', 
                    vm.imageCropUploadForm.$error);
        };
        vm.cancel = () => {
            if (Upload.isUploadInProgress()) {
                try {
                    vm.uploader.abort();
                    console.log('Upload aborted @:', vm.upload.progress + '%');
                    toaster.info({
                        toasterId: 'main',
                        title: 'Afbeelding opladen is geannuleerd!',
                        body: 'Bij: ' + vm.upload.progress + '%'
                    });
                } catch (err) {
                    console.error('Aborting upload failed:', err);
                }
            }
            $uibModalInstance.dismiss('cancel');
        }
    }
);
/*!
 * uiCropper v1.0.9
 * https://crackerakiua.github.io/ui-cropper/
 *
 * Copyright (c) 2019 Alex Kaul
 * License: MIT
 *
 * Generated at Monday, May 20th, 2019, 10:17:03 PM
 */
(function() {
angular.module('uiCropper', []);

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

        this._boxResizeBaseSize = 25;
        this._boxResizeNormalRatio = 1;
        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.getType = function() {
        return 'circle';
    };

    CropAreaCircle.prototype._calcCirclePerimeterCoords = function(angleDegrees) {
        var hSize = this._size.w / 2;
        var angleRadians = angleDegrees * (Math.PI / 180),
            circlePerimeterX = this.getCenterPoint().x + hSize * Math.cos(angleRadians),
            circlePerimeterY = this.getCenterPoint().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.getCenterPoint().x) * (coord[0] - this.getCenterPoint().x) + (coord[1] - this.getCenterPoint().y) * (coord[1] - this.getCenterPoint().y)) < this._size.w / 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.x, centerCoords.y, size.w / 2, 0, 2 * Math.PI);
    };

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

        // draw move icon
        var center = this.getCenterPoint();
        this._cropCanvas.drawIconMove([center.x, center.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.setSizeByScale = function(scale) {
        var center = this.getCenterPoint();
        var size = this.getSize();
        var newRadius = size.w * scale / 2;
        var northWestPoint = {
            x: center.x - newRadius,
            y: center.y - newRadius
        };
        var southEastPoint = {
            x: center.x + newRadius,
            y: center.y + newRadius
        };
        this.circleOnMove(northWestPoint, southEastPoint);
        this._events.trigger('area-resize');
    };

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

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

        if (this._areaIsDragging) {
            this.setCenterPointOnMove({
                x: mouseCurX - this._posDragStartX,
                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.w + iFY * 2;
            } else {
                iFR = this._posResizeStartSize.w + iFX * 2;
            }

            var newNO = {},
                newSE = {};

            newNO.x = this.getCenterPoint().x - iFR * 0.5;
            newSE.x = this.getCenterPoint().x + iFR * 0.5;

            newNO.y = this.getCenterPoint().y - iFR * 0.5;
            newSE.y = this.getCenterPoint().y + iFR * 0.5;

            this.circleOnMove(newNO, newSE);
            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;
            var center = this.getCenterPoint();
            this._posDragStartX = mouseDownX - center.x;
            this._posDragStartY = mouseDownY - center.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;
}]);

angular.module('uiCropper').factory('cropAreaRectangle', ['cropArea', function (CropArea) {
    var CropAreaRectangle = function () {
        CropArea.apply(this, arguments);

        this._resizeCtrlBaseRadius = 15;
        this._resizeCtrlNormalRatio = 0.6;
        this._resizeCtrlHoverRatio = 0.70;
        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 = {
            w: 0,
            h: 0
        };

        this._resizeCtrlIsHover = -1;
        this._areaIsHover = false;
        this._resizeCtrlIsDragging = -1;
        this._areaIsDragging = false;
    };

    CropAreaRectangle.prototype = new CropArea();

    // return a type string
    CropAreaRectangle.prototype.getType = function () {
        return 'rectangle';
    };

    CropAreaRectangle.prototype._calcRectangleCorners = function () {
        var size = this.getSize();
        var se = this.getSouthEastBound();
        return [
            [size.x, size.y], //northwest
            [se.x, size.y], //northeast
            [size.x, se.y], //southwest
            [se.x, se.y] //southeast
        ];
    };

    CropAreaRectangle.prototype._calcRectangleDimensions = function () {
        var size = this.getSize();
        var se = this.getSouthEastBound();
        return {
            left: size.x,
            top: size.y,
            right: se.x,
            bottom: se.y
        };
    };

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

    CropAreaRectangle.prototype._isCoordWithinResizeCtrl = function (coord) {
        var resizeIconsCenterCoords = this._calcRectangleCorners();
        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;
    };

    CropAreaRectangle.prototype._drawArea = function (ctx, center, size) {
        ctx.rect(size.x, size.y, size.w, size.h);
    };

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

        var center = this.getCenterPoint();
        // draw move icon
        this._cropCanvas.drawIconMove([center.x, center.y], this._areaIsHover ? this._iconMoveHoverRatio : this._iconMoveNormalRatio);

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

    CropAreaRectangle.prototype.setSizeByScale = function(scale, direction) {
        var center = this.getCenterPoint();
        var size = this.getSize();
        var northWestPoint;
        var southEastPoint;

        if (this._aspect) {
            var newWidth = size.w * scale;
            var newHeight = size.h * scale;
            northWestPoint = {
                x: center.x - (newWidth / 2),
                y: center.y - (newHeight / 2)
            };
            southEastPoint = {
                x: center.x + (newWidth / 2),
                y: center.y + (newHeight / 2)
            };
        } else {
            northWestPoint = {
                x: size.x,
                y: size.y
            };
            southEastPoint = {
                x: size.x + size.w,
                y: size.y + size.h
            };
            switch (direction) {
                case 'up':
                case 'down':
                    southEastPoint.y = size.y + (size.h * scale);
                    break;
                case 'left':
                case 'right':
                    southEastPoint.x = size.x + (size.w * scale);
                    break;
            }
        }

        this.setSizeByCorners(northWestPoint, southEastPoint);
        this._events.trigger('area-resize');
    };

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

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

        if (this._areaIsDragging) {
            this.setCenterPointOnMove({
                x: mouseCurX - this._posDragStartX,
                y: mouseCurY - this._posDragStartY
            });
            this._areaIsHover = true;
            cursor = 'move';
            res = true;
            this._events.trigger('area-move');
        } else if (this._resizeCtrlIsDragging > -1) {
            var s = this.getSize();
            var se = this.getSouthEastBound();
            var posX = mouseCurX;
            switch (this._resizeCtrlIsDragging) {
                case 0: // Top Left
                    if (this._aspect) {
                        posX = se.x - ((se.y - mouseCurY) * this._aspect);
                    }
                    this.setSizeByCorners({
                        x: posX,
                        y: mouseCurY
                    }, {
                        x: se.x,
                        y: se.y
                    });
                    cursor = 'nwse-resize';
                    break;
                case 1: // Top Right
                    if (this._aspect) {
                        posX = s.x + ((se.y - mouseCurY) * this._aspect);
                    }
                    this.setSizeByCorners({
                        x: s.x,
                        y: mouseCurY
                    }, {
                        x: posX,
                        y: se.y
                    });
                    cursor = 'nesw-resize';
                    break;
                case 2: // Bottom Left
                    if (this._aspect) {
                        posX = se.x - ((mouseCurY - s.y) * this._aspect);
                    }
                    this.setSizeByCorners({
                        x: posX,
                        y: s.y
                    }, {
                        x: se.x,
                        y: mouseCurY
                    });
                    cursor = 'nesw-resize';
                    break;
                case 3: // Bottom Right
                    if (this._aspect) {
                        posX = s.x + ((mouseCurY - s.y) * this._aspect);
                    }
                    this.setSizeByCorners({
                        x: s.x,
                        y: s.y
                    }, {
                        x: posX,
                        y: mouseCurY
                    });
                    cursor = 'nwse-resize';
                    break;
            }

            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;
            }
        }

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

        return res;
    };

    CropAreaRectangle.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;
            var center = this.getCenterPoint();
            this._posDragStartX = mouseDownX - center.x;
            this._posDragStartY = mouseDownY - center.y;
            this._events.trigger('area-move-start');
        }
    };

    CropAreaRectangle.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 CropAreaRectangle;
}]);

angular.module('uiCropper').factory('cropAreaSquare', ['cropArea', function(CropArea) {
    var CropAreaSquare = function() {
        CropArea.apply(this, arguments);

        this._resizeCtrlBaseRadius = 15;
        this._resizeCtrlNormalRatio = 0.6;
        this._resizeCtrlHoverRatio = 0.70;
        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.getType = function() {
        return 'square';
    };

    CropAreaSquare.prototype._calcSquareCorners = function() {
        var size = this.getSize(),
            se = this.getSouthEastBound();
        return [
            [size.x, size.y], //northwest
            [se.x, size.y], //northeast
            [size.x, se.y], //southwest
            [se.x, se.y] //southeast
        ];
    };

    CropAreaSquare.prototype._calcSquareDimensions = function() {
        var size = this.getSize(),
            se = this.getSouthEastBound();
        return {
            left: size.x,
            top: size.y,
            right: se.x,
            bottom: se.y
        };
    };

    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) {
        ctx.rect(size.x, size.y, size.w, size.h);
    };

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

        // draw move icon
        var center = this.getCenterPoint();
        this._cropCanvas.drawIconMove([center.x, center.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.drawIconResizeBoxBase(resizeIconCenterCoords, this._resizeCtrlBaseRadius, this._resizeCtrlIsHover === i ? this._resizeCtrlHoverRatio : this._resizeCtrlNormalRatio);
        }
    };

    CropAreaSquare.prototype._clampPoint = function(x, y) {
        var size = this._ctx.canvas.width;

        if(x < 0) {
            y -= Math.abs(x);
            x = 0;
        }

        if(y < 0) {
            x -= Math.abs(y);
            y = 0;
        }

        if(x > size) {
            y -= (size - x);
            x = size;
        }

        if(y > size) {
            x -= (size - y);
            y = size;
        }

        return {
            x: x,
            y: y
        };
    };

    CropAreaSquare.prototype.setSizeByScale = function(scale) {
        var center = this.getCenterPoint();
        var size = this.getSize();
        var newSize = size.w * scale;
        if (newSize < this._minSize.w) {
            newSize = this._minSize.w;
        }
        var northWestPoint = {
            x: center.x - (newSize / 2),
            y: center.y - (newSize / 2)
        };
        var southEastPoint = {
            x: center.x + (newSize / 2),
            y: center.y + (newSize / 2)
        };
        this.setSizeByCorners(northWestPoint, southEastPoint);
        this._events.trigger('area-resize');
    };

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

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

        if (this._areaIsDragging) {
            this.setCenterPointOnMove({
                x: mouseCurX - this._posDragStartX,
                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,
                iFY = (mouseCurY - this._posResizeStartY) * yMulti,
                iFR;
            if (iFX > iFY) {
                iFR = this._posResizeStartSize.w + iFY;
            } else {
                iFR = this._posResizeStartSize.w + iFX;
            }
            var newSize = Math.max(this._minSize.w, iFR),
                newNO = {},
                newSE = {},
                newSO = {},
                newNE = {},
                s = this.getSize(),
                se = this.getSouthEastBound();

            switch (this._resizeCtrlIsDragging) {
                case 0: // Top Left
                    newNO.x = se.x - newSize;
                    newNO.y = se.y - newSize;

                    newNO = this._clampPoint(newNO.x, newNO.y);

                    this.setSizeByCorners(newNO, {
                        x: se.x,
                        y: se.y
                    });

                    cursor = 'nwse-resize';
                    break;
                case 1: // Top Right

                    newNE.x = s.x + newSize;
                    newNE.y = se.y - newSize;

                    newNE = this._clampPoint(newNE.x, newNE.y);

                    this.setSizeByCorners({
                        x: s.x,
                        y: newNE.y
                    }, {
                        x: newNE.x,
                        y: se.y
                    });

                    cursor = 'nesw-resize';
                    break;
                case 2: // Bottom Left
                    newSO.x = se.x - newSize;
                    newSO.y = s.y + newSize;

                    newSO = this._clampPoint(newSO.x, newSO.y);

                    this.setSizeByCorners({
                        x: newSO.x,
                        y: s.y
                    }, {
                        x: se.x,
                        y: newSO.y
                    });

                    cursor = 'nesw-resize';
                    break;
                case 3: // Bottom Right

                    newSE.x = s.x + newSize;
                    newSE.y = s.y + newSize;

                    newSE = this._clampPoint(newSE.x, newSE.y);

                    this.setSizeByCorners({
                        x: s.x,
                        y: s.y
                    }, newSE);

                    cursor = 'nwse-resize';
                    break;
            }
            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;
            }
        }

        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;
            var center = this.getCenterPoint();
            this._posDragStartX = mouseDownX - center.x;
            this._posDragStartY = mouseDownY - center.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;
}]);

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

        this._minSize = {
            x: 0,
            y: 0,
            w: 80,
            h: 80
        };

        this._initSize = undefined;
        this._initCoords = undefined;
        this._allowCropResizeOnCorners = false;

        this._forceAspectRatio = false;
        this._aspect = null;
        this._disableCrop = false;

        this._cropCanvas = new CropCanvas(ctx, this._disableCrop);

        this._image = new Image();
        this._size = {
            x: 0,
            y: 0,
            w: 150,
            h: 150
        };
    };

    /* GETTERS/SETTERS */

    CropArea.prototype.setAllowCropResizeOnCorners = function (bool) {
        this._allowCropResizeOnCorners = bool;
    };
    CropArea.prototype.getImage = function () {
        return this._image;
    };
    CropArea.prototype.setImage = function (image) {
        this._image = image;
    };
    CropArea.prototype.setForceAspectRatio = function (force) {
        this._forceAspectRatio = force;
    };
    CropArea.prototype.setAspect = function (aspect) {
        this._aspect = aspect;
    };
    CropArea.prototype.getAspect = function () {
        return this._aspect;
    };
    CropArea.prototype.getCanvasSize = function () {
        return {
            w: this._ctx.canvas.width,
            h: this._ctx.canvas.height
        };
    };
    CropArea.prototype.getSize = function () {
        return this._size;
    };
    CropArea.prototype.setSize = function (size) {
        size = this._processSize(size);
        this._size = this._preventBoundaryCollision(size);
    };
    CropArea.prototype.setSizeOnMove = function (size) {
        size = this._processSize(size);
        if (this._allowCropResizeOnCorners) {
            this._size = this._preventBoundaryCollision(size);
        } else {
            this._size = this._allowMouseOutsideCanvas(size);
        }
    };
    CropArea.prototype.circleOnMove = function (northWestCorner, southEastCorner) {
        var size = {
            x: northWestCorner.x,
            y: northWestCorner.y,
            w: southEastCorner.x - northWestCorner.x,
            h: southEastCorner.y - northWestCorner.y
        };
        var canvasH = this._ctx.canvas.height,
            canvasW = this._ctx.canvas.width;
        if (size.w > canvasW || size.h > canvasH) {
            if (canvasW < canvasH) {
                size.w = canvasW;
                size.h = canvasW;
            } else {
                size.w = canvasH;
                size.h = canvasH;
            }
        }
        if (size.x + size.w > canvasW) {
            size.x = canvasW - size.w;
        }
        if (size.y + size.h > canvasH) {
            size.y = canvasH - size.h;
        }
        if (size.x < 0) {
            size.x = 0;
        }
        if (size.y < 0) {
            size.y = 0;
        }
        if (this._minSize.w > size.w) {
            size.w = this._minSize.w;
            size.x = this._size.x;
        }
        if (this._minSize.h > size.h) {
            size.h = this._minSize.h;
            size.y = this._size.y;
        }
        this._size = size;
    };

    CropArea.prototype.setSizeByCorners = function (northWestCorner, southEastCorner) {

        var size = {
            x: northWestCorner.x,
            y: northWestCorner.y,
            w: southEastCorner.x - northWestCorner.x,
            h: southEastCorner.y - northWestCorner.y
        };
        this.setSize(size);
    };

    CropArea.prototype.getSouthEastBound = function () {
        return this._southEastBound(this.getSize());
    };

    CropArea.prototype.setMinSize = function (size) {
        this._minSize = this._processSize(size);
        // ARTE 2025-10-10
        // Patch: The size should not be set to the min-size, but it should be
        // set to its current value, so the boundaries are applied again.
        //this.setSize(this._minSize);
        this.setSize(this.getSize());
    };

    CropArea.prototype.getMinSize = function () {
        return this._minSize;
    };

    CropArea.prototype.getCenterPoint = function () {
        var s = this.getSize();
        return {
            x: s.x + (s.w / 2),
            y: s.y + (s.h / 2)
        };
    };

    CropArea.prototype.setCenterPoint = function (point) {
        var s = this.getSize();
        this.setSize({
            x: point.x - s.w / 2,
            y: point.y - s.h / 2,
            w: s.w,
            h: s.h
        });
    };

    CropArea.prototype.setCenterPointOnMove = function (point) {
        var s = this.getSize();
        this.setSizeOnMove({
            x: point.x - s.w / 2,
            y: point.y - s.h / 2,
            w: s.w,
            h: s.h
        });
    };

    CropArea.prototype.setInitSize = function (size) {
        this._initSize = this._processSize(size);
        this.setSize(this._initSize);
    };

    CropArea.prototype.getInitSize = function () {
        return this._initSize;
    };

    CropArea.prototype.setInitCoords = function (coords) {
        //add h/w-data to coords-object
        coords.h = this.getSize().h;
        coords.w = this.getSize().w;
        this._initCoords = this._processSize(coords);
        this.setSize(this._initCoords);
    };

    CropArea.prototype.setDisableCrop = function(value){
        this._disableCrop = value;
        this._cropCanvas = new CropCanvas(this._ctx, this._disableCrop);
    };

    CropArea.prototype.getInitCoords = function () {
        return this._initCoords;
    };

    // return a type string
    CropArea.prototype.getType = function () {
        //default to circle
        return 'circle';
    };

    /* FUNCTIONS */
    CropArea.prototype._allowMouseOutsideCanvas = function (size) {
        var canvasH = this._ctx.canvas.height,
            canvasW = this._ctx.canvas.width;
        var newSize = {
            w: size.w,
            h: size.h,
        };
        if (size.x < 0) {
            newSize.x = 0;
        }
        else if (size.x + size.w > canvasW) {
            newSize.x = canvasW - size.w;
        }
        else {
            newSize.x = size.x;
        }
        if (size.y < 0) {
            newSize.y = 0;
        }
        else if (size.y + size.h > canvasH) {
            newSize.y = canvasH - size.h;
        }
        else {
            newSize.y = size.y;
        }
        return newSize;
    };

    CropArea.prototype._preventBoundaryCollision = function (size) {
        var canvasH = this._ctx.canvas.height,
            canvasW = this._ctx.canvas.width;

        var nw = {
            x: size.x,
            y: size.y
        };
        var se = this._southEastBound(size);

        // check northwest corner
        if (nw.x < 0) {
            nw.x = 0;
        }
        if (nw.y < 0) {
            nw.y = 0;
        }

        // check southeast corner
        if (se.x > canvasW) {
            se.x = canvasW;
        }
        if (se.y > canvasH) {
            se.y = canvasH;
        }

        var newSizeWidth = (this._forceAspectRatio) ? size.w : se.x - nw.x,
            newSizeHeight = (this._forceAspectRatio) ? size.h : se.y - nw.y;

        if (newSizeHeight > canvasH) {
            newSizeHeight = canvasH;
        }

        // save rectangle scale
        if (this._aspect) {
            newSizeWidth = newSizeHeight * this._aspect;
            if (nw.x + newSizeWidth > canvasW) {
                newSizeWidth = canvasW - nw.x;
                newSizeHeight = newSizeWidth / this._aspect;
                if (this._minSize.w > newSizeWidth) {
                    newSizeWidth = this._minSize.w;
                }
                if (this._minSize.h > newSizeHeight) {
                    newSizeHeight = this._minSize.h;
                }
                nw.x = canvasW - newSizeWidth;
            }
            if (nw.y + newSizeHeight > canvasH) {
                nw.y = canvasH - newSizeHeight;
            }
        }

        // save square scale
        if (this._forceAspectRatio) {
            newSizeWidth = newSizeHeight;
            if (nw.x + newSizeWidth > canvasW) {
                newSizeWidth = canvasW - nw.x;
                if (newSizeWidth < this._minSize.w) {
                    newSizeWidth = this._minSize.w;
                }
                newSizeHeight = newSizeWidth;
            }
        }

        var newSize = {
            x: nw.x,
            y: nw.y,
            w: newSizeWidth,
            h: newSizeHeight
        };

        //check size (if < min, adjust nw corner)
        if ((newSize.w < this._minSize.w) && !this._forceAspectRatio) {
            newSize.w = this._minSize.w;
            se = this._southEastBound(newSize);
            //adjust se corner, if it's out of bounds
            if (se.x > canvasW) {
                se.x = canvasW;
                //adjust nw corner according to min width
                nw.x = Math.max(se.x - canvasW, se.x - this._minSize.w);
                newSize = {
                    x: nw.x,
                    y: nw.y,
                    w: se.x - nw.x,
                    h: se.y - nw.y
                };
            }
        }

        if ((newSize.h < this._minSize.h) && !this._forceAspectRatio) {
            newSize.h = this._minSize.h;
            se = this._southEastBound(newSize);

            if (se.y > canvasH) {
                se.y = canvasH;
                //adjust nw corner according to min height
                nw.y = Math.max(se.y - canvasH, se.y - this._minSize.h);
                newSize = {
                    x: nw.x,
                    y: nw.y,
                    w: se.x - nw.x,
                    h: se.y - nw.y
                };
            }
        }

        if (this._forceAspectRatio) {
            //check if outside SE bound
            se = this._southEastBound(newSize);
            if (se.y > canvasH) {
                newSize.y = canvasH - newSize.h;
            }
            if (se.x > canvasW) {
                newSize.x = canvasW - newSize.w;
            }
        }

        return newSize;
    };

    CropArea.prototype._dontDragOutside = function () {
        var h = this._ctx.canvas.height,
            w = this._ctx.canvas.width;

        if (this._width > w) {
            this._width = w;
        }
        if (this._height > h) {
            this._height = h;
        }
        if (this._x < this._width / 2) {
            this._x = this._width / 2;
        }
        if (this._x > w - this._width / 2) {
            this._x = w - this._width / 2;
        }
        if (this._y < this._height / 2) {
            this._y = this._height / 2;
        }
        if (this._y > h - this._height / 2) {
            this._y = h - this._height / 2;
        }
    };

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

    CropArea.prototype._processSize = function (size) {
        // make this polymorphic to accept a single floating point number
        // for square-like sizes (including circle)
        if (typeof size === 'number') {
            size = {
                w: size,
                h: size
            };
        }
        var width = size.w;
        var height = size.h;
        if (this._aspect) {
            // In order to apply the initMax from the crop-host we need to update the width only when the aspect ratio is above 1.
            if (this.aspect >= 1) {
                width = size.h * this._aspect;
            } else {
                height = size.w / this._aspect;
            }
        }
        return {
            x: (typeof size.x === 'undefined') ? this.getSize().x : size.x,
            y: (typeof size.y === 'undefined') ? this.getSize().y : size.y,
            w: width || this._minSize.w,
            h: height || this._minSize.h
        };
    };

    CropArea.prototype._southEastBound = function (size) {
        return {
            x: size.x + size.w,
            y: size.y + size.h
        };
    };

    CropArea.prototype.draw = function () {
        // draw crop area
        this._cropCanvas.drawCropArea(this._image, this.getCenterPoint(), this._size, this._drawArea);
    };

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

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

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

    return CropArea;
}]);

angular.module('uiCropper').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: '#bababa',
        resizeBoxFill: '#444',
        resizeBoxArrowFill: '#fff',
        resizeCircleStroke: '#bababa',
        resizeCircleFill: '#444',
        moveIconFill: '#fff'
    };

    var cropper = {
        strokeWidth: 1
    };

    return function(ctx, disable) {

        /* 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) {
            if(disable) {
                return;
            }

            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) {
            if(disable) {
                return;
            }

            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) {
            if(disable) {
                return;
            }

            var scaledCircleRadius = circleRadius * scale;
            ctx.save();
            ctx.strokeStyle = colors.resizeCircleStroke;
            ctx.lineWidth = cropper.strokeWidth;
            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) {
            if(disable) {
                return;
            }

            var scaledBoxSize = boxSize * scale;
            ctx.save();
            ctx.strokeStyle = colors.resizeBoxStroke;
            ctx.lineWidth = cropper.strokeWidth;
            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) {
            if(disable) {
                return;
            }

            this.drawIconResizeBoxBase(centerCoords, boxSize, scale);
            drawFilledPolygon(shapeArrowNE, colors.resizeBoxArrowFill, centerCoords, scale);
            drawFilledPolygon(shapeArrowSW, colors.resizeBoxArrowFill, centerCoords, scale);
        };
        this.drawIconResizeBoxNWSE = function(centerCoords, boxSize, scale) {
            if(disable) {
                return;
            }

            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, fnDrawClipPath) {
            if(disable) {
                return;
            }

            var xRatio = Math.abs(image.width / ctx.canvas.width),
                yRatio = Math.abs(image.height / ctx.canvas.height),
                xLeft = Math.abs(centerCoords.x - size.w / 2),
                yTop = Math.abs(centerCoords.y - size.h / 2);

            ctx.save();
            ctx.strokeStyle = colors.areaOutline;
            ctx.lineWidth = cropper.strokeWidth;
            ctx.setLineDash([5, 5]);
            ctx.beginPath();
            fnDrawClipPath(ctx, centerCoords, size);
            ctx.stroke();
            ctx.clip();

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

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

            ctx.restore();
        };
    };
}]);

/**
 * EXIF service is based on the exif-js library (https://github.com/jseidelin/exif-js)
 */
/*eslint-disable */
//Disable eslint as this is a 3rd party lib
angular.module('uiCropper').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);
            }
        }

        var fileReader = new FileReader();

        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
                fileReader.onload = function (e) {
                    handleBinaryFile(e.target.result);
                };
                objectURLToBlob(img.src, function (blob) {
                    fileReader.readAsArrayBuffer(blob);
                });
            }
            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';
            try {
                http.send(null);
            } catch(e) {
            }
        } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) {
            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);
            }

            // 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);
                }
                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);
                }
                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);
                }
                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 = {}; //@TODO need to inspect if this fix is really working
                    val.numerator = numerator;
                    val.denominator = denominator;
                    return val;
                }
                vals = [];
                for (n = 0; n < numValues; n++) {
                    numerator = file.getUint32(valueOffset + 8 * n, !bigEnd);
                    denominator = file.getUint32(valueOffset + 4 + 8 * n, !bigEnd);
                    vals[n] = {}; //@TODO need to inspect if this fix is really working
                    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);
                }
                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);
                }
                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);
    };
}]);

angular.module('uiCropper').factory('cropHost', ['$document', '$q', 'cropAreaCircle', 'cropAreaSquare', 'cropAreaRectangle', 'cropEXIF', function ($document, $q, CropAreaCircle, CropAreaSquare, CropAreaRectangle, cropEXIF) {
    /* STATIC FUNCTIONS */
    var colorPaletteLength = 8;

    // 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,
            initMax = null,
            isAspectRatio = null,
            self = this,

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

            scalemode = null,

            // Result Image size
            resImgSizeArray = [],
            resImgSize = {
                w: 200,
                h: 200
            },
            areaMinRelativeSize = null,

            // Result Image type
            resImgFormat = 'image/png',

            // Result Image quality
            resImgQuality = null,

            keys = {
                up: 38,
                down: 40,
                left: 37,
                right: 39
            },

            forceAspectRatio = false;

        /* 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
                if(!theArea._disableCrop){
                    ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
                    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

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

        this.setInitMax = function (bool) {
            initMax = bool;
        };

        this.setAllowCropResizeOnCorners = function (bool) {
            theArea.setAllowCropResizeOnCorners(bool);
        };

        this.setDisableCrop = function(value){
            theArea.setDisableCrop(value);
            drawScene();
        };

        var focusOnCanvas = function () {
            elCanvas[0].focus();
        };

        function fitCanvasDims(width, height) {
            var imageRatio = width / height;
            var canvasDims = [width, height];
            var margins = {left: 0, top: 0};
            if (canvasDims[0] > maxCanvasDims[0]) {
                canvasDims[0] = maxCanvasDims[0];
                canvasDims[1] = canvasDims[0] / imageRatio;
                margins.top = (maxCanvasDims[1] - canvasDims[1]) / 2;
            }
            else if (canvasDims[1] > maxCanvasDims[1]) {
                canvasDims[1] = maxCanvasDims[1];
                canvasDims[0] = canvasDims[1] * imageRatio;
                margins.left = (maxCanvasDims[0] - canvasDims[0]) / 2;
            }
            else {
                margins.top = (maxCanvasDims[1] - canvasDims[1]) / 2;
                margins.left = (maxCanvasDims[0] - canvasDims[0]) / 2;
            }
            return {
                dims: canvasDims,
                margins: margins,
            };
        }

        // Resets CropHost
        var resetCropHost = function () {
            var canvasDims = [0, 0];
            if (image !== null) {
                focusOnCanvas();
                theArea.setImage(image);
                if (scalemode === 'fit') {
                    var results = fitCanvasDims(image.width, image.height);
                    canvasDims = results.dims;
                    var margins = results.margins;
                    elCanvas.css({
                        'margin-left': margins.left + 'px',
                        'margin-top': margins.top + 'px'
                    });
                }
                else {
                    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 (scalemode === 'fixed-height' && 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]);
                if (scalemode === 'fixed-height') {
                    elCanvas.css({
                        'margin-left': -canvasDims[0] / 2 + 'px',
                        'margin-top': -canvasDims[1] / 2 + 'px'
                    });
                }

                var cw = ctx.canvas.width;
                var ch = ctx.canvas.height;

                var areaType = self.getAreaType();
                // enforce 1:1 aspect ratio for square-like selections
                if ((areaType === 'circle') || (areaType === 'square')) {
                    if (ch < cw) {
                        cw = ch;
                    }
                    ch = cw;
                } else if (areaType === 'rectangle' && isAspectRatio) {
                    var aspectRatio = theArea.getAspect(); // use `aspectRatio` instead of `resImgSize` dimensions bc `resImgSize` can be 'selection' string
                    if (cw / ch > aspectRatio) {
                        cw = aspectRatio * ch;
                    } else {
                        ch = aspectRatio * cw;
                    }
                }

                if (initMax) {
                    theArea.setSize({
                        w: cw,
                        h: ch
                    });
                } else if (undefined !== theArea.getInitSize()) {
                    theArea.setSize({
                        w: Math.min(theArea.getInitSize().w, cw / 2),
                        h: Math.min(theArea.getInitSize().h, ch / 2)
                    });
                } else {
                    theArea.setSize({
                        w: Math.min(200, cw / 2),
                        h: Math.min(200, ch / 2)
                    });
                }

                if (theArea.getInitCoords()) {
                    if (self.areaInitIsRelativeToImage) {
                        var ratio = image.width / canvasDims[0];
                        theArea.setSize({
                            w: theArea.getInitSize().w / ratio,
                            h: theArea.getInitSize().h / ratio,
                            x: theArea.getInitCoords().x / ratio,
                            y: theArea.getInitCoords().y / ratio
                        });
                    } else {
                        theArea.setSize({
                            w: theArea.getSize().w,
                            h: theArea.getSize().h,
                            x: theArea.getInitCoords().x,
                            y: theArea.getInitCoords().y
                        });
                    }
                } else {
                    theArea.setCenterPoint({
                        x: ctx.canvas.width / 2,
                        y: ctx.canvas.height / 2
                    });
                }

            } else {
                elCanvas.prop('width', 0).prop('height', 0).css({
                    'margin-top': 0
                });
            }

            drawScene();
        };

        var getChangedTouches = function (event) {
            if (angular.isDefined(event.changedTouches)) {
                return event.changedTouches;
            } else {
                return event.originalEvent.changedTouches;
            }
        };

        var onMouseMove = function (e) {
            if(theArea._disableCrop) {
                return;
            }

            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();
            if(theArea._disableCrop) {
                return;
            }

            if (!opts.allowPropagation) {
                e.stopPropagation();
            }

            if (image !== null) {
                focusOnCanvas();
                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(theArea._disableCrop) {
                return;
            }

            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();
            }
        };

        var getDirectionByKey = function (key) {
            return Object.keys(keys).reduce(function(result, direction) {
                return key === keys[direction] ? direction : result;
            }, null);
        };

        var resizeCropAreaByDirection = function (direction) {
            if(theArea._disableCrop) {
                return;
            }

            var scale;
            switch (direction) {
                case 'up':
                case 'left':
                    scale = 0.95;
                    break;
                case 'down':
                case 'right':
                    scale = 1.05;
                    break;
                default:
                    return;
            }
            theArea.setSizeByScale(scale, direction);
            drawScene();
        };

        var moveCropArea = function (direction) {
            if(theArea._disableCrop) {
                return;
            }

            var center = theArea.getCenterPoint();
            var step = 5;
            var point = {
                x: center.x,
                y: center.y
            };

            switch (direction) {
                case 'up':
                    point.y -= step;
                    break;
                case 'down':
                    point.y += step;
                    break;
                case 'left':
                    point.x -= step;
                    break;
                case 'right':
                    point.x += step;
                    break;
                default:
                    return;
            }

            theArea.setCenterPointOnMove(point);
            drawScene();
        };

        var onKeyDown = function (e) {
            if(theArea._disableCrop) {
                return;
            }
            
            if (image !== null && opts.disableKeyboardAccess !== true) {
                var key = e.which;
                switch (key) {
                    case keys.up:
                    case keys.down:
                    case keys.left:
                    case keys.right:
                        e.preventDefault();
                        e.stopPropagation();
                        var direction = getDirectionByKey(key);
                        if (e.shiftKey) {
                            resizeCropAreaByDirection(direction);
                        } else {
                            moveCropArea(direction);
                        }
                        break;
                    default:
                        return;
                }
            }
        };

        var renderTempCanvas = function (ris, center) {
            var temp_ctx, temp_canvas;
            temp_canvas = angular.element('<canvas></canvas>')[0];
            temp_ctx = temp_canvas.getContext('2d');
            temp_canvas.width = ris.w;
            temp_canvas.height = ris.h;
            if (image !== null) {
                var x = (center.x - theArea.getSize().w / 2) * (image.width / ctx.canvas.width),
                    y = (center.y - theArea.getSize().h / 2) * (image.height / ctx.canvas.height),
                    areaWidth = theArea.getSize().w * (image.width / ctx.canvas.width),
                    areaHeight = theArea.getSize().h * (image.height / ctx.canvas.height);

                if (forceAspectRatio) {
                    temp_ctx.drawImage(image, x, y,
                        areaWidth,
                        areaHeight,
                        0,
                        0,
                        ris.w,
                        ris.h);
                } else {
                    var aspectRatio = areaWidth / areaHeight;
                    var resultHeight, resultWidth;

                    if (aspectRatio > 1) {
                        resultWidth = ris.w;
                        resultHeight = resultWidth / aspectRatio;
                    } else {
                        resultHeight = ris.h;
                        resultWidth = resultHeight * aspectRatio;
                    }

                    temp_canvas.width = resultWidth;
                    temp_canvas.height = resultHeight;

                    temp_ctx.drawImage(image,
                        x,
                        y,
                        areaWidth,
                        areaHeight,
                        0,
                        0,
                        Math.round(resultWidth),
                        Math.round(resultHeight));
                }

            }
            return temp_canvas;

        };

        var renderImageToDataURL = function (getResultImageSize) {
            var temp_canvas,
                retObj = {
                    dataURI: null,
                    imageData: null
                };
            temp_canvas = renderTempCanvas(getResultImageSize, theArea.getCenterPoint());
            if (image !== null) {
                if (resImgQuality !== null) {
                    retObj.dataURI = temp_canvas.toDataURL(resImgFormat, resImgQuality);
                } else {
                    retObj.dataURI = temp_canvas.toDataURL(resImgFormat);
                }
            }
            return retObj;
        };

        this.getResultImage = function () {
            if (resImgSizeArray.length === 0) {
                return renderImageToDataURL(this.getResultImageSize());
            }

            var arrayResultImages = [];
            for (var i = 0; i < resImgSizeArray.length; i++) {
                arrayResultImages.push({
                    dataURI: renderImageToDataURL(resImgSizeArray[i]).dataURI,
                    w: resImgSizeArray[i].w,
                    h: resImgSizeArray[i].h
                });
            }

            return arrayResultImages;
        };

        this.getResultImageDataBlob = function () {
            var temp_canvas,
                _p = $q.defer();
            temp_canvas = renderTempCanvas(this.getResultImageSize(), theArea.getCenterPoint());
            if (resImgQuality !== null) {
                temp_canvas.toBlob(function (blob) {
                    _p.resolve(blob);
                }, resImgFormat, resImgQuality);
            } else {
                temp_canvas.toBlob(function (blob) {
                    _p.resolve(blob);
                }, resImgFormat);
            }

            return _p.promise;
        };

        this.getAreaCoords = function () {
            return theArea.getSize();
        };

        this.getArea = function () {
            return theArea;
        };

        this.setNewImageSource = function (imageSource) {
            image = null;
            resetCropHost();
            if (imageSource) {
                var newImage = new Image();
                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,
                                rw = 0,
                                rh = 0;
                            rw = cw;
                            rh = ch;
                            switch (orientation) {
                                case 3:
                                    cx = -newImage.width;
                                    cy = -newImage.height;
                                    deg = 180;
                                    break;
                                case 6:
                                    cw = newImage.height;
                                    ch = newImage.width;
                                    cy = -newImage.height;
                                    rw = ch;
                                    rh = cw;
                                    deg = 90;
                                    break;
                                case 8:
                                    cw = newImage.height;
                                    ch = newImage.width;
                                    cx = -newImage.width;
                                    rw = ch;
                                    rh = cw;
                                    deg = 270;
                                    break;
                            }

                            //// canvas.toDataURL will only work if the canvas isn't too large. Resize to 1000px.
                            var maxWorH = 1000;
                            if (cw > maxWorH || ch > maxWorH) {
                                var p = 0;
                                if (cw > maxWorH) {
                                    p = (maxWorH) / cw;
                                    cw = maxWorH;
                                    ch = p * ch;
                                } else if (ch > maxWorH) {
                                    p = (maxWorH) / ch;
                                    ch = maxWorH;
                                    cw = p * cw;
                                }

                                cy = p * cy;
                                cx = p * cx;
                                rw = p * rw;
                                rh = p * rh;
                            }

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

                            image = new Image();
                            image.onload = function () {
                                resetCropHost();
                                events.trigger('image-updated');
                            };

                            image.src = canvas.toDataURL(resImgFormat);
                        } else {
                            image = newImage;
                            events.trigger('image-updated');
                        }
                        resetCropHost();
                    });
                };
                newImage.onerror = function () {
                    events.trigger('load-error');
                };
                events.trigger('load-start');
                if (imageSource instanceof window.Blob) {
                    newImage.src = URL.createObjectURL(imageSource);
                } else {
                    if (imageSource.substring(0, 4).toLowerCase() === 'http' || imageSource.substring(0, 2) === '//') {
                        newImage.crossOrigin = 'anonymous';
                    }
                    newImage.src = imageSource;
                }
            }
        };

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

            if (image !== null) {
                var curWidth = ctx.canvas.width,
                    curHeight = ctx.canvas.height;
                var canvasDims = [0, 0];
                if (scalemode === 'fit') {
                    var results = fitCanvasDims(image.width, image.height);
                    canvasDims = results.dims;
                    var margins = results.margins;
                    elCanvas.css({
                        'margin-left': margins.left + 'px',
                        'margin-top': margins.top + 'px'
                    });
                }
                else {
                    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 (scalemode === 'fixed-height' && 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]);

                if (scalemode === 'fixed-height') {
                    elCanvas.css({
                        'margin-left': -canvasDims[0] / 2 + 'px',
                        'margin-top': -canvasDims[1] / 2 + 'px'
                    });
                }

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

                //TODO: use top left corner point
                var center = theArea.getCenterPoint();
                theArea.setSize({
                    w: theArea.getSize().w * ratioMin,
                    h: theArea.getSize().h * ratioMin
                });
                theArea.setCenterPoint({
                    x: center.x * ratioNewCurWidth,
                    y: center.y * ratioNewCurHeight
                });

            } else {
                elCanvas.prop('width', 0).prop('height', 0).css({
                    'margin-top': 0
                });
            }

            drawScene();

        };

        this.setAreaMinSize = function (size) {
            if (angular.isUndefined(size)) {
                return;
            } else if (typeof size === 'number' || typeof size === 'string') {
                size = {
                    w: parseInt(parseInt(size), 10),
                    h: parseInt(parseInt(size), 10)
                };
            } else {
                size = {
                    w: parseInt(size.w, 10),
                    h: parseInt(size.h, 10)
                };
            }
            if (!isNaN(size.w) && !isNaN(size.h)) {
                theArea.setMinSize(size);
                drawScene();
            }
        };

        this.setAreaMinRelativeSize = function (size) {
            if (image === null || angular.isUndefined(size)) {
                return;
            }

            var canvasSize = theArea.getCanvasSize();

            if (typeof size === 'number' || typeof size === 'string') {
                areaMinRelativeSize = {
                    w: size,
                    h: size
                };
                size = {
                    w: canvasSize.w / (image.width / parseInt(parseInt(size), 10)),
                    h: canvasSize.h / (image.height / parseInt(parseInt(size), 10))
                };
            } else {
                areaMinRelativeSize = size;
                size = {
                    w: canvasSize.w / (image.width / parseInt(parseInt(size.w), 10)),
                    h: canvasSize.h / (image.height / parseInt(parseInt(size.h), 10))
                };
            }

            if (!isNaN(size.w) && !isNaN(size.h)) {
                theArea.setMinSize(size);
                drawScene();
            }
        };

        this.setAreaInitSize = function (size) {
            if (angular.isUndefined(size)) {
                return;
            } else if (typeof size === 'number' || typeof size === 'string') {
                size = {
                    w: parseInt(parseInt(size), 10),
                    h: parseInt(parseInt(size), 10)
                };
            } else {
                size = {
                    w: parseInt(size.w, 10),
                    h: parseInt(size.h, 10)
                };
            }
            if (!isNaN(size.w) && !isNaN(size.h)) {
                theArea.setInitSize(size);
                drawScene();
            }
        };

        this.setAreaInitCoords = function (coords) {
            if (angular.isUndefined(coords)) {
                return;
            } else {
                coords = {
                    x: parseInt(coords.x, 10),
                    y: parseInt(coords.y, 10)
                };
            }
            if (!isNaN(coords.x) && !isNaN(coords.y)) {
                theArea.setInitCoords(coords);
                drawScene();
            }
        };

        this.setMaxCanvasDimensions = function (maxCanvasDimensions) {
            if (!angular.isUndefined(maxCanvasDimensions)) {
                var newMaxCanvasDims = [];
                if (typeof maxCanvasDimensions === 'number' || typeof maxCanvasDimensions === 'string') {
                    newMaxCanvasDims = [
                        parseInt(parseInt(maxCanvasDimensions), 10),
                        parseInt(parseInt(maxCanvasDimensions), 10)
                    ];
                } else {
                    newMaxCanvasDims = [
                        parseInt(maxCanvasDimensions.w, 10),
                        parseInt(maxCanvasDimensions.h, 10)
                    ];
                }
                if ((!isNaN(newMaxCanvasDims[0]) &&
                    newMaxCanvasDims[0] > 0 &&
                    newMaxCanvasDims[0] > minCanvasDims[0]) &&
                    (!isNaN(newMaxCanvasDims[1]) &&
                    newMaxCanvasDims[1] > 0 &&
                    newMaxCanvasDims[1] > minCanvasDims[1])) {
                    maxCanvasDims = newMaxCanvasDims;
                }
            }
        };

        this.setMinCanvasDimensions = function (minCanvasDimensions) {
            if (!angular.isUndefined(minCanvasDimensions)) {
                var newMinCanvasDims = [];
                if (typeof minCanvasDimensions === 'number' || typeof minCanvasDimensions === 'string') {
                    newMinCanvasDims = [
                        parseInt(parseInt(minCanvasDimensions), 10),
                        parseInt(parseInt(minCanvasDimensions), 10)
                    ];
                } else {
                    newMinCanvasDims = [
                        parseInt(minCanvasDimensions.w, 10),
                        parseInt(minCanvasDimensions.h, 10)
                    ];
                }
                if ((!isNaN(newMinCanvasDims[0]) &&
                    newMinCanvasDims[0] >= 0) &&
                    (!isNaN(newMinCanvasDims[1]) &&
                    newMinCanvasDims[1] >= 0)) {
                    minCanvasDims = newMinCanvasDims;
                }
            }
        };

        this.setScalemode = function (value) {
            scalemode = value;
        };

        this.getScalemode = function () {
            return scalemode;
        };

        this.getResultImageSize = function () {
            if (resImgSize === 'selection') {
                return theArea.getSize();
            }

            if (resImgSize === 'max') {
                // We maximize the rendered size
                var zoom = 1;
                if (image && ctx && ctx.canvas) {
                    zoom = image.width / ctx.canvas.width;
                }
                var size = {
                    w: zoom * theArea.getSize().w,
                    h: zoom * theArea.getSize().h
                };

                if (areaMinRelativeSize) {
                    if (size.w < areaMinRelativeSize.w) {
                        size.w = areaMinRelativeSize.w;
                    }
                    if (size.h < areaMinRelativeSize.h) {
                        size.h = areaMinRelativeSize.h;
                    }
                }

                return size;
            }

            return resImgSize;
        };

        this.setResultImageSize = function (size) {
            if (angular.isArray(size)) {
                resImgSizeArray = size.slice();
                size = {
                    w: parseInt(size[0].w, 10),
                    h: parseInt(size[0].h, 10)
                };
                return;
            }
            if (angular.isUndefined(size)) {
                return;
            }
            //allow setting of size to "selection" for mirroring selection's dimensions
            if (angular.isString(size)) {
                resImgSize = size;
                return;
            }
            //allow scalar values for square-like selection shapes
            if (angular.isNumber(size)) {
                size = parseInt(size, 10);
                size = {
                    w: size,
                    h: size
                };
            }
            size = {
                w: parseInt(size.w, 10),
                h: parseInt(size.h, 10)
            };
            if (!isNaN(size.w) && !isNaN(size.h)) {
                resImgSize = size;
                drawScene();
            }
        };

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

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

        // returns a string of the selection area's type
        this.getAreaType = function () {
            return theArea.getType();
        };

        this.setAreaType = function (type) {
            var center = theArea.getCenterPoint();
            var curSize = theArea.getSize(),
                curMinSize = theArea.getMinSize(),
                curX = center.x,
                curY = center.y;

            var AreaClass = CropAreaCircle;
            if (type === 'square') {
                AreaClass = CropAreaSquare;
            } else if (type === 'rectangle') {
                AreaClass = CropAreaRectangle;
            }
            theArea = new AreaClass(ctx, events);
            theArea.setMinSize(curMinSize);
            theArea.setSize(curSize);
            if (type === 'square' || type === 'circle') {
                forceAspectRatio = true;
                theArea.setForceAspectRatio(true);
            } else {
                forceAspectRatio = false;
                theArea.setForceAspectRatio(false);
            }

            //TODO: use top left point
            theArea.setCenterPoint({
                x: curX,
                y: curY
            });

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

            drawScene();
        };

        this.getDominantColor = function (uri) {
            var imageDC = new Image(),
                colorThief = new ColorThief(),
                dominantColor = null,
                _p = $q.defer();
            imageDC.src = uri;
            imageDC.onload = function () {
                dominantColor = colorThief.getColor(imageDC);
                _p.resolve(dominantColor);
            };

            return _p.promise;
        };

        this.getPalette = function (uri) {
            var imageDC = new Image(),
                colorThief = new ColorThief(),
                palette = null,
                _p = $q.defer();
            imageDC.src = uri;
            imageDC.onload = function () {
                palette = colorThief.getPalette(imageDC, colorPaletteLength);
                _p.resolve(palette);
            };

            return _p.promise;
        };

        this.setPaletteColorLength = function (lg) {
            colorPaletteLength = lg;
        };

        this.setAspect = function (aspect) {
            isAspectRatio = true;
            theArea.setAspect(aspect);
            var minSize = theArea.getMinSize();
            minSize.w = minSize.h * aspect;
            theArea.setMinSize(minSize);
            var size = theArea.getSize();
            size.w = size.h * aspect;
            theArea.setSize(size);
        };

        /* 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);

        elCanvas.prop('tabindex', '0');

        elCanvas.on('keydown', onKeyDown);

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

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

            elCanvas.off('keydown', onKeyDown);

            elCanvas.remove();
        };
    };
}]);

angular.module('uiCropper').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;
        };
    };
}]);

angular.module('uiCropper').directive('uiCropper', ['$timeout', 'cropHost', 'cropPubSub', function ($timeout, CropHost, CropPubSub) {
    return {
        restrict: 'E',
        scope: {
            image: '=',
            resultImage: '=',
            resultArrayImage: '=?',
            resultBlob: '=?',
            urlBlob: '=?',
            chargement: '=?',
            cropject: '=?',
            maxCanvasDimensions: '=?',
            minCanvasDimensions: '=?',
            canvasScalemode: '@?', /* String. If set to 'full-width' the directive uses all width available */
            /* and the canvas expands in height as much as it need to maintain the aspect ratio */
            /* if set to 'fixed-height', the directive is restricted by a parent element in height */
            /* set to 'fit' to fit image into ui-cropper container */

            changeOnFly: '=?',
            disableKeyboardAccess: '=?',
            allowPropagation: '=?',
            liveView: '=?',
            initMaxArea: '=?',
            areaCoords: '=?',
            areaType: '@',
            areaMinSize: '=?',
            areaInitSize: '=?',
            areaInitCoords: '=?',
            areaInitIsRelativeToImage: '=?', /* Boolean: If true the areaInitCoords and areaInitSize is scaled according to canvas size. */
            /* No matter how big/small the canvas is, the resultImage remains the same */
            /* Example: areaInitCoords are {x: 100, y: 100}, areaInitSize {w: 100, h: 100}   */
            /* Image is 1000x1000
             /* if canvas is 500x500 Crop coordinates will be x: 50, y: 50, w: 50, h: 50 */
            /* if canvas is 100x100 crop coordinates will be x: 10, y: 10, w: 10, h: 10 */
            areaMinRelativeSize: '=?',
            resultImageSize: '=?',
            // ARTE 2025-10-12
            // Patch: the result-image-format should not be set to '@' but '='
            // to support assigning a variable.
            // resultImageFormat: '@',
            resultImageFormat: '=',
            resultImageQuality: '=?',

            aspectRatio: '=?',
            allowCropResizeOnCorners: '=?',

            dominantColor: '=?',
            paletteColor: '=?',
            paletteColorLength: '=?',
            disableCrop: '=?',

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

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

            if (scope.canvasScalemode) {
                cropHost.setScalemode(scope.canvasScalemode);
            } else {
                cropHost.setScalemode('fixed-height');
            }

            element.addClass(cropHost.getScalemode());

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

            var updateAreaCoords = function (scope) {
                scope.areaCoords = cropHost.getAreaCoords();
            };

            var updateResultImage = function (scope, force, callback) {
                if (scope.image !== '' && (!scope.liveView.block || force)) {
                    var resultImageObj = cropHost.getResultImage();
                    var resultImage;
                    if (angular.isArray(resultImageObj)) {
                        resultImage = resultImageObj[0].dataURI;
                        scope.resultArrayImage = resultImageObj;
                    } else {
                        resultImage = resultImageObj.dataURI;
                    }

                    var urlCreator = window.URL || window.webkitURL;
                    if (storedResultImage !== resultImage) {
                        storedResultImage = resultImage;
                        scope.resultImage = resultImage;
                        if (scope.liveView.callback) {
                            scope.liveView.callback(resultImage);
                        }
                        if (callback) {
                            callback(resultImage);
                        }
                        cropHost.getResultImageDataBlob().then(function (blob) {
                            scope.resultBlob = blob;
                            scope.urlBlob = blob ? urlCreator.createObjectURL(blob) : null;
                        });

                        if (scope.resultImage) {
                            cropHost.getDominantColor(scope.resultImage).then(function (dominantColor) {
                                scope.dominantColor = dominantColor;
                            });
                            cropHost.getPalette(scope.resultImage).then(function (palette) {
                                scope.paletteColor = palette;
                            });
                        }

                        updateAreaCoords(scope);
                        scope.onChange({
                            $dataURI: scope.resultImage
                        });
                    }
                }
            };

            if (scope.liveView && typeof scope.liveView.block === 'boolean') {
                scope.liveView.render = function (callback) {
                    updateResultImage(scope, true, callback);
                };
            } else {
                scope.liveView = {block: false};
            }

            var updateCropject = function (scope) {
                var areaCoords = cropHost.getAreaCoords();

                var dimRatio = {
                    x: cropHost.getArea().getImage().width / cropHost.getArea().getCanvasSize().w,
                    y: cropHost.getArea().getImage().height / cropHost.getArea().getCanvasSize().h
                };

                scope.cropject = {
                    canvasSize: cropHost.getArea().getCanvasSize(),
                    areaCoords: areaCoords,
                    cropWidth: areaCoords.w,
                    cropHeight: areaCoords.h,
                    cropTop: areaCoords.y,
                    cropLeft: areaCoords.x,
                    cropImageWidth: Math.round(areaCoords.w * dimRatio.x),
                    cropImageHeight: Math.round(areaCoords.h * dimRatio.y),
                    cropImageTop: Math.round(areaCoords.y * dimRatio.y),
                    cropImageLeft: Math.round(areaCoords.x * dimRatio.x)
                };
            };

            // 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);
                        });
                    });
                };
            };

            // Will get the users language settings, and return the appropriate loading text
            var printLoadMsg = function () {
                var language = window.navigator.userLanguage || window.navigator.language;

                switch (language) {
                    case 'nl':
                    case 'nl_NL':
                        return 'Aan het laden';

                    case 'fr':
                    case 'fr-FR':
                        return 'Chargement';

                    case 'es':
                    case 'es-ES':
                        return 'Cargando';

                    case 'ca':
                    case 'ca-ES':
                        return 'CĂ rrega';

                    case 'de':
                    case 'de-DE':
                        return 'Laden';

                    case 'pt':
                    case 'pt-BR':
                        return 'Carregando';

                    default:
                        return 'Loading';
                }
            };

            if (!scope.chargement) {
                scope.chargement = printLoadMsg();
            }
            var displayLoading = function () {
                element.append('<div class="loading"><span>' + scope.chargement + '...</span></div>');
            };

            // Setup CropHost Event Handlers
            events
                .on('load-start', fnSafeApply(function (scope) {
                    scope.onLoadBegin({});
                }))
                .on('load-done', fnSafeApply(function (scope) {
                    var children = element.children();
                    angular.forEach(children, function (child) {
                        if (angular.element(child).hasClass('loading')) {
                            angular.element(child).remove();
                        }
                    });
                    updateCropject(scope);
                    scope.onLoadDone({});
                }))
                .on('load-error', fnSafeApply(function (scope) {
                    scope.onLoadError({});
                }))
                .on('area-move area-resize', fnSafeApply(function (scope) {
                    if (scope.changeOnFly === true) {
                        updateResultImage(scope);
                    }
                    updateCropject(scope);
                }))
                .on('image-updated', fnSafeApply(function (scope) {
                    cropHost.setAreaMinRelativeSize(scope.areaMinRelativeSize);
                }))
                .on('area-move-end area-resize-end image-updated', fnSafeApply(function (scope) {
                    updateResultImage(scope);
                    updateCropject(scope);
                }));

            // Sync CropHost with Directive's options
            scope.$watch('image', function (newVal) {
                // reset the original size and position to 0
                // it's mandatory because if not reset the size of the crop area won't maximise when the image was replaced
                var area = cropHost.getArea();
                if (area) {
                    cropHost.getArea()._size = { x: 0, y: 0, w: 0, h: 0 };
                }
                if (newVal) {
                    displayLoading();
                }
                // cancel timeout if necessary
                if (scope.timeout !== null) {
                    $timeout.cancel(scope.timeout);
                }
                scope.timeout = $timeout(function () {
                    scope.timeout = null;
                    cropHost.setInitMax(scope.initMaxArea);
                    cropHost.setNewImageSource(scope.image);
                }, 100);
            });
            scope.$watch('areaType', function () {
                cropHost.setAreaType(scope.areaType);
                updateResultImage(scope);
            });
            scope.$watch('areaMinSize', function () {
                cropHost.setAreaMinSize(scope.areaMinSize);
                updateResultImage(scope);
            });
            scope.$watch('areaMinRelativeSize', function () {
                if (scope.image !== '') {
                    cropHost.setAreaMinRelativeSize(scope.areaMinRelativeSize);
                    updateResultImage(scope);
                }
            });
            scope.$watch('areaInitSize', function () {
                cropHost.setAreaInitSize(scope.areaInitSize);
                updateResultImage(scope);
            });
            scope.$watch('areaInitCoords', function () {
                cropHost.setAreaInitCoords(scope.areaInitCoords);
                cropHost.areaInitIsRelativeToImage = scope.areaInitIsRelativeToImage;
                updateResultImage(scope);
            });
            scope.$watch('maxCanvasDimensions', function () {
                cropHost.setMaxCanvasDimensions(scope.maxCanvasDimensions);
            });
            scope.$watch('minCanvasDimensions', function () {
                cropHost.setMinCanvasDimensions(scope.minCanvasDimensions);
            });
            scope.$watch('resultImageFormat', function () {
                cropHost.setResultImageFormat(scope.resultImageFormat);
                updateResultImage(scope);
            });
            scope.$watch('resultImageQuality', function () {
                cropHost.setResultImageQuality(scope.resultImageQuality);
                updateResultImage(scope);
            });
            scope.$watch('resultImageSize', function () {
                cropHost.setResultImageSize(scope.resultImageSize);
                updateResultImage(scope);
            });
            scope.$watch('paletteColorLength', function () {
                cropHost.setPaletteColorLength(scope.paletteColorLength);
            });
            scope.$watch('aspectRatio', function () {
                if (typeof scope.aspectRatio === 'string' && scope.aspectRatio !== '') {
                    scope.aspectRatio = parseFloat(scope.aspectRatio);
                }
                if (scope.aspectRatio) {
                    cropHost.setAspect(scope.aspectRatio);
                }
            });
            scope.$watch('allowCropResizeOnCorners', function () {
                if (scope.allowCropResizeOnCorners) {
                    cropHost.setAllowCropResizeOnCorners(scope.allowCropResizeOnCorners);
                }
            });

            scope.$watch('disableCrop', function () {
                cropHost.setDisableCrop(scope.disableCrop);
            });

            // Update CropHost dimensions when the directive element is resized
            scope.$watch(
                function () {
                    if (cropHost.getScalemode() === 'fit') {
                        return [element[0].clientWidth, element[0].clientHeight];
                    }
                    if (cropHost.getScalemode() === 'fixed-height') {
                        return [element[0].clientWidth, element[0].clientHeight];
                    }
                    if (cropHost.getScalemode() === 'full-width') {
                        return element[0].clientWidth;
                    }
                },
                function (value) {

                    if (cropHost.getScalemode() === 'fit') {
                        cropHost.setMaxDimensions(value[0], value[1]);
                        updateResultImage(scope);
                    }
                    if (cropHost.getScalemode() === 'fixed-height') {
                        if (value[0] > 0 && value[1] > 0) {
                            cropHost.setMaxDimensions(value[0], value[1]);
                            updateResultImage(scope);
                        }
                    }
                    if (cropHost.getScalemode() === 'full-width') {
                        if (value > 0) {
                            cropHost.setMaxDimensions(value);
                        }
                    }
                },
                true
            );

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

}());
/* canvas-toBlob.js
 * A canvas.toBlob() implementation.
 * 2013-12-27
 * 
 * By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr
 * License: X11/MIT
 *   See https://github.com/eligrey/canvas-toBlob.js/blob/master/LICENSE.md
 */

/*global self */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
  plusplus: true */

/*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */

(function(view) {
    "use strict";
    var
        Uint8Array = view.Uint8Array,
        HTMLCanvasElement = view.HTMLCanvasElement,
        canvas_proto = HTMLCanvasElement && HTMLCanvasElement.prototype,
        is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i,
        to_data_url = "toDataURL",
        base64_ranks, decode_base64 = function(base64) {
            var
                len = base64.length,
                buffer = new Uint8Array(len / 4 * 3 | 0),
                i = 0,
                outptr = 0,
                last = [0, 0],
                state = 0,
                save = 0,
                rank, code, undef;
            while (len--) {
                code = base64.charCodeAt(i++);
                rank = base64_ranks[code - 43];
                if (rank !== 255 && rank !== undef) {
                    last[1] = last[0];
                    last[0] = code;
                    save = (save << 6) | rank;
                    state++;
                    if (state === 4) {
                        buffer[outptr++] = save >>> 16;
                        if (last[1] !== 61 /* padding character */ ) {
                            buffer[outptr++] = save >>> 8;
                        }
                        if (last[0] !== 61 /* padding character */ ) {
                            buffer[outptr++] = save;
                        }
                        state = 0;
                    }
                }
            }
            // 2/3 chance there's going to be some null bytes at the end, but that
            // doesn't really matter with most image formats.
            // If it somehow matters for you, truncate the buffer up outptr.
            return buffer;
        };
    if (Uint8Array) {
        base64_ranks = new Uint8Array([
            62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
        ]);
    }
    if (HTMLCanvasElement && !canvas_proto.toBlob) {
        canvas_proto.toBlob = function(callback, type /*, ...args*/ ) {
            if (!type) {
                type = "image/png";
            }
            if (this.mozGetAsFile) {
                callback(this.mozGetAsFile("canvas", type));
                return;
            }
            if (this.msToBlob && /^\s*image\/png\s*(?:$|;)/i.test(type)) {
                callback(this.msToBlob());
                return;
            }

            var
                args = Array.prototype.slice.call(arguments, 1),
                dataURI = this[to_data_url].apply(this, args),
                header_end = dataURI.indexOf(","),
                data = dataURI.substring(header_end + 1),
                is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)),
                blob;
            if (Blob.fake) {
                // no reason to decode a data: URI that's just going to become a data URI again
                blob = new Blob
                if (is_base64) {
                    blob.encoding = "base64";
                } else {
                    blob.encoding = "URI";
                }
                blob.data = data;
                blob.size = data.length;
            } else if (Uint8Array) {
                if (is_base64) {
                    blob = new Blob([decode_base64(data)], {
                        type: type
                    });
                } else {
                    blob = new Blob([decodeURIComponent(data)], {
                        type: type
                    });
                }
            }
            if (typeof callback !== 'undefined') {
                callback(blob);
            }
        };

        if (canvas_proto.toDataURLHD) {
            canvas_proto.toBlobHD = function() {
                to_data_url = "toDataURLHD";
                var blob = this.toBlob();
                to_data_url = "toDataURL";
                return blob;
            }
        } else {
            canvas_proto.toBlobHD = canvas_proto.toBlob;
        }
    }
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

/*!
 * Color Thief v2.0
 * by Lokesh Dhakar - http://www.lokeshdhakar.com
 *
 * Thanks
 * ------
 * Nick Rabinowitz - For creating quantize.js.
 * John Schulz - For clean up and optimization. @JFSIII
 * Nathan Spady - For adding drag and drop support to the demo page.
 *
 * License
 * -------
 * Copyright 2011, 2015 Lokesh Dhakar
 * Released under the MIT license
 * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE
 *
 */
(function() {
    /*!
     * Color Thief v2.0
     * by Lokesh Dhakar - http://www.lokeshdhakar.com
     *
     * Thanks
     * ------
     * Nick Rabinowitz - For creating quantize.js.
     * John Schulz - For clean up and optimization. @JFSIII
     * Nathan Spady - For adding drag and drop support to the demo page.
     *
     * License
     * -------
     * Copyright 2011, 2015 Lokesh Dhakar
     * Released under the MIT license
     * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE
     *
     */

    /*
      CanvasImage Class
      Class that wraps the html image element and canvas.
      It also simplifies some of the canvas context manipulation
      with a set of helper functions.
    */
    var CanvasImage = function(image) {
        this.canvas = document.createElement('canvas');
        this.context = this.canvas.getContext('2d');

        document.body.appendChild(this.canvas);

        this.width = this.canvas.width = image.width;
        this.height = this.canvas.height = image.height;

        this.context.drawImage(image, 0, 0, this.width, this.height);
    };

    CanvasImage.prototype.clear = function() {
        this.context.clearRect(0, 0, this.width, this.height);
    };

    CanvasImage.prototype.update = function(imageData) {
        this.context.putImageData(imageData, 0, 0);
    };

    CanvasImage.prototype.getPixelCount = function() {
        return this.width * this.height;
    };

    CanvasImage.prototype.getImageData = function() {
        return this.context.getImageData(0, 0, this.width, this.height);
    };

    CanvasImage.prototype.removeCanvas = function() {
        this.canvas.parentNode.removeChild(this.canvas);
    };

    var ColorThief = function() {};
    window.ColorThief = ColorThief;

    /*
     * getColor(sourceImage[, quality])
     * returns {r: num, g: num, b: num}
     *
     * Use the median cut algorithm provided by quantize.js to cluster similar
     * colors and return the base color from the largest cluster.
     *
     * Quality is an optional argument. It needs to be an integer. 1 is the highest quality settings.
     * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the
     * faster a color will be returned but the greater the likelihood that it will not be the visually
     * most dominant color.
     *
     * */
    ColorThief.prototype.getColor = function(sourceImage, quality) {
        var palette = this.getPalette(sourceImage, 5, quality);
        var dominantColor = palette[0];
        return dominantColor;
    };

    /*
     * getPalette(sourceImage[, colorCount, quality])
     * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...]
     *
     * Use the median cut algorithm provided by quantize.js to cluster similar colors.
     *
     * colorCount determines the size of the palette; the number of colors returned. If not set, it
     * defaults to 10.
     *
     * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2.
     *
     * quality is an optional argument. It needs to be an integer. 1 is the highest quality settings.
     * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the
     * faster the palette generation but the greater the likelihood that colors will be missed.
     *
     *
     */
    ColorThief.prototype.getPalette = function(sourceImage, colorCount, quality) {

        if (typeof colorCount === 'undefined') {
            colorCount = 10;
        }
        if (typeof quality === 'undefined' || quality < 1) {
            quality = 10;
        }

        // Create custom CanvasImage object
        var image = new CanvasImage(sourceImage);
        var imageData = image.getImageData();
        var pixels = imageData.data;
        var pixelCount = image.getPixelCount();

        // Store the RGB values in an array format suitable for quantize function
        var pixelArray = [];
        for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
            offset = i * 4;
            r = pixels[offset + 0];
            g = pixels[offset + 1];
            b = pixels[offset + 2];
            a = pixels[offset + 3];
            // If pixel is mostly opaque and not white
            if (a >= 125) {
                if (!(r > 250 && g > 250 && b > 250)) {
                    pixelArray.push([r, g, b]);
                }
            }
        }

        // Send array to quantize function which clusters values
        // using median cut algorithm
        var cmap = MMCQ.quantize(pixelArray, colorCount);
        var palette = cmap ? cmap.palette() : null;

        // Clean up
        image.removeCanvas();

        return palette;
    };

    /*!
     * quantize.js Copyright 2008 Nick Rabinowitz.
     * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
     */

    // fill out a couple protovis dependencies
    /*!
     * Block below copied from Protovis: http://mbostock.github.com/protovis/
     * Copyright 2010 Stanford Visualization Group
     * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
     */
    if (!pv) {
        var pv = {
            map: function(array, f) {
                var o = {};
                return f ? array.map(function(d, i) {
                    o.index = i;
                    return f.call(o, d);
                }) : array.slice();
            },
            naturalOrder: function(a, b) {
                return (a < b) ? -1 : ((a > b) ? 1 : 0);
            },
            sum: function(array, f) {
                var o = {};
                return array.reduce(f ? function(p, d, i) {
                    o.index = i;
                    return p + f.call(o, d);
                } : function(p, d) {
                    return p + d;
                }, 0);
            },
            max: function(array, f) {
                return Math.max.apply(null, f ? pv.map(array, f) : array);
            }
        };
    }

    /**
     * Basic Javascript port of the MMCQ (modified median cut quantization)
     * algorithm from the Leptonica library (http://www.leptonica.com/).
     * Returns a color map you can use to map original pixels to the reduced
     * palette. Still a work in progress.
     *
     * @author Nick Rabinowitz
     * @example

    // array of pixels as [R,G,B] arrays
    var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207]
                    // etc
                    ];
    var maxColors = 4;

    var cmap = MMCQ.quantize(myPixels, maxColors);
    var newPalette = cmap.palette();
    var newPixels = myPixels.map(function(p) {
        return cmap.map(p);
    });

     */
    var MMCQ = (function() {
        // private constants
        var sigbits = 5,
            rshift = 8 - sigbits,
            maxIterations = 1000,
            fractByPopulations = 0.75;

        // get reduced-space color index for a pixel
        function getColorIndex(r, g, b) {
            return (r << (2 * sigbits)) + (g << sigbits) + b;
        }

        // Simple priority queue
        function PQueue(comparator) {
            var contents = [],
                sorted = false;

            function sort() {
                contents.sort(comparator);
                sorted = true;
            }

            return {
                push: function(o) {
                    contents.push(o);
                    sorted = false;
                },
                peek: function(index) {
                    if (!sorted) sort();
                    if (index === undefined) index = contents.length - 1;
                    return contents[index];
                },
                pop: function() {
                    if (!sorted) sort();
                    return contents.pop();
                },
                size: function() {
                    return contents.length;
                },
                map: function(f) {
                    return contents.map(f);
                },
                debug: function() {
                    if (!sorted) sort();
                    return contents;
                }
            };
        }

        // 3d color space box
        function VBox(r1, r2, g1, g2, b1, b2, histo) {
            var vbox = this;
            vbox.r1 = r1;
            vbox.r2 = r2;
            vbox.g1 = g1;
            vbox.g2 = g2;
            vbox.b1 = b1;
            vbox.b2 = b2;
            vbox.histo = histo;
        }
        VBox.prototype = {
            volume: function(force) {
                var vbox = this;
                if (!vbox._volume || force) {
                    vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
                }
                return vbox._volume;
            },
            count: function(force) {
                var vbox = this,
                    histo = vbox.histo;
                if (!vbox._count_set || force) {
                    var npix = 0,
                        i, j, k;
                    for (i = vbox.r1; i <= vbox.r2; i++) {
                        for (j = vbox.g1; j <= vbox.g2; j++) {
                            for (k = vbox.b1; k <= vbox.b2; k++) {
                                var index = getColorIndex(i, j, k);
                                npix += (histo[index] || 0);
                            }
                        }
                    }
                    vbox._count = npix;
                    vbox._count_set = true;
                }
                return vbox._count;
            },
            copy: function() {
                var vbox = this;
                return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
            },
            avg: function(force) {
                var vbox = this,
                    histo = vbox.histo;
                if (!vbox._avg || force) {
                    var ntot = 0,
                        mult = 1 << (8 - sigbits),
                        rsum = 0,
                        gsum = 0,
                        bsum = 0,
                        hval,
                        i, j, k, histoindex;
                    for (i = vbox.r1; i <= vbox.r2; i++) {
                        for (j = vbox.g1; j <= vbox.g2; j++) {
                            for (k = vbox.b1; k <= vbox.b2; k++) {
                                histoindex = getColorIndex(i, j, k);
                                hval = histo[histoindex] || 0;
                                ntot += hval;
                                rsum += (hval * (i + 0.5) * mult);
                                gsum += (hval * (j + 0.5) * mult);
                                bsum += (hval * (k + 0.5) * mult);
                            }
                        }
                    }
                    if (ntot) {
                        vbox._avg = [~~(rsum / ntot), ~~(gsum / ntot), ~~(bsum / ntot)];
                    } else {
                        //                    console.log('empty box');
                        vbox._avg = [~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2), ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)];
                    }
                }
                return vbox._avg;
            },
            contains: function(pixel) {
                var vbox = this,
                    rval = pixel[0] >> rshift;
                gval = pixel[1] >> rshift;
                bval = pixel[2] >> rshift;
                return (rval >= vbox.r1 && rval <= vbox.r2 &&
                    gval >= vbox.g1 && gval <= vbox.g2 &&
                    bval >= vbox.b1 && bval <= vbox.b2);
            }
        };

        // Color map
        function CMap() {
            this.vboxes = new PQueue(function(a, b) {
                return pv.naturalOrder(
                    a.vbox.count() * a.vbox.volume(),
                    b.vbox.count() * b.vbox.volume()
                );
            });
        }
        CMap.prototype = {
            push: function(vbox) {
                this.vboxes.push({
                    vbox: vbox,
                    color: vbox.avg()
                });
            },
            palette: function() {
                return this.vboxes.map(function(vb) {
                    return vb.color;
                });
            },
            size: function() {
                return this.vboxes.size();
            },
            map: function(color) {
                var vboxes = this.vboxes;
                for (var i = 0; i < vboxes.size(); i++) {
                    if (vboxes.peek(i).vbox.contains(color)) {
                        return vboxes.peek(i).color;
                    }
                }
                return this.nearest(color);
            },
            nearest: function(color) {
                var vboxes = this.vboxes,
                    d1, d2, pColor;
                for (var i = 0; i < vboxes.size(); i++) {
                    d2 = Math.sqrt(
                        Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
                        Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
                        Math.pow(color[2] - vboxes.peek(i).color[2], 2)
                    );
                    if (d2 < d1 || d1 === undefined) {
                        d1 = d2;
                        pColor = vboxes.peek(i).color;
                    }
                }
                return pColor;
            },
            forcebw: function() {
                // XXX: won't  work yet
                var vboxes = this.vboxes;
                vboxes.sort(function(a, b) {
                    return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));
                });

                // force darkest color to black if everything < 5
                var lowest = vboxes[0].color;
                if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
                    vboxes[0].color = [0, 0, 0];

                // force lightest color to white if everything > 251
                var idx = vboxes.length - 1,
                    highest = vboxes[idx].color;
                if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
                    vboxes[idx].color = [255, 255, 255];
            }
        };

        // histo (1-d array, giving the number of pixels in
        // each quantized region of color space), or null on error
        function getHisto(pixels) {
            var histosize = 1 << (3 * sigbits),
                histo = new Array(histosize),
                index, rval, gval, bval;
            pixels.forEach(function(pixel) {
                rval = pixel[0] >> rshift;
                gval = pixel[1] >> rshift;
                bval = pixel[2] >> rshift;
                index = getColorIndex(rval, gval, bval);
                histo[index] = (histo[index] || 0) + 1;
            });
            return histo;
        }

        function vboxFromPixels(pixels, histo) {
            var rmin = 1000000,
                rmax = 0,
                gmin = 1000000,
                gmax = 0,
                bmin = 1000000,
                bmax = 0,
                rval, gval, bval;
            // find min/max
            pixels.forEach(function(pixel) {
                rval = pixel[0] >> rshift;
                gval = pixel[1] >> rshift;
                bval = pixel[2] >> rshift;
                if (rval < rmin) rmin = rval;
                else if (rval > rmax) rmax = rval;
                if (gval < gmin) gmin = gval;
                else if (gval > gmax) gmax = gval;
                if (bval < bmin) bmin = bval;
                else if (bval > bmax) bmax = bval;
            });
            return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
        }

        function medianCutApply(histo, vbox) {
            if (!vbox.count()) return;

            var rw = vbox.r2 - vbox.r1 + 1,
                gw = vbox.g2 - vbox.g1 + 1,
                bw = vbox.b2 - vbox.b1 + 1,
                maxw = pv.max([rw, gw, bw]);
            // only one pixel, no split
            if (vbox.count() == 1) {
                return [vbox.copy()];
            }
            /* Find the partial sum arrays along the selected axis. */
            var total = 0,
                partialsum = [],
                lookaheadsum = [],
                i, j, k, sum, index;
            if (maxw == rw) {
                for (i = vbox.r1; i <= vbox.r2; i++) {
                    sum = 0;
                    for (j = vbox.g1; j <= vbox.g2; j++) {
                        for (k = vbox.b1; k <= vbox.b2; k++) {
                            index = getColorIndex(i, j, k);
                            sum += (histo[index] || 0);
                        }
                    }
                    total += sum;
                    partialsum[i] = total;
                }
            } else if (maxw == gw) {
                for (i = vbox.g1; i <= vbox.g2; i++) {
                    sum = 0;
                    for (j = vbox.r1; j <= vbox.r2; j++) {
                        for (k = vbox.b1; k <= vbox.b2; k++) {
                            index = getColorIndex(j, i, k);
                            sum += (histo[index] || 0);
                        }
                    }
                    total += sum;
                    partialsum[i] = total;
                }
            } else { /* maxw == bw */
                for (i = vbox.b1; i <= vbox.b2; i++) {
                    sum = 0;
                    for (j = vbox.r1; j <= vbox.r2; j++) {
                        for (k = vbox.g1; k <= vbox.g2; k++) {
                            index = getColorIndex(j, k, i);
                            sum += (histo[index] || 0);
                        }
                    }
                    total += sum;
                    partialsum[i] = total;
                }
            }
            partialsum.forEach(function(d, i) {
                lookaheadsum[i] = total - d;
            });

            function doCut(color) {
                var dim1 = color + '1',
                    dim2 = color + '2',
                    left, right, vbox1, vbox2, d2, count2 = 0;
                for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
                    if (partialsum[i] > total / 2) {
                        vbox1 = vbox.copy();
                        vbox2 = vbox.copy();
                        left = i - vbox[dim1];
                        right = vbox[dim2] - i;
                        if (left <= right)
                            d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
                        else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
                        // avoid 0-count boxes
                        while (!partialsum[d2]) d2++;
                        count2 = lookaheadsum[d2];
                        while (!count2 && partialsum[d2 - 1]) count2 = lookaheadsum[--d2];
                        // set dimensions
                        vbox1[dim2] = d2;
                        vbox2[dim1] = vbox1[dim2] + 1;
                        //                    console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
                        return [vbox1, vbox2];
                    }
                }

            }
            // determine the cut planes
            return maxw == rw ? doCut('r') :
                maxw == gw ? doCut('g') :
                doCut('b');
        }

        function quantize(pixels, maxcolors) {
            // short-circuit
            if (!pixels.length || maxcolors < 2 || maxcolors > 256) {
                //            console.log('wrong number of maxcolors');
                return false;
            }

            // XXX: check color content and convert to grayscale if insufficient

            var histo = getHisto(pixels),
                histosize = 1 << (3 * sigbits);

            // check that we aren't below maxcolors already
            var nColors = 0;
            histo.forEach(function() {
                nColors++;
            });
            if (nColors <= maxcolors) {
                // XXX: generate the new colors from the histo and return
            }

            // get the beginning vbox from the colors
            var vbox = vboxFromPixels(pixels, histo),
                pq = new PQueue(function(a, b) {
                    return pv.naturalOrder(a.count(), b.count());
                });
            pq.push(vbox);

            // inner function to do the iteration
            function iter(lh, target) {
                var ncolors = 1,
                    niters = 0,
                    vbox;
                while (niters < maxIterations) {
                    vbox = lh.pop();
                    if (!vbox.count()) { /* just put it back */
                        lh.push(vbox);
                        niters++;
                        continue;
                    }
                    // do the cut
                    var vboxes = medianCutApply(histo, vbox),
                        vbox1 = vboxes[0],
                        vbox2 = vboxes[1];

                    if (!vbox1) {
                        //                    console.log("vbox1 not defined; shouldn't happen!");
                        return;
                    }
                    lh.push(vbox1);
                    if (vbox2) { /* vbox2 can be null */
                        lh.push(vbox2);
                        ncolors++;
                    }
                    if (ncolors >= target) return;
                    if (niters++ > maxIterations) {
                        //                    console.log("infinite loop; perhaps too few pixels!");
                        return;
                    }
                }
            }

            // first set of colors, sorted by population
            iter(pq, fractByPopulations * maxcolors);

            // Re-sort by the product of pixel occupancy times the size in color space.
            var pq2 = new PQueue(function(a, b) {
                return pv.naturalOrder(a.count() * a.volume(), b.count() * b.volume());
            });
            while (pq.size()) {
                pq2.push(pq.pop());
            }

            // next set - generate the median cuts using the (npix * vol) sorting.
            iter(pq2, maxcolors - pq2.size());

            // calculate the actual colors
            var cmap = new CMap();
            while (pq2.size()) {
                cmap.push(pq2.pop());
            }

            return cmap;
        }

        return {
            quantize: quantize
        };
    })();

    /**
     * Export class to global
     */
    if (typeof define === 'function' && define.amd) {
        define([], function() {
            return ColorThief;
        }); // for AMD loader
    } else if (typeof exports === 'object') {
        module.exports = ColorThief; // for CommonJS
    } else {
        this.ColorThief = ColorThief;
    }
}.call(this));

/**
 * Mega pixel image rendering library for iOS6 Safari
 *
 * Fixes iOS6 Safari's image file rendering issue for large size image (over mega-pixel),
 * which causes unexpected subsampling when drawing it in canvas.
 * By using this library, you can safely render the image with proper stretching.
 *
 * Copyright (c) 2012 Shinichi Tomita <shinichi.tomita@gmail.com>
 * Released under the MIT license
 */
(function () {

    /**
     * Detect subsampling in loaded image.
     * In iOS, larger images than 2M pixels may be subsampled in rendering.
     */
    function detectSubsampling(img) {
        var iw = img.naturalWidth, ih = img.naturalHeight;
        if (iw * ih > 1024 * 1024) { // subsampling may happen over megapixel image
            var canvas = document.createElement('canvas');
            canvas.width = canvas.height = 1;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, -iw + 1, 0);
            // subsampled image becomes half smaller in rendering size.
            // check alpha channel value to confirm image is covering edge pixel or not.
            // if alpha value is 0 image is not covering, hence subsampled.
            return ctx.getImageData(0, 0, 1, 1).data[3] === 0;
        } else {
            return false;
        }
    }

    /**
     * Detecting vertical squash in loaded image.
     * Fixes a bug which squash image vertically while drawing into canvas for some images.
     */
    function detectVerticalSquash(img, iw, ih) {
        var canvas = document.createElement('canvas');
        canvas.width = 1;
        canvas.height = ih;
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        var data = ctx.getImageData(0, 0, 1, ih).data;
        // search image edge pixel position in case it is squashed vertically.
        var sy = 0;
        var ey = ih;
        var py = ih;
        while (py > sy) {
            var alpha = data[(py - 1) * 4 + 3];
            if (alpha === 0) {
                ey = py;
            } else {
                sy = py;
            }
            py = (ey + sy) >> 1;
        }
        var ratio = (py / ih);
        return (ratio === 0) ? 1 : ratio;
    }

    /**
     * Rendering image element (with resizing) and get its data URL
     */
    function renderImageToDataURL(img, options, doSquash) {
        var canvas = document.createElement('canvas');
        renderImageToCanvas(img, canvas, options, doSquash);
        return canvas.toDataURL('image/jpeg', options.quality || 0.8);
    }

    /**
     * Rendering image element (with resizing) into the canvas element
     */
    function renderImageToCanvas(img, canvas, options, doSquash) {
        var iw = img.naturalWidth, ih = img.naturalHeight;
        if ((iw + ih) === false) {
            return;
        }
        var width = options.width, height = options.height;
        var ctx = canvas.getContext('2d');
        ctx.save();
        transformCoordinate(canvas, ctx, width, height, options.orientation);
        var subsampled = detectSubsampling(img);
        if (subsampled) {
            iw /= 2;
            ih /= 2;
        }
        var d = 1024; // size of tiling canvas
        var tmpCanvas = document.createElement('canvas');
        tmpCanvas.width = tmpCanvas.height = d;
        var tmpCtx = tmpCanvas.getContext('2d');
        var vertSquashRatio = doSquash ? detectVerticalSquash(img, iw, ih) : 1;
        var dw = Math.ceil(d * width / iw);
        var dh = Math.ceil(d * height / ih / vertSquashRatio);
        var sy = 0;
        var dy = 0;
        while (sy < ih) {
            var sx = 0;
            var dx = 0;
            while (sx < iw) {
                tmpCtx.clearRect(0, 0, d, d);
                tmpCtx.drawImage(img, -sx, -sy);
                ctx.drawImage(tmpCanvas, 0, 0, d, d, dx, dy, dw, dh);
                sx += d;
                dx += dw;
            }
            sy += d;
            dy += dh;
        }
        ctx.restore();
        tmpCanvas = tmpCtx = null;
    }

    /**
     * Transform canvas coordination according to specified frame size and orientation
     * Orientation value is from EXIF tag
     */
    function transformCoordinate(canvas, ctx, width, height, orientation) {
        switch (orientation) {
            case 5:
            case 6:
            case 7:
            case 8:
                canvas.width = height;
                canvas.height = width;
                break;
            default:
                canvas.width = width;
                canvas.height = height;
        }
        switch (orientation) {
            case 2:
                // horizontal flip
                ctx.translate(width, 0);
                ctx.scale(-1, 1);
                break;
            case 3:
                // 180 rotate left
                ctx.translate(width, height);
                ctx.rotate(Math.PI);
                break;
            case 4:
                // vertical flip
                ctx.translate(0, height);
                ctx.scale(1, -1);
                break;
            case 5:
                // vertical flip + 90 rotate right
                ctx.rotate(0.5 * Math.PI);
                ctx.scale(1, -1);
                break;
            case 6:
                // 90 rotate right
                ctx.rotate(0.5 * Math.PI);
                ctx.translate(0, -height);
                break;
            case 7:
                // horizontal flip + 90 rotate right
                ctx.rotate(0.5 * Math.PI);
                ctx.translate(width, -height);
                ctx.scale(-1, 1);
                break;
            case 8:
                // 90 rotate left
                ctx.rotate(-0.5 * Math.PI);
                ctx.translate(-width, 0);
                break;
            default:
                break;
        }
    }

    var URL = window.URL && window.URL.createObjectURL ? window.URL :
        window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL :
            null;

    /**
     * MegaPixImage class
     */
    function MegaPixImage(srcImage) {
        if (window.Blob && srcImage instanceof Blob) {
            if (!URL) {
                throw Error('No createObjectURL function found to create blob url');
            }
            var img = new Image();
            img.src = URL.createObjectURL(srcImage);
            this.blob = srcImage;
            srcImage = img;
        }
        if (!srcImage.naturalWidth && !srcImage.naturalHeight) {
            var _this = this;
            srcImage.onload = srcImage.onerror = function () {
                var listeners = _this.imageLoadListeners;
                if (listeners) {
                    _this.imageLoadListeners = null;
                    for (var i = 0, len = listeners.length; i < len; i++) {
                        listeners[i]();
                    }
                }
            };
            this.imageLoadListeners = [];
        }
        this.srcImage = srcImage;
    }

    /**
     * Rendering megapix image into specified target element
     */
    MegaPixImage.prototype.render = function (target, options, callback) {
        if (this.imageLoadListeners) {
            var _this = this;
            this.imageLoadListeners.push(function () {
                _this.render(target, options, callback);
            });
            return;
        }
        options = options || {};
        var imgWidth = this.srcImage.naturalWidth, imgHeight = this.srcImage.naturalHeight,
            width = options.width, height = options.height,
            maxWidth = options.maxWidth, maxHeight = options.maxHeight,
            doSquash = !this.blob || this.blob.type === 'image/jpeg';
        if (width && !height) {
            height = (imgHeight * width / imgWidth) << 0;
        } else if (height && !width) {
            width = (imgWidth * height / imgHeight) << 0;
        } else {
            width = imgWidth;
            height = imgHeight;
        }
        if (maxWidth && width > maxWidth) {
            width = maxWidth;
            height = (imgHeight * width / imgWidth) << 0;
        }
        if (maxHeight && height > maxHeight) {
            height = maxHeight;
            width = (imgWidth * height / imgHeight) << 0;
        }
        var opt = {width: width, height: height};
        for (var k in options) {
            opt[k] = options[k];
        }

        var tagName = target.tagName.toLowerCase();
        if (tagName === 'img') {
            target.src = renderImageToDataURL(this.srcImage, opt, doSquash);
        } else if (tagName === 'canvas') {
            renderImageToCanvas(this.srcImage, target, opt, doSquash);
        }
        if (typeof this.onrender === 'function') {
            this.onrender(target);
        }
        if (callback) {
            callback();
        }
        if (this.blob) {
            this.blob = null;
            URL.revokeObjectURL(this.srcImage.src);
        }
    };

    /**
     * Export class to global
     */
    if (typeof define === 'function' && define.amd) {
        define([], function () {
            return MegaPixImage;
        }); // for AMD loader
    } else if (typeof exports === 'object') {
        module.exports = MegaPixImage; // for CommonJS
    } else {
        this.MegaPixImage = MegaPixImage;
    }

}.call(this));
.cropArea {
    background: var(--off-white);
    border: 1px solid rgba(0, 0, 0, 0.5);
    border-radius: 4px;
    overflow: hidden;
    width:480px;
    height:360px;
    margin: 4px auto;
    position: relative;
}

.ui-cropper-overlay {
    color: var(--off-black);
    border: 2px dashed var(--off-black);
    opacity: .75;
    position: absolute;
    width: 100%;
    top: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 0;
}

.ui-cropper-overlay .x-large {
    font-size: xxx-large;
}

form[name="imageCropUploadForm"] {
  text-align: center;
}

.display {
    color: var(--off-black);
    background: rgba(255, 255, 255, .5);
    border: 2px ridge rgba(0, 0, 0, .75);
    /* border: 2px groove rgba(0, 0, 0, .75); */
    border-radius: 2px;
    padding: 1px;
}

.progress {
    display: inline-block;
    width: 200px;
    line-height: 2px;
    border: 1px solid rgba(0, 0, 0, .5);
    height: 4px;
    margin: 0;
}

.progress .bar {
    background: orange;
    height: 2px;
    width: 0;
}

/*******************************************************************************
 * Toaster
 */
#toast-container > div {
    color: var(--off-white);
}

#toast-container.tiny {
     font-size: smaller;
}

@media (max-width: 240px) {
    #toast-container.tiny > div {
        padding: 1px 1px 1px 28px;
        width: 8em;
    }
}

@media (min-width: 241px) and (max-width: 480px) {
    #toast-container,tiny > div {
        padding: 2px 2px 2px 30px;
        width: 10em;
    }
}

@media (min-width: 481px) and (max-width: 768px) {
    #toast-container.tiny > div {
        padding: 3px 3px 3px 30px;
        width: 11em;
    }
}

#toast-container.tiny > div {
    margin: 0 0 4px;
    padding: 4px 4px 4px 32px;
    width: 12em;
    background-position: 6px center;
    background-size: 18px;
}

#toast-container.tiny * {
    margin: 0;
}

#toast-container.tiny ul,
#toast-container.tiny ol {
    padding-left: 16px;
}

#toast-container.tiny.toast-center > div,
#toast-container.tiny.toast-top-center > div,
#toast-container.tiny.toast-bottom-center > div {
  margin: 0 auto 4px;
  pointer-events: auto;
}
<div class="modal-header">
    <h3 class="form-horizontal-header">Afbeelding voor</h3>
    <h3 class="form-horizontal-header">{{vm.animal.name}}
        <span ng-show="vm.animal.id">#{{vm.animal.id}}</span>
    </h3>

</div>

<div class="modal-body">
    <form name="vm.imageCropUploadForm">
        <button ngf-select="true"
                ng-model="vm.imgFile"
                ngf-multiple="false"
                ngf-pattern="'image/*'"
                ngf-min-height="vm.imgDimensions.minHeight"
                ngf-min-width="vm.imgDimensions.minWidth"
                ngf-change="vm.onFileChange($files, $file, $newFiles, $duplicateFiles, $invalidFiles, $event)"
                ngf-accept="'image/*'"
                ng-click="vm.stopClick($event)"
                class="btn">Selecteer bestand</button>
        <div ngf-drop="true"
             ng-model="vm.imgFile"
             ngf-multiple="false"
             ngf-pattern="'image/*'"
             ngf-min-height="vm.imgDimensions.minHeight"
             ngf-min-width="vm.imgDimensions.minWidth"
             ngf-change="vm.onFileChange($files, $file, $newFiles, $duplicateFiles, $invalidFiles, $event)"
             ng-required="true"
             name="imgFile"
             class="cropArea">
            <div class="ui-cropper-overlay">
                <div class="fa fa-cloud-upload x-large"
                     aria-hidden="true"></div>
                <div>Drop afbeelding hier</div>
            </div>
            <ui-cropper image="vm.imgFile | ngfDataUrl"
                        result-image="vm.croppedDataUrl"
                        result-image-size="'max'"
                        result-image-quality="1.0"
                        result-image-format="vm.upload.imgFormat"
                        area-min-relative-size="vm.imgDimensions.minHeight"
                        ng-init="vm.croppedDataUrl=''"
                        area-type="square"
                        cropject="vm.cropper">
            </ui-cropper>
        </div>
        <!-- <div>
            <input type="text" ng-model="cropper.cropWidth">
            <input type="text" ng-model="cropper.cropHeight">
        </div> -->
        <div ng-show="vm.cropper.cropImageWidth && vm.cropper.cropImageHeight">
            Uitsnijding:
            <span class="display">
                {{vm.cropper.cropImageWidth}} x {{vm.cropper.cropImageHeight}}
            </span>
        </div>
        <!-- <div>
            <img ng-src="{{croppedDataUrl}}" />
        </div> -->
        <!-- <button ng-click="vm.uploadImg(vm.croppedDataUrl, vm.imgFile.name)"
                class="btn">Opladen</button>  -->
        <div>
            <div class="progress" ng-show="vm.upload.progress >= 0">
                <div class="bar" style="width:{{vm.upload.progress}}%"></div>
            </div>
            <div ng-show="vm.upload.progress >= 0"
                 ng-bind="vm.upload.progress + '%'"></div>
        </div>
    </form>
</div>

<div class="modal-footer">
    <button ng-if="vm.imgFile && vm.imageCropUploadForm.$valid"
            class="btn" ng-click="vm.ok()">Opladen
    </button>
    <button class="btn" ng-click="vm.cancel()">Annuleren</button>
</div>