/** * Copyright (C) 2021 Double Bastion LLC * * This file is part of Roundpin, which is licensed under the * GNU Affero General Public License Version 3.0. The license terms * are detailed in the "LICENSE.txt" file located in the root directory. * * The file content from below is identical with that of the * original file "croppie.js" released under the MIT License: * https://github.com/Foliotek/Croppie/blob/master/LICENSE . * The copyright notice for the original content follows: */ /************************* * Croppie * Copyright 2019 * Foliotek * Version: 2.6.4 *************************/ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(factory); } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { // CommonJS module.exports = factory(); } else { // Browser globals root.Croppie = factory(); } }(typeof self !== 'undefined' ? self : this, function () { /* Polyfills */ if (typeof Promise !== 'function') { /*! promise-polyfill 3.1.0 */ !function(a){function b(a,b){return function(){a.apply(b,arguments)}}function c(a){if("object"!==typeof this)throw new TypeError("Promises must be constructed via new");if("function"!==typeof a)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],i(a,b(e,this),b(f,this))}function d(a){var b=this;return null===this._state?void this._deferreds.push(a):void k(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(e){return void a.reject(e)}a.resolve(d)})}function e(a){try{if(a===this)throw new TypeError("A promise cannot be resolved with itself.");if(a&&("object"===typeof a||"function"===typeof a)){var c=a.then;if("function"===typeof c)return void i(b(c,a),b(e,this),b(f,this))}this._state=!0,this._value=a,g.call(this)}catch(d){f.call(this,d)}}function f(a){this._state=!1,this._value=a,g.call(this)}function g(){for(var a=0,b=this._deferreds.length;b>a;a++)d.call(this,this._deferreds[a]);this._deferreds=null}function h(a,b,c,d){this.onFulfilled="function"===typeof a?a:null,this.onRejected="function"===typeof b?b:null,this.resolve=c,this.reject=d}function i(a,b,c){var d=!1;try{a(function(a){d||(d=!0,b(a))},function(a){d||(d=!0,c(a))})}catch(e){if(d)return;d=!0,c(e)}}var j=setTimeout,k="function"===typeof setImmediate&&setImmediate||function(a){j(a,1)},l=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)};c.prototype["catch"]=function(a){return this.then(null,a)},c.prototype.then=function(a,b){var e=this;return new c(function(c,f){d.call(e,new h(a,b,c,f))})},c.all=function(){var a=Array.prototype.slice.call(1===arguments.length&&l(arguments[0])?arguments[0]:arguments);return new c(function(b,c){function d(f,g){try{if(g&&("object"===typeof g||"function"===typeof g)){var h=g.then;if("function"===typeof h)return void h.call(g,function(a){d(f,a)},c)}a[f]=g,0===--e&&b(a)}catch(i){c(i)}}if(0===a.length)return b([]);for(var e=a.length,f=0;fd;d++)a[d].then(b,c)})},c._setImmediateFn=function(a){k=a},"undefined"!==typeof module&&module.exports?module.exports=c:a.Promise||(a.Promise=c)}(this); } if ( typeof window.CustomEvent !== "function" ) { (function(){ function CustomEvent ( event, params ) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent( 'CustomEvent' ); evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); return evt; } CustomEvent.prototype = window.Event.prototype; window.CustomEvent = CustomEvent; }()); } if (!HTMLCanvasElement.prototype.toBlob) { Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { value: function (callback, type, quality) { var binStr = atob( this.toDataURL(type, quality).split(',')[1] ), len = binStr.length, arr = new Uint8Array(len); for (var i=0; i -1 ? EXIF_NORM : EXIF_FLIP, index = arr.indexOf(ornt), offset = (rotate / 90) % arr.length;// 180 = 2%4 = 2 shift exif by 2 indexes return arr[(arr.length + index + (offset % arr.length)) % arr.length]; } // Credits to : Andrew Dupont - http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/ function deepExtend(destination, source) { destination = destination || {}; for (var property in source) { if (source[property] && source[property].constructor && source[property].constructor === Object) { destination[property] = destination[property] || {}; deepExtend(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; } function clone(object) { return deepExtend({}, object); } function debounce(func, wait, immediate) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function dispatchChange(element) { if ("createEvent" in document) { var evt = document.createEvent("HTMLEvents"); evt.initEvent("change", false, true); element.dispatchEvent(evt); } else { element.fireEvent("onchange"); } } //http://jsperf.com/vanilla-css function css(el, styles, val) { if (typeof (styles) === 'string') { var tmp = styles; styles = {}; styles[tmp] = val; } for (var prop in styles) { el.style[prop] = styles[prop]; } } function addClass(el, c) { if (el.classList) { el.classList.add(c); } else { el.className += ' ' + c; } } function removeClass(el, c) { if (el.classList) { el.classList.remove(c); } else { el.className = el.className.replace(c, ''); } } function setAttributes(el, attrs) { for (var key in attrs) { el.setAttribute(key, attrs[key]); } } function num(v) { return parseInt(v, 10); } /* Utilities */ function loadImage(src, doExif) { var img = new Image(); img.style.opacity = '0'; return new Promise(function (resolve, reject) { function _resolve() { img.style.opacity = '1'; setTimeout(function () { resolve(img); }, 1); } img.removeAttribute('crossOrigin'); if (src.match(/^https?:\/\/|^\/\//)) { img.setAttribute('crossOrigin', 'anonymous'); } img.onload = function () { if (doExif) { EXIF.getData(img, function () { _resolve(); }); } else { _resolve(); } }; img.onerror = function (ev) { img.style.opacity = 1; setTimeout(function () { reject(ev); }, 1); }; img.src = src; }); } function naturalImageDimensions(img, ornt) { var w = img.naturalWidth; var h = img.naturalHeight; var orient = ornt || getExifOrientation(img); if (orient && orient >= 5) { var x= w; w = h; h = x; } return { width: w, height: h }; } /* CSS Transform Prototype */ var TRANSLATE_OPTS = { 'translate3d': { suffix: ', 0px' }, 'translate': { suffix: '' } }; var Transform = function (x, y, scale) { this.x = parseFloat(x); this.y = parseFloat(y); this.scale = parseFloat(scale); }; Transform.parse = function (v) { if (v.style) { return Transform.parse(v.style[CSS_TRANSFORM]); } else if (v.indexOf('matrix') > -1 || v.indexOf('none') > -1) { return Transform.fromMatrix(v); } else { return Transform.fromString(v); } }; Transform.fromMatrix = function (v) { var vals = v.substring(7).split(','); if (!vals.length || v === 'none') { vals = [1, 0, 0, 1, 0, 0]; } return new Transform(num(vals[4]), num(vals[5]), parseFloat(vals[0])); }; Transform.fromString = function (v) { var values = v.split(') '), translate = values[0].substring(Croppie.globals.translate.length + 1).split(','), scale = values.length > 1 ? values[1].substring(6) : 1, x = translate.length > 1 ? translate[0] : 0, y = translate.length > 1 ? translate[1] : 0; return new Transform(x, y, scale); }; Transform.prototype.toString = function () { var suffix = TRANSLATE_OPTS[Croppie.globals.translate].suffix || ''; return Croppie.globals.translate + '(' + this.x + 'px, ' + this.y + 'px' + suffix + ') scale(' + this.scale + ')'; }; var TransformOrigin = function (el) { if (!el || !el.style[CSS_TRANS_ORG]) { this.x = 0; this.y = 0; return; } var css = el.style[CSS_TRANS_ORG].split(' '); this.x = parseFloat(css[0]); this.y = parseFloat(css[1]); }; TransformOrigin.prototype.toString = function () { return this.x + 'px ' + this.y + 'px'; }; function getExifOrientation (img) { return img.exifdata && img.exifdata.Orientation ? num(img.exifdata.Orientation) : 1; } function drawCanvas(canvas, img, orientation) { var width = img.width, height = img.height, ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.save(); switch (orientation) { case 2: ctx.translate(width, 0); ctx.scale(-1, 1); break; case 3: ctx.translate(width, height); ctx.rotate(180*Math.PI/180); break; case 4: ctx.translate(0, height); ctx.scale(1, -1); break; case 5: canvas.width = height; canvas.height = width; ctx.rotate(90*Math.PI/180); ctx.scale(1, -1); break; case 6: canvas.width = height; canvas.height = width; ctx.rotate(90*Math.PI/180); ctx.translate(0, -height); break; case 7: canvas.width = height; canvas.height = width; ctx.rotate(-90*Math.PI/180); ctx.translate(-width, height); ctx.scale(1, -1); break; case 8: canvas.width = height; canvas.height = width; ctx.translate(0, width); ctx.rotate(-90*Math.PI/180); break; } ctx.drawImage(img, 0,0, width, height); ctx.restore(); } /* Private Methods */ function _create() { var self = this, contClass = 'croppie-container', customViewportClass = self.options.viewport.type ? 'cr-vp-' + self.options.viewport.type : null, boundary, img, viewport, overlay, bw, bh; self.options.useCanvas = self.options.enableOrientation || _hasExif.call(self); // Properties on class self.data = {}; self.elements = {}; boundary = self.elements.boundary = document.createElement('div'); viewport = self.elements.viewport = document.createElement('div'); img = self.elements.img = document.createElement('img'); overlay = self.elements.overlay = document.createElement('div'); if (self.options.useCanvas) { self.elements.canvas = document.createElement('canvas'); self.elements.preview = self.elements.canvas; } else { self.elements.preview = img; } addClass(boundary, 'cr-boundary'); boundary.setAttribute('aria-dropeffect', 'none'); bw = self.options.boundary.width; bh = self.options.boundary.height; css(boundary, { width: (bw + (isNaN(bw) ? '' : 'px')), height: (bh + (isNaN(bh) ? '' : 'px')) }); addClass(viewport, 'cr-viewport'); if (customViewportClass) { addClass(viewport, customViewportClass); } css(viewport, { width: self.options.viewport.width + 'px', height: self.options.viewport.height + 'px' }); viewport.setAttribute('tabindex', 0); addClass(self.elements.preview, 'cr-image'); setAttributes(self.elements.preview, { 'alt': 'preview', 'aria-grabbed': 'false' }); addClass(overlay, 'cr-overlay'); self.element.appendChild(boundary); boundary.appendChild(self.elements.preview); boundary.appendChild(viewport); boundary.appendChild(overlay); addClass(self.element, contClass); if (self.options.customClass) { addClass(self.element, self.options.customClass); } _initDraggable.call(this); if (self.options.enableZoom) { _initializeZoom.call(self); } // if (self.options.enableOrientation) { // _initRotationControls.call(self); // } if (self.options.enableResize) { _initializeResize.call(self); } } // function _initRotationControls () { // var self = this, // wrap, btnLeft, btnRight, iLeft, iRight; // wrap = document.createElement('div'); // self.elements.orientationBtnLeft = btnLeft = document.createElement('button'); // self.elements.orientationBtnRight = btnRight = document.createElement('button'); // wrap.appendChild(btnLeft); // wrap.appendChild(btnRight); // iLeft = document.createElement('i'); // iRight = document.createElement('i'); // btnLeft.appendChild(iLeft); // btnRight.appendChild(iRight); // addClass(wrap, 'cr-rotate-controls'); // addClass(btnLeft, 'cr-rotate-l'); // addClass(btnRight, 'cr-rotate-r'); // self.elements.boundary.appendChild(wrap); // btnLeft.addEventListener('click', function () { // self.rotate(-90); // }); // btnRight.addEventListener('click', function () { // self.rotate(90); // }); // } function _hasExif() { return this.options.enableExif && window.EXIF; } function _initializeResize () { var self = this; var wrap = document.createElement('div'); var isDragging = false; var direction; var originalX; var originalY; var minSize = 50; var maxWidth; var maxHeight; var vr; var hr; addClass(wrap, 'cr-resizer'); css(wrap, { width: this.options.viewport.width + 'px', height: this.options.viewport.height + 'px' }); if (this.options.resizeControls.height) { vr = document.createElement('div'); addClass(vr, 'cr-resizer-vertical'); wrap.appendChild(vr); } if (this.options.resizeControls.width) { hr = document.createElement('div'); addClass(hr, 'cr-resizer-horisontal'); wrap.appendChild(hr); } function mouseDown(ev) { if (ev.button !== undefined && ev.button !== 0) return; ev.preventDefault(); if (isDragging) { return; } var overlayRect = self.elements.overlay.getBoundingClientRect(); isDragging = true; originalX = ev.pageX; originalY = ev.pageY; direction = ev.currentTarget.className.indexOf('vertical') !== -1 ? 'v' : 'h'; maxWidth = overlayRect.width; maxHeight = overlayRect.height; if (ev.touches) { var touches = ev.touches[0]; originalX = touches.pageX; originalY = touches.pageY; } window.addEventListener('mousemove', mouseMove); window.addEventListener('touchmove', mouseMove); window.addEventListener('mouseup', mouseUp); window.addEventListener('touchend', mouseUp); document.body.style[CSS_USERSELECT] = 'none'; } function mouseMove(ev) { var pageX = ev.pageX; var pageY = ev.pageY; ev.preventDefault(); if (ev.touches) { var touches = ev.touches[0]; pageX = touches.pageX; pageY = touches.pageY; } var deltaX = pageX - originalX; var deltaY = pageY - originalY; var newHeight = self.options.viewport.height + deltaY; var newWidth = self.options.viewport.width + deltaX; if (direction === 'v' && newHeight >= minSize && newHeight <= maxHeight) { css(wrap, { height: newHeight + 'px' }); self.options.boundary.height += deltaY; css(self.elements.boundary, { height: self.options.boundary.height + 'px' }); self.options.viewport.height += deltaY; css(self.elements.viewport, { height: self.options.viewport.height + 'px' }); } else if (direction === 'h' && newWidth >= minSize && newWidth <= maxWidth) { css(wrap, { width: newWidth + 'px' }); self.options.boundary.width += deltaX; css(self.elements.boundary, { width: self.options.boundary.width + 'px' }); self.options.viewport.width += deltaX; css(self.elements.viewport, { width: self.options.viewport.width + 'px' }); } _updateOverlay.call(self); _updateZoomLimits.call(self); _updateCenterPoint.call(self); _triggerUpdate.call(self); originalY = pageY; originalX = pageX; } function mouseUp() { isDragging = false; window.removeEventListener('mousemove', mouseMove); window.removeEventListener('touchmove', mouseMove); window.removeEventListener('mouseup', mouseUp); window.removeEventListener('touchend', mouseUp); document.body.style[CSS_USERSELECT] = ''; } if (vr) { vr.addEventListener('mousedown', mouseDown); vr.addEventListener('touchstart', mouseDown); } if (hr) { hr.addEventListener('mousedown', mouseDown); hr.addEventListener('touchstart', mouseDown); } this.elements.boundary.appendChild(wrap); } function _setZoomerVal(v) { if (this.options.enableZoom) { var z = this.elements.zoomer, val = fix(v, 4); z.value = Math.max(parseFloat(z.min), Math.min(parseFloat(z.max), val)).toString(); } } function _initializeZoom() { var self = this, wrap = self.elements.zoomerWrap = document.createElement('div'), zoomer = self.elements.zoomer = document.createElement('input'); addClass(wrap, 'cr-slider-wrap'); addClass(zoomer, 'cr-slider'); zoomer.type = 'range'; zoomer.step = '0.0001'; zoomer.value = '1'; zoomer.style.display = self.options.showZoomer ? '' : 'none'; zoomer.setAttribute('aria-label', 'zoom'); self.element.appendChild(wrap); wrap.appendChild(zoomer); self._currentZoom = 1; function change() { _onZoom.call(self, { value: parseFloat(zoomer.value), origin: new TransformOrigin(self.elements.preview), viewportRect: self.elements.viewport.getBoundingClientRect(), transform: Transform.parse(self.elements.preview) }); } function scroll(ev) { var delta, targetZoom; if(self.options.mouseWheelZoom === 'ctrl' && ev.ctrlKey !== true){ return 0; } else if (ev.wheelDelta) { delta = ev.wheelDelta / 1200; //wheelDelta min: -120 max: 120 // max x 10 x 2 } else if (ev.deltaY) { delta = ev.deltaY / 1060; //deltaY min: -53 max: 53 // max x 10 x 2 } else if (ev.detail) { delta = ev.detail / -60; //delta min: -3 max: 3 // max x 10 x 2 } else { delta = 0; } targetZoom = self._currentZoom + (delta * self._currentZoom); ev.preventDefault(); _setZoomerVal.call(self, targetZoom); change.call(self); } self.elements.zoomer.addEventListener('input', change);// this is being fired twice on keypress self.elements.zoomer.addEventListener('change', change); if (self.options.mouseWheelZoom) { self.elements.boundary.addEventListener('mousewheel', scroll); self.elements.boundary.addEventListener('DOMMouseScroll', scroll); } } function _onZoom(ui) { var self = this, transform = ui ? ui.transform : Transform.parse(self.elements.preview), vpRect = ui ? ui.viewportRect : self.elements.viewport.getBoundingClientRect(), origin = ui ? ui.origin : new TransformOrigin(self.elements.preview); function applyCss() { var transCss = {}; transCss[CSS_TRANSFORM] = transform.toString(); transCss[CSS_TRANS_ORG] = origin.toString(); css(self.elements.preview, transCss); } self._currentZoom = ui ? ui.value : self._currentZoom; transform.scale = self._currentZoom; self.elements.zoomer.setAttribute('aria-valuenow', self._currentZoom); applyCss(); if (self.options.enforceBoundary) { var boundaries = _getVirtualBoundaries.call(self, vpRect), transBoundaries = boundaries.translate, oBoundaries = boundaries.origin; if (transform.x >= transBoundaries.maxX) { origin.x = oBoundaries.minX; transform.x = transBoundaries.maxX; } if (transform.x <= transBoundaries.minX) { origin.x = oBoundaries.maxX; transform.x = transBoundaries.minX; } if (transform.y >= transBoundaries.maxY) { origin.y = oBoundaries.minY; transform.y = transBoundaries.maxY; } if (transform.y <= transBoundaries.minY) { origin.y = oBoundaries.maxY; transform.y = transBoundaries.minY; } } applyCss(); _debouncedOverlay.call(self); _triggerUpdate.call(self); } function _getVirtualBoundaries(viewport) { var self = this, scale = self._currentZoom, vpWidth = viewport.width, vpHeight = viewport.height, centerFromBoundaryX = self.elements.boundary.clientWidth / 2, centerFromBoundaryY = self.elements.boundary.clientHeight / 2, imgRect = self.elements.preview.getBoundingClientRect(), curImgWidth = imgRect.width, curImgHeight = imgRect.height, halfWidth = vpWidth / 2, halfHeight = vpHeight / 2; var maxX = ((halfWidth / scale) - centerFromBoundaryX) * -1; var minX = maxX - ((curImgWidth * (1 / scale)) - (vpWidth * (1 / scale))); var maxY = ((halfHeight / scale) - centerFromBoundaryY) * -1; var minY = maxY - ((curImgHeight * (1 / scale)) - (vpHeight * (1 / scale))); var originMinX = (1 / scale) * halfWidth; var originMaxX = (curImgWidth * (1 / scale)) - originMinX; var originMinY = (1 / scale) * halfHeight; var originMaxY = (curImgHeight * (1 / scale)) - originMinY; return { translate: { maxX: maxX, minX: minX, maxY: maxY, minY: minY }, origin: { maxX: originMaxX, minX: originMinX, maxY: originMaxY, minY: originMinY } }; } function _updateCenterPoint(rotate) { var self = this, scale = self._currentZoom, data = self.elements.preview.getBoundingClientRect(), vpData = self.elements.viewport.getBoundingClientRect(), transform = Transform.parse(self.elements.preview.style[CSS_TRANSFORM]), pc = new TransformOrigin(self.elements.preview), top = (vpData.top - data.top) + (vpData.height / 2), left = (vpData.left - data.left) + (vpData.width / 2), center = {}, adj = {}; if (rotate) { var cx = pc.x; var cy = pc.y; var tx = transform.x; var ty = transform.y; center.y = cx; center.x = cy; transform.y = tx; transform.x = ty; } else { center.y = top / scale; center.x = left / scale; adj.y = (center.y - pc.y) * (1 - scale); adj.x = (center.x - pc.x) * (1 - scale); transform.x -= adj.x; transform.y -= adj.y; } var newCss = {}; newCss[CSS_TRANS_ORG] = center.x + 'px ' + center.y + 'px'; newCss[CSS_TRANSFORM] = transform.toString(); css(self.elements.preview, newCss); } function _initDraggable() { var self = this, isDragging = false, originalX, originalY, originalDistance, vpRect, transform; function assignTransformCoordinates(deltaX, deltaY) { var imgRect = self.elements.preview.getBoundingClientRect(), top = transform.y + deltaY, left = transform.x + deltaX; if (self.options.enforceBoundary) { if (vpRect.top > imgRect.top + deltaY && vpRect.bottom < imgRect.bottom + deltaY) { transform.y = top; } if (vpRect.left > imgRect.left + deltaX && vpRect.right < imgRect.right + deltaX) { transform.x = left; } } else { transform.y = top; transform.x = left; } } function toggleGrabState(isDragging) { self.elements.preview.setAttribute('aria-grabbed', isDragging); self.elements.boundary.setAttribute('aria-dropeffect', isDragging? 'move': 'none'); } function keyDown(ev) { var LEFT_ARROW = 37, UP_ARROW = 38, RIGHT_ARROW = 39, DOWN_ARROW = 40; if (ev.shiftKey && (ev.keyCode === UP_ARROW || ev.keyCode === DOWN_ARROW)) { var zoom; if (ev.keyCode === UP_ARROW) { zoom = parseFloat(self.elements.zoomer.value) + parseFloat(self.elements.zoomer.step) } else { zoom = parseFloat(self.elements.zoomer.value) - parseFloat(self.elements.zoomer.step) } self.setZoom(zoom); } else if (self.options.enableKeyMovement && (ev.keyCode >= 37 && ev.keyCode <= 40)) { ev.preventDefault(); var movement = parseKeyDown(ev.keyCode); transform = Transform.parse(self.elements.preview); document.body.style[CSS_USERSELECT] = 'none'; vpRect = self.elements.viewport.getBoundingClientRect(); keyMove(movement); } function parseKeyDown(key) { switch (key) { case LEFT_ARROW: return [1, 0]; case UP_ARROW: return [0, 1]; case RIGHT_ARROW: return [-1, 0]; case DOWN_ARROW: return [0, -1]; } } } function keyMove(movement) { var deltaX = movement[0], deltaY = movement[1], newCss = {}; assignTransformCoordinates(deltaX, deltaY); newCss[CSS_TRANSFORM] = transform.toString(); css(self.elements.preview, newCss); _updateOverlay.call(self); document.body.style[CSS_USERSELECT] = ''; _updateCenterPoint.call(self); _triggerUpdate.call(self); originalDistance = 0; } function mouseDown(ev) { if (ev.button !== undefined && ev.button !== 0) return; ev.preventDefault(); if (isDragging) return; isDragging = true; originalX = ev.pageX; originalY = ev.pageY; if (ev.touches) { var touches = ev.touches[0]; originalX = touches.pageX; originalY = touches.pageY; } toggleGrabState(isDragging); transform = Transform.parse(self.elements.preview); window.addEventListener('mousemove', mouseMove); window.addEventListener('touchmove', mouseMove); window.addEventListener('mouseup', mouseUp); window.addEventListener('touchend', mouseUp); document.body.style[CSS_USERSELECT] = 'none'; vpRect = self.elements.viewport.getBoundingClientRect(); } function mouseMove(ev) { ev.preventDefault(); var pageX = ev.pageX, pageY = ev.pageY; if (ev.touches) { var touches = ev.touches[0]; pageX = touches.pageX; pageY = touches.pageY; } var deltaX = pageX - originalX, deltaY = pageY - originalY, newCss = {}; if (ev.type === 'touchmove') { if (ev.touches.length > 1) { var touch1 = ev.touches[0]; var touch2 = ev.touches[1]; var dist = Math.sqrt((touch1.pageX - touch2.pageX) * (touch1.pageX - touch2.pageX) + (touch1.pageY - touch2.pageY) * (touch1.pageY - touch2.pageY)); if (!originalDistance) { originalDistance = dist / self._currentZoom; } var scale = dist / originalDistance; _setZoomerVal.call(self, scale); dispatchChange(self.elements.zoomer); return; } } assignTransformCoordinates(deltaX, deltaY); newCss[CSS_TRANSFORM] = transform.toString(); css(self.elements.preview, newCss); _updateOverlay.call(self); originalY = pageY; originalX = pageX; } function mouseUp() { isDragging = false; toggleGrabState(isDragging); window.removeEventListener('mousemove', mouseMove); window.removeEventListener('touchmove', mouseMove); window.removeEventListener('mouseup', mouseUp); window.removeEventListener('touchend', mouseUp); document.body.style[CSS_USERSELECT] = ''; _updateCenterPoint.call(self); _triggerUpdate.call(self); originalDistance = 0; } self.elements.overlay.addEventListener('mousedown', mouseDown); self.elements.viewport.addEventListener('keydown', keyDown); self.elements.overlay.addEventListener('touchstart', mouseDown); } function _updateOverlay() { if (!this.elements) return; // since this is debounced, it can be fired after destroy var self = this, boundRect = self.elements.boundary.getBoundingClientRect(), imgData = self.elements.preview.getBoundingClientRect(); css(self.elements.overlay, { width: imgData.width + 'px', height: imgData.height + 'px', top: (imgData.top - boundRect.top) + 'px', left: (imgData.left - boundRect.left) + 'px' }); } var _debouncedOverlay = debounce(_updateOverlay, 500); function _triggerUpdate() { var self = this, data = self.get(); if (!_isVisible.call(self)) { return; } self.options.update.call(self, data); if (self.$ && typeof Prototype === 'undefined') { self.$(self.element).trigger('update.croppie', data); } else { var ev; if (window.CustomEvent) { ev = new CustomEvent('update', { detail: data }); } else { ev = document.createEvent('CustomEvent'); ev.initCustomEvent('update', true, true, data); } self.element.dispatchEvent(ev); } } function _isVisible() { return this.elements.preview.offsetHeight > 0 && this.elements.preview.offsetWidth > 0; } function _updatePropertiesFromImage() { var self = this, initialZoom = 1, cssReset = {}, img = self.elements.preview, imgData, transformReset = new Transform(0, 0, initialZoom), originReset = new TransformOrigin(), isVisible = _isVisible.call(self); if (!isVisible || self.data.bound) {// if the croppie isn't visible or it doesn't need binding return; } self.data.bound = true; cssReset[CSS_TRANSFORM] = transformReset.toString(); cssReset[CSS_TRANS_ORG] = originReset.toString(); cssReset['opacity'] = 1; css(img, cssReset); imgData = self.elements.preview.getBoundingClientRect(); self._originalImageWidth = imgData.width; self._originalImageHeight = imgData.height; self.data.orientation = getExifOrientation(self.elements.img); if (self.options.enableZoom) { _updateZoomLimits.call(self, true); } else { self._currentZoom = initialZoom; } transformReset.scale = self._currentZoom; cssReset[CSS_TRANSFORM] = transformReset.toString(); css(img, cssReset); if (self.data.points.length) { _bindPoints.call(self, self.data.points); } else { _centerImage.call(self); } _updateCenterPoint.call(self); _updateOverlay.call(self); } function _updateZoomLimits (initial) { var self = this, minZoom = Math.max(self.options.minZoom, 0) || 0, maxZoom = self.options.maxZoom || 1.5, initialZoom, defaultInitialZoom, zoomer = self.elements.zoomer, scale = parseFloat(zoomer.value), boundaryData = self.elements.boundary.getBoundingClientRect(), imgData = naturalImageDimensions(self.elements.img, self.data.orientation), vpData = self.elements.viewport.getBoundingClientRect(), minW, minH; if (self.options.enforceBoundary) { minW = vpData.width / imgData.width; minH = vpData.height / imgData.height; minZoom = Math.max(minW, minH); } if (minZoom >= maxZoom) { maxZoom = minZoom + 1; } zoomer.min = fix(minZoom, 4); zoomer.max = fix(maxZoom, 4); if (!initial && (scale < zoomer.min || scale > zoomer.max)) { _setZoomerVal.call(self, scale < zoomer.min ? zoomer.min : zoomer.max); } else if (initial) { defaultInitialZoom = Math.max((boundaryData.width / imgData.width), (boundaryData.height / imgData.height)); initialZoom = self.data.boundZoom !== null ? self.data.boundZoom : defaultInitialZoom; _setZoomerVal.call(self, initialZoom); } dispatchChange(zoomer); } function _bindPoints(points) { if (points.length !== 4) { throw "Croppie - Invalid number of points supplied: " + points; } var self = this, pointsWidth = points[2] - points[0], // pointsHeight = points[3] - points[1], vpData = self.elements.viewport.getBoundingClientRect(), boundRect = self.elements.boundary.getBoundingClientRect(), vpOffset = { left: vpData.left - boundRect.left, top: vpData.top - boundRect.top }, scale = vpData.width / pointsWidth, originTop = points[1], originLeft = points[0], transformTop = (-1 * points[1]) + vpOffset.top, transformLeft = (-1 * points[0]) + vpOffset.left, newCss = {}; newCss[CSS_TRANS_ORG] = originLeft + 'px ' + originTop + 'px'; newCss[CSS_TRANSFORM] = new Transform(transformLeft, transformTop, scale).toString(); css(self.elements.preview, newCss); _setZoomerVal.call(self, scale); self._currentZoom = scale; } function _centerImage() { var self = this, imgDim = self.elements.preview.getBoundingClientRect(), vpDim = self.elements.viewport.getBoundingClientRect(), boundDim = self.elements.boundary.getBoundingClientRect(), vpLeft = vpDim.left - boundDim.left, vpTop = vpDim.top - boundDim.top, w = vpLeft - ((imgDim.width - vpDim.width) / 2), h = vpTop - ((imgDim.height - vpDim.height) / 2), transform = new Transform(w, h, self._currentZoom); css(self.elements.preview, CSS_TRANSFORM, transform.toString()); } function _transferImageToCanvas(customOrientation) { var self = this, canvas = self.elements.canvas, img = self.elements.img, ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = img.width; canvas.height = img.height; var orientation = self.options.enableOrientation && customOrientation || getExifOrientation(img); drawCanvas(canvas, img, orientation); } function _getCanvas(data) { var self = this, points = data.points, left = num(points[0]), top = num(points[1]), right = num(points[2]), bottom = num(points[3]), width = right-left, height = bottom-top, circle = data.circle, canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'), startX = 0, startY = 0, canvasWidth = data.outputWidth || width, canvasHeight = data.outputHeight || height; canvas.width = canvasWidth; canvas.height = canvasHeight; if (data.backgroundColor) { ctx.fillStyle = data.backgroundColor; ctx.fillRect(0, 0, canvasWidth, canvasHeight); } // By default assume we're going to draw the entire // source image onto the destination canvas. var sx = left, sy = top, sWidth = width, sHeight = height, dx = 0, dy = 0, dWidth = canvasWidth, dHeight = canvasHeight; // // Do not go outside of the original image's bounds along the x-axis. // Handle translations when projecting onto the destination canvas. // // The smallest possible source x-position is 0. if (left < 0) { sx = 0; dx = (Math.abs(left) / width) * canvasWidth; } // The largest possible source width is the original image's width. if (sWidth + sx > self._originalImageWidth) { sWidth = self._originalImageWidth - sx; dWidth = (sWidth / width) * canvasWidth; } // // Do not go outside of the original image's bounds along the y-axis. // // The smallest possible source y-position is 0. if (top < 0) { sy = 0; dy = (Math.abs(top) / height) * canvasHeight; } // The largest possible source height is the original image's height. if (sHeight + sy > self._originalImageHeight) { sHeight = self._originalImageHeight - sy; dHeight = (sHeight / height) * canvasHeight; } // console.table({ left, right, top, bottom, canvasWidth, canvasHeight, width, height, startX, startY, circle, sx, sy, dx, dy, sWidth, sHeight, dWidth, dHeight }); ctx.drawImage(this.elements.preview, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); if (circle) { ctx.fillStyle = '#fff'; ctx.globalCompositeOperation = 'destination-in'; ctx.beginPath(); ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); } return canvas; } function _getHtmlResult(data) { var points = data.points, div = document.createElement('div'), img = document.createElement('img'), width = points[2] - points[0], height = points[3] - points[1]; addClass(div, 'croppie-result'); div.appendChild(img); css(img, { left: (-1 * points[0]) + 'px', top: (-1 * points[1]) + 'px' }); img.src = data.url; css(div, { width: width + 'px', height: height + 'px' }); return div; } function _getBase64Result(data) { return _getCanvas.call(this, data).toDataURL(data.format, data.quality); } function _getBlobResult(data) { var self = this; return new Promise(function (resolve) { _getCanvas.call(self, data).toBlob(function (blob) { resolve(blob); }, data.format, data.quality); }); } function _replaceImage(img) { if (this.elements.img.parentNode) { Array.prototype.forEach.call(this.elements.img.classList, function(c) { img.classList.add(c); }); this.elements.img.parentNode.replaceChild(img, this.elements.img); this.elements.preview = img; // if the img is attached to the DOM, they're not using the canvas } this.elements.img = img; } function _bind(options, cb) { var self = this, url, points = [], zoom = null, hasExif = _hasExif.call(self); if (typeof (options) === 'string') { url = options; options = {}; } else if (Array.isArray(options)) { points = options.slice(); } else if (typeof (options) === 'undefined' && self.data.url) { //refreshing _updatePropertiesFromImage.call(self); _triggerUpdate.call(self); return null; } else { url = options.url; points = options.points || []; zoom = typeof(options.zoom) === 'undefined' ? null : options.zoom; } self.data.bound = false; self.data.url = url || self.data.url; self.data.boundZoom = zoom; return loadImage(url, hasExif).then(function (img) { _replaceImage.call(self, img); if (!points.length) { var natDim = naturalImageDimensions(img); var rect = self.elements.viewport.getBoundingClientRect(); var aspectRatio = rect.width / rect.height; var imgAspectRatio = natDim.width / natDim.height; var width, height; if (imgAspectRatio > aspectRatio) { height = natDim.height; width = height * aspectRatio; } else { width = natDim.width; height = natDim.height / aspectRatio; } var x0 = (natDim.width - width) / 2; var y0 = (natDim.height - height) / 2; var x1 = x0 + width; var y1 = y0 + height; self.data.points = [x0, y0, x1, y1]; } else if (self.options.relative) { points = [ points[0] * img.naturalWidth / 100, points[1] * img.naturalHeight / 100, points[2] * img.naturalWidth / 100, points[3] * img.naturalHeight / 100 ]; } self.data.points = points.map(function (p) { return parseFloat(p); }); if (self.options.useCanvas) { _transferImageToCanvas.call(self, options.orientation); } _updatePropertiesFromImage.call(self); _triggerUpdate.call(self); cb && cb(); }); } function fix(v, decimalPoints) { return parseFloat(v).toFixed(decimalPoints || 0); } function _get() { var self = this, imgData = self.elements.preview.getBoundingClientRect(), vpData = self.elements.viewport.getBoundingClientRect(), x1 = vpData.left - imgData.left, y1 = vpData.top - imgData.top, widthDiff = (vpData.width - self.elements.viewport.offsetWidth) / 2, //border heightDiff = (vpData.height - self.elements.viewport.offsetHeight) / 2, x2 = x1 + self.elements.viewport.offsetWidth + widthDiff, y2 = y1 + self.elements.viewport.offsetHeight + heightDiff, scale = self._currentZoom; if (scale === Infinity || isNaN(scale)) { scale = 1; } var max = self.options.enforceBoundary ? 0 : Number.NEGATIVE_INFINITY; x1 = Math.max(max, x1 / scale); y1 = Math.max(max, y1 / scale); x2 = Math.max(max, x2 / scale); y2 = Math.max(max, y2 / scale); return { points: [fix(x1), fix(y1), fix(x2), fix(y2)], zoom: scale, orientation: self.data.orientation }; } var RESULT_DEFAULTS = { type: 'canvas', format: 'png', quality: 1 }, RESULT_FORMATS = ['jpeg', 'webp', 'png']; function _result(options) { var self = this, data = _get.call(self), opts = deepExtend(clone(RESULT_DEFAULTS), clone(options)), resultType = (typeof (options) === 'string' ? options : (opts.type || 'base64')), size = opts.size || 'viewport', format = opts.format, quality = opts.quality, backgroundColor = opts.backgroundColor, circle = typeof opts.circle === 'boolean' ? opts.circle : (self.options.viewport.type === 'circle'), vpRect = self.elements.viewport.getBoundingClientRect(), ratio = vpRect.width / vpRect.height, prom; if (size === 'viewport') { data.outputWidth = vpRect.width; data.outputHeight = vpRect.height; } else if (typeof size === 'object') { if (size.width && size.height) { data.outputWidth = size.width; data.outputHeight = size.height; } else if (size.width) { data.outputWidth = size.width; data.outputHeight = size.width / ratio; } else if (size.height) { data.outputWidth = size.height * ratio; data.outputHeight = size.height; } } if (RESULT_FORMATS.indexOf(format) > -1) { data.format = 'image/' + format; data.quality = quality; } data.circle = circle; data.url = self.data.url; data.backgroundColor = backgroundColor; prom = new Promise(function (resolve) { switch(resultType.toLowerCase()) { case 'rawcanvas': resolve(_getCanvas.call(self, data)); break; case 'canvas': case 'base64': resolve(_getBase64Result.call(self, data)); break; case 'blob': _getBlobResult.call(self, data).then(resolve); break; default: resolve(_getHtmlResult.call(self, data)); break; } }); return prom; } function _refresh() { _updatePropertiesFromImage.call(this); } function _rotate(deg) { if (!this.options.useCanvas || !this.options.enableOrientation) { throw 'Croppie: Cannot rotate without enableOrientation && EXIF.js included'; } var self = this, canvas = self.elements.canvas; self.data.orientation = getExifOffset(self.data.orientation, deg); drawCanvas(canvas, self.elements.img, self.data.orientation); _updateCenterPoint.call(self, true); _updateZoomLimits.call(self); } function _destroy() { var self = this; self.element.removeChild(self.elements.boundary); removeClass(self.element, 'croppie-container'); if (self.options.enableZoom) { self.element.removeChild(self.elements.zoomerWrap); } delete self.elements; } if (window.jQuery) { var $ = window.jQuery; $.fn.croppie = function (opts) { var ot = typeof opts; if (ot === 'string') { var args = Array.prototype.slice.call(arguments, 1); var singleInst = $(this).data('croppie'); if (opts === 'get') { return singleInst.get(); } else if (opts === 'result') { return singleInst.result.apply(singleInst, args); } else if (opts === 'bind') { return singleInst.bind.apply(singleInst, args); } return this.each(function () { var i = $(this).data('croppie'); if (!i) return; var method = i[opts]; if ($.isFunction(method)) { method.apply(i, args); if (opts === 'destroy') { $(this).removeData('croppie'); } } else { throw 'Croppie ' + opts + ' method not found'; } }); } else { return this.each(function () { var i = new Croppie(this, opts); i.$ = $; $(this).data('croppie', i); }); } }; } function Croppie(element, opts) { if (element.className.indexOf('croppie-container') > -1) { throw new Error("Croppie: Can't initialize croppie more than once"); } this.element = element; this.options = deepExtend(clone(Croppie.defaults), opts); if (this.element.tagName.toLowerCase() === 'img') { var origImage = this.element; addClass(origImage, 'cr-original-image'); setAttributes(origImage, {'aria-hidden' : 'true', 'alt' : '' }); var replacementDiv = document.createElement('div'); this.element.parentNode.appendChild(replacementDiv); replacementDiv.appendChild(origImage); this.element = replacementDiv; this.options.url = this.options.url || origImage.src; } _create.call(this); if (this.options.url) { var bindOpts = { url: this.options.url, points: this.options.points }; delete this.options['url']; delete this.options['points']; _bind.call(this, bindOpts); } } Croppie.defaults = { viewport: { width: 100, height: 100, type: 'square' }, boundary: { }, orientationControls: { enabled: true, leftClass: '', rightClass: '' }, resizeControls: { width: true, height: true }, customClass: '', showZoomer: true, enableZoom: true, enableResize: false, mouseWheelZoom: true, enableExif: false, enforceBoundary: true, enableOrientation: false, enableKeyMovement: true, update: function () { } }; Croppie.globals = { translate: 'translate3d' }; deepExtend(Croppie.prototype, { bind: function (options, cb) { return _bind.call(this, options, cb); }, get: function () { var data = _get.call(this); var points = data.points; if (this.options.relative) { points[0] /= this.elements.img.naturalWidth / 100; points[1] /= this.elements.img.naturalHeight / 100; points[2] /= this.elements.img.naturalWidth / 100; points[3] /= this.elements.img.naturalHeight / 100; } return data; }, result: function (type) { return _result.call(this, type); }, refresh: function () { return _refresh.call(this); }, setZoom: function (v) { _setZoomerVal.call(this, v); dispatchChange(this.elements.zoomer); }, rotate: function (deg) { _rotate.call(this, deg); }, destroy: function () { return _destroy.call(this); } }); return Croppie; }));