├── .gitattributes ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── bower.json ├── compile ├── minified │ ├── ng-img-crop.css │ └── ng-img-crop.js └── unminified │ ├── ng-img-crop.css │ └── ng-img-crop.js ├── gulpfile.js ├── package.json ├── screenshots ├── circle_1.jpg └── square_1.jpg ├── source ├── js │ ├── classes │ │ ├── crop-area-circle.js │ │ ├── crop-area-square.js │ │ ├── crop-area.js │ │ ├── crop-canvas.js │ │ ├── crop-exif.js │ │ ├── crop-host.js │ │ └── crop-pubsub.js │ ├── init.js │ └── ng-img-crop.js └── scss │ └── ng-img-crop.scss └── test ├── ng-img-crop.html └── test.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | 39 | # SASS Cache 40 | .sass_cache/ 41 | 42 | # Installed Bower/Node modules 43 | bower_components/ 44 | node_modules/ 45 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "noempty": true, 7 | "noarg": true, 8 | "quotmark": "single", 9 | "undef": true, 10 | "eqnull": true, 11 | "freeze": true, 12 | "indent": 2, 13 | "newcap": true, 14 | "latedef": false, 15 | "trailing": true, 16 | "globals": { 17 | "angular": true, 18 | "require": true, 19 | "console": true, 20 | "process": true, 21 | "crop": true, 22 | "CropHost": true, 23 | "PubSub": true 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alex Kaul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngImgCrop 2 | 3 | ## WARNING 4 | 5 | THIS PROJECT IS NOT MAINTAINED ANYMORE. You are free to fork it and start a new project. 6 | 7 | ## Overview 8 | 9 | Simple Image Crop directive for AngularJS. Enables to crop a circle or a square out of an image. 10 | 11 | ## Screenshots 12 | 13 | ![Circle Crop](https://raw.github.com/alexk111/ngImgCrop/master/screenshots/circle_1.jpg "Circle Crop") 14 | 15 | ![Square Crop](https://raw.github.com/alexk111/ngImgCrop/master/screenshots/square_1.jpg "Square Crop") 16 | 17 | ## Live demo 18 | 19 | [Live demo on JSFiddle](http://jsfiddle.net/alexk111/rw6q9/) 20 | 21 | ## Requirements 22 | 23 | - AngularJS 24 | - Modern Browser supporting 25 | 26 | ## Installing 27 | 28 | ### Download 29 | 30 | You have two options to get the files: 31 | - [Download ngImgCrop](https://github.com/alexk111/ngImgCrop/archive/master.zip) files from GitHub. 32 | - Use Bower to download the files. Just run `bower install ngImgCrop`. 33 | 34 | ### Add files 35 | 36 | Add the scripts to your application. Make sure the `ng-img-crop.js` file is inserted **after** the `angular.js` library: 37 | 38 | ```html 39 | 40 | 41 | 42 | ``` 43 | 44 | ### Add a dependancy 45 | 46 | Add the image crop module as a dependancy to your application module: 47 | 48 | ```js 49 | var myAppModule = angular.module('MyApp', ['ngImgCrop']); 50 | ``` 51 | 52 | ## Usage 53 | 54 | 1. Add the image crop directive `` to the HTML file where you want to use an image crop control. *Note:* a container, you place the directive to, should have some pre-defined size (absolute or relative to its parent). That's required, because the image crop control fits the size of its container. 55 | 2. Bind the directive to a source image property (using **image=""** option). The directive will read the image data from that property and watch for updates. The property can be a url to an image, or a data uri. 56 | 3. Bind the directive to a result image property (using **result-image=""** option). On each update, the directive will put the content of the crop area to that property in the data uri format. 57 | 4. Set up the options that make sense to your application. 58 | 5. Done! 59 | 60 | ## Result image 61 | 62 | The result image will always be a square for the both circle and square area types. It's highly recommended to store the image as a square on your back-end, because this will enable you to easily update your pics later, if you decide to implement some design changes. Showing a square image as a circle on the front-end is not a problem - it is as easy as adding a *border-radius* style for that image in a css. 63 | 64 | ## Example code 65 | 66 | The following code enables to select an image using a file input and crop it. The cropped image data is inserted into img each time the crop area updates. 67 | 68 | ```html 69 | 70 | 71 | 72 | 73 | 74 | 82 | 101 | 102 | 103 |
Select an image file:
104 |
105 | 106 |
107 |
Cropped Image:
108 |
109 | 110 | 111 | ``` 112 | 113 | ## Options 114 | 115 | ```html 116 | 130 | ``` 131 | 132 | ### image 133 | 134 | Assignable angular expression to data-bind to. NgImgCrop gets an image for cropping from it. 135 | 136 | ### result-image 137 | 138 | Assignable angular expression to data-bind to. NgImgCrop puts a data uri of a cropped image into it. 139 | 140 | ### change-on-fly 141 | 142 | *Optional*. By default, to reduce CPU usage, when a user drags/resizes the crop area, the result image is only updated after the user stops dragging/resizing. Set true to always update the result image as the user drags/resizes the crop area. 143 | 144 | ### area-type 145 | 146 | *Optional*. Type of the crop area. Possible values: circle|square. Default: circle. 147 | 148 | ### area-min-size 149 | 150 | *Optional*. Min. width/height of the crop area (in pixels). Default: 80. 151 | 152 | ### result-image-size 153 | 154 | *Optional*. Width/height of the result image (in pixels). Default: 200. 155 | 156 | ### result-image-format 157 | 158 | *Optional*. Format of result image. Possible values include image/jpeg, image/png, and image/webp. Browser support varies. Default: image/png. 159 | 160 | ### result-image-quality 161 | 162 | *Optional*. Quality of result image. Possible values between 0.0 and 1.0 inclusive. Default: browser default. 163 | 164 | ### on-change 165 | 166 | *Optional*. Expression to evaluate upon changing the cropped part of the image. The cropped image data is available as $dataURI. 167 | 168 | ### on-load-begin 169 | 170 | *Optional*. Expression to evaluate when the source image starts loading. 171 | 172 | ### on-load-done 173 | 174 | *Optional*. Expression to evaluate when the source image successfully loaded. 175 | 176 | ### on-load-error 177 | 178 | *Optional*. Expression to evaluate when the source image didn't load. 179 | 180 | 181 | ## License 182 | 183 | See the [LICENSE](https://github.com/alexk111/ngImgCrop/blob/master/LICENSE) file. 184 | 185 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-img-crop", 3 | "version": "0.3.2", 4 | "authors": [ 5 | "Alex Kaul " 6 | ], 7 | "description": "Image crop directive for AngularJS", 8 | "main": [ 9 | "compile/minified/ng-img-crop.js", 10 | "compile/minified/ng-img-crop.css" 11 | ], 12 | "keywords": [ 13 | "angular", 14 | "image", 15 | "crop", 16 | "cropper" 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://github.com/alexk111/ngImgCrop", 20 | "ignore": [ 21 | "node_modules", 22 | "bower_components", 23 | "test" 24 | ], 25 | "devDependencies": { 26 | "angular": "~1.2.19" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /compile/minified/ng-img-crop.css: -------------------------------------------------------------------------------- 1 | img-crop{width:100%;height:100%;display:block;position:relative;overflow:hidden}img-crop canvas{display:block;position:absolute;top:50%;left:50%;outline:0;-webkit-tap-highlight-color:transparent} -------------------------------------------------------------------------------- /compile/minified/ng-img-crop.js: -------------------------------------------------------------------------------- 1 | /*! ngImgCrop v0.3.2 License: MIT */!function(){"use strict";var e=angular.module("ngImgCrop",[]);e.factory("cropAreaCircle",["cropArea",function(e){var t=function(){e.apply(this,arguments),this._boxResizeBaseSize=20,this._boxResizeNormalRatio=.9,this._boxResizeHoverRatio=1.2,this._iconMoveNormalRatio=.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=!1,this._areaIsHover=!1,this._boxResizeIsDragging=!1,this._areaIsDragging=!1};return t.prototype=new e,t.prototype._calcCirclePerimeterCoords=function(e){var t=this._size/2,i=e*(Math.PI/180),r=this._x+t*Math.cos(i),s=this._y+t*Math.sin(i);return[r,s]},t.prototype._calcResizeIconCenterCoords=function(){return this._calcCirclePerimeterCoords(-45)},t.prototype._isCoordWithinArea=function(e){return Math.sqrt((e[0]-this._x)*(e[0]-this._x)+(e[1]-this._y)*(e[1]-this._y))t[0]-i&&e[0]t[1]-i&&e[1]a?this._posResizeStartSize+2*a:this._posResizeStartSize+2*o,this._size=Math.max(this._minSize,s),this._boxResizeIsHover=!0,r=!0,this._events.trigger("area-resize")}else this._isCoordWithinBoxResize([e,t])?(i="nesw-resize",this._areaIsHover=!1,this._boxResizeIsHover=!0,r=!0):this._isCoordWithinArea([e,t])&&(i="move",this._areaIsHover=!0,r=!0);return this._dontDragOutside(),angular.element(this._ctx.canvas).css({cursor:i}),r},t.prototype.processMouseDown=function(e,t){this._isCoordWithinBoxResize([e,t])?(this._areaIsDragging=!1,this._areaIsHover=!1,this._boxResizeIsDragging=!0,this._boxResizeIsHover=!0,this._posResizeStartX=e,this._posResizeStartY=t,this._posResizeStartSize=this._size,this._events.trigger("area-resize-start")):this._isCoordWithinArea([e,t])&&(this._areaIsDragging=!0,this._areaIsHover=!0,this._boxResizeIsDragging=!1,this._boxResizeIsHover=!1,this._posDragStartX=e-this._x,this._posDragStartY=t-this._y,this._events.trigger("area-move-start"))},t.prototype.processMouseUp=function(){this._areaIsDragging&&(this._areaIsDragging=!1,this._events.trigger("area-move-end")),this._boxResizeIsDragging&&(this._boxResizeIsDragging=!1,this._events.trigger("area-resize-end")),this._areaIsHover=!1,this._boxResizeIsHover=!1,this._posDragStartX=0,this._posDragStartY=0},t}]),e.factory("cropAreaSquare",["cropArea",function(e){var t=function(){e.apply(this,arguments),this._resizeCtrlBaseRadius=10,this._resizeCtrlNormalRatio=.75,this._resizeCtrlHoverRatio=1,this._iconMoveNormalRatio=.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=!1,this._resizeCtrlIsDragging=-1,this._areaIsDragging=!1};return t.prototype=new e,t.prototype._calcSquareCorners=function(){var e=this._size/2;return[[this._x-e,this._y-e],[this._x+e,this._y-e],[this._x-e,this._y+e],[this._x+e,this._y+e]]},t.prototype._calcSquareDimensions=function(){var e=this._size/2;return{left:this._x-e,top:this._y-e,right:this._x+e,bottom:this._y+e}},t.prototype._isCoordWithinArea=function(e){var t=this._calcSquareDimensions();return e[0]>=t.left&&e[0]<=t.right&&e[1]>=t.top&&e[1]<=t.bottom},t.prototype._isCoordWithinResizeCtrl=function(e){for(var t=this._calcSquareCorners(),i=-1,r=0,s=t.length;s>r;r++){var o=t[r];if(e[0]>o[0]-this._resizeCtrlHoverRadius&&e[0]o[1]-this._resizeCtrlHoverRadius&&e[1]i;i++){var s=t[i];this._cropCanvas.drawIconResizeCircle(s,this._resizeCtrlBaseRadius,this._resizeCtrlIsHover===i?this._resizeCtrlHoverRatio:this._resizeCtrlNormalRatio)}},t.prototype.processMouseMove=function(e,t){var i="default",r=!1;if(this._resizeCtrlIsHover=-1,this._areaIsHover=!1,this._areaIsDragging)this._x=e-this._posDragStartX,this._y=t-this._posDragStartY,this._areaIsHover=!0,i="move",r=!0,this._events.trigger("area-move");else if(this._resizeCtrlIsDragging>-1){var s,o;switch(this._resizeCtrlIsDragging){case 0:s=-1,o=-1,i="nwse-resize";break;case 1:s=1,o=-1,i="nesw-resize";break;case 2:s=-1,o=1,i="nesw-resize";break;case 3:s=1,o=1,i="nwse-resize"}var a,n=(e-this._posResizeStartX)*s,h=(t-this._posResizeStartY)*o;a=n>h?this._posResizeStartSize+h:this._posResizeStartSize+n;var c=this._size;this._size=Math.max(this._minSize,a);var l=(this._size-c)/2;this._x+=l*s,this._y+=l*o,this._resizeCtrlIsHover=this._resizeCtrlIsDragging,r=!0,this._events.trigger("area-resize")}else{var u=this._isCoordWithinResizeCtrl([e,t]);if(u>-1){switch(u){case 0:i="nwse-resize";break;case 1:i="nesw-resize";break;case 2:i="nesw-resize";break;case 3:i="nwse-resize"}this._areaIsHover=!1,this._resizeCtrlIsHover=u,r=!0}else this._isCoordWithinArea([e,t])&&(i="move",this._areaIsHover=!0,r=!0)}return this._dontDragOutside(),angular.element(this._ctx.canvas).css({cursor:i}),r},t.prototype.processMouseDown=function(e,t){var i=this._isCoordWithinResizeCtrl([e,t]);i>-1?(this._areaIsDragging=!1,this._areaIsHover=!1,this._resizeCtrlIsDragging=i,this._resizeCtrlIsHover=i,this._posResizeStartX=e,this._posResizeStartY=t,this._posResizeStartSize=this._size,this._events.trigger("area-resize-start")):this._isCoordWithinArea([e,t])&&(this._areaIsDragging=!0,this._areaIsHover=!0,this._resizeCtrlIsDragging=-1,this._resizeCtrlIsHover=-1,this._posDragStartX=e-this._x,this._posDragStartY=t-this._y,this._events.trigger("area-move-start"))},t.prototype.processMouseUp=function(){this._areaIsDragging&&(this._areaIsDragging=!1,this._events.trigger("area-move-end")),this._resizeCtrlIsDragging>-1&&(this._resizeCtrlIsDragging=-1,this._events.trigger("area-resize-end")),this._areaIsHover=!1,this._resizeCtrlIsHover=-1,this._posDragStartX=0,this._posDragStartY=0},t}]),e.factory("cropArea",["cropCanvas",function(e){var t=function(t,i){this._ctx=t,this._events=i,this._minSize=80,this._cropCanvas=new e(t),this._image=new Image,this._x=0,this._y=0,this._size=200};return t.prototype.getImage=function(){return this._image},t.prototype.setImage=function(e){this._image=e},t.prototype.getX=function(){return this._x},t.prototype.setX=function(e){this._x=e,this._dontDragOutside()},t.prototype.getY=function(){return this._y},t.prototype.setY=function(e){this._y=e,this._dontDragOutside()},t.prototype.getSize=function(){return this._size},t.prototype.setSize=function(e){this._size=Math.max(this._minSize,e),this._dontDragOutside()},t.prototype.getMinSize=function(){return this._minSize},t.prototype.setMinSize=function(e){this._minSize=e,this._size=Math.max(this._minSize,this._size),this._dontDragOutside()},t.prototype._dontDragOutside=function(){var e=this._ctx.canvas.height,t=this._ctx.canvas.width;this._size>t&&(this._size=t),this._size>e&&(this._size=e),this._xt-this._size/2&&(this._x=t-this._size/2),this._ye-this._size/2&&(this._y=e-this._size/2)},t.prototype._drawArea=function(){},t.prototype.draw=function(){this._cropCanvas.drawCropArea(this._image,[this._x,this._y],this._size,this._drawArea)},t.prototype.processMouseMove=function(){},t.prototype.processMouseDown=function(){},t.prototype.processMouseUp=function(){},t}]),e.factory("cropCanvas",[function(){var e=[[-.5,-2],[-3,-4.5],[-.5,-7],[-7,-7],[-7,-.5],[-4.5,-3],[-2,-.5]],t=[[.5,-2],[3,-4.5],[.5,-7],[7,-7],[7,-.5],[4.5,-3],[2,-.5]],i=[[-.5,2],[-3,4.5],[-.5,7],[-7,7],[-7,.5],[-4.5,3],[-2,.5]],r=[[.5,2],[3,4.5],[.5,7],[7,7],[7,.5],[4.5,3],[2,.5]],s=[[-1.5,-2.5],[-1.5,-6],[-5,-6],[0,-11],[5,-6],[1.5,-6],[1.5,-2.5]],o=[[-2.5,-1.5],[-6,-1.5],[-6,-5],[-11,0],[-6,5],[-6,1.5],[-2.5,1.5]],a=[[-1.5,2.5],[-1.5,6],[-5,6],[0,11],[5,6],[1.5,6],[1.5,2.5]],n=[[2.5,-1.5],[6,-1.5],[6,-5],[11,0],[6,5],[6,1.5],[2.5,1.5]],h={areaOutline:"#fff",resizeBoxStroke:"#fff",resizeBoxFill:"#444",resizeBoxArrowFill:"#fff",resizeCircleStroke:"#fff",resizeCircleFill:"#444",moveIconFill:"#fff"};return function(c){var l=function(e,t,i){return[i*e[0]+t[0],i*e[1]+t[1]]},u=function(e,t,i,r){c.save(),c.fillStyle=t,c.beginPath();var s,o=l(e[0],i,r);c.moveTo(o[0],o[1]);for(var a in e)a>0&&(s=l(e[a],i,r),c.lineTo(s[0],s[1]));c.lineTo(o[0],o[1]),c.fill(),c.closePath(),c.restore()};this.drawIconMove=function(e,t){u(s,h.moveIconFill,e,t),u(o,h.moveIconFill,e,t),u(a,h.moveIconFill,e,t),u(n,h.moveIconFill,e,t)},this.drawIconResizeCircle=function(e,t,i){var r=t*i;c.save(),c.strokeStyle=h.resizeCircleStroke,c.lineWidth=2,c.fillStyle=h.resizeCircleFill,c.beginPath(),c.arc(e[0],e[1],r,0,2*Math.PI),c.fill(),c.stroke(),c.closePath(),c.restore()},this.drawIconResizeBoxBase=function(e,t,i){var r=t*i;c.save(),c.strokeStyle=h.resizeBoxStroke,c.lineWidth=2,c.fillStyle=h.resizeBoxFill,c.fillRect(e[0]-r/2,e[1]-r/2,r,r),c.strokeRect(e[0]-r/2,e[1]-r/2,r,r),c.restore()},this.drawIconResizeBoxNESW=function(e,r,s){this.drawIconResizeBoxBase(e,r,s),u(t,h.resizeBoxArrowFill,e,s),u(i,h.resizeBoxArrowFill,e,s)},this.drawIconResizeBoxNWSE=function(t,i,s){this.drawIconResizeBoxBase(t,i,s),u(e,h.resizeBoxArrowFill,t,s),u(r,h.resizeBoxArrowFill,t,s)},this.drawCropArea=function(e,t,i,r){var s=e.width/c.canvas.width,o=e.height/c.canvas.height,a=t[0]-i/2,n=t[1]-i/2;c.save(),c.strokeStyle=h.areaOutline,c.lineWidth=2,c.beginPath(),r(c,t,i),c.stroke(),c.clip(),i>0&&c.drawImage(e,a*s,n*o,i*s,i*o,a,n,i,i),c.beginPath(),r(c,t,i),c.stroke(),c.clip(),c.restore()}}}]),e.service("cropEXIF",[function(){function e(e){return!!e.exifdata}function t(e,t){t=t||e.match(/^data\:([^\;]+)\;base64,/im)[1]||"",e=e.replace(/^data\:([^\;]+)\;base64,/gim,"");for(var i=atob(e),r=i.length,s=new ArrayBuffer(r),o=new Uint8Array(s),a=0;r>a;a++)o[a]=i.charCodeAt(a);return s}function i(e,t){var i=new XMLHttpRequest;i.open("GET",e,!0),i.responseType="blob",i.onload=function(){(200==this.status||0===this.status)&&t(this.response)},i.send()}function r(e,r){function a(t){var i=s(t),a=o(t);e.exifdata=i||{},e.iptcdata=a||{},r&&r.call(e)}if(e.src)if(/^data\:/i.test(e.src)){var n=t(e.src);a(n)}else if(/^blob\:/i.test(e.src)){var h=new FileReader;h.onload=function(e){a(e.target.result)},i(e.src,function(e){h.readAsArrayBuffer(e)})}else{var c=new XMLHttpRequest;c.onload=function(){if(200!=this.status&&0!==this.status)throw"Could not load image";a(c.response),c=null},c.open("GET",e.src,!0),c.responseType="arraybuffer",c.send(null)}else if(window.FileReader&&(e instanceof window.Blob||e instanceof window.File)){var h=new FileReader;h.onload=function(e){u&&console.log("Got file of length "+e.target.result.byteLength),a(e.target.result)},h.readAsArrayBuffer(e)}}function s(e){var t=new DataView(e);if(u&&console.log("Got file of length "+e.byteLength),255!=t.getUint8(0)||216!=t.getUint8(1))return u&&console.log("Not a valid JPEG"),!1;for(var i,r=2,s=e.byteLength;s>r;){if(255!=t.getUint8(r))return u&&console.log("Not a valid marker at offset "+r+", found: "+t.getUint8(r)),!1;if(i=t.getUint8(r+1),u&&console.log(i),225==i)return u&&console.log("Found 0xFFE1 marker"),l(t,r+4,t.getUint16(r+2)-2);r+=2+t.getUint16(r+2)}}function o(e){var t=new DataView(e);if(u&&console.log("Got file of length "+e.byteLength),255!=t.getUint8(0)||216!=t.getUint8(1))return u&&console.log("Not a valid JPEG"),!1;for(var i=2,r=e.byteLength,s=function(e,t){return 56===e.getUint8(t)&&66===e.getUint8(t+1)&&73===e.getUint8(t+2)&&77===e.getUint8(t+3)&&4===e.getUint8(t+4)&&4===e.getUint8(t+5)};r>i;){if(s(t,i)){var o=t.getUint8(i+7);o%2!==0&&(o+=1),0===o&&(o=4);var n=i+8+o,h=t.getUint16(i+6+o);return a(e,n,h)}i++}}function a(e,t,i){for(var r,s,o,a,n,h=new DataView(e),l={},u=t;t+i>u;)28===h.getUint8(u)&&2===h.getUint8(u+1)&&(a=h.getUint8(u+2),a in _&&(o=h.getInt16(u+3),n=o+5,s=_[a],r=c(h,u+5,o),l.hasOwnProperty(s)?l[s]instanceof Array?l[s].push(r):l[s]=[l[s],r]:l[s]=r)),u++;return l}function n(e,t,i,r,s){var o,a,n,c=e.getUint16(i,!s),l={};for(n=0;c>n;n++)o=i+12*n+2,a=r[e.getUint16(o,!s)],!a&&u&&console.log("Unknown tag: "+e.getUint16(o,!s)),l[a]=h(e,o,t,i,s);return l}function h(e,t,i,r,s){var o,a,n,h,l,u,g=e.getUint16(t+2,!s),d=e.getUint32(t+4,!s),f=e.getUint32(t+8,!s)+i;switch(g){case 1:case 7:if(1==d)return e.getUint8(t+8,!s);for(o=d>4?f:t+8,a=[],h=0;d>h;h++)a[h]=e.getUint8(o+h);return a;case 2:return o=d>4?f:t+8,c(e,o,d-1);case 3:if(1==d)return e.getUint16(t+8,!s);for(o=d>2?f:t+8,a=[],h=0;d>h;h++)a[h]=e.getUint16(o+2*h,!s);return a;case 4:if(1==d)return e.getUint32(t+8,!s);for(a=[],h=0;d>h;h++)a[h]=e.getUint32(f+4*h,!s);return a;case 5:if(1==d)return l=e.getUint32(f,!s),u=e.getUint32(f+4,!s),n=new Number(l/u),n.numerator=l,n.denominator=u,n;for(a=[],h=0;d>h;h++)l=e.getUint32(f+8*h,!s),u=e.getUint32(f+4+8*h,!s),a[h]=new Number(l/u),a[h].numerator=l,a[h].denominator=u;return a;case 9:if(1==d)return e.getInt32(t+8,!s);for(a=[],h=0;d>h;h++)a[h]=e.getInt32(f+4*h,!s);return a;case 10:if(1==d)return e.getInt32(f,!s)/e.getInt32(f+4,!s);for(a=[],h=0;d>h;h++)a[h]=e.getInt32(f+8*h,!s)/e.getInt32(f+4+8*h,!s);return a}}function c(e,t,i){for(var r="",s=t;t+i>s;s++)r+=String.fromCharCode(e.getUint8(s));return r}function l(e,t){if("Exif"!=c(e,t,4))return u&&console.log("Not valid EXIF data! "+c(e,t,4)),!1;var i,r,s,o,a,h=t+6;if(18761==e.getUint16(h))i=!1;else{if(19789!=e.getUint16(h))return u&&console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"),!1;i=!0}if(42!=e.getUint16(h+2,!i))return u&&console.log("Not valid TIFF data! (no 0x002A)"),!1;var l=e.getUint32(h+4,!i);if(8>l)return u&&console.log("Not valid TIFF data! (First offset less than 8)",e.getUint32(h+4,!i)),!1;if(r=n(e,h,h+l,d,i),r.ExifIFDPointer){o=n(e,h,h+r.ExifIFDPointer,g,i);for(s in o){switch(s){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":o[s]=p[s][o[s]];break;case"ExifVersion":case"FlashpixVersion":o[s]=String.fromCharCode(o[s][0],o[s][1],o[s][2],o[s][3]);break;case"ComponentsConfiguration":o[s]=p.Components[o[s][0]]+p.Components[o[s][1]]+p.Components[o[s][2]]+p.Components[o[s][3]]}r[s]=o[s]}}if(r.GPSInfoIFDPointer){a=n(e,h,h+r.GPSInfoIFDPointer,f,i);for(s in a){switch(s){case"GPSVersionID":a[s]=a[s][0]+"."+a[s][1]+"."+a[s][2]+"."+a[s][3]}r[s]=a[s]}}return r}var u=!1,g=this.Tags={36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubsecTime",37521:"SubsecTimeOriginal",37522:"SubsecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"ISOSpeedRatings",34856:"OECF",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRation",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",40965:"InteroperabilityIFDPointer",42016:"ImageUniqueID"},d=this.TiffTags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright"},f=this.GPSTags={0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential"},p=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:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"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"}},_={120:"caption",110:"credit",25:"keywords",55:"dateCreated",80:"byline",85:"bylineTitle",122:"captionWriter",105:"headline",116:"copyright",15:"category"};this.getData=function(t,i){return(t instanceof Image||t instanceof HTMLImageElement)&&!t.complete?!1:(e(t)?i&&i.call(t):r(t,i),!0)},this.getTag=function(t,i){return e(t)?t.exifdata[i]:void 0},this.getAllTags=function(t){if(!e(t))return{};var i,r=t.exifdata,s={};for(i in r)r.hasOwnProperty(i)&&(s[i]=r[i]);return s},this.pretty=function(t){if(!e(t))return"";var i,r=t.exifdata,s="";for(i in r)r.hasOwnProperty(i)&&(s+="object"==typeof r[i]?r[i]instanceof Number?i+" : "+r[i]+" ["+r[i].numerator+"/"+r[i].denominator+"]\r\n":i+" : ["+r[i].length+" values]\r\n":i+" : "+r[i]+"\r\n");return s},this.readFromBinaryFile=function(e){return s(e)}}]),e.factory("cropHost",["$document","cropAreaCircle","cropAreaSquare","cropEXIF",function(e,t,i,r){var s=function(e){var t=e.getBoundingClientRect(),i=document.body,r=document.documentElement,s=window.pageYOffset||r.scrollTop||i.scrollTop,o=window.pageXOffset||r.scrollLeft||i.scrollLeft,a=r.clientTop||i.clientTop||0,n=r.clientLeft||i.clientLeft||0,h=t.top+s-a,c=t.left+o-n;return{top:Math.round(h),left:Math.round(c)}};return function(o,a,n){function h(){c.clearRect(0,0,c.canvas.width,c.canvas.height),null!==l&&(c.drawImage(l,0,0,c.canvas.width,c.canvas.height),c.save(),c.fillStyle="rgba(0, 0, 0, 0.65)",c.fillRect(0,0,c.canvas.width,c.canvas.height),c.restore(),u.draw())}var c=null,l=null,u=null,g=[100,100],d=[300,300],f=200,p="image/png",_=null,m=function(){if(null!==l){u.setImage(l);var e=[l.width,l.height],t=l.width/l.height,i=e;i[0]>d[0]?(i[0]=d[0],i[1]=i[0]/t):i[0]d[1]?(i[1]=d[1],i[0]=i[1]*t):i[1]
")[0],e=t.getContext("2d"),t.width=f,t.height=f,null!==l&&e.drawImage(l,(u.getX()-u.getSize()/2)*(l.width/c.canvas.width),(u.getY()-u.getSize()/2)*(l.height/c.canvas.height),u.getSize()*(l.width/c.canvas.width),u.getSize()*(l.height/c.canvas.height),0,0,f,f),null!==_?t.toDataURL(p,_):t.toDataURL(p)},this.setNewImageSource=function(e){if(l=null,m(),n.trigger("image-updated"),e){var t=new Image;"http"===e.substring(0,4).toLowerCase()&&(t.crossOrigin="anonymous"),t.onload=function(){n.trigger("load-done"),r.getData(t,function(){var e=r.getTag(t,"Orientation");if([3,6,8].indexOf(e)>-1){var i=document.createElement("canvas"),s=i.getContext("2d"),o=t.width,a=t.height,h=0,c=0,u=0;switch(e){case 3:h=-t.width,c=-t.height,u=180;break;case 6:o=t.height,a=t.width,c=-t.height,u=90;break;case 8:o=t.height,a=t.width,h=-t.width,u=270}i.width=o,i.height=a,s.rotate(u*Math.PI/180),s.drawImage(t,h,c),l=new Image,l.src=i.toDataURL("image/png")}else l=t;m(),n.trigger("image-updated")})},t.onerror=function(){n.trigger("load-error")},n.trigger("load-start"),t.src=e}},this.setMaxDimensions=function(e,t){if(d=[e,t],null!==l){var i=c.canvas.width,r=c.canvas.height,s=[l.width,l.height],a=l.width/l.height,n=s;n[0]>d[0]?(n[0]=d[0],n[1]=n[0]/a):n[0]d[1]?(n[1]=d[1],n[0]=n[1]*a):n[1]=0&&1>=e&&(_=e)},this.setAreaType=function(e){var r=u.getSize(),s=u.getMinSize(),o=u.getX(),a=u.getY(),g=t;"square"===e&&(g=i),u=new g(c,n),u.setMinSize(s),u.setSize(r),u.setX(o),u.setY(a),null!==l&&u.setImage(l),h()},c=o[0].getContext("2d"),u=new t(c,n),e.on("mousemove",S),o.on("mousedown",z),e.on("mouseup",I),e.on("touchmove",S),o.on("touchstart",z),e.on("touchend",I),this.destroy=function(){e.off("mousemove",S),o.off("mousedown",z),e.off("mouseup",S),e.off("touchmove",S),o.off("touchstart",z),e.off("touchend",S),o.remove()}}}]),e.factory("cropPubSub",[function(){return function(){var e={};this.on=function(t,i){return t.split(" ").forEach(function(t){e[t]||(e[t]=[]),e[t].push(i)}),this},this.trigger=function(t,i){return angular.forEach(e[t],function(e){e.call(null,i)}),this}}}]),e.directive("imgCrop",["$timeout","cropHost","cropPubSub",function(e,t,i){return{restrict:"E",scope:{image:"=",resultImage:"=",changeOnFly:"=",areaType:"@",areaMinSize:"=",resultImageSize:"=",resultImageFormat:"@",resultImageQuality:"=",onChange:"&",onLoadBegin:"&",onLoadDone:"&",onLoadError:"&"},template:"",controller:["$scope",function(e){e.events=new i}],link:function(i,r){var s,o=i.events,a=new t(r.find("canvas"),{},o),n=function(e){var t=a.getResultImageDataURI();s!==t&&(s=t,angular.isDefined(e.resultImage)&&(e.resultImage=t),e.onChange({$dataURI:e.resultImage}))},h=function(t){return function(){e(function(){i.$apply(function(e){t(e)})})}};o.on("load-start",h(function(e){e.onLoadBegin({})})).on("load-done",h(function(e){e.onLoadDone({})})).on("load-error",h(function(e){e.onLoadError({})})).on("area-move area-resize",h(function(e){e.changeOnFly&&n(e)})).on("area-move-end area-resize-end image-updated",h(function(e){n(e)})),i.$watch("image",function(){a.setNewImageSource(i.image)}),i.$watch("areaType",function(){a.setAreaType(i.areaType),n(i)}),i.$watch("areaMinSize",function(){a.setAreaMinSize(i.areaMinSize),n(i)}),i.$watch("resultImageSize",function(){a.setResultImageSize(i.resultImageSize),n(i)}),i.$watch("resultImageFormat",function(){a.setResultImageFormat(i.resultImageFormat),n(i)}),i.$watch("resultImageQuality",function(){a.setResultImageQuality(i.resultImageQuality),n(i)}),i.$watch(function(){return[r[0].clientWidth,r[0].clientHeight]},function(e){a.setMaxDimensions(e[0],e[1]),n(i)},!0),i.$on("$destroy",function(){a.destroy()})}}}])}(); -------------------------------------------------------------------------------- /compile/unminified/ng-img-crop.css: -------------------------------------------------------------------------------- 1 | /* line 1, ../../source/scss/ng-img-crop.scss */ 2 | img-crop { 3 | width: 100%; 4 | height: 100%; 5 | display: block; 6 | position: relative; 7 | overflow: hidden; 8 | } 9 | /* line 7, ../../source/scss/ng-img-crop.scss */ 10 | img-crop canvas { 11 | display: block; 12 | position: absolute; 13 | top: 50%; 14 | left: 50%; 15 | outline: none; 16 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 17 | /* mobile webkit */ 18 | } 19 | -------------------------------------------------------------------------------- /compile/unminified/ng-img-crop.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngImgCrop v0.3.2 3 | * https://github.com/alexk111/ngImgCrop 4 | * 5 | * Copyright (c) 2014 Alex Kaul 6 | * License: MIT 7 | * 8 | * Generated at Wednesday, December 3rd, 2014, 3:54:12 PM 9 | */ 10 | (function() { 11 | 'use strict'; 12 | 13 | var crop = angular.module('ngImgCrop', []); 14 | 15 | crop.factory('cropAreaCircle', ['cropArea', function(CropArea) { 16 | var CropAreaCircle = function() { 17 | CropArea.apply(this, arguments); 18 | 19 | this._boxResizeBaseSize = 20; 20 | this._boxResizeNormalRatio = 0.9; 21 | this._boxResizeHoverRatio = 1.2; 22 | this._iconMoveNormalRatio = 0.9; 23 | this._iconMoveHoverRatio = 1.2; 24 | 25 | this._boxResizeNormalSize = this._boxResizeBaseSize*this._boxResizeNormalRatio; 26 | this._boxResizeHoverSize = this._boxResizeBaseSize*this._boxResizeHoverRatio; 27 | 28 | this._posDragStartX=0; 29 | this._posDragStartY=0; 30 | this._posResizeStartX=0; 31 | this._posResizeStartY=0; 32 | this._posResizeStartSize=0; 33 | 34 | this._boxResizeIsHover = false; 35 | this._areaIsHover = false; 36 | this._boxResizeIsDragging = false; 37 | this._areaIsDragging = false; 38 | }; 39 | 40 | CropAreaCircle.prototype = new CropArea(); 41 | 42 | CropAreaCircle.prototype._calcCirclePerimeterCoords=function(angleDegrees) { 43 | var hSize=this._size/2; 44 | var angleRadians=angleDegrees * (Math.PI / 180), 45 | circlePerimeterX=this._x + hSize * Math.cos(angleRadians), 46 | circlePerimeterY=this._y + hSize * Math.sin(angleRadians); 47 | return [circlePerimeterX, circlePerimeterY]; 48 | }; 49 | 50 | CropAreaCircle.prototype._calcResizeIconCenterCoords=function() { 51 | return this._calcCirclePerimeterCoords(-45); 52 | }; 53 | 54 | CropAreaCircle.prototype._isCoordWithinArea=function(coord) { 55 | return Math.sqrt((coord[0]-this._x)*(coord[0]-this._x) + (coord[1]-this._y)*(coord[1]-this._y)) < this._size/2; 56 | }; 57 | CropAreaCircle.prototype._isCoordWithinBoxResize=function(coord) { 58 | var resizeIconCenterCoords=this._calcResizeIconCenterCoords(); 59 | var hSize=this._boxResizeHoverSize/2; 60 | return(coord[0] > resizeIconCenterCoords[0] - hSize && coord[0] < resizeIconCenterCoords[0] + hSize && 61 | coord[1] > resizeIconCenterCoords[1] - hSize && coord[1] < resizeIconCenterCoords[1] + hSize); 62 | }; 63 | 64 | CropAreaCircle.prototype._drawArea=function(ctx,centerCoords,size){ 65 | ctx.arc(centerCoords[0],centerCoords[1],size/2,0,2*Math.PI); 66 | }; 67 | 68 | CropAreaCircle.prototype.draw=function() { 69 | CropArea.prototype.draw.apply(this, arguments); 70 | 71 | // draw move icon 72 | this._cropCanvas.drawIconMove([this._x,this._y], this._areaIsHover?this._iconMoveHoverRatio:this._iconMoveNormalRatio); 73 | 74 | // draw resize cubes 75 | this._cropCanvas.drawIconResizeBoxNESW(this._calcResizeIconCenterCoords(), this._boxResizeBaseSize, this._boxResizeIsHover?this._boxResizeHoverRatio:this._boxResizeNormalRatio); 76 | }; 77 | 78 | CropAreaCircle.prototype.processMouseMove=function(mouseCurX, mouseCurY) { 79 | var cursor='default'; 80 | var res=false; 81 | 82 | this._boxResizeIsHover = false; 83 | this._areaIsHover = false; 84 | 85 | if (this._areaIsDragging) { 86 | this._x = mouseCurX - this._posDragStartX; 87 | this._y = mouseCurY - this._posDragStartY; 88 | this._areaIsHover = true; 89 | cursor='move'; 90 | res=true; 91 | this._events.trigger('area-move'); 92 | } else if (this._boxResizeIsDragging) { 93 | cursor = 'nesw-resize'; 94 | var iFR, iFX, iFY; 95 | iFX = mouseCurX - this._posResizeStartX; 96 | iFY = this._posResizeStartY - mouseCurY; 97 | if(iFX>iFY) { 98 | iFR = this._posResizeStartSize + iFY*2; 99 | } else { 100 | iFR = this._posResizeStartSize + iFX*2; 101 | } 102 | 103 | this._size = Math.max(this._minSize, iFR); 104 | this._boxResizeIsHover = true; 105 | res=true; 106 | this._events.trigger('area-resize'); 107 | } else if (this._isCoordWithinBoxResize([mouseCurX,mouseCurY])) { 108 | cursor = 'nesw-resize'; 109 | this._areaIsHover = false; 110 | this._boxResizeIsHover = true; 111 | res=true; 112 | } else if(this._isCoordWithinArea([mouseCurX,mouseCurY])) { 113 | cursor = 'move'; 114 | this._areaIsHover = true; 115 | res=true; 116 | } 117 | 118 | this._dontDragOutside(); 119 | angular.element(this._ctx.canvas).css({'cursor': cursor}); 120 | 121 | return res; 122 | }; 123 | 124 | CropAreaCircle.prototype.processMouseDown=function(mouseDownX, mouseDownY) { 125 | if (this._isCoordWithinBoxResize([mouseDownX,mouseDownY])) { 126 | this._areaIsDragging = false; 127 | this._areaIsHover = false; 128 | this._boxResizeIsDragging = true; 129 | this._boxResizeIsHover = true; 130 | this._posResizeStartX=mouseDownX; 131 | this._posResizeStartY=mouseDownY; 132 | this._posResizeStartSize = this._size; 133 | this._events.trigger('area-resize-start'); 134 | } else if (this._isCoordWithinArea([mouseDownX,mouseDownY])) { 135 | this._areaIsDragging = true; 136 | this._areaIsHover = true; 137 | this._boxResizeIsDragging = false; 138 | this._boxResizeIsHover = false; 139 | this._posDragStartX = mouseDownX - this._x; 140 | this._posDragStartY = mouseDownY - this._y; 141 | this._events.trigger('area-move-start'); 142 | } 143 | }; 144 | 145 | CropAreaCircle.prototype.processMouseUp=function(/*mouseUpX, mouseUpY*/) { 146 | if(this._areaIsDragging) { 147 | this._areaIsDragging = false; 148 | this._events.trigger('area-move-end'); 149 | } 150 | if(this._boxResizeIsDragging) { 151 | this._boxResizeIsDragging = false; 152 | this._events.trigger('area-resize-end'); 153 | } 154 | this._areaIsHover = false; 155 | this._boxResizeIsHover = false; 156 | 157 | this._posDragStartX = 0; 158 | this._posDragStartY = 0; 159 | }; 160 | 161 | return CropAreaCircle; 162 | }]); 163 | 164 | 165 | 166 | crop.factory('cropAreaSquare', ['cropArea', function(CropArea) { 167 | var CropAreaSquare = function() { 168 | CropArea.apply(this, arguments); 169 | 170 | this._resizeCtrlBaseRadius = 10; 171 | this._resizeCtrlNormalRatio = 0.75; 172 | this._resizeCtrlHoverRatio = 1; 173 | this._iconMoveNormalRatio = 0.9; 174 | this._iconMoveHoverRatio = 1.2; 175 | 176 | this._resizeCtrlNormalRadius = this._resizeCtrlBaseRadius*this._resizeCtrlNormalRatio; 177 | this._resizeCtrlHoverRadius = this._resizeCtrlBaseRadius*this._resizeCtrlHoverRatio; 178 | 179 | this._posDragStartX=0; 180 | this._posDragStartY=0; 181 | this._posResizeStartX=0; 182 | this._posResizeStartY=0; 183 | this._posResizeStartSize=0; 184 | 185 | this._resizeCtrlIsHover = -1; 186 | this._areaIsHover = false; 187 | this._resizeCtrlIsDragging = -1; 188 | this._areaIsDragging = false; 189 | }; 190 | 191 | CropAreaSquare.prototype = new CropArea(); 192 | 193 | CropAreaSquare.prototype._calcSquareCorners=function() { 194 | var hSize=this._size/2; 195 | return [ 196 | [this._x-hSize, this._y-hSize], 197 | [this._x+hSize, this._y-hSize], 198 | [this._x-hSize, this._y+hSize], 199 | [this._x+hSize, this._y+hSize] 200 | ]; 201 | }; 202 | 203 | CropAreaSquare.prototype._calcSquareDimensions=function() { 204 | var hSize=this._size/2; 205 | return { 206 | left: this._x-hSize, 207 | top: this._y-hSize, 208 | right: this._x+hSize, 209 | bottom: this._y+hSize 210 | }; 211 | }; 212 | 213 | CropAreaSquare.prototype._isCoordWithinArea=function(coord) { 214 | var squareDimensions=this._calcSquareDimensions(); 215 | return (coord[0]>=squareDimensions.left&&coord[0]<=squareDimensions.right&&coord[1]>=squareDimensions.top&&coord[1]<=squareDimensions.bottom); 216 | }; 217 | 218 | CropAreaSquare.prototype._isCoordWithinResizeCtrl=function(coord) { 219 | var resizeIconsCenterCoords=this._calcSquareCorners(); 220 | var res=-1; 221 | for(var i=0,len=resizeIconsCenterCoords.length;i resizeIconCenterCoords[0] - this._resizeCtrlHoverRadius && coord[0] < resizeIconCenterCoords[0] + this._resizeCtrlHoverRadius && 224 | coord[1] > resizeIconCenterCoords[1] - this._resizeCtrlHoverRadius && coord[1] < resizeIconCenterCoords[1] + this._resizeCtrlHoverRadius) { 225 | res=i; 226 | break; 227 | } 228 | } 229 | return res; 230 | }; 231 | 232 | CropAreaSquare.prototype._drawArea=function(ctx,centerCoords,size){ 233 | var hSize=size/2; 234 | ctx.rect(centerCoords[0]-hSize,centerCoords[1]-hSize,size,size); 235 | }; 236 | 237 | CropAreaSquare.prototype.draw=function() { 238 | CropArea.prototype.draw.apply(this, arguments); 239 | 240 | // draw move icon 241 | this._cropCanvas.drawIconMove([this._x,this._y], this._areaIsHover?this._iconMoveHoverRatio:this._iconMoveNormalRatio); 242 | 243 | // draw resize cubes 244 | var resizeIconsCenterCoords=this._calcSquareCorners(); 245 | for(var i=0,len=resizeIconsCenterCoords.length;i-1) { 266 | var xMulti, yMulti; 267 | switch(this._resizeCtrlIsDragging) { 268 | case 0: // Top Left 269 | xMulti=-1; 270 | yMulti=-1; 271 | cursor = 'nwse-resize'; 272 | break; 273 | case 1: // Top Right 274 | xMulti=1; 275 | yMulti=-1; 276 | cursor = 'nesw-resize'; 277 | break; 278 | case 2: // Bottom Left 279 | xMulti=-1; 280 | yMulti=1; 281 | cursor = 'nesw-resize'; 282 | break; 283 | case 3: // Bottom Right 284 | xMulti=1; 285 | yMulti=1; 286 | cursor = 'nwse-resize'; 287 | break; 288 | } 289 | var iFX = (mouseCurX - this._posResizeStartX)*xMulti; 290 | var iFY = (mouseCurY - this._posResizeStartY)*yMulti; 291 | var iFR; 292 | if(iFX>iFY) { 293 | iFR = this._posResizeStartSize + iFY; 294 | } else { 295 | iFR = this._posResizeStartSize + iFX; 296 | } 297 | var wasSize=this._size; 298 | this._size = Math.max(this._minSize, iFR); 299 | var posModifier=(this._size-wasSize)/2; 300 | this._x+=posModifier*xMulti; 301 | this._y+=posModifier*yMulti; 302 | this._resizeCtrlIsHover = this._resizeCtrlIsDragging; 303 | res=true; 304 | this._events.trigger('area-resize'); 305 | } else { 306 | var hoveredResizeBox=this._isCoordWithinResizeCtrl([mouseCurX,mouseCurY]); 307 | if (hoveredResizeBox>-1) { 308 | switch(hoveredResizeBox) { 309 | case 0: 310 | cursor = 'nwse-resize'; 311 | break; 312 | case 1: 313 | cursor = 'nesw-resize'; 314 | break; 315 | case 2: 316 | cursor = 'nesw-resize'; 317 | break; 318 | case 3: 319 | cursor = 'nwse-resize'; 320 | break; 321 | } 322 | this._areaIsHover = false; 323 | this._resizeCtrlIsHover = hoveredResizeBox; 324 | res=true; 325 | } else if(this._isCoordWithinArea([mouseCurX,mouseCurY])) { 326 | cursor = 'move'; 327 | this._areaIsHover = true; 328 | res=true; 329 | } 330 | } 331 | 332 | this._dontDragOutside(); 333 | angular.element(this._ctx.canvas).css({'cursor': cursor}); 334 | 335 | return res; 336 | }; 337 | 338 | CropAreaSquare.prototype.processMouseDown=function(mouseDownX, mouseDownY) { 339 | var isWithinResizeCtrl=this._isCoordWithinResizeCtrl([mouseDownX,mouseDownY]); 340 | if (isWithinResizeCtrl>-1) { 341 | this._areaIsDragging = false; 342 | this._areaIsHover = false; 343 | this._resizeCtrlIsDragging = isWithinResizeCtrl; 344 | this._resizeCtrlIsHover = isWithinResizeCtrl; 345 | this._posResizeStartX=mouseDownX; 346 | this._posResizeStartY=mouseDownY; 347 | this._posResizeStartSize = this._size; 348 | this._events.trigger('area-resize-start'); 349 | } else if (this._isCoordWithinArea([mouseDownX,mouseDownY])) { 350 | this._areaIsDragging = true; 351 | this._areaIsHover = true; 352 | this._resizeCtrlIsDragging = -1; 353 | this._resizeCtrlIsHover = -1; 354 | this._posDragStartX = mouseDownX - this._x; 355 | this._posDragStartY = mouseDownY - this._y; 356 | this._events.trigger('area-move-start'); 357 | } 358 | }; 359 | 360 | CropAreaSquare.prototype.processMouseUp=function(/*mouseUpX, mouseUpY*/) { 361 | if(this._areaIsDragging) { 362 | this._areaIsDragging = false; 363 | this._events.trigger('area-move-end'); 364 | } 365 | if(this._resizeCtrlIsDragging>-1) { 366 | this._resizeCtrlIsDragging = -1; 367 | this._events.trigger('area-resize-end'); 368 | } 369 | this._areaIsHover = false; 370 | this._resizeCtrlIsHover = -1; 371 | 372 | this._posDragStartX = 0; 373 | this._posDragStartY = 0; 374 | }; 375 | 376 | return CropAreaSquare; 377 | }]); 378 | 379 | crop.factory('cropArea', ['cropCanvas', function(CropCanvas) { 380 | var CropArea = function(ctx, events) { 381 | this._ctx=ctx; 382 | this._events=events; 383 | 384 | this._minSize=80; 385 | 386 | this._cropCanvas=new CropCanvas(ctx); 387 | 388 | this._image=new Image(); 389 | this._x = 0; 390 | this._y = 0; 391 | this._size = 200; 392 | }; 393 | 394 | /* GETTERS/SETTERS */ 395 | 396 | CropArea.prototype.getImage = function () { 397 | return this._image; 398 | }; 399 | CropArea.prototype.setImage = function (image) { 400 | this._image = image; 401 | }; 402 | 403 | CropArea.prototype.getX = function () { 404 | return this._x; 405 | }; 406 | CropArea.prototype.setX = function (x) { 407 | this._x = x; 408 | this._dontDragOutside(); 409 | }; 410 | 411 | CropArea.prototype.getY = function () { 412 | return this._y; 413 | }; 414 | CropArea.prototype.setY = function (y) { 415 | this._y = y; 416 | this._dontDragOutside(); 417 | }; 418 | 419 | CropArea.prototype.getSize = function () { 420 | return this._size; 421 | }; 422 | CropArea.prototype.setSize = function (size) { 423 | this._size = Math.max(this._minSize, size); 424 | this._dontDragOutside(); 425 | }; 426 | 427 | CropArea.prototype.getMinSize = function () { 428 | return this._minSize; 429 | }; 430 | CropArea.prototype.setMinSize = function (size) { 431 | this._minSize = size; 432 | this._size = Math.max(this._minSize, this._size); 433 | this._dontDragOutside(); 434 | }; 435 | 436 | /* FUNCTIONS */ 437 | CropArea.prototype._dontDragOutside=function() { 438 | var h=this._ctx.canvas.height, 439 | w=this._ctx.canvas.width; 440 | if(this._size>w) { this._size=w; } 441 | if(this._size>h) { this._size=h; } 442 | if(this._xw-this._size/2) { this._x=w-this._size/2; } 444 | if(this._yh-this._size/2) { this._y=h-this._size/2; } 446 | }; 447 | 448 | CropArea.prototype._drawArea=function() {}; 449 | 450 | CropArea.prototype.draw=function() { 451 | // draw crop area 452 | this._cropCanvas.drawCropArea(this._image,[this._x,this._y],this._size,this._drawArea); 453 | }; 454 | 455 | CropArea.prototype.processMouseMove=function() {}; 456 | 457 | CropArea.prototype.processMouseDown=function() {}; 458 | 459 | CropArea.prototype.processMouseUp=function() {}; 460 | 461 | return CropArea; 462 | }]); 463 | 464 | crop.factory('cropCanvas', [function() { 465 | // Shape = Array of [x,y]; [0, 0] - center 466 | var shapeArrowNW=[[-0.5,-2],[-3,-4.5],[-0.5,-7],[-7,-7],[-7,-0.5],[-4.5,-3],[-2,-0.5]]; 467 | var shapeArrowNE=[[0.5,-2],[3,-4.5],[0.5,-7],[7,-7],[7,-0.5],[4.5,-3],[2,-0.5]]; 468 | var shapeArrowSW=[[-0.5,2],[-3,4.5],[-0.5,7],[-7,7],[-7,0.5],[-4.5,3],[-2,0.5]]; 469 | var shapeArrowSE=[[0.5,2],[3,4.5],[0.5,7],[7,7],[7,0.5],[4.5,3],[2,0.5]]; 470 | var shapeArrowN=[[-1.5,-2.5],[-1.5,-6],[-5,-6],[0,-11],[5,-6],[1.5,-6],[1.5,-2.5]]; 471 | var shapeArrowW=[[-2.5,-1.5],[-6,-1.5],[-6,-5],[-11,0],[-6,5],[-6,1.5],[-2.5,1.5]]; 472 | var shapeArrowS=[[-1.5,2.5],[-1.5,6],[-5,6],[0,11],[5,6],[1.5,6],[1.5,2.5]]; 473 | var shapeArrowE=[[2.5,-1.5],[6,-1.5],[6,-5],[11,0],[6,5],[6,1.5],[2.5,1.5]]; 474 | 475 | // Colors 476 | var colors={ 477 | areaOutline: '#fff', 478 | resizeBoxStroke: '#fff', 479 | resizeBoxFill: '#444', 480 | resizeBoxArrowFill: '#fff', 481 | resizeCircleStroke: '#fff', 482 | resizeCircleFill: '#444', 483 | moveIconFill: '#fff' 484 | }; 485 | 486 | return function(ctx){ 487 | 488 | /* Base functions */ 489 | 490 | // Calculate Point 491 | var calcPoint=function(point,offset,scale) { 492 | return [scale*point[0]+offset[0], scale*point[1]+offset[1]]; 493 | }; 494 | 495 | // Draw Filled Polygon 496 | var drawFilledPolygon=function(shape,fillStyle,centerCoords,scale) { 497 | ctx.save(); 498 | ctx.fillStyle = fillStyle; 499 | ctx.beginPath(); 500 | var pc, pc0=calcPoint(shape[0],centerCoords,scale); 501 | ctx.moveTo(pc0[0],pc0[1]); 502 | 503 | for(var p in shape) { 504 | if (p > 0) { 505 | pc=calcPoint(shape[p],centerCoords,scale); 506 | ctx.lineTo(pc[0],pc[1]); 507 | } 508 | } 509 | 510 | ctx.lineTo(pc0[0],pc0[1]); 511 | ctx.fill(); 512 | ctx.closePath(); 513 | ctx.restore(); 514 | }; 515 | 516 | /* Icons */ 517 | 518 | this.drawIconMove=function(centerCoords, scale) { 519 | drawFilledPolygon(shapeArrowN, colors.moveIconFill, centerCoords, scale); 520 | drawFilledPolygon(shapeArrowW, colors.moveIconFill, centerCoords, scale); 521 | drawFilledPolygon(shapeArrowS, colors.moveIconFill, centerCoords, scale); 522 | drawFilledPolygon(shapeArrowE, colors.moveIconFill, centerCoords, scale); 523 | }; 524 | 525 | this.drawIconResizeCircle=function(centerCoords, circleRadius, scale) { 526 | var scaledCircleRadius=circleRadius*scale; 527 | ctx.save(); 528 | ctx.strokeStyle = colors.resizeCircleStroke; 529 | ctx.lineWidth = 2; 530 | ctx.fillStyle = colors.resizeCircleFill; 531 | ctx.beginPath(); 532 | ctx.arc(centerCoords[0],centerCoords[1],scaledCircleRadius,0,2*Math.PI); 533 | ctx.fill(); 534 | ctx.stroke(); 535 | ctx.closePath(); 536 | ctx.restore(); 537 | }; 538 | 539 | this.drawIconResizeBoxBase=function(centerCoords, boxSize, scale) { 540 | var scaledBoxSize=boxSize*scale; 541 | ctx.save(); 542 | ctx.strokeStyle = colors.resizeBoxStroke; 543 | ctx.lineWidth = 2; 544 | ctx.fillStyle = colors.resizeBoxFill; 545 | ctx.fillRect(centerCoords[0] - scaledBoxSize/2, centerCoords[1] - scaledBoxSize/2, scaledBoxSize, scaledBoxSize); 546 | ctx.strokeRect(centerCoords[0] - scaledBoxSize/2, centerCoords[1] - scaledBoxSize/2, scaledBoxSize, scaledBoxSize); 547 | ctx.restore(); 548 | }; 549 | this.drawIconResizeBoxNESW=function(centerCoords, boxSize, scale) { 550 | this.drawIconResizeBoxBase(centerCoords, boxSize, scale); 551 | drawFilledPolygon(shapeArrowNE, colors.resizeBoxArrowFill, centerCoords, scale); 552 | drawFilledPolygon(shapeArrowSW, colors.resizeBoxArrowFill, centerCoords, scale); 553 | }; 554 | this.drawIconResizeBoxNWSE=function(centerCoords, boxSize, scale) { 555 | this.drawIconResizeBoxBase(centerCoords, boxSize, scale); 556 | drawFilledPolygon(shapeArrowNW, colors.resizeBoxArrowFill, centerCoords, scale); 557 | drawFilledPolygon(shapeArrowSE, colors.resizeBoxArrowFill, centerCoords, scale); 558 | }; 559 | 560 | /* Crop Area */ 561 | 562 | this.drawCropArea=function(image, centerCoords, size, fnDrawClipPath) { 563 | var xRatio=image.width/ctx.canvas.width, 564 | yRatio=image.height/ctx.canvas.height, 565 | xLeft=centerCoords[0]-size/2, 566 | yTop=centerCoords[1]-size/2; 567 | 568 | ctx.save(); 569 | ctx.strokeStyle = colors.areaOutline; 570 | ctx.lineWidth = 2; 571 | ctx.beginPath(); 572 | fnDrawClipPath(ctx, centerCoords, size); 573 | ctx.stroke(); 574 | ctx.clip(); 575 | 576 | // draw part of original image 577 | if (size > 0) { 578 | ctx.drawImage(image, xLeft*xRatio, yTop*yRatio, size*xRatio, size*yRatio, xLeft, yTop, size, size); 579 | } 580 | 581 | ctx.beginPath(); 582 | fnDrawClipPath(ctx, centerCoords, size); 583 | ctx.stroke(); 584 | ctx.clip(); 585 | 586 | ctx.restore(); 587 | }; 588 | 589 | }; 590 | }]); 591 | 592 | /** 593 | * EXIF service is based on the exif-js library (https://github.com/jseidelin/exif-js) 594 | */ 595 | 596 | crop.service('cropEXIF', [function() { 597 | var debug = false; 598 | 599 | var ExifTags = this.Tags = { 600 | 601 | // version tags 602 | 0x9000 : "ExifVersion", // EXIF version 603 | 0xA000 : "FlashpixVersion", // Flashpix format version 604 | 605 | // colorspace tags 606 | 0xA001 : "ColorSpace", // Color space information tag 607 | 608 | // image configuration 609 | 0xA002 : "PixelXDimension", // Valid width of meaningful image 610 | 0xA003 : "PixelYDimension", // Valid height of meaningful image 611 | 0x9101 : "ComponentsConfiguration", // Information about channels 612 | 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel 613 | 614 | // user information 615 | 0x927C : "MakerNote", // Any desired information written by the manufacturer 616 | 0x9286 : "UserComment", // Comments by user 617 | 618 | // related file 619 | 0xA004 : "RelatedSoundFile", // Name of related sound file 620 | 621 | // date and time 622 | 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated 623 | 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally 624 | 0x9290 : "SubsecTime", // Fractions of seconds for DateTime 625 | 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal 626 | 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized 627 | 628 | // picture-taking conditions 629 | 0x829A : "ExposureTime", // Exposure time (in seconds) 630 | 0x829D : "FNumber", // F number 631 | 0x8822 : "ExposureProgram", // Exposure program 632 | 0x8824 : "SpectralSensitivity", // Spectral sensitivity 633 | 0x8827 : "ISOSpeedRatings", // ISO speed rating 634 | 0x8828 : "OECF", // Optoelectric conversion factor 635 | 0x9201 : "ShutterSpeedValue", // Shutter speed 636 | 0x9202 : "ApertureValue", // Lens aperture 637 | 0x9203 : "BrightnessValue", // Value of brightness 638 | 0x9204 : "ExposureBias", // Exposure bias 639 | 0x9205 : "MaxApertureValue", // Smallest F number of lens 640 | 0x9206 : "SubjectDistance", // Distance to subject in meters 641 | 0x9207 : "MeteringMode", // Metering mode 642 | 0x9208 : "LightSource", // Kind of light source 643 | 0x9209 : "Flash", // Flash status 644 | 0x9214 : "SubjectArea", // Location and area of main subject 645 | 0x920A : "FocalLength", // Focal length of the lens in mm 646 | 0xA20B : "FlashEnergy", // Strobe energy in BCPS 647 | 0xA20C : "SpatialFrequencyResponse", // 648 | 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit 649 | 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit 650 | 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution 651 | 0xA214 : "SubjectLocation", // Location of subject in image 652 | 0xA215 : "ExposureIndex", // Exposure index selected on camera 653 | 0xA217 : "SensingMethod", // Image sensor type 654 | 0xA300 : "FileSource", // Image source (3 == DSC) 655 | 0xA301 : "SceneType", // Scene type (1 == directly photographed) 656 | 0xA302 : "CFAPattern", // Color filter array geometric pattern 657 | 0xA401 : "CustomRendered", // Special processing 658 | 0xA402 : "ExposureMode", // Exposure mode 659 | 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual 660 | 0xA404 : "DigitalZoomRation", // Digital zoom ratio 661 | 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) 662 | 0xA406 : "SceneCaptureType", // Type of scene 663 | 0xA407 : "GainControl", // Degree of overall image gain adjustment 664 | 0xA408 : "Contrast", // Direction of contrast processing applied by camera 665 | 0xA409 : "Saturation", // Direction of saturation processing applied by camera 666 | 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera 667 | 0xA40B : "DeviceSettingDescription", // 668 | 0xA40C : "SubjectDistanceRange", // Distance to subject 669 | 670 | // other tags 671 | 0xA005 : "InteroperabilityIFDPointer", 672 | 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image 673 | }; 674 | 675 | var TiffTags = this.TiffTags = { 676 | 0x0100 : "ImageWidth", 677 | 0x0101 : "ImageHeight", 678 | 0x8769 : "ExifIFDPointer", 679 | 0x8825 : "GPSInfoIFDPointer", 680 | 0xA005 : "InteroperabilityIFDPointer", 681 | 0x0102 : "BitsPerSample", 682 | 0x0103 : "Compression", 683 | 0x0106 : "PhotometricInterpretation", 684 | 0x0112 : "Orientation", 685 | 0x0115 : "SamplesPerPixel", 686 | 0x011C : "PlanarConfiguration", 687 | 0x0212 : "YCbCrSubSampling", 688 | 0x0213 : "YCbCrPositioning", 689 | 0x011A : "XResolution", 690 | 0x011B : "YResolution", 691 | 0x0128 : "ResolutionUnit", 692 | 0x0111 : "StripOffsets", 693 | 0x0116 : "RowsPerStrip", 694 | 0x0117 : "StripByteCounts", 695 | 0x0201 : "JPEGInterchangeFormat", 696 | 0x0202 : "JPEGInterchangeFormatLength", 697 | 0x012D : "TransferFunction", 698 | 0x013E : "WhitePoint", 699 | 0x013F : "PrimaryChromaticities", 700 | 0x0211 : "YCbCrCoefficients", 701 | 0x0214 : "ReferenceBlackWhite", 702 | 0x0132 : "DateTime", 703 | 0x010E : "ImageDescription", 704 | 0x010F : "Make", 705 | 0x0110 : "Model", 706 | 0x0131 : "Software", 707 | 0x013B : "Artist", 708 | 0x8298 : "Copyright" 709 | }; 710 | 711 | var GPSTags = this.GPSTags = { 712 | 0x0000 : "GPSVersionID", 713 | 0x0001 : "GPSLatitudeRef", 714 | 0x0002 : "GPSLatitude", 715 | 0x0003 : "GPSLongitudeRef", 716 | 0x0004 : "GPSLongitude", 717 | 0x0005 : "GPSAltitudeRef", 718 | 0x0006 : "GPSAltitude", 719 | 0x0007 : "GPSTimeStamp", 720 | 0x0008 : "GPSSatellites", 721 | 0x0009 : "GPSStatus", 722 | 0x000A : "GPSMeasureMode", 723 | 0x000B : "GPSDOP", 724 | 0x000C : "GPSSpeedRef", 725 | 0x000D : "GPSSpeed", 726 | 0x000E : "GPSTrackRef", 727 | 0x000F : "GPSTrack", 728 | 0x0010 : "GPSImgDirectionRef", 729 | 0x0011 : "GPSImgDirection", 730 | 0x0012 : "GPSMapDatum", 731 | 0x0013 : "GPSDestLatitudeRef", 732 | 0x0014 : "GPSDestLatitude", 733 | 0x0015 : "GPSDestLongitudeRef", 734 | 0x0016 : "GPSDestLongitude", 735 | 0x0017 : "GPSDestBearingRef", 736 | 0x0018 : "GPSDestBearing", 737 | 0x0019 : "GPSDestDistanceRef", 738 | 0x001A : "GPSDestDistance", 739 | 0x001B : "GPSProcessingMethod", 740 | 0x001C : "GPSAreaInformation", 741 | 0x001D : "GPSDateStamp", 742 | 0x001E : "GPSDifferential" 743 | }; 744 | 745 | var StringValues = this.StringValues = { 746 | ExposureProgram : { 747 | 0 : "Not defined", 748 | 1 : "Manual", 749 | 2 : "Normal program", 750 | 3 : "Aperture priority", 751 | 4 : "Shutter priority", 752 | 5 : "Creative program", 753 | 6 : "Action program", 754 | 7 : "Portrait mode", 755 | 8 : "Landscape mode" 756 | }, 757 | MeteringMode : { 758 | 0 : "Unknown", 759 | 1 : "Average", 760 | 2 : "CenterWeightedAverage", 761 | 3 : "Spot", 762 | 4 : "MultiSpot", 763 | 5 : "Pattern", 764 | 6 : "Partial", 765 | 255 : "Other" 766 | }, 767 | LightSource : { 768 | 0 : "Unknown", 769 | 1 : "Daylight", 770 | 2 : "Fluorescent", 771 | 3 : "Tungsten (incandescent light)", 772 | 4 : "Flash", 773 | 9 : "Fine weather", 774 | 10 : "Cloudy weather", 775 | 11 : "Shade", 776 | 12 : "Daylight fluorescent (D 5700 - 7100K)", 777 | 13 : "Day white fluorescent (N 4600 - 5400K)", 778 | 14 : "Cool white fluorescent (W 3900 - 4500K)", 779 | 15 : "White fluorescent (WW 3200 - 3700K)", 780 | 17 : "Standard light A", 781 | 18 : "Standard light B", 782 | 19 : "Standard light C", 783 | 20 : "D55", 784 | 21 : "D65", 785 | 22 : "D75", 786 | 23 : "D50", 787 | 24 : "ISO studio tungsten", 788 | 255 : "Other" 789 | }, 790 | Flash : { 791 | 0x0000 : "Flash did not fire", 792 | 0x0001 : "Flash fired", 793 | 0x0005 : "Strobe return light not detected", 794 | 0x0007 : "Strobe return light detected", 795 | 0x0009 : "Flash fired, compulsory flash mode", 796 | 0x000D : "Flash fired, compulsory flash mode, return light not detected", 797 | 0x000F : "Flash fired, compulsory flash mode, return light detected", 798 | 0x0010 : "Flash did not fire, compulsory flash mode", 799 | 0x0018 : "Flash did not fire, auto mode", 800 | 0x0019 : "Flash fired, auto mode", 801 | 0x001D : "Flash fired, auto mode, return light not detected", 802 | 0x001F : "Flash fired, auto mode, return light detected", 803 | 0x0020 : "No flash function", 804 | 0x0041 : "Flash fired, red-eye reduction mode", 805 | 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", 806 | 0x0047 : "Flash fired, red-eye reduction mode, return light detected", 807 | 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", 808 | 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 809 | 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 810 | 0x0059 : "Flash fired, auto mode, red-eye reduction mode", 811 | 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", 812 | 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" 813 | }, 814 | SensingMethod : { 815 | 1 : "Not defined", 816 | 2 : "One-chip color area sensor", 817 | 3 : "Two-chip color area sensor", 818 | 4 : "Three-chip color area sensor", 819 | 5 : "Color sequential area sensor", 820 | 7 : "Trilinear sensor", 821 | 8 : "Color sequential linear sensor" 822 | }, 823 | SceneCaptureType : { 824 | 0 : "Standard", 825 | 1 : "Landscape", 826 | 2 : "Portrait", 827 | 3 : "Night scene" 828 | }, 829 | SceneType : { 830 | 1 : "Directly photographed" 831 | }, 832 | CustomRendered : { 833 | 0 : "Normal process", 834 | 1 : "Custom process" 835 | }, 836 | WhiteBalance : { 837 | 0 : "Auto white balance", 838 | 1 : "Manual white balance" 839 | }, 840 | GainControl : { 841 | 0 : "None", 842 | 1 : "Low gain up", 843 | 2 : "High gain up", 844 | 3 : "Low gain down", 845 | 4 : "High gain down" 846 | }, 847 | Contrast : { 848 | 0 : "Normal", 849 | 1 : "Soft", 850 | 2 : "Hard" 851 | }, 852 | Saturation : { 853 | 0 : "Normal", 854 | 1 : "Low saturation", 855 | 2 : "High saturation" 856 | }, 857 | Sharpness : { 858 | 0 : "Normal", 859 | 1 : "Soft", 860 | 2 : "Hard" 861 | }, 862 | SubjectDistanceRange : { 863 | 0 : "Unknown", 864 | 1 : "Macro", 865 | 2 : "Close view", 866 | 3 : "Distant view" 867 | }, 868 | FileSource : { 869 | 3 : "DSC" 870 | }, 871 | 872 | Components : { 873 | 0 : "", 874 | 1 : "Y", 875 | 2 : "Cb", 876 | 3 : "Cr", 877 | 4 : "R", 878 | 5 : "G", 879 | 6 : "B" 880 | } 881 | }; 882 | 883 | function addEvent(element, event, handler) { 884 | if (element.addEventListener) { 885 | element.addEventListener(event, handler, false); 886 | } else if (element.attachEvent) { 887 | element.attachEvent("on" + event, handler); 888 | } 889 | } 890 | 891 | function imageHasData(img) { 892 | return !!(img.exifdata); 893 | } 894 | 895 | function base64ToArrayBuffer(base64, contentType) { 896 | contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' 897 | base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); 898 | var binary = atob(base64); 899 | var len = binary.length; 900 | var buffer = new ArrayBuffer(len); 901 | var view = new Uint8Array(buffer); 902 | for (var i = 0; i < len; i++) { 903 | view[i] = binary.charCodeAt(i); 904 | } 905 | return buffer; 906 | } 907 | 908 | function objectURLToBlob(url, callback) { 909 | var http = new XMLHttpRequest(); 910 | http.open("GET", url, true); 911 | http.responseType = "blob"; 912 | http.onload = function(e) { 913 | if (this.status == 200 || this.status === 0) { 914 | callback(this.response); 915 | } 916 | }; 917 | http.send(); 918 | } 919 | 920 | function getImageData(img, callback) { 921 | function handleBinaryFile(binFile) { 922 | var data = findEXIFinJPEG(binFile); 923 | var iptcdata = findIPTCinJPEG(binFile); 924 | img.exifdata = data || {}; 925 | img.iptcdata = iptcdata || {}; 926 | if (callback) { 927 | callback.call(img); 928 | } 929 | } 930 | 931 | if (img.src) { 932 | if (/^data\:/i.test(img.src)) { // Data URI 933 | var arrayBuffer = base64ToArrayBuffer(img.src); 934 | handleBinaryFile(arrayBuffer); 935 | 936 | } else if (/^blob\:/i.test(img.src)) { // Object URL 937 | var fileReader = new FileReader(); 938 | fileReader.onload = function(e) { 939 | handleBinaryFile(e.target.result); 940 | }; 941 | objectURLToBlob(img.src, function (blob) { 942 | fileReader.readAsArrayBuffer(blob); 943 | }); 944 | } else { 945 | var http = new XMLHttpRequest(); 946 | http.onload = function() { 947 | if (this.status == 200 || this.status === 0) { 948 | handleBinaryFile(http.response); 949 | } else { 950 | throw "Could not load image"; 951 | } 952 | http = null; 953 | }; 954 | http.open("GET", img.src, true); 955 | http.responseType = "arraybuffer"; 956 | http.send(null); 957 | } 958 | } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) { 959 | var fileReader = new FileReader(); 960 | fileReader.onload = function(e) { 961 | if (debug) console.log("Got file of length " + e.target.result.byteLength); 962 | handleBinaryFile(e.target.result); 963 | }; 964 | 965 | fileReader.readAsArrayBuffer(img); 966 | } 967 | } 968 | 969 | function findEXIFinJPEG(file) { 970 | var dataView = new DataView(file); 971 | 972 | if (debug) console.log("Got file of length " + file.byteLength); 973 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 974 | if (debug) console.log("Not a valid JPEG"); 975 | return false; // not a valid jpeg 976 | } 977 | 978 | var offset = 2, 979 | length = file.byteLength, 980 | marker; 981 | 982 | while (offset < length) { 983 | if (dataView.getUint8(offset) != 0xFF) { 984 | if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); 985 | return false; // not a valid marker, something is wrong 986 | } 987 | 988 | marker = dataView.getUint8(offset + 1); 989 | if (debug) console.log(marker); 990 | 991 | // we could implement handling for other markers here, 992 | // but we're only looking for 0xFFE1 for EXIF data 993 | 994 | if (marker == 225) { 995 | if (debug) console.log("Found 0xFFE1 marker"); 996 | 997 | return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); 998 | 999 | // offset += 2 + file.getShortAt(offset+2, true); 1000 | 1001 | } else { 1002 | offset += 2 + dataView.getUint16(offset+2); 1003 | } 1004 | 1005 | } 1006 | 1007 | } 1008 | 1009 | function findIPTCinJPEG(file) { 1010 | var dataView = new DataView(file); 1011 | 1012 | if (debug) console.log("Got file of length " + file.byteLength); 1013 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 1014 | if (debug) console.log("Not a valid JPEG"); 1015 | return false; // not a valid jpeg 1016 | } 1017 | 1018 | var offset = 2, 1019 | length = file.byteLength; 1020 | 1021 | var isFieldSegmentStart = function(dataView, offset){ 1022 | return ( 1023 | dataView.getUint8(offset) === 0x38 && 1024 | dataView.getUint8(offset+1) === 0x42 && 1025 | dataView.getUint8(offset+2) === 0x49 && 1026 | dataView.getUint8(offset+3) === 0x4D && 1027 | dataView.getUint8(offset+4) === 0x04 && 1028 | dataView.getUint8(offset+5) === 0x04 1029 | ); 1030 | }; 1031 | 1032 | while (offset < length) { 1033 | 1034 | if ( isFieldSegmentStart(dataView, offset )){ 1035 | 1036 | // Get the length of the name header (which is padded to an even number of bytes) 1037 | var nameHeaderLength = dataView.getUint8(offset+7); 1038 | if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; 1039 | // Check for pre photoshop 6 format 1040 | if(nameHeaderLength === 0) { 1041 | // Always 4 1042 | nameHeaderLength = 4; 1043 | } 1044 | 1045 | var startOffset = offset + 8 + nameHeaderLength; 1046 | var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); 1047 | 1048 | return readIPTCData(file, startOffset, sectionLength); 1049 | 1050 | break; 1051 | 1052 | } 1053 | 1054 | // Not the marker, continue searching 1055 | offset++; 1056 | 1057 | } 1058 | 1059 | } 1060 | var IptcFieldMap = { 1061 | 0x78 : 'caption', 1062 | 0x6E : 'credit', 1063 | 0x19 : 'keywords', 1064 | 0x37 : 'dateCreated', 1065 | 0x50 : 'byline', 1066 | 0x55 : 'bylineTitle', 1067 | 0x7A : 'captionWriter', 1068 | 0x69 : 'headline', 1069 | 0x74 : 'copyright', 1070 | 0x0F : 'category' 1071 | }; 1072 | function readIPTCData(file, startOffset, sectionLength){ 1073 | var dataView = new DataView(file); 1074 | var data = {}; 1075 | var fieldValue, fieldName, dataSize, segmentType, segmentSize; 1076 | var segmentStartPos = startOffset; 1077 | while(segmentStartPos < startOffset+sectionLength) { 1078 | if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ 1079 | segmentType = dataView.getUint8(segmentStartPos+2); 1080 | if(segmentType in IptcFieldMap) { 1081 | dataSize = dataView.getInt16(segmentStartPos+3); 1082 | segmentSize = dataSize + 5; 1083 | fieldName = IptcFieldMap[segmentType]; 1084 | fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); 1085 | // Check if we already stored a value with this name 1086 | if(data.hasOwnProperty(fieldName)) { 1087 | // Value already stored with this name, create multivalue field 1088 | if(data[fieldName] instanceof Array) { 1089 | data[fieldName].push(fieldValue); 1090 | } 1091 | else { 1092 | data[fieldName] = [data[fieldName], fieldValue]; 1093 | } 1094 | } 1095 | else { 1096 | data[fieldName] = fieldValue; 1097 | } 1098 | } 1099 | 1100 | } 1101 | segmentStartPos++; 1102 | } 1103 | return data; 1104 | } 1105 | 1106 | function readTags(file, tiffStart, dirStart, strings, bigEnd) { 1107 | var entries = file.getUint16(dirStart, !bigEnd), 1108 | tags = {}, 1109 | entryOffset, tag, 1110 | i; 1111 | 1112 | for (i=0;i 4 ? valueOffset : (entryOffset + 8); 1136 | vals = []; 1137 | for (n=0;n 4 ? valueOffset : (entryOffset + 8); 1145 | return getStringFromDB(file, offset, numValues-1); 1146 | 1147 | case 3: // short, 16 bit int 1148 | if (numValues == 1) { 1149 | return file.getUint16(entryOffset + 8, !bigEnd); 1150 | } else { 1151 | offset = numValues > 2 ? valueOffset : (entryOffset + 8); 1152 | vals = []; 1153 | for (n=0;nmaxCanvasDims[0]) { 1447 | canvasDims[0]=maxCanvasDims[0]; 1448 | canvasDims[1]=canvasDims[0]/imageRatio; 1449 | } else if(canvasDims[0]maxCanvasDims[1]) { 1454 | canvasDims[1]=maxCanvasDims[1]; 1455 | canvasDims[0]=canvasDims[1]*imageRatio; 1456 | } else if(canvasDims[1]')[0]; 1537 | temp_ctx = temp_canvas.getContext('2d'); 1538 | temp_canvas.width = resImgSize; 1539 | temp_canvas.height = resImgSize; 1540 | if(image!==null){ 1541 | temp_ctx.drawImage(image, (theArea.getX()-theArea.getSize()/2)*(image.width/ctx.canvas.width), (theArea.getY()-theArea.getSize()/2)*(image.height/ctx.canvas.height), theArea.getSize()*(image.width/ctx.canvas.width), theArea.getSize()*(image.height/ctx.canvas.height), 0, 0, resImgSize, resImgSize); 1542 | } 1543 | if (resImgQuality!==null ){ 1544 | return temp_canvas.toDataURL(resImgFormat, resImgQuality); 1545 | } 1546 | return temp_canvas.toDataURL(resImgFormat); 1547 | }; 1548 | 1549 | this.setNewImageSource=function(imageSource) { 1550 | image=null; 1551 | resetCropHost(); 1552 | events.trigger('image-updated'); 1553 | if(!!imageSource) { 1554 | var newImage = new Image(); 1555 | if(imageSource.substring(0,4).toLowerCase()==='http') { 1556 | newImage.crossOrigin = 'anonymous'; 1557 | } 1558 | newImage.onload = function(){ 1559 | events.trigger('load-done'); 1560 | 1561 | cropEXIF.getData(newImage,function(){ 1562 | var orientation=cropEXIF.getTag(newImage,'Orientation'); 1563 | 1564 | if([3,6,8].indexOf(orientation)>-1) { 1565 | var canvas = document.createElement("canvas"), 1566 | ctx=canvas.getContext("2d"), 1567 | cw = newImage.width, ch = newImage.height, cx = 0, cy = 0, deg=0; 1568 | switch(orientation) { 1569 | case 3: 1570 | cx=-newImage.width; 1571 | cy=-newImage.height; 1572 | deg=180; 1573 | break; 1574 | case 6: 1575 | cw = newImage.height; 1576 | ch = newImage.width; 1577 | cy=-newImage.height; 1578 | deg=90; 1579 | break; 1580 | case 8: 1581 | cw = newImage.height; 1582 | ch = newImage.width; 1583 | cx=-newImage.width; 1584 | deg=270; 1585 | break; 1586 | } 1587 | 1588 | canvas.width = cw; 1589 | canvas.height = ch; 1590 | ctx.rotate(deg*Math.PI/180); 1591 | ctx.drawImage(newImage, cx, cy); 1592 | 1593 | image=new Image(); 1594 | image.src = canvas.toDataURL("image/png"); 1595 | } else { 1596 | image=newImage; 1597 | } 1598 | resetCropHost(); 1599 | events.trigger('image-updated'); 1600 | }); 1601 | }; 1602 | newImage.onerror=function() { 1603 | events.trigger('load-error'); 1604 | }; 1605 | events.trigger('load-start'); 1606 | newImage.src=imageSource; 1607 | } 1608 | }; 1609 | 1610 | this.setMaxDimensions=function(width, height) { 1611 | maxCanvasDims=[width,height]; 1612 | 1613 | if(image!==null) { 1614 | var curWidth=ctx.canvas.width, 1615 | curHeight=ctx.canvas.height; 1616 | 1617 | var imageDims=[image.width, image.height], 1618 | imageRatio=image.width/image.height, 1619 | canvasDims=imageDims; 1620 | 1621 | if(canvasDims[0]>maxCanvasDims[0]) { 1622 | canvasDims[0]=maxCanvasDims[0]; 1623 | canvasDims[1]=canvasDims[0]/imageRatio; 1624 | } else if(canvasDims[0]maxCanvasDims[1]) { 1629 | canvasDims[1]=maxCanvasDims[1]; 1630 | canvasDims[0]=canvasDims[1]*imageRatio; 1631 | } else if(canvasDims[1]=0 && quality<=1){ 1674 | resImgQuality = quality; 1675 | } 1676 | }; 1677 | 1678 | this.setAreaType=function(type) { 1679 | var curSize=theArea.getSize(), 1680 | curMinSize=theArea.getMinSize(), 1681 | curX=theArea.getX(), 1682 | curY=theArea.getY(); 1683 | 1684 | var AreaClass=CropAreaCircle; 1685 | if(type==='square') { 1686 | AreaClass=CropAreaSquare; 1687 | } 1688 | theArea = new AreaClass(ctx, events); 1689 | theArea.setMinSize(curMinSize); 1690 | theArea.setSize(curSize); 1691 | theArea.setX(curX); 1692 | theArea.setY(curY); 1693 | 1694 | // resetCropHost(); 1695 | if(image!==null) { 1696 | theArea.setImage(image); 1697 | } 1698 | 1699 | drawScene(); 1700 | }; 1701 | 1702 | /* Life Cycle begins */ 1703 | 1704 | // Init Context var 1705 | ctx = elCanvas[0].getContext('2d'); 1706 | 1707 | // Init CropArea 1708 | theArea = new CropAreaCircle(ctx, events); 1709 | 1710 | // Init Mouse Event Listeners 1711 | $document.on('mousemove',onMouseMove); 1712 | elCanvas.on('mousedown',onMouseDown); 1713 | $document.on('mouseup',onMouseUp); 1714 | 1715 | // Init Touch Event Listeners 1716 | $document.on('touchmove',onMouseMove); 1717 | elCanvas.on('touchstart',onMouseDown); 1718 | $document.on('touchend',onMouseUp); 1719 | 1720 | // CropHost Destructor 1721 | this.destroy=function() { 1722 | $document.off('mousemove',onMouseMove); 1723 | elCanvas.off('mousedown',onMouseDown); 1724 | $document.off('mouseup',onMouseMove); 1725 | 1726 | $document.off('touchmove',onMouseMove); 1727 | elCanvas.off('touchstart',onMouseDown); 1728 | $document.off('touchend',onMouseMove); 1729 | 1730 | elCanvas.remove(); 1731 | }; 1732 | }; 1733 | 1734 | }]); 1735 | 1736 | 1737 | crop.factory('cropPubSub', [function() { 1738 | return function() { 1739 | var events = {}; 1740 | // Subscribe 1741 | this.on = function(names, handler) { 1742 | names.split(' ').forEach(function(name) { 1743 | if (!events[name]) { 1744 | events[name] = []; 1745 | } 1746 | events[name].push(handler); 1747 | }); 1748 | return this; 1749 | }; 1750 | // Publish 1751 | this.trigger = function(name, args) { 1752 | angular.forEach(events[name], function(handler) { 1753 | handler.call(null, args); 1754 | }); 1755 | return this; 1756 | }; 1757 | }; 1758 | }]); 1759 | 1760 | crop.directive('imgCrop', ['$timeout', 'cropHost', 'cropPubSub', function($timeout, CropHost, CropPubSub) { 1761 | return { 1762 | restrict: 'E', 1763 | scope: { 1764 | image: '=', 1765 | resultImage: '=', 1766 | 1767 | changeOnFly: '=', 1768 | areaType: '@', 1769 | areaMinSize: '=', 1770 | resultImageSize: '=', 1771 | resultImageFormat: '@', 1772 | resultImageQuality: '=', 1773 | 1774 | onChange: '&', 1775 | onLoadBegin: '&', 1776 | onLoadDone: '&', 1777 | onLoadError: '&' 1778 | }, 1779 | template: '', 1780 | controller: ['$scope', function($scope) { 1781 | $scope.events = new CropPubSub(); 1782 | }], 1783 | link: function(scope, element/*, attrs*/) { 1784 | // Init Events Manager 1785 | var events = scope.events; 1786 | 1787 | // Init Crop Host 1788 | var cropHost=new CropHost(element.find('canvas'), {}, events); 1789 | 1790 | // Store Result Image to check if it's changed 1791 | var storedResultImage; 1792 | 1793 | var updateResultImage=function(scope) { 1794 | var resultImage=cropHost.getResultImageDataURI(); 1795 | if(storedResultImage!==resultImage) { 1796 | storedResultImage=resultImage; 1797 | if(angular.isDefined(scope.resultImage)) { 1798 | scope.resultImage=resultImage; 1799 | } 1800 | scope.onChange({$dataURI: scope.resultImage}); 1801 | } 1802 | }; 1803 | 1804 | // Wrapper to safely exec functions within $apply on a running $digest cycle 1805 | var fnSafeApply=function(fn) { 1806 | return function(){ 1807 | $timeout(function(){ 1808 | scope.$apply(function(scope){ 1809 | fn(scope); 1810 | }); 1811 | }); 1812 | }; 1813 | }; 1814 | 1815 | // Setup CropHost Event Handlers 1816 | events 1817 | .on('load-start', fnSafeApply(function(scope){ 1818 | scope.onLoadBegin({}); 1819 | })) 1820 | .on('load-done', fnSafeApply(function(scope){ 1821 | scope.onLoadDone({}); 1822 | })) 1823 | .on('load-error', fnSafeApply(function(scope){ 1824 | scope.onLoadError({}); 1825 | })) 1826 | .on('area-move area-resize', fnSafeApply(function(scope){ 1827 | if(!!scope.changeOnFly) { 1828 | updateResultImage(scope); 1829 | } 1830 | })) 1831 | .on('area-move-end area-resize-end image-updated', fnSafeApply(function(scope){ 1832 | updateResultImage(scope); 1833 | })); 1834 | 1835 | // Sync CropHost with Directive's options 1836 | scope.$watch('image',function(){ 1837 | cropHost.setNewImageSource(scope.image); 1838 | }); 1839 | scope.$watch('areaType',function(){ 1840 | cropHost.setAreaType(scope.areaType); 1841 | updateResultImage(scope); 1842 | }); 1843 | scope.$watch('areaMinSize',function(){ 1844 | cropHost.setAreaMinSize(scope.areaMinSize); 1845 | updateResultImage(scope); 1846 | }); 1847 | scope.$watch('resultImageSize',function(){ 1848 | cropHost.setResultImageSize(scope.resultImageSize); 1849 | updateResultImage(scope); 1850 | }); 1851 | scope.$watch('resultImageFormat',function(){ 1852 | cropHost.setResultImageFormat(scope.resultImageFormat); 1853 | updateResultImage(scope); 1854 | }); 1855 | scope.$watch('resultImageQuality',function(){ 1856 | cropHost.setResultImageQuality(scope.resultImageQuality); 1857 | updateResultImage(scope); 1858 | }); 1859 | 1860 | // Update CropHost dimensions when the directive element is resized 1861 | scope.$watch( 1862 | function () { 1863 | return [element[0].clientWidth, element[0].clientHeight]; 1864 | }, 1865 | function (value) { 1866 | cropHost.setMaxDimensions(value[0],value[1]); 1867 | updateResultImage(scope); 1868 | }, 1869 | true 1870 | ); 1871 | 1872 | // Destroy CropHost Instance when the directive is destroying 1873 | scope.$on('$destroy', function(){ 1874 | cropHost.destroy(); 1875 | }); 1876 | } 1877 | }; 1878 | }]); 1879 | }()); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var argv = require('minimist')(process.argv.slice(2)), 3 | gulp = require('gulp'), 4 | header = require('gulp-header'), 5 | gutil = require('gulp-util'), 6 | ngAnnotate = require('gulp-ng-annotate'), 7 | compass = require('gulp-compass'), 8 | refresh = require('gulp-livereload'), 9 | prefix = require('gulp-autoprefixer'), 10 | minifyCss = require('gulp-minify-css'), 11 | uglify = require('gulp-uglify'), 12 | clean = require('gulp-rimraf'), 13 | concat = require('gulp-concat-util'), 14 | express = require('express'), 15 | express_lr = require('connect-livereload'), 16 | tinylr = require('tiny-lr'), 17 | opn = require('opn'), 18 | jshint = require('gulp-jshint'), 19 | jshintStylish= require('jshint-stylish'), 20 | pkg = require('./package.json'), 21 | lr, 22 | refresh_lr; 23 | 24 | var today = new Date(); 25 | 26 | // Configuration 27 | 28 | var Config = { 29 | port: 9000, 30 | livereloadPort: 35728, 31 | testPage: 'test/ng-img-crop.html', 32 | cache: (typeof argv.cache !== 'undefined' ? !!argv.cache : true), 33 | paths: { 34 | source: { 35 | root: 'source', 36 | js: 'source/js', 37 | scss: 'source/scss' 38 | }, 39 | compileUnminified: { 40 | root: 'compile/unminified', 41 | js: 'compile/unminified', 42 | css: 'compile/unminified' 43 | }, 44 | compileMinified: { 45 | root: 'compile/minified', 46 | js: 'compile/minified', 47 | css: 'compile/minified' 48 | } 49 | }, 50 | banners: { 51 | unminified: '/*!\n' + 52 | ' * ' + pkg.prettyName + ' v' + pkg.version + '\n' + 53 | ' * ' + pkg.homepage + '\n' + 54 | ' *\n' + 55 | ' * Copyright (c) ' + (today.getFullYear()) + ' ' + pkg.author.name +'\n' + 56 | ' * License: ' + pkg.license + '\n' + 57 | ' *\n' + 58 | ' * Generated at ' + gutil.date(today, 'dddd, mmmm dS, yyyy, h:MM:ss TT') + '\n' + 59 | ' */', 60 | minified: '/*! ' + pkg.prettyName + ' v' + pkg.version + ' License: ' + pkg.license + ' */' 61 | } 62 | }; 63 | 64 | // Tasks 65 | // ===== 66 | 67 | // Compile Styles 68 | gulp.task('styles', function(){ 69 | return gulp.src(Config.paths.source.scss + '/'+pkg.name+'.scss') 70 | .pipe(compass({ 71 | sass: Config.paths.source.scss, 72 | css: Config.paths.compileUnminified.css, 73 | errLogToConsole: true 74 | })) 75 | .pipe(prefix('last 2 version', '> 5%', 'safari 5', 'ie 8', 'ie 7', 'opera 12.1', 'ios 6', 'android 4')) 76 | .pipe(gulp.dest(Config.paths.compileUnminified.css)); 77 | }); 78 | 79 | // Compile Scripts 80 | gulp.task('scripts', function(){ 81 | return gulp.src([ 82 | Config.paths.source.js + '/init.js', 83 | Config.paths.source.js + '/classes/*.js', 84 | Config.paths.source.js + '/ng-img-crop.js' 85 | ]) 86 | .pipe(concat(pkg.name+'.js', { 87 | separator: '\n\n', 88 | process: function(src) { 89 | // Remove all 'use strict'; from the code and 90 | // replaces all double blank lines with one 91 | return src.replace(/\r\n/g, '\n') 92 | .replace(/'use strict';\n+/g, '') 93 | .replace(/\n\n\s*\n/g, '\n\n'); 94 | } 95 | })) 96 | .pipe(concat.header(Config.banners.unminified + '\n' + 97 | '(function() {\n\'use strict\';\n\n')) 98 | .pipe(concat.footer('\n}());')) 99 | .pipe(gulp.dest(Config.paths.compileUnminified.js)); 100 | }); 101 | 102 | 103 | // Make a Distrib 104 | gulp.task('dist:js:clean', function(){ 105 | return gulp.src([Config.paths.compileMinified.root + '/**/*.js'], { read: false }) 106 | .pipe(clean()); 107 | }); 108 | gulp.task('dist:css:clean', function(){ 109 | return gulp.src([Config.paths.compileMinified.root + '/**/*.css'], { read: false }) 110 | .pipe(clean()); 111 | }); 112 | gulp.task('dist:js', ['dist:js:clean', 'scripts'], function(){ 113 | return gulp.src(Config.paths.compileUnminified.js + '/**/*.js') 114 | .pipe(ngAnnotate()) 115 | .pipe(uglify()) 116 | .pipe(header(Config.banners.minified)) 117 | .pipe(gulp.dest(Config.paths.compileMinified.js)); 118 | }); 119 | gulp.task('dist:css', ['dist:css:clean', 'styles'], function(){ 120 | return gulp.src(Config.paths.compileUnminified.css + '/**/*.css') 121 | .pipe(minifyCss()) 122 | .pipe(gulp.dest(Config.paths.compileMinified.css)); 123 | }); 124 | 125 | // Server 126 | gulp.task('server', function(){ 127 | express() 128 | .use(express_lr()) 129 | .use(express.static('.')) 130 | .listen(Config.port); 131 | gutil.log('Server listening on port ' + Config.port); 132 | }); 133 | 134 | // LiveReload 135 | gulp.task('livereload', function(){ 136 | lr = tinylr(); 137 | lr.listen(Config.livereloadPort, function(err) { 138 | if(err) { 139 | gutil.log('Livereload error:', err); 140 | } 141 | }); 142 | refresh_lr=refresh(lr); 143 | }); 144 | 145 | // Watches 146 | gulp.task('watch', function(){ 147 | gulp.watch(Config.paths.source.scss + '/**/*.scss', ['styles']); 148 | gulp.watch([Config.paths.source.js + '/**/*.js'], ['scripts']); 149 | gulp.watch([ 150 | Config.paths.compileUnminified.css + '/**/*.css', 151 | Config.paths.compileUnminified.js + '/**/*.js', 152 | Config.testPage 153 | ], function(evt){ 154 | refresh_lr.changed(evt.path); 155 | }); 156 | }); 157 | 158 | 159 | // User commands 160 | // ============= 161 | 162 | // Code linter 163 | gulp.task('lint', function() { 164 | return gulp.src(Config.paths.source.js + '/**/*.js') 165 | .pipe(jshint()) 166 | .pipe(jshint.reporter(jshintStylish)); 167 | }); 168 | 169 | // Build 170 | gulp.task('build', ['dist:js', 'dist:css']); 171 | 172 | // Start server and watch for changes 173 | gulp.task('default', ['server', 'livereload', 'styles', 'scripts', 'watch'], function(){ 174 | // use the -o arg to open the test page in the browser 175 | if(argv.o) { 176 | opn('http://localhost:' + Config.port+'/'+Config.testPage); 177 | } 178 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Alex Kaul", 4 | "email": "alexkaul@googlemail.com" 5 | }, 6 | "name": "ng-img-crop", 7 | "prettyName": "ngImgCrop", 8 | "version": "0.3.2", 9 | "description": "Image crop directive for AngularJS", 10 | "license": "MIT", 11 | "homepage": "https://github.com/alexk111/ngImgCrop", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/alexk111/ngImgCrop.git" 15 | }, 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "connect-livereload": "^0.4.0", 19 | "express": "^4.4.5", 20 | "gulp": "^3.8.5", 21 | "gulp-autoprefixer": "0.0.8", 22 | "gulp-compass": "^1.1.9", 23 | "gulp-concat": "^2.2.0", 24 | "gulp-concat-util": "^0.2.3", 25 | "gulp-header": "^1.0.2", 26 | "gulp-jshint": "^1.6.4", 27 | "gulp-livereload": "^2.1.0", 28 | "gulp-minify-css": "^0.3.6", 29 | "gulp-ng-annotate": "^0.2.0", 30 | "gulp-open": "^0.2.8", 31 | "gulp-plumber": "^0.6.3", 32 | "gulp-rimraf": "^0.1.0", 33 | "gulp-uglify": "^0.3.1", 34 | "gulp-util": "^2.2.19", 35 | "jshint-stylish": "^0.2.0", 36 | "minimist": "^0.2.0", 37 | "opn": "^0.1.2", 38 | "tiny-lr": "0.0.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /screenshots/circle_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk111/ngImgCrop/585fd8c87d453ab27ce29082d62dd7c7a5891ea5/screenshots/circle_1.jpg -------------------------------------------------------------------------------- /screenshots/square_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk111/ngImgCrop/585fd8c87d453ab27ce29082d62dd7c7a5891ea5/screenshots/square_1.jpg -------------------------------------------------------------------------------- /source/js/classes/crop-area-circle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.factory('cropAreaCircle', ['cropArea', function(CropArea) { 4 | var CropAreaCircle = function() { 5 | CropArea.apply(this, arguments); 6 | 7 | this._boxResizeBaseSize = 20; 8 | this._boxResizeNormalRatio = 0.9; 9 | this._boxResizeHoverRatio = 1.2; 10 | this._iconMoveNormalRatio = 0.9; 11 | this._iconMoveHoverRatio = 1.2; 12 | 13 | this._boxResizeNormalSize = this._boxResizeBaseSize*this._boxResizeNormalRatio; 14 | this._boxResizeHoverSize = this._boxResizeBaseSize*this._boxResizeHoverRatio; 15 | 16 | this._posDragStartX=0; 17 | this._posDragStartY=0; 18 | this._posResizeStartX=0; 19 | this._posResizeStartY=0; 20 | this._posResizeStartSize=0; 21 | 22 | this._boxResizeIsHover = false; 23 | this._areaIsHover = false; 24 | this._boxResizeIsDragging = false; 25 | this._areaIsDragging = false; 26 | }; 27 | 28 | CropAreaCircle.prototype = new CropArea(); 29 | 30 | CropAreaCircle.prototype._calcCirclePerimeterCoords=function(angleDegrees) { 31 | var hSize=this._size/2; 32 | var angleRadians=angleDegrees * (Math.PI / 180), 33 | circlePerimeterX=this._x + hSize * Math.cos(angleRadians), 34 | circlePerimeterY=this._y + hSize * Math.sin(angleRadians); 35 | return [circlePerimeterX, circlePerimeterY]; 36 | }; 37 | 38 | CropAreaCircle.prototype._calcResizeIconCenterCoords=function() { 39 | return this._calcCirclePerimeterCoords(-45); 40 | }; 41 | 42 | CropAreaCircle.prototype._isCoordWithinArea=function(coord) { 43 | return Math.sqrt((coord[0]-this._x)*(coord[0]-this._x) + (coord[1]-this._y)*(coord[1]-this._y)) < this._size/2; 44 | }; 45 | CropAreaCircle.prototype._isCoordWithinBoxResize=function(coord) { 46 | var resizeIconCenterCoords=this._calcResizeIconCenterCoords(); 47 | var hSize=this._boxResizeHoverSize/2; 48 | return(coord[0] > resizeIconCenterCoords[0] - hSize && coord[0] < resizeIconCenterCoords[0] + hSize && 49 | coord[1] > resizeIconCenterCoords[1] - hSize && coord[1] < resizeIconCenterCoords[1] + hSize); 50 | }; 51 | 52 | CropAreaCircle.prototype._drawArea=function(ctx,centerCoords,size){ 53 | ctx.arc(centerCoords[0],centerCoords[1],size/2,0,2*Math.PI); 54 | }; 55 | 56 | CropAreaCircle.prototype.draw=function() { 57 | CropArea.prototype.draw.apply(this, arguments); 58 | 59 | // draw move icon 60 | this._cropCanvas.drawIconMove([this._x,this._y], this._areaIsHover?this._iconMoveHoverRatio:this._iconMoveNormalRatio); 61 | 62 | // draw resize cubes 63 | this._cropCanvas.drawIconResizeBoxNESW(this._calcResizeIconCenterCoords(), this._boxResizeBaseSize, this._boxResizeIsHover?this._boxResizeHoverRatio:this._boxResizeNormalRatio); 64 | }; 65 | 66 | CropAreaCircle.prototype.processMouseMove=function(mouseCurX, mouseCurY) { 67 | var cursor='default'; 68 | var res=false; 69 | 70 | this._boxResizeIsHover = false; 71 | this._areaIsHover = false; 72 | 73 | if (this._areaIsDragging) { 74 | this._x = mouseCurX - this._posDragStartX; 75 | this._y = mouseCurY - this._posDragStartY; 76 | this._areaIsHover = true; 77 | cursor='move'; 78 | res=true; 79 | this._events.trigger('area-move'); 80 | } else if (this._boxResizeIsDragging) { 81 | cursor = 'nesw-resize'; 82 | var iFR, iFX, iFY; 83 | iFX = mouseCurX - this._posResizeStartX; 84 | iFY = this._posResizeStartY - mouseCurY; 85 | if(iFX>iFY) { 86 | iFR = this._posResizeStartSize + iFY*2; 87 | } else { 88 | iFR = this._posResizeStartSize + iFX*2; 89 | } 90 | 91 | this._size = Math.max(this._minSize, iFR); 92 | this._boxResizeIsHover = true; 93 | res=true; 94 | this._events.trigger('area-resize'); 95 | } else if (this._isCoordWithinBoxResize([mouseCurX,mouseCurY])) { 96 | cursor = 'nesw-resize'; 97 | this._areaIsHover = false; 98 | this._boxResizeIsHover = true; 99 | res=true; 100 | } else if(this._isCoordWithinArea([mouseCurX,mouseCurY])) { 101 | cursor = 'move'; 102 | this._areaIsHover = true; 103 | res=true; 104 | } 105 | 106 | this._dontDragOutside(); 107 | angular.element(this._ctx.canvas).css({'cursor': cursor}); 108 | 109 | return res; 110 | }; 111 | 112 | CropAreaCircle.prototype.processMouseDown=function(mouseDownX, mouseDownY) { 113 | if (this._isCoordWithinBoxResize([mouseDownX,mouseDownY])) { 114 | this._areaIsDragging = false; 115 | this._areaIsHover = false; 116 | this._boxResizeIsDragging = true; 117 | this._boxResizeIsHover = true; 118 | this._posResizeStartX=mouseDownX; 119 | this._posResizeStartY=mouseDownY; 120 | this._posResizeStartSize = this._size; 121 | this._events.trigger('area-resize-start'); 122 | } else if (this._isCoordWithinArea([mouseDownX,mouseDownY])) { 123 | this._areaIsDragging = true; 124 | this._areaIsHover = true; 125 | this._boxResizeIsDragging = false; 126 | this._boxResizeIsHover = false; 127 | this._posDragStartX = mouseDownX - this._x; 128 | this._posDragStartY = mouseDownY - this._y; 129 | this._events.trigger('area-move-start'); 130 | } 131 | }; 132 | 133 | CropAreaCircle.prototype.processMouseUp=function(/*mouseUpX, mouseUpY*/) { 134 | if(this._areaIsDragging) { 135 | this._areaIsDragging = false; 136 | this._events.trigger('area-move-end'); 137 | } 138 | if(this._boxResizeIsDragging) { 139 | this._boxResizeIsDragging = false; 140 | this._events.trigger('area-resize-end'); 141 | } 142 | this._areaIsHover = false; 143 | this._boxResizeIsHover = false; 144 | 145 | this._posDragStartX = 0; 146 | this._posDragStartY = 0; 147 | }; 148 | 149 | 150 | return CropAreaCircle; 151 | }]); 152 | 153 | -------------------------------------------------------------------------------- /source/js/classes/crop-area-square.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.factory('cropAreaSquare', ['cropArea', function(CropArea) { 4 | var CropAreaSquare = function() { 5 | CropArea.apply(this, arguments); 6 | 7 | this._resizeCtrlBaseRadius = 10; 8 | this._resizeCtrlNormalRatio = 0.75; 9 | this._resizeCtrlHoverRatio = 1; 10 | this._iconMoveNormalRatio = 0.9; 11 | this._iconMoveHoverRatio = 1.2; 12 | 13 | this._resizeCtrlNormalRadius = this._resizeCtrlBaseRadius*this._resizeCtrlNormalRatio; 14 | this._resizeCtrlHoverRadius = this._resizeCtrlBaseRadius*this._resizeCtrlHoverRatio; 15 | 16 | this._posDragStartX=0; 17 | this._posDragStartY=0; 18 | this._posResizeStartX=0; 19 | this._posResizeStartY=0; 20 | this._posResizeStartSize=0; 21 | 22 | this._resizeCtrlIsHover = -1; 23 | this._areaIsHover = false; 24 | this._resizeCtrlIsDragging = -1; 25 | this._areaIsDragging = false; 26 | }; 27 | 28 | CropAreaSquare.prototype = new CropArea(); 29 | 30 | CropAreaSquare.prototype._calcSquareCorners=function() { 31 | var hSize=this._size/2; 32 | return [ 33 | [this._x-hSize, this._y-hSize], 34 | [this._x+hSize, this._y-hSize], 35 | [this._x-hSize, this._y+hSize], 36 | [this._x+hSize, this._y+hSize] 37 | ]; 38 | }; 39 | 40 | CropAreaSquare.prototype._calcSquareDimensions=function() { 41 | var hSize=this._size/2; 42 | return { 43 | left: this._x-hSize, 44 | top: this._y-hSize, 45 | right: this._x+hSize, 46 | bottom: this._y+hSize 47 | }; 48 | }; 49 | 50 | CropAreaSquare.prototype._isCoordWithinArea=function(coord) { 51 | var squareDimensions=this._calcSquareDimensions(); 52 | return (coord[0]>=squareDimensions.left&&coord[0]<=squareDimensions.right&&coord[1]>=squareDimensions.top&&coord[1]<=squareDimensions.bottom); 53 | }; 54 | 55 | CropAreaSquare.prototype._isCoordWithinResizeCtrl=function(coord) { 56 | var resizeIconsCenterCoords=this._calcSquareCorners(); 57 | var res=-1; 58 | for(var i=0,len=resizeIconsCenterCoords.length;i resizeIconCenterCoords[0] - this._resizeCtrlHoverRadius && coord[0] < resizeIconCenterCoords[0] + this._resizeCtrlHoverRadius && 61 | coord[1] > resizeIconCenterCoords[1] - this._resizeCtrlHoverRadius && coord[1] < resizeIconCenterCoords[1] + this._resizeCtrlHoverRadius) { 62 | res=i; 63 | break; 64 | } 65 | } 66 | return res; 67 | }; 68 | 69 | CropAreaSquare.prototype._drawArea=function(ctx,centerCoords,size){ 70 | var hSize=size/2; 71 | ctx.rect(centerCoords[0]-hSize,centerCoords[1]-hSize,size,size); 72 | }; 73 | 74 | CropAreaSquare.prototype.draw=function() { 75 | CropArea.prototype.draw.apply(this, arguments); 76 | 77 | // draw move icon 78 | this._cropCanvas.drawIconMove([this._x,this._y], this._areaIsHover?this._iconMoveHoverRatio:this._iconMoveNormalRatio); 79 | 80 | // draw resize cubes 81 | var resizeIconsCenterCoords=this._calcSquareCorners(); 82 | for(var i=0,len=resizeIconsCenterCoords.length;i-1) { 103 | var xMulti, yMulti; 104 | switch(this._resizeCtrlIsDragging) { 105 | case 0: // Top Left 106 | xMulti=-1; 107 | yMulti=-1; 108 | cursor = 'nwse-resize'; 109 | break; 110 | case 1: // Top Right 111 | xMulti=1; 112 | yMulti=-1; 113 | cursor = 'nesw-resize'; 114 | break; 115 | case 2: // Bottom Left 116 | xMulti=-1; 117 | yMulti=1; 118 | cursor = 'nesw-resize'; 119 | break; 120 | case 3: // Bottom Right 121 | xMulti=1; 122 | yMulti=1; 123 | cursor = 'nwse-resize'; 124 | break; 125 | } 126 | var iFX = (mouseCurX - this._posResizeStartX)*xMulti; 127 | var iFY = (mouseCurY - this._posResizeStartY)*yMulti; 128 | var iFR; 129 | if(iFX>iFY) { 130 | iFR = this._posResizeStartSize + iFY; 131 | } else { 132 | iFR = this._posResizeStartSize + iFX; 133 | } 134 | var wasSize=this._size; 135 | this._size = Math.max(this._minSize, iFR); 136 | var posModifier=(this._size-wasSize)/2; 137 | this._x+=posModifier*xMulti; 138 | this._y+=posModifier*yMulti; 139 | this._resizeCtrlIsHover = this._resizeCtrlIsDragging; 140 | res=true; 141 | this._events.trigger('area-resize'); 142 | } else { 143 | var hoveredResizeBox=this._isCoordWithinResizeCtrl([mouseCurX,mouseCurY]); 144 | if (hoveredResizeBox>-1) { 145 | switch(hoveredResizeBox) { 146 | case 0: 147 | cursor = 'nwse-resize'; 148 | break; 149 | case 1: 150 | cursor = 'nesw-resize'; 151 | break; 152 | case 2: 153 | cursor = 'nesw-resize'; 154 | break; 155 | case 3: 156 | cursor = 'nwse-resize'; 157 | break; 158 | } 159 | this._areaIsHover = false; 160 | this._resizeCtrlIsHover = hoveredResizeBox; 161 | res=true; 162 | } else if(this._isCoordWithinArea([mouseCurX,mouseCurY])) { 163 | cursor = 'move'; 164 | this._areaIsHover = true; 165 | res=true; 166 | } 167 | } 168 | 169 | this._dontDragOutside(); 170 | angular.element(this._ctx.canvas).css({'cursor': cursor}); 171 | 172 | return res; 173 | }; 174 | 175 | CropAreaSquare.prototype.processMouseDown=function(mouseDownX, mouseDownY) { 176 | var isWithinResizeCtrl=this._isCoordWithinResizeCtrl([mouseDownX,mouseDownY]); 177 | if (isWithinResizeCtrl>-1) { 178 | this._areaIsDragging = false; 179 | this._areaIsHover = false; 180 | this._resizeCtrlIsDragging = isWithinResizeCtrl; 181 | this._resizeCtrlIsHover = isWithinResizeCtrl; 182 | this._posResizeStartX=mouseDownX; 183 | this._posResizeStartY=mouseDownY; 184 | this._posResizeStartSize = this._size; 185 | this._events.trigger('area-resize-start'); 186 | } else if (this._isCoordWithinArea([mouseDownX,mouseDownY])) { 187 | this._areaIsDragging = true; 188 | this._areaIsHover = true; 189 | this._resizeCtrlIsDragging = -1; 190 | this._resizeCtrlIsHover = -1; 191 | this._posDragStartX = mouseDownX - this._x; 192 | this._posDragStartY = mouseDownY - this._y; 193 | this._events.trigger('area-move-start'); 194 | } 195 | }; 196 | 197 | CropAreaSquare.prototype.processMouseUp=function(/*mouseUpX, mouseUpY*/) { 198 | if(this._areaIsDragging) { 199 | this._areaIsDragging = false; 200 | this._events.trigger('area-move-end'); 201 | } 202 | if(this._resizeCtrlIsDragging>-1) { 203 | this._resizeCtrlIsDragging = -1; 204 | this._events.trigger('area-resize-end'); 205 | } 206 | this._areaIsHover = false; 207 | this._resizeCtrlIsHover = -1; 208 | 209 | this._posDragStartX = 0; 210 | this._posDragStartY = 0; 211 | }; 212 | 213 | 214 | return CropAreaSquare; 215 | }]); -------------------------------------------------------------------------------- /source/js/classes/crop-area.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.factory('cropArea', ['cropCanvas', function(CropCanvas) { 4 | var CropArea = function(ctx, events) { 5 | this._ctx=ctx; 6 | this._events=events; 7 | 8 | this._minSize=80; 9 | 10 | this._cropCanvas=new CropCanvas(ctx); 11 | 12 | this._image=new Image(); 13 | this._x = 0; 14 | this._y = 0; 15 | this._size = 200; 16 | }; 17 | 18 | /* GETTERS/SETTERS */ 19 | 20 | CropArea.prototype.getImage = function () { 21 | return this._image; 22 | }; 23 | CropArea.prototype.setImage = function (image) { 24 | this._image = image; 25 | }; 26 | 27 | CropArea.prototype.getX = function () { 28 | return this._x; 29 | }; 30 | CropArea.prototype.setX = function (x) { 31 | this._x = x; 32 | this._dontDragOutside(); 33 | }; 34 | 35 | CropArea.prototype.getY = function () { 36 | return this._y; 37 | }; 38 | CropArea.prototype.setY = function (y) { 39 | this._y = y; 40 | this._dontDragOutside(); 41 | }; 42 | 43 | CropArea.prototype.getSize = function () { 44 | return this._size; 45 | }; 46 | CropArea.prototype.setSize = function (size) { 47 | this._size = Math.max(this._minSize, size); 48 | this._dontDragOutside(); 49 | }; 50 | 51 | CropArea.prototype.getMinSize = function () { 52 | return this._minSize; 53 | }; 54 | CropArea.prototype.setMinSize = function (size) { 55 | this._minSize = size; 56 | this._size = Math.max(this._minSize, this._size); 57 | this._dontDragOutside(); 58 | }; 59 | 60 | /* FUNCTIONS */ 61 | CropArea.prototype._dontDragOutside=function() { 62 | var h=this._ctx.canvas.height, 63 | w=this._ctx.canvas.width; 64 | if(this._size>w) { this._size=w; } 65 | if(this._size>h) { this._size=h; } 66 | if(this._xw-this._size/2) { this._x=w-this._size/2; } 68 | if(this._yh-this._size/2) { this._y=h-this._size/2; } 70 | }; 71 | 72 | CropArea.prototype._drawArea=function() {}; 73 | 74 | CropArea.prototype.draw=function() { 75 | // draw crop area 76 | this._cropCanvas.drawCropArea(this._image,[this._x,this._y],this._size,this._drawArea); 77 | }; 78 | 79 | CropArea.prototype.processMouseMove=function() {}; 80 | 81 | CropArea.prototype.processMouseDown=function() {}; 82 | 83 | CropArea.prototype.processMouseUp=function() {}; 84 | 85 | return CropArea; 86 | }]); -------------------------------------------------------------------------------- /source/js/classes/crop-canvas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.factory('cropCanvas', [function() { 4 | // Shape = Array of [x,y]; [0, 0] - center 5 | var shapeArrowNW=[[-0.5,-2],[-3,-4.5],[-0.5,-7],[-7,-7],[-7,-0.5],[-4.5,-3],[-2,-0.5]]; 6 | var shapeArrowNE=[[0.5,-2],[3,-4.5],[0.5,-7],[7,-7],[7,-0.5],[4.5,-3],[2,-0.5]]; 7 | var shapeArrowSW=[[-0.5,2],[-3,4.5],[-0.5,7],[-7,7],[-7,0.5],[-4.5,3],[-2,0.5]]; 8 | var shapeArrowSE=[[0.5,2],[3,4.5],[0.5,7],[7,7],[7,0.5],[4.5,3],[2,0.5]]; 9 | var shapeArrowN=[[-1.5,-2.5],[-1.5,-6],[-5,-6],[0,-11],[5,-6],[1.5,-6],[1.5,-2.5]]; 10 | var shapeArrowW=[[-2.5,-1.5],[-6,-1.5],[-6,-5],[-11,0],[-6,5],[-6,1.5],[-2.5,1.5]]; 11 | var shapeArrowS=[[-1.5,2.5],[-1.5,6],[-5,6],[0,11],[5,6],[1.5,6],[1.5,2.5]]; 12 | var shapeArrowE=[[2.5,-1.5],[6,-1.5],[6,-5],[11,0],[6,5],[6,1.5],[2.5,1.5]]; 13 | 14 | // Colors 15 | var colors={ 16 | areaOutline: '#fff', 17 | resizeBoxStroke: '#fff', 18 | resizeBoxFill: '#444', 19 | resizeBoxArrowFill: '#fff', 20 | resizeCircleStroke: '#fff', 21 | resizeCircleFill: '#444', 22 | moveIconFill: '#fff' 23 | }; 24 | 25 | return function(ctx){ 26 | 27 | /* Base functions */ 28 | 29 | // Calculate Point 30 | var calcPoint=function(point,offset,scale) { 31 | return [scale*point[0]+offset[0], scale*point[1]+offset[1]]; 32 | }; 33 | 34 | // Draw Filled Polygon 35 | var drawFilledPolygon=function(shape,fillStyle,centerCoords,scale) { 36 | ctx.save(); 37 | ctx.fillStyle = fillStyle; 38 | ctx.beginPath(); 39 | var pc, pc0=calcPoint(shape[0],centerCoords,scale); 40 | ctx.moveTo(pc0[0],pc0[1]); 41 | 42 | for(var p in shape) { 43 | if (p > 0) { 44 | pc=calcPoint(shape[p],centerCoords,scale); 45 | ctx.lineTo(pc[0],pc[1]); 46 | } 47 | } 48 | 49 | ctx.lineTo(pc0[0],pc0[1]); 50 | ctx.fill(); 51 | ctx.closePath(); 52 | ctx.restore(); 53 | }; 54 | 55 | 56 | /* Icons */ 57 | 58 | this.drawIconMove=function(centerCoords, scale) { 59 | drawFilledPolygon(shapeArrowN, colors.moveIconFill, centerCoords, scale); 60 | drawFilledPolygon(shapeArrowW, colors.moveIconFill, centerCoords, scale); 61 | drawFilledPolygon(shapeArrowS, colors.moveIconFill, centerCoords, scale); 62 | drawFilledPolygon(shapeArrowE, colors.moveIconFill, centerCoords, scale); 63 | }; 64 | 65 | this.drawIconResizeCircle=function(centerCoords, circleRadius, scale) { 66 | var scaledCircleRadius=circleRadius*scale; 67 | ctx.save(); 68 | ctx.strokeStyle = colors.resizeCircleStroke; 69 | ctx.lineWidth = 2; 70 | ctx.fillStyle = colors.resizeCircleFill; 71 | ctx.beginPath(); 72 | ctx.arc(centerCoords[0],centerCoords[1],scaledCircleRadius,0,2*Math.PI); 73 | ctx.fill(); 74 | ctx.stroke(); 75 | ctx.closePath(); 76 | ctx.restore(); 77 | }; 78 | 79 | this.drawIconResizeBoxBase=function(centerCoords, boxSize, scale) { 80 | var scaledBoxSize=boxSize*scale; 81 | ctx.save(); 82 | ctx.strokeStyle = colors.resizeBoxStroke; 83 | ctx.lineWidth = 2; 84 | ctx.fillStyle = colors.resizeBoxFill; 85 | ctx.fillRect(centerCoords[0] - scaledBoxSize/2, centerCoords[1] - scaledBoxSize/2, scaledBoxSize, scaledBoxSize); 86 | ctx.strokeRect(centerCoords[0] - scaledBoxSize/2, centerCoords[1] - scaledBoxSize/2, scaledBoxSize, scaledBoxSize); 87 | ctx.restore(); 88 | }; 89 | this.drawIconResizeBoxNESW=function(centerCoords, boxSize, scale) { 90 | this.drawIconResizeBoxBase(centerCoords, boxSize, scale); 91 | drawFilledPolygon(shapeArrowNE, colors.resizeBoxArrowFill, centerCoords, scale); 92 | drawFilledPolygon(shapeArrowSW, colors.resizeBoxArrowFill, centerCoords, scale); 93 | }; 94 | this.drawIconResizeBoxNWSE=function(centerCoords, boxSize, scale) { 95 | this.drawIconResizeBoxBase(centerCoords, boxSize, scale); 96 | drawFilledPolygon(shapeArrowNW, colors.resizeBoxArrowFill, centerCoords, scale); 97 | drawFilledPolygon(shapeArrowSE, colors.resizeBoxArrowFill, centerCoords, scale); 98 | }; 99 | 100 | /* Crop Area */ 101 | 102 | this.drawCropArea=function(image, centerCoords, size, fnDrawClipPath) { 103 | var xRatio=image.width/ctx.canvas.width, 104 | yRatio=image.height/ctx.canvas.height, 105 | xLeft=centerCoords[0]-size/2, 106 | yTop=centerCoords[1]-size/2; 107 | 108 | ctx.save(); 109 | ctx.strokeStyle = colors.areaOutline; 110 | ctx.lineWidth = 2; 111 | ctx.beginPath(); 112 | fnDrawClipPath(ctx, centerCoords, size); 113 | ctx.stroke(); 114 | ctx.clip(); 115 | 116 | // draw part of original image 117 | if (size > 0) { 118 | ctx.drawImage(image, xLeft*xRatio, yTop*yRatio, size*xRatio, size*yRatio, xLeft, yTop, size, size); 119 | } 120 | 121 | ctx.beginPath(); 122 | fnDrawClipPath(ctx, centerCoords, size); 123 | ctx.stroke(); 124 | ctx.clip(); 125 | 126 | ctx.restore(); 127 | }; 128 | 129 | }; 130 | }]); -------------------------------------------------------------------------------- /source/js/classes/crop-exif.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EXIF service is based on the exif-js library (https://github.com/jseidelin/exif-js) 3 | */ 4 | 5 | 'use strict'; 6 | 7 | crop.service('cropEXIF', [function() { 8 | var debug = false; 9 | 10 | var ExifTags = this.Tags = { 11 | 12 | // version tags 13 | 0x9000 : "ExifVersion", // EXIF version 14 | 0xA000 : "FlashpixVersion", // Flashpix format version 15 | 16 | // colorspace tags 17 | 0xA001 : "ColorSpace", // Color space information tag 18 | 19 | // image configuration 20 | 0xA002 : "PixelXDimension", // Valid width of meaningful image 21 | 0xA003 : "PixelYDimension", // Valid height of meaningful image 22 | 0x9101 : "ComponentsConfiguration", // Information about channels 23 | 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel 24 | 25 | // user information 26 | 0x927C : "MakerNote", // Any desired information written by the manufacturer 27 | 0x9286 : "UserComment", // Comments by user 28 | 29 | // related file 30 | 0xA004 : "RelatedSoundFile", // Name of related sound file 31 | 32 | // date and time 33 | 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated 34 | 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally 35 | 0x9290 : "SubsecTime", // Fractions of seconds for DateTime 36 | 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal 37 | 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized 38 | 39 | // picture-taking conditions 40 | 0x829A : "ExposureTime", // Exposure time (in seconds) 41 | 0x829D : "FNumber", // F number 42 | 0x8822 : "ExposureProgram", // Exposure program 43 | 0x8824 : "SpectralSensitivity", // Spectral sensitivity 44 | 0x8827 : "ISOSpeedRatings", // ISO speed rating 45 | 0x8828 : "OECF", // Optoelectric conversion factor 46 | 0x9201 : "ShutterSpeedValue", // Shutter speed 47 | 0x9202 : "ApertureValue", // Lens aperture 48 | 0x9203 : "BrightnessValue", // Value of brightness 49 | 0x9204 : "ExposureBias", // Exposure bias 50 | 0x9205 : "MaxApertureValue", // Smallest F number of lens 51 | 0x9206 : "SubjectDistance", // Distance to subject in meters 52 | 0x9207 : "MeteringMode", // Metering mode 53 | 0x9208 : "LightSource", // Kind of light source 54 | 0x9209 : "Flash", // Flash status 55 | 0x9214 : "SubjectArea", // Location and area of main subject 56 | 0x920A : "FocalLength", // Focal length of the lens in mm 57 | 0xA20B : "FlashEnergy", // Strobe energy in BCPS 58 | 0xA20C : "SpatialFrequencyResponse", // 59 | 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit 60 | 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit 61 | 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution 62 | 0xA214 : "SubjectLocation", // Location of subject in image 63 | 0xA215 : "ExposureIndex", // Exposure index selected on camera 64 | 0xA217 : "SensingMethod", // Image sensor type 65 | 0xA300 : "FileSource", // Image source (3 == DSC) 66 | 0xA301 : "SceneType", // Scene type (1 == directly photographed) 67 | 0xA302 : "CFAPattern", // Color filter array geometric pattern 68 | 0xA401 : "CustomRendered", // Special processing 69 | 0xA402 : "ExposureMode", // Exposure mode 70 | 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual 71 | 0xA404 : "DigitalZoomRation", // Digital zoom ratio 72 | 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) 73 | 0xA406 : "SceneCaptureType", // Type of scene 74 | 0xA407 : "GainControl", // Degree of overall image gain adjustment 75 | 0xA408 : "Contrast", // Direction of contrast processing applied by camera 76 | 0xA409 : "Saturation", // Direction of saturation processing applied by camera 77 | 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera 78 | 0xA40B : "DeviceSettingDescription", // 79 | 0xA40C : "SubjectDistanceRange", // Distance to subject 80 | 81 | // other tags 82 | 0xA005 : "InteroperabilityIFDPointer", 83 | 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image 84 | }; 85 | 86 | var TiffTags = this.TiffTags = { 87 | 0x0100 : "ImageWidth", 88 | 0x0101 : "ImageHeight", 89 | 0x8769 : "ExifIFDPointer", 90 | 0x8825 : "GPSInfoIFDPointer", 91 | 0xA005 : "InteroperabilityIFDPointer", 92 | 0x0102 : "BitsPerSample", 93 | 0x0103 : "Compression", 94 | 0x0106 : "PhotometricInterpretation", 95 | 0x0112 : "Orientation", 96 | 0x0115 : "SamplesPerPixel", 97 | 0x011C : "PlanarConfiguration", 98 | 0x0212 : "YCbCrSubSampling", 99 | 0x0213 : "YCbCrPositioning", 100 | 0x011A : "XResolution", 101 | 0x011B : "YResolution", 102 | 0x0128 : "ResolutionUnit", 103 | 0x0111 : "StripOffsets", 104 | 0x0116 : "RowsPerStrip", 105 | 0x0117 : "StripByteCounts", 106 | 0x0201 : "JPEGInterchangeFormat", 107 | 0x0202 : "JPEGInterchangeFormatLength", 108 | 0x012D : "TransferFunction", 109 | 0x013E : "WhitePoint", 110 | 0x013F : "PrimaryChromaticities", 111 | 0x0211 : "YCbCrCoefficients", 112 | 0x0214 : "ReferenceBlackWhite", 113 | 0x0132 : "DateTime", 114 | 0x010E : "ImageDescription", 115 | 0x010F : "Make", 116 | 0x0110 : "Model", 117 | 0x0131 : "Software", 118 | 0x013B : "Artist", 119 | 0x8298 : "Copyright" 120 | }; 121 | 122 | var GPSTags = this.GPSTags = { 123 | 0x0000 : "GPSVersionID", 124 | 0x0001 : "GPSLatitudeRef", 125 | 0x0002 : "GPSLatitude", 126 | 0x0003 : "GPSLongitudeRef", 127 | 0x0004 : "GPSLongitude", 128 | 0x0005 : "GPSAltitudeRef", 129 | 0x0006 : "GPSAltitude", 130 | 0x0007 : "GPSTimeStamp", 131 | 0x0008 : "GPSSatellites", 132 | 0x0009 : "GPSStatus", 133 | 0x000A : "GPSMeasureMode", 134 | 0x000B : "GPSDOP", 135 | 0x000C : "GPSSpeedRef", 136 | 0x000D : "GPSSpeed", 137 | 0x000E : "GPSTrackRef", 138 | 0x000F : "GPSTrack", 139 | 0x0010 : "GPSImgDirectionRef", 140 | 0x0011 : "GPSImgDirection", 141 | 0x0012 : "GPSMapDatum", 142 | 0x0013 : "GPSDestLatitudeRef", 143 | 0x0014 : "GPSDestLatitude", 144 | 0x0015 : "GPSDestLongitudeRef", 145 | 0x0016 : "GPSDestLongitude", 146 | 0x0017 : "GPSDestBearingRef", 147 | 0x0018 : "GPSDestBearing", 148 | 0x0019 : "GPSDestDistanceRef", 149 | 0x001A : "GPSDestDistance", 150 | 0x001B : "GPSProcessingMethod", 151 | 0x001C : "GPSAreaInformation", 152 | 0x001D : "GPSDateStamp", 153 | 0x001E : "GPSDifferential" 154 | }; 155 | 156 | var StringValues = this.StringValues = { 157 | ExposureProgram : { 158 | 0 : "Not defined", 159 | 1 : "Manual", 160 | 2 : "Normal program", 161 | 3 : "Aperture priority", 162 | 4 : "Shutter priority", 163 | 5 : "Creative program", 164 | 6 : "Action program", 165 | 7 : "Portrait mode", 166 | 8 : "Landscape mode" 167 | }, 168 | MeteringMode : { 169 | 0 : "Unknown", 170 | 1 : "Average", 171 | 2 : "CenterWeightedAverage", 172 | 3 : "Spot", 173 | 4 : "MultiSpot", 174 | 5 : "Pattern", 175 | 6 : "Partial", 176 | 255 : "Other" 177 | }, 178 | LightSource : { 179 | 0 : "Unknown", 180 | 1 : "Daylight", 181 | 2 : "Fluorescent", 182 | 3 : "Tungsten (incandescent light)", 183 | 4 : "Flash", 184 | 9 : "Fine weather", 185 | 10 : "Cloudy weather", 186 | 11 : "Shade", 187 | 12 : "Daylight fluorescent (D 5700 - 7100K)", 188 | 13 : "Day white fluorescent (N 4600 - 5400K)", 189 | 14 : "Cool white fluorescent (W 3900 - 4500K)", 190 | 15 : "White fluorescent (WW 3200 - 3700K)", 191 | 17 : "Standard light A", 192 | 18 : "Standard light B", 193 | 19 : "Standard light C", 194 | 20 : "D55", 195 | 21 : "D65", 196 | 22 : "D75", 197 | 23 : "D50", 198 | 24 : "ISO studio tungsten", 199 | 255 : "Other" 200 | }, 201 | Flash : { 202 | 0x0000 : "Flash did not fire", 203 | 0x0001 : "Flash fired", 204 | 0x0005 : "Strobe return light not detected", 205 | 0x0007 : "Strobe return light detected", 206 | 0x0009 : "Flash fired, compulsory flash mode", 207 | 0x000D : "Flash fired, compulsory flash mode, return light not detected", 208 | 0x000F : "Flash fired, compulsory flash mode, return light detected", 209 | 0x0010 : "Flash did not fire, compulsory flash mode", 210 | 0x0018 : "Flash did not fire, auto mode", 211 | 0x0019 : "Flash fired, auto mode", 212 | 0x001D : "Flash fired, auto mode, return light not detected", 213 | 0x001F : "Flash fired, auto mode, return light detected", 214 | 0x0020 : "No flash function", 215 | 0x0041 : "Flash fired, red-eye reduction mode", 216 | 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", 217 | 0x0047 : "Flash fired, red-eye reduction mode, return light detected", 218 | 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", 219 | 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 220 | 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 221 | 0x0059 : "Flash fired, auto mode, red-eye reduction mode", 222 | 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", 223 | 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" 224 | }, 225 | SensingMethod : { 226 | 1 : "Not defined", 227 | 2 : "One-chip color area sensor", 228 | 3 : "Two-chip color area sensor", 229 | 4 : "Three-chip color area sensor", 230 | 5 : "Color sequential area sensor", 231 | 7 : "Trilinear sensor", 232 | 8 : "Color sequential linear sensor" 233 | }, 234 | SceneCaptureType : { 235 | 0 : "Standard", 236 | 1 : "Landscape", 237 | 2 : "Portrait", 238 | 3 : "Night scene" 239 | }, 240 | SceneType : { 241 | 1 : "Directly photographed" 242 | }, 243 | CustomRendered : { 244 | 0 : "Normal process", 245 | 1 : "Custom process" 246 | }, 247 | WhiteBalance : { 248 | 0 : "Auto white balance", 249 | 1 : "Manual white balance" 250 | }, 251 | GainControl : { 252 | 0 : "None", 253 | 1 : "Low gain up", 254 | 2 : "High gain up", 255 | 3 : "Low gain down", 256 | 4 : "High gain down" 257 | }, 258 | Contrast : { 259 | 0 : "Normal", 260 | 1 : "Soft", 261 | 2 : "Hard" 262 | }, 263 | Saturation : { 264 | 0 : "Normal", 265 | 1 : "Low saturation", 266 | 2 : "High saturation" 267 | }, 268 | Sharpness : { 269 | 0 : "Normal", 270 | 1 : "Soft", 271 | 2 : "Hard" 272 | }, 273 | SubjectDistanceRange : { 274 | 0 : "Unknown", 275 | 1 : "Macro", 276 | 2 : "Close view", 277 | 3 : "Distant view" 278 | }, 279 | FileSource : { 280 | 3 : "DSC" 281 | }, 282 | 283 | Components : { 284 | 0 : "", 285 | 1 : "Y", 286 | 2 : "Cb", 287 | 3 : "Cr", 288 | 4 : "R", 289 | 5 : "G", 290 | 6 : "B" 291 | } 292 | }; 293 | 294 | function addEvent(element, event, handler) { 295 | if (element.addEventListener) { 296 | element.addEventListener(event, handler, false); 297 | } else if (element.attachEvent) { 298 | element.attachEvent("on" + event, handler); 299 | } 300 | } 301 | 302 | function imageHasData(img) { 303 | return !!(img.exifdata); 304 | } 305 | 306 | 307 | function base64ToArrayBuffer(base64, contentType) { 308 | contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' 309 | base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); 310 | var binary = atob(base64); 311 | var len = binary.length; 312 | var buffer = new ArrayBuffer(len); 313 | var view = new Uint8Array(buffer); 314 | for (var i = 0; i < len; i++) { 315 | view[i] = binary.charCodeAt(i); 316 | } 317 | return buffer; 318 | } 319 | 320 | function objectURLToBlob(url, callback) { 321 | var http = new XMLHttpRequest(); 322 | http.open("GET", url, true); 323 | http.responseType = "blob"; 324 | http.onload = function(e) { 325 | if (this.status == 200 || this.status === 0) { 326 | callback(this.response); 327 | } 328 | }; 329 | http.send(); 330 | } 331 | 332 | function getImageData(img, callback) { 333 | function handleBinaryFile(binFile) { 334 | var data = findEXIFinJPEG(binFile); 335 | var iptcdata = findIPTCinJPEG(binFile); 336 | img.exifdata = data || {}; 337 | img.iptcdata = iptcdata || {}; 338 | if (callback) { 339 | callback.call(img); 340 | } 341 | } 342 | 343 | if (img.src) { 344 | if (/^data\:/i.test(img.src)) { // Data URI 345 | var arrayBuffer = base64ToArrayBuffer(img.src); 346 | handleBinaryFile(arrayBuffer); 347 | 348 | } else if (/^blob\:/i.test(img.src)) { // Object URL 349 | var fileReader = new FileReader(); 350 | fileReader.onload = function(e) { 351 | handleBinaryFile(e.target.result); 352 | }; 353 | objectURLToBlob(img.src, function (blob) { 354 | fileReader.readAsArrayBuffer(blob); 355 | }); 356 | } else { 357 | var http = new XMLHttpRequest(); 358 | http.onload = function() { 359 | if (this.status == 200 || this.status === 0) { 360 | handleBinaryFile(http.response); 361 | } else { 362 | throw "Could not load image"; 363 | } 364 | http = null; 365 | }; 366 | http.open("GET", img.src, true); 367 | http.responseType = "arraybuffer"; 368 | http.send(null); 369 | } 370 | } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) { 371 | var fileReader = new FileReader(); 372 | fileReader.onload = function(e) { 373 | if (debug) console.log("Got file of length " + e.target.result.byteLength); 374 | handleBinaryFile(e.target.result); 375 | }; 376 | 377 | fileReader.readAsArrayBuffer(img); 378 | } 379 | } 380 | 381 | function findEXIFinJPEG(file) { 382 | var dataView = new DataView(file); 383 | 384 | if (debug) console.log("Got file of length " + file.byteLength); 385 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 386 | if (debug) console.log("Not a valid JPEG"); 387 | return false; // not a valid jpeg 388 | } 389 | 390 | var offset = 2, 391 | length = file.byteLength, 392 | marker; 393 | 394 | while (offset < length) { 395 | if (dataView.getUint8(offset) != 0xFF) { 396 | if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); 397 | return false; // not a valid marker, something is wrong 398 | } 399 | 400 | marker = dataView.getUint8(offset + 1); 401 | if (debug) console.log(marker); 402 | 403 | // we could implement handling for other markers here, 404 | // but we're only looking for 0xFFE1 for EXIF data 405 | 406 | if (marker == 225) { 407 | if (debug) console.log("Found 0xFFE1 marker"); 408 | 409 | return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); 410 | 411 | // offset += 2 + file.getShortAt(offset+2, true); 412 | 413 | } else { 414 | offset += 2 + dataView.getUint16(offset+2); 415 | } 416 | 417 | } 418 | 419 | } 420 | 421 | function findIPTCinJPEG(file) { 422 | var dataView = new DataView(file); 423 | 424 | if (debug) console.log("Got file of length " + file.byteLength); 425 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 426 | if (debug) console.log("Not a valid JPEG"); 427 | return false; // not a valid jpeg 428 | } 429 | 430 | var offset = 2, 431 | length = file.byteLength; 432 | 433 | 434 | var isFieldSegmentStart = function(dataView, offset){ 435 | return ( 436 | dataView.getUint8(offset) === 0x38 && 437 | dataView.getUint8(offset+1) === 0x42 && 438 | dataView.getUint8(offset+2) === 0x49 && 439 | dataView.getUint8(offset+3) === 0x4D && 440 | dataView.getUint8(offset+4) === 0x04 && 441 | dataView.getUint8(offset+5) === 0x04 442 | ); 443 | }; 444 | 445 | while (offset < length) { 446 | 447 | if ( isFieldSegmentStart(dataView, offset )){ 448 | 449 | // Get the length of the name header (which is padded to an even number of bytes) 450 | var nameHeaderLength = dataView.getUint8(offset+7); 451 | if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; 452 | // Check for pre photoshop 6 format 453 | if(nameHeaderLength === 0) { 454 | // Always 4 455 | nameHeaderLength = 4; 456 | } 457 | 458 | var startOffset = offset + 8 + nameHeaderLength; 459 | var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); 460 | 461 | return readIPTCData(file, startOffset, sectionLength); 462 | 463 | break; 464 | 465 | } 466 | 467 | 468 | // Not the marker, continue searching 469 | offset++; 470 | 471 | } 472 | 473 | } 474 | var IptcFieldMap = { 475 | 0x78 : 'caption', 476 | 0x6E : 'credit', 477 | 0x19 : 'keywords', 478 | 0x37 : 'dateCreated', 479 | 0x50 : 'byline', 480 | 0x55 : 'bylineTitle', 481 | 0x7A : 'captionWriter', 482 | 0x69 : 'headline', 483 | 0x74 : 'copyright', 484 | 0x0F : 'category' 485 | }; 486 | function readIPTCData(file, startOffset, sectionLength){ 487 | var dataView = new DataView(file); 488 | var data = {}; 489 | var fieldValue, fieldName, dataSize, segmentType, segmentSize; 490 | var segmentStartPos = startOffset; 491 | while(segmentStartPos < startOffset+sectionLength) { 492 | if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ 493 | segmentType = dataView.getUint8(segmentStartPos+2); 494 | if(segmentType in IptcFieldMap) { 495 | dataSize = dataView.getInt16(segmentStartPos+3); 496 | segmentSize = dataSize + 5; 497 | fieldName = IptcFieldMap[segmentType]; 498 | fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); 499 | // Check if we already stored a value with this name 500 | if(data.hasOwnProperty(fieldName)) { 501 | // Value already stored with this name, create multivalue field 502 | if(data[fieldName] instanceof Array) { 503 | data[fieldName].push(fieldValue); 504 | } 505 | else { 506 | data[fieldName] = [data[fieldName], fieldValue]; 507 | } 508 | } 509 | else { 510 | data[fieldName] = fieldValue; 511 | } 512 | } 513 | 514 | } 515 | segmentStartPos++; 516 | } 517 | return data; 518 | } 519 | 520 | 521 | 522 | function readTags(file, tiffStart, dirStart, strings, bigEnd) { 523 | var entries = file.getUint16(dirStart, !bigEnd), 524 | tags = {}, 525 | entryOffset, tag, 526 | i; 527 | 528 | for (i=0;i 4 ? valueOffset : (entryOffset + 8); 553 | vals = []; 554 | for (n=0;n 4 ? valueOffset : (entryOffset + 8); 562 | return getStringFromDB(file, offset, numValues-1); 563 | 564 | case 3: // short, 16 bit int 565 | if (numValues == 1) { 566 | return file.getUint16(entryOffset + 8, !bigEnd); 567 | } else { 568 | offset = numValues > 2 ? valueOffset : (entryOffset + 8); 569 | vals = []; 570 | for (n=0;nmaxCanvasDims[0]) { 79 | canvasDims[0]=maxCanvasDims[0]; 80 | canvasDims[1]=canvasDims[0]/imageRatio; 81 | } else if(canvasDims[0]maxCanvasDims[1]) { 86 | canvasDims[1]=maxCanvasDims[1]; 87 | canvasDims[0]=canvasDims[1]*imageRatio; 88 | } else if(canvasDims[1]')[0]; 170 | temp_ctx = temp_canvas.getContext('2d'); 171 | temp_canvas.width = resImgSize; 172 | temp_canvas.height = resImgSize; 173 | if(image!==null){ 174 | temp_ctx.drawImage(image, (theArea.getX()-theArea.getSize()/2)*(image.width/ctx.canvas.width), (theArea.getY()-theArea.getSize()/2)*(image.height/ctx.canvas.height), theArea.getSize()*(image.width/ctx.canvas.width), theArea.getSize()*(image.height/ctx.canvas.height), 0, 0, resImgSize, resImgSize); 175 | } 176 | if (resImgQuality!==null ){ 177 | return temp_canvas.toDataURL(resImgFormat, resImgQuality); 178 | } 179 | return temp_canvas.toDataURL(resImgFormat); 180 | }; 181 | 182 | this.setNewImageSource=function(imageSource) { 183 | image=null; 184 | resetCropHost(); 185 | events.trigger('image-updated'); 186 | if(!!imageSource) { 187 | var newImage = new Image(); 188 | if(imageSource.substring(0,4).toLowerCase()==='http') { 189 | newImage.crossOrigin = 'anonymous'; 190 | } 191 | newImage.onload = function(){ 192 | events.trigger('load-done'); 193 | 194 | cropEXIF.getData(newImage,function(){ 195 | var orientation=cropEXIF.getTag(newImage,'Orientation'); 196 | 197 | if([3,6,8].indexOf(orientation)>-1) { 198 | var canvas = document.createElement("canvas"), 199 | ctx=canvas.getContext("2d"), 200 | cw = newImage.width, ch = newImage.height, cx = 0, cy = 0, deg=0; 201 | switch(orientation) { 202 | case 3: 203 | cx=-newImage.width; 204 | cy=-newImage.height; 205 | deg=180; 206 | break; 207 | case 6: 208 | cw = newImage.height; 209 | ch = newImage.width; 210 | cy=-newImage.height; 211 | deg=90; 212 | break; 213 | case 8: 214 | cw = newImage.height; 215 | ch = newImage.width; 216 | cx=-newImage.width; 217 | deg=270; 218 | break; 219 | } 220 | 221 | canvas.width = cw; 222 | canvas.height = ch; 223 | ctx.rotate(deg*Math.PI/180); 224 | ctx.drawImage(newImage, cx, cy); 225 | 226 | image=new Image(); 227 | image.src = canvas.toDataURL("image/png"); 228 | } else { 229 | image=newImage; 230 | } 231 | resetCropHost(); 232 | events.trigger('image-updated'); 233 | }); 234 | }; 235 | newImage.onerror=function() { 236 | events.trigger('load-error'); 237 | }; 238 | events.trigger('load-start'); 239 | newImage.src=imageSource; 240 | } 241 | }; 242 | 243 | this.setMaxDimensions=function(width, height) { 244 | maxCanvasDims=[width,height]; 245 | 246 | if(image!==null) { 247 | var curWidth=ctx.canvas.width, 248 | curHeight=ctx.canvas.height; 249 | 250 | var imageDims=[image.width, image.height], 251 | imageRatio=image.width/image.height, 252 | canvasDims=imageDims; 253 | 254 | if(canvasDims[0]>maxCanvasDims[0]) { 255 | canvasDims[0]=maxCanvasDims[0]; 256 | canvasDims[1]=canvasDims[0]/imageRatio; 257 | } else if(canvasDims[0]maxCanvasDims[1]) { 262 | canvasDims[1]=maxCanvasDims[1]; 263 | canvasDims[0]=canvasDims[1]*imageRatio; 264 | } else if(canvasDims[1]=0 && quality<=1){ 307 | resImgQuality = quality; 308 | } 309 | }; 310 | 311 | this.setAreaType=function(type) { 312 | var curSize=theArea.getSize(), 313 | curMinSize=theArea.getMinSize(), 314 | curX=theArea.getX(), 315 | curY=theArea.getY(); 316 | 317 | var AreaClass=CropAreaCircle; 318 | if(type==='square') { 319 | AreaClass=CropAreaSquare; 320 | } 321 | theArea = new AreaClass(ctx, events); 322 | theArea.setMinSize(curMinSize); 323 | theArea.setSize(curSize); 324 | theArea.setX(curX); 325 | theArea.setY(curY); 326 | 327 | // resetCropHost(); 328 | if(image!==null) { 329 | theArea.setImage(image); 330 | } 331 | 332 | drawScene(); 333 | }; 334 | 335 | /* Life Cycle begins */ 336 | 337 | // Init Context var 338 | ctx = elCanvas[0].getContext('2d'); 339 | 340 | // Init CropArea 341 | theArea = new CropAreaCircle(ctx, events); 342 | 343 | // Init Mouse Event Listeners 344 | $document.on('mousemove',onMouseMove); 345 | elCanvas.on('mousedown',onMouseDown); 346 | $document.on('mouseup',onMouseUp); 347 | 348 | // Init Touch Event Listeners 349 | $document.on('touchmove',onMouseMove); 350 | elCanvas.on('touchstart',onMouseDown); 351 | $document.on('touchend',onMouseUp); 352 | 353 | // CropHost Destructor 354 | this.destroy=function() { 355 | $document.off('mousemove',onMouseMove); 356 | elCanvas.off('mousedown',onMouseDown); 357 | $document.off('mouseup',onMouseMove); 358 | 359 | $document.off('touchmove',onMouseMove); 360 | elCanvas.off('touchstart',onMouseDown); 361 | $document.off('touchend',onMouseMove); 362 | 363 | elCanvas.remove(); 364 | }; 365 | }; 366 | 367 | }]); 368 | -------------------------------------------------------------------------------- /source/js/classes/crop-pubsub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.factory('cropPubSub', [function() { 4 | return function() { 5 | var events = {}; 6 | // Subscribe 7 | this.on = function(names, handler) { 8 | names.split(' ').forEach(function(name) { 9 | if (!events[name]) { 10 | events[name] = []; 11 | } 12 | events[name].push(handler); 13 | }); 14 | return this; 15 | }; 16 | // Publish 17 | this.trigger = function(name, args) { 18 | angular.forEach(events[name], function(handler) { 19 | handler.call(null, args); 20 | }); 21 | return this; 22 | }; 23 | }; 24 | }]); -------------------------------------------------------------------------------- /source/js/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crop = angular.module('ngImgCrop', []); -------------------------------------------------------------------------------- /source/js/ng-img-crop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | crop.directive('imgCrop', ['$timeout', 'cropHost', 'cropPubSub', function($timeout, CropHost, CropPubSub) { 4 | return { 5 | restrict: 'E', 6 | scope: { 7 | image: '=', 8 | resultImage: '=', 9 | 10 | changeOnFly: '=', 11 | areaType: '@', 12 | areaMinSize: '=', 13 | resultImageSize: '=', 14 | resultImageFormat: '@', 15 | resultImageQuality: '=', 16 | 17 | onChange: '&', 18 | onLoadBegin: '&', 19 | onLoadDone: '&', 20 | onLoadError: '&' 21 | }, 22 | template: '', 23 | controller: ['$scope', function($scope) { 24 | $scope.events = new CropPubSub(); 25 | }], 26 | link: function(scope, element/*, attrs*/) { 27 | // Init Events Manager 28 | var events = scope.events; 29 | 30 | // Init Crop Host 31 | var cropHost=new CropHost(element.find('canvas'), {}, events); 32 | 33 | // Store Result Image to check if it's changed 34 | var storedResultImage; 35 | 36 | var updateResultImage=function(scope) { 37 | var resultImage=cropHost.getResultImageDataURI(); 38 | if(storedResultImage!==resultImage) { 39 | storedResultImage=resultImage; 40 | if(angular.isDefined(scope.resultImage)) { 41 | scope.resultImage=resultImage; 42 | } 43 | scope.onChange({$dataURI: scope.resultImage}); 44 | } 45 | }; 46 | 47 | // Wrapper to safely exec functions within $apply on a running $digest cycle 48 | var fnSafeApply=function(fn) { 49 | return function(){ 50 | $timeout(function(){ 51 | scope.$apply(function(scope){ 52 | fn(scope); 53 | }); 54 | }); 55 | }; 56 | }; 57 | 58 | // Setup CropHost Event Handlers 59 | events 60 | .on('load-start', fnSafeApply(function(scope){ 61 | scope.onLoadBegin({}); 62 | })) 63 | .on('load-done', fnSafeApply(function(scope){ 64 | scope.onLoadDone({}); 65 | })) 66 | .on('load-error', fnSafeApply(function(scope){ 67 | scope.onLoadError({}); 68 | })) 69 | .on('area-move area-resize', fnSafeApply(function(scope){ 70 | if(!!scope.changeOnFly) { 71 | updateResultImage(scope); 72 | } 73 | })) 74 | .on('area-move-end area-resize-end image-updated', fnSafeApply(function(scope){ 75 | updateResultImage(scope); 76 | })); 77 | 78 | // Sync CropHost with Directive's options 79 | scope.$watch('image',function(){ 80 | cropHost.setNewImageSource(scope.image); 81 | }); 82 | scope.$watch('areaType',function(){ 83 | cropHost.setAreaType(scope.areaType); 84 | updateResultImage(scope); 85 | }); 86 | scope.$watch('areaMinSize',function(){ 87 | cropHost.setAreaMinSize(scope.areaMinSize); 88 | updateResultImage(scope); 89 | }); 90 | scope.$watch('resultImageSize',function(){ 91 | cropHost.setResultImageSize(scope.resultImageSize); 92 | updateResultImage(scope); 93 | }); 94 | scope.$watch('resultImageFormat',function(){ 95 | cropHost.setResultImageFormat(scope.resultImageFormat); 96 | updateResultImage(scope); 97 | }); 98 | scope.$watch('resultImageQuality',function(){ 99 | cropHost.setResultImageQuality(scope.resultImageQuality); 100 | updateResultImage(scope); 101 | }); 102 | 103 | // Update CropHost dimensions when the directive element is resized 104 | scope.$watch( 105 | function () { 106 | return [element[0].clientWidth, element[0].clientHeight]; 107 | }, 108 | function (value) { 109 | cropHost.setMaxDimensions(value[0],value[1]); 110 | updateResultImage(scope); 111 | }, 112 | true 113 | ); 114 | 115 | // Destroy CropHost Instance when the directive is destroying 116 | scope.$on('$destroy', function(){ 117 | cropHost.destroy(); 118 | }); 119 | } 120 | }; 121 | }]); -------------------------------------------------------------------------------- /source/scss/ng-img-crop.scss: -------------------------------------------------------------------------------- 1 | img-crop { 2 | width:100%; 3 | height:100%; 4 | display:block; 5 | position:relative; 6 | overflow:hidden; 7 | canvas { 8 | display:block; 9 | position:absolute; 10 | top:50%; 11 | left:50%; 12 | 13 | // Disable Outline 14 | outline: none; 15 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */ 16 | } 17 | } -------------------------------------------------------------------------------- /test/ng-img-crop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ngImgCrop Test Page 5 | 6 | 7 | 8 | 9 | 28 | 29 | 30 |
31 | 32 |
33 |
34 | Container size: 35 | 36 | 37 | 38 |
39 |
40 | Area type: 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 | 53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | Result Image Format: 61 | 62 | 63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 |
81 | 82 |
83 | 96 | 97 |
98 | 99 |
100 |

Result

101 |
102 | 103 |
104 |
105 | 106 | 146 | 147 | -------------------------------------------------------------------------------- /test/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk111/ngImgCrop/585fd8c87d453ab27ce29082d62dd7c7a5891ea5/test/test.jpg --------------------------------------------------------------------------------