From 67cf2fb38882c92f367ba02bbdd2ea13810292a6 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 5 Nov 2024 10:47:00 +0100 Subject: [PATCH 01/35] Install cropperjs --- package-lock.json | 131 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 132 insertions(+) diff --git a/package-lock.json b/package-lock.json index d2252eb399..a086c9cc38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@woltlab/editor": "git+https://github.com/WoltLab/editor.git#b9a8e10528a42c5aac06ec1837881a7dd141aefb", "@woltlab/visual-dom-diff": "git+https://github.com/WoltLab/visual-dom-diff.git#e5b51fce3157d1eda310566fc1f86101341d1fea", "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#5b582b24e437f1883ccad3c37dae7c3c5f1e7da3", + "cropperjs": "2.0.0-rc.2", "emoji-picker-element": "^1.22.8", "focus-trap": "^7.6.0", "html-parsed-element": "^0.4.1", @@ -796,6 +797,126 @@ "lodash-es": "4.17.21" } }, + "node_modules/@cropper/element": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.0-rc.2.tgz", + "integrity": "sha512-4G6lTJblndwzpsb43YKeHiKcocOkDIWystGzbHNbqRysE0U0lYHuRyvV7FW6a9S63wtMFSYuwFxcdUdUcmkF8w==", + "license": "MIT", + "dependencies": { + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-canvas": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.0-rc.2.tgz", + "integrity": "sha512-0aqbJ3ycQM6/yn4T03vw8K/OeTB8C6+Z/jimuavy4UM2CENH9ucSLM4hAG0yYCgghIyv9Zd0unaBmtgW+I5+SQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-crosshair": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.0-rc.2.tgz", + "integrity": "sha512-yopINLvaZhL3E2GNienju1zeQ1Cifkn5f/0R7ZabXcAgUI0s2sLzNqL8+2XV2J3DzEzYEIYc+49KmMle04nVWQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-grid": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.0-rc.2.tgz", + "integrity": "sha512-PzAfEya6CmIc/o/lcA/NZ1rohszz42wjq2z3E2zq2jMfNDxY/EIoFnGI6+hJrxCAaoKD8UlKOEHQdRQbtnjcMg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-handle": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.0-rc.2.tgz", + "integrity": "sha512-wOWX4xpryxKcrhnJC2mHebqQQ622UN2oyQoDZcaMzvlwt7nnX3bInF+SFrIj9/aCxtCUYY0oD2gaJkfd6aNJ0g==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-image": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.0-rc.2.tgz", + "integrity": "sha512-RTKnuJrqn1K8FscS11auit2W57AG04mxRNOxBldYs3lKTkwZjzJdQFkZ/Nxu+cwVXT+c6IeEiayNKvu4B7CAQg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/element-canvas": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-selection": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.0-rc.2.tgz", + "integrity": "sha512-UIgIHKHz4qNKlm5YRnC/Pu9+VrInm5TSOzkmU8kPt2swUk0WHNRv3ZcOjCQZ2ccTQnAH3FVM3FYDZ8HjRwLcBg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/element-canvas": "^2.0.0-rc.2", + "@cropper/element-image": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-shade": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.0-rc.2.tgz", + "integrity": "sha512-vHAGFxlqgflGZWkRYNWNHUY0zsV72YZGmCgtUu4sMrnWLZL/jMGhxmm8zZCe/aB94F829XcQ6uf3BoiApB+7Ng==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/element-canvas": "^2.0.0-rc.2", + "@cropper/element-selection": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/element-viewer": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.0-rc.2.tgz", + "integrity": "sha512-2z9mIA7ic3enNS4xvq9Gq6hnRZ1tPr0h+lCrOHP55NL4he63lE9oTVJfDx19rL95wUS4VxL2ANvr2BVLNiBM7A==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/element-canvas": "^2.0.0-rc.2", + "@cropper/element-image": "^2.0.0-rc.2", + "@cropper/element-selection": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/elements": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.0-rc.2.tgz", + "integrity": "sha512-NG5kdqpv7/tGvUfNjJiIHr2Ip431v5t/P5cIXTcYAgt8PRyFJmjx3fatC7NLnP/FUlv+bbzd8PMRI4LY4Gaw3Q==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0-rc.2", + "@cropper/element-canvas": "^2.0.0-rc.2", + "@cropper/element-crosshair": "^2.0.0-rc.2", + "@cropper/element-grid": "^2.0.0-rc.2", + "@cropper/element-handle": "^2.0.0-rc.2", + "@cropper/element-image": "^2.0.0-rc.2", + "@cropper/element-selection": "^2.0.0-rc.2", + "@cropper/element-shade": "^2.0.0-rc.2", + "@cropper/element-viewer": "^2.0.0-rc.2" + } + }, + "node_modules/@cropper/utils": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.0-rc.2.tgz", + "integrity": "sha512-EEivNsyV6BtL496m4Q/IeAC6FGlyKjKIT1qMtwaxtkR+2ZlKnf9O7AdcGpClemIBA+TbwWAzp0UyIvYFtKUZ1Q==", + "license": "MIT" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2373,6 +2494,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cropperjs": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.0-rc.2.tgz", + "integrity": "sha512-BTuz+UeZphGOEnBCuQiNT4rk1uFfKJaKmTgoH9XU7Q8IMkLdodW7YPWINmXJXwWMt1nXiKze5qKADVbz9xtVFg==", + "license": "MIT", + "dependencies": { + "@cropper/elements": "^2.0.0-rc.2", + "@cropper/utils": "^2.0.0-rc.2" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 43e9e1ce9d..967a4ac1df 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@woltlab/editor": "git+https://github.com/WoltLab/editor.git#b9a8e10528a42c5aac06ec1837881a7dd141aefb", "@woltlab/visual-dom-diff": "git+https://github.com/WoltLab/visual-dom-diff.git#e5b51fce3157d1eda310566fc1f86101341d1fea", "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#5b582b24e437f1883ccad3c37dae7c3c5f1e7da3", + "cropperjs": "2.0.0-rc.2", "emoji-picker-element": "^1.22.8", "focus-trap": "^7.6.0", "html-parsed-element": "^0.4.1", From 313311240336d52b16f6143705cb1e761e6f485a Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 5 Nov 2024 10:50:02 +0100 Subject: [PATCH 02/35] Add `cropper.min.js` --- .github/workflows/javascript.yml | 3 +++ wcfsetup/install/files/js/3rdParty/cropper.min.js | 2 ++ wcfsetup/install/files/js/require.config.js | 1 + 3 files changed, 6 insertions(+) create mode 100644 wcfsetup/install/files/js/3rdParty/cropper.min.js diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index e2566dcc3b..90565a5ec4 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -68,3 +68,6 @@ jobs: - name: "Check '@ckeditor/ckeditor5-inspector'" run: | diff -wu wcfsetup/install/files/js/3rdParty/ckeditor/ckeditor5-inspector/inspector.js node_modules/@ckeditor/ckeditor5-inspector/build/inspector.js + - name: "Check 'cropperjs'" + run: | + diff -wu wcfsetup/install/files/js/3rdParty/cropper.min.js node_modules/cropperjs/dist/cropper.min.js diff --git a/wcfsetup/install/files/js/3rdParty/cropper.min.js b/wcfsetup/install/files/js/3rdParty/cropper.min.js new file mode 100644 index 0000000000..9bbf19b372 --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/cropper.min.js @@ -0,0 +1,2 @@ +/*! Cropper.js v2.0.0-rc | (c) 2015-present Chen Fengyuan | MIT */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Cropper={})}(this,(function(t){"use strict";const e="undefined"!=typeof window&&void 0!==window.document,i=e?window:{},s=!!e&&"ontouchstart"in i.document.documentElement,n=!!e&&"PointerEvent"in i,a="cropper",o=`${a}-canvas`,r=`${a}-crosshair`,h=`${a}-grid`,c=`${a}-handle`,l=`${a}-image`,d=`${a}-selection`,u=`${a}-shade`,$=`${a}-viewer`,p="select",g="move",m="scale",b="rotate",f="transform",v="none",C="n-resize",w="e-resize",y="s-resize",E="w-resize",S="ne-resize",A="nw-resize",T="se-resize",k="sw-resize",x="action",O=s?"touchend touchcancel":"mouseup",N=s?"touchmove":"mousemove",I=s?"touchstart":"mousedown",R=n?"pointerdown":I,z=n?"pointermove":N,M=n?"pointerup pointercancel":O,P="error",D="keydown",_="load",W="wheel",Y="action",L="actionend",X="actionmove",H="actionstart",j="change",V="transform";function U(t){return"string"==typeof t}const q=Number.isNaN||i.isNaN;function B(t){return"number"==typeof t&&!q(t)}function K(t){return B(t)&&t>0&&t<1/0}function Z(t){return void 0===t}function F(t){return"object"==typeof t&&null!==t}const{hasOwnProperty:G}=Object.prototype;function J(t){if(!F(t))return!1;try{const{constructor:e}=t,{prototype:i}=e;return e&&i&&G.call(i,"isPrototypeOf")}catch(t){return!1}}function Q(t){return"function"==typeof t}function tt(t){return"object"==typeof t&&null!==t&&1===t.nodeType}const et=/([a-z\d])([A-Z])/g;function it(t){return String(t).replace(et,"$1-$2").toLowerCase()}const st=/-[A-z\d]/g;function nt(t){return t.replace(st,(t=>t.slice(1).toUpperCase()))}const at=/\s\s*/;function ot(t,e,i,s){e.trim().split(at).forEach((e=>{t.removeEventListener(e,i,s)}))}function rt(t,e,i,s){e.trim().split(at).forEach((e=>{t.addEventListener(e,i,s)}))}function ht(t,e,i,s){rt(t,e,i,Object.assign(Object.assign({},s),{once:!0}))}const ct={bubbles:!0,cancelable:!0,composed:!0};function lt(t,e,i,s){return t.dispatchEvent(new CustomEvent(e,Object.assign(Object.assign(Object.assign({},ct),{detail:i}),s)))}const dt=Promise.resolve();function ut(t,e){return e?dt.then(t?e.bind(t):e):dt}function $t(t){const{documentElement:e}=t.ownerDocument,s=t.getBoundingClientRect();return{left:s.left+(i.pageXOffset-e.clientLeft),top:s.top+(i.pageYOffset-e.clientTop)}}const pt=/deg|g?rad|turn$/i;function gt(t){const e=parseFloat(t)||0;if(0!==e){const[i="rad"]=String(t).match(pt)||[];switch(i.toLowerCase()){case"deg":return e/360*(2*Math.PI);case"grad":return e/400*(2*Math.PI);case"turn":return e*(2*Math.PI)}}return e}const mt="contain";function bt(t,e=mt){const{aspectRatio:i}=t;let{width:s,height:n}=t;const a=K(s),o=K(n);if(a&&o){const t=n*i;e===mt&&t>s||"cover"===e&&t{const e=nt(t);let i=this[e];Z(i)||this.$propertyChangedCallback(e,void 0,i),Object.defineProperty(this,e,{enumerable:!0,configurable:!0,get:()=>i,set(t){const s=i;i=t,this.$propertyChangedCallback(e,s,t)}})}));const t=this.attachShadow({mode:this.shadowRootMode||Ct});if(this.shadowRoot||wt.set(this,t),yt.set(this,this.$addStyles(this.$sharedStyle)),this.$style&&this.$addStyles(this.$style),this.$template){const e=document.createElement("template");e.innerHTML=this.$template,t.appendChild(e.content)}if(this.slottable){const e=document.createElement("slot");t.appendChild(e)}}disconnectedCallback(){yt.has(this)&&yt.delete(this),wt.has(this)&&wt.delete(this)}$getTagNameOf(t){var e;return null!==(e=Et.get(t))&&void 0!==e?e:t}$setStyles(t){return Object.keys(t).forEach((e=>{let i=t[e];B(i)&&(i=0!==i&&vt.test(e)?`${i}px`:String(i)),this.style[e]=i})),this}$getShadowRoot(){return this.shadowRoot||wt.get(this)}$addStyles(t){let e;const i=this.$getShadowRoot();return St?(e=new CSSStyleSheet,e.replaceSync(t),i.adoptedStyleSheets=i.adoptedStyleSheets.concat(e)):(e=document.createElement("style"),e.textContent=t,i.appendChild(e)),e}$emit(t,e,i){return lt(this,t,e,i)}$nextTick(t){return ut(this,t)}static $define(t,s){F(t)&&(s=t,t=""),t||(t=this.$name||this.name),t=it(t),e&&i.customElements&&!i.customElements.get(t)&&customElements.define(t,this,s)}}At.$version="2.0.0-rc";class Tt extends At{constructor(){super(...arguments),this.$onPointerDown=null,this.$onPointerMove=null,this.$onPointerUp=null,this.$onWheel=null,this.$wheeling=!1,this.$pointers=new Map,this.$style=':host{display:block;min-height:100px;min-width:200px;overflow:hidden;position:relative;touch-action:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([background]){background-color:#fff;background-image:repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc),repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc);background-image:repeating-conic-gradient(#ccc 0 25%,#fff 0 50%);background-position:0 0,.5rem .5rem;background-size:1rem 1rem}:host([disabled]){pointer-events:none}:host([disabled]):after{bottom:0;content:"";cursor:not-allowed;display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}',this.$action=v,this.background=!1,this.disabled=!1,this.scaleStep=.1,this.themeColor="#39f"}static get observedAttributes(){return super.observedAttributes.concat(["background","disabled","scale-step"])}connectedCallback(){super.connectedCallback(),this.disabled||this.$bind()}disconnectedCallback(){this.disabled||this.$unbind(),super.disconnectedCallback()}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"disabled"===t))i?this.$unbind():this.$bind()}$bind(){this.$onPointerDown||(this.$onPointerDown=this.$handlePointerDown.bind(this),rt(this,R,this.$onPointerDown)),this.$onPointerMove||(this.$onPointerMove=this.$handlePointerMove.bind(this),rt(this.ownerDocument,z,this.$onPointerMove)),this.$onPointerUp||(this.$onPointerUp=this.$handlePointerUp.bind(this),rt(this.ownerDocument,M,this.$onPointerUp)),this.$onWheel||(this.$onWheel=this.$handleWheel.bind(this),rt(this,W,this.$onWheel,{passive:!1,capture:!0}))}$unbind(){this.$onPointerDown&&(ot(this,R,this.$onPointerDown),this.$onPointerDown=null),this.$onPointerMove&&(ot(this.ownerDocument,z,this.$onPointerMove),this.$onPointerMove=null),this.$onPointerUp&&(ot(this.ownerDocument,M,this.$onPointerUp),this.$onPointerUp=null),this.$onWheel&&(ot(this,W,this.$onWheel,{capture:!0}),this.$onWheel=null)}$handlePointerDown(t){const{buttons:e,button:i,type:s}=t;if(this.disabled||("pointerdown"===s&&"mouse"===t.pointerType||"mousedown"===s)&&(B(e)&&1!==e||B(i)&&0!==i||t.ctrlKey))return;const{$pointers:n}=this;let a="";if(t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:i})=>{n.set(t,{startX:e,startY:i,endX:e,endY:i})}));else{const{pointerId:e=0,pageX:i,pageY:s}=t;n.set(e,{startX:i,startY:s,endX:i,endY:s})}n.size>1?a=f:tt(t.target)&&(a=t.target.action||t.target.getAttribute(x)||""),!1!==this.$emit(H,{action:a,relatedEvent:t})&&(t.preventDefault(),this.$action=a,this.style.willChange="transform")}$handlePointerMove(t){const{$action:e,$pointers:i}=this;if(this.disabled||e===v||0===i.size)return;if(!1===this.$emit(X,{action:e,relatedEvent:t}))return;if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:s})=>{const n=i.get(t);n&&Object.assign(n,{endX:e,endY:s})}));else{const{pointerId:e=0,pageX:s,pageY:n}=t,a=i.get(e);a&&Object.assign(a,{endX:s,endY:n})}const s={action:e,relatedEvent:t};if(e===f){const e=new Map(i);let n=0,a=0,o=0,r=0,h=t.pageX,c=t.pageY;i.forEach(((t,i)=>{e.delete(i),e.forEach((e=>{let i=e.startX-t.startX,s=e.startY-t.startY,l=e.endX-t.endX,d=e.endY-t.endY,u=0,$=0,p=0,g=0;if(0===i?s<0?p=2*Math.PI:s>0&&(p=Math.PI):i>0?p=Math.PI/2+Math.atan(s/i):i<0&&(p=1.5*Math.PI+Math.atan(s/i)),0===l?d<0?g=2*Math.PI:d>0&&(g=Math.PI):l>0?g=Math.PI/2+Math.atan(d/l):l<0&&(g=1.5*Math.PI+Math.atan(d/l)),g>0||p>0){const i=g-p,s=Math.abs(i);s>n&&(n=s,o=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}if(i=Math.abs(i),s=Math.abs(s),l=Math.abs(l),d=Math.abs(d),i>0&&s>0?u=Math.sqrt(i*i+s*s):i>0?u=i:s>0&&(u=s),l>0&&d>0?$=Math.sqrt(l*l+d*d):l>0?$=l:d>0&&($=d),u>0&&$>0){const i=($-u)/u,s=Math.abs(i);s>a&&(a=s,r=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}}))}));const l=n>0,d=a>0;l&&d?(s.rotate=o,s.scale=r,s.centerX=h,s.centerY=c):l?(s.action=b,s.rotate=o,s.centerX=h,s.centerY=c):d?(s.action=m,s.scale=r,s.centerX=h,s.centerY=c):s.action=v}else{const[t]=Array.from(i.values());Object.assign(s,t)}i.forEach((t=>{t.startX=t.endX,t.startY=t.endY})),s.action!==v&&this.$emit(Y,s,{cancelable:!1})}$handlePointerUp(t){const{$action:e,$pointers:i}=this;if(!this.disabled&&e!==v&&!1!==this.$emit(L,{action:e,relatedEvent:t})){if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t})=>{i.delete(t)}));else{const{pointerId:e=0}=t;i.delete(e)}0===i.size&&(this.style.willChange="",this.$action=v)}}$handleWheel(t){if(this.disabled)return;if(t.preventDefault(),this.$wheeling)return;this.$wheeling=!0,setTimeout((()=>{this.$wheeling=!1}),50);const e=(t.deltaY>0?-1:1)*this.scaleStep;this.$emit(Y,{action:m,scale:e,relatedEvent:t},{cancelable:!1})}$setAction(t){return U(t)&&(this.$action=t),this}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let n=this.offsetWidth,a=this.offsetHeight,o=1;J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.offsetWidth),s.width=n,s.height=a;const r=this.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform();let p=u,g=$,m=i.naturalWidth,b=i.naturalHeight;1!==o&&(p*=o,g*=o,m*=o,b*=o);const f=m/2,v=b/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(f,v),h.transform(e,c,l,d,p,g),h.translate(-f,-v),h.drawImage(i,0,0,m,b),h.restore()}e(s)})).catch(i):e(s)}))}}Tt.$name=o,Tt.$version="2.0.0-rc";const kt=new WeakMap,xt=["alt","crossorigin","decoding","importance","loading","referrerpolicy","sizes","src","srcset"];class Ot extends At{constructor(){super(...arguments),this.$matrix=[1,0,0,1,0,0],this.$onLoad=null,this.$onCanvasAction=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$actionStartTarget=null,this.$style=":host{display:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}",this.$image=new Image,this.initialCenterSize="contain",this.rotatable=!1,this.scalable=!1,this.skewable=!1,this.slottable=!1,this.translatable=!1}set $canvas(t){kt.set(this,t)}get $canvas(){return kt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(xt,["initial-center-size","rotatable","scalable","skewable","translatable"])}attributeChangedCallback(t,e,i){Object.is(i,e)||(super.attributeChangedCallback(t,e,i),xt.includes(t)&&this.$image.setAttribute(t,i))}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"initialCenterSize"===t))this.$nextTick((()=>{this.$center(i)}))}connectedCallback(){super.connectedCallback();const{$image:t}=this,e=this.closest(this.$getTagNameOf(o));e&&(this.$canvas=e,this.$setStyles({display:"block",position:"absolute"}),this.$onCanvasActionStart=t=>{var e,i;this.$actionStartTarget=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target},this.$onCanvasActionEnd=()=>{this.$actionStartTarget=null},this.$onCanvasAction=this.$handleAction.bind(this),rt(e,H,this.$onCanvasActionStart),rt(e,L,this.$onCanvasActionEnd),rt(e,Y,this.$onCanvasAction)),this.$onLoad=this.$handleLoad.bind(this),rt(t,_,this.$onLoad),this.$getShadowRoot().appendChild(t)}disconnectedCallback(){const{$image:t,$canvas:e}=this;e&&(this.$onCanvasActionStart&&(ot(e,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(e,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(e,Y,this.$onCanvasAction),this.$onCanvasAction=null)),t&&this.$onLoad&&(ot(t,_,this.$onLoad),this.$onLoad=null),this.$getShadowRoot().removeChild(t),super.disconnectedCallback()}$handleLoad(){const{$image:t}=this;this.$setStyles({width:t.naturalWidth,height:t.naturalHeight}),this.$canvas&&this.$center(this.initialCenterSize)}$handleAction(t){if(this.hidden||!(this.rotatable||this.scalable||this.translatable))return;const{$canvas:e}=this,{detail:i}=t;if(i){const{relatedEvent:t}=i;let{action:s}=i;switch(s!==f||this.rotatable&&this.scalable||(s=this.rotatable?b:this.scalable?m:v),s){case g:if(this.translatable){let s=null;t&&(s=t.target.closest(this.$getTagNameOf(d))),s||(s=e.querySelector(this.$getTagNameOf(d))),s&&s.multiple&&!s.active&&(s=e.querySelector(`${this.$getTagNameOf(d)}[active]`)),s&&!s.hidden&&s.movable&&!s.linked&&this.$actionStartTarget&&s.contains(this.$actionStartTarget)||this.$move(i.endX-i.startX,i.endY-i.startY)}break;case b:if(this.rotatable)if(t){const{x:e,y:s}=this.getBoundingClientRect();this.$rotate(i.rotate,t.clientX-e,t.clientY-s)}else this.$rotate(i.rotate);break;case m:if(this.scalable)if(t){const e=t.target.closest(this.$getTagNameOf(d));if(!e||e.linked){const{x:e,y:s}=this.getBoundingClientRect();this.$zoom(i.scale,t.clientX-e,t.clientY-s)}}else this.$zoom(i.scale);break;case f:if(this.rotatable&&this.scalable){const{rotate:e}=i;let{scale:s}=i;s<0?s=1/(1-s):s+=1;const n=Math.cos(e),a=Math.sin(e),[o,r,h,c]=[n*s,a*s,-a*s,n*s];if(t){const e=this.getBoundingClientRect(),i=t.clientX-e.x,s=t.clientY-e.y,[n,a,l,d]=this.$matrix,u=i-e.width/2,$=s-e.height/2,p=(u*d-l*$)/(n*d-l*a),g=($*n-a*u)/(n*d-l*a);this.$transform(o,r,h,c,p*(1-o)+g*h,g*(1-c)+p*r)}else this.$transform(o,r,h,c,0,0)}}}}$ready(t){const{$image:e}=this,i=new Promise(((t,i)=>{const s=new Error("Failed to load the image source");if(e.complete)e.naturalWidth>0&&e.naturalHeight>0?t(e):i(s);else{const n=()=>{ot(e,P,a),t(e)},a=()=>{ot(e,_,n),i(s)};ht(e,_,n),ht(e,P,a)}}));return Q(t)&&i.then((e=>(t(e),e))),i}$center(t){const{parentElement:e}=this;if(!e)return this;const i=e.getBoundingClientRect(),s=i.width,n=i.height,{x:a,y:o,width:r,height:h}=this.getBoundingClientRect(),c=a+r/2,l=o+h/2,d=i.x+s/2,u=i.y+n/2;if(this.$move(d-c,u-l),t&&(r!==s||h!==n)){const e=s/r,i=n/h;switch(t){case"cover":this.$scale(Math.max(e,i));break;case"contain":this.$scale(Math.min(e,i))}}return this}$move(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$translate(o,r)}return this}$moveTo(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$setTransform(i,s,n,a,o,r)}return this}$rotate(t,e,i){if(this.rotatable){const s=gt(t),n=Math.cos(s),a=Math.sin(s),[o,r,h,c]=[n,a,-a,n];if(B(e)&&B(i)){const[t,s,n,a]=this.$matrix,{width:l,height:d}=this.getBoundingClientRect(),u=e-l/2,$=i-d/2,p=(u*a-n*$)/(t*a-n*s),g=($*t-s*u)/(t*a-n*s);this.$transform(o,r,h,c,p*(1-o)-g*h,g*(1-c)-p*r)}else this.$transform(o,r,h,c,0,0)}return this}$zoom(t,e,i){if(!this.scalable||0===t)return this;if(t<0?t=1/(1-t):t+=1,B(e)&&B(i)){const[s,n,a,o]=this.$matrix,{width:r,height:h}=this.getBoundingClientRect(),c=e-r/2,l=i-h/2,d=(c*o-a*l)/(s*o-a*n),u=(l*s-n*c)/(s*o-a*n);this.$transform(t,0,0,t,d*(1-t),u*(1-t))}else this.$scale(t);return this}$scale(t,e=t){return this.scalable&&this.$transform(t,0,0,e,0,0),this}$skew(t,e=0){if(this.skewable){const i=gt(t),s=gt(e);this.$transform(1,Math.tan(s),Math.tan(i),1,0,0)}return this}$translate(t,e=t){return this.translatable&&B(t)&&B(e)&&this.$transform(1,0,0,1,t,e),this}$transform(t,e,i,s,n,a){return B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a)?this.$setTransform(ft(this.$matrix,[t,e,i,s,n,a])):this}$setTransform(t,e,i,s,n,a){if((this.rotatable||this.scalable||this.skewable||this.translatable)&&(Array.isArray(t)&&([t,e,i,s,n,a]=t),B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a))){const o=[...this.$matrix],r=[t,e,i,s,n,a];if(!1===this.$emit(V,{matrix:r,oldMatrix:o}))return this;this.$matrix=r,this.style.transform=`matrix(${r.join(", ")})`}return this}$getTransform(){return this.$matrix.slice()}$resetTransform(){return this.$setTransform([1,0,0,1,0,0])}}Ot.$name=l,Ot.$version="2.0.0-rc";const Nt=new WeakMap;class It extends At{constructor(){super(...arguments),this.$onCanvasChange=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$style=":host{display:block;height:0;left:0;outline:var(--theme-color) solid 1px;position:relative;top:0;width:0}:host([transparent]){outline-color:transparent}",this.x=0,this.y=0,this.width=0,this.height=0,this.slottable=!1,this.themeColor="rgba(0, 0, 0, 0.65)"}set $canvas(t){Nt.set(this,t)}get $canvas(){return Nt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["height","width","x","y"])}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));if(t){this.$canvas=t,this.style.position="absolute";const e=t.querySelector(this.$getTagNameOf(d));e&&(this.$onCanvasActionStart=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!1)},this.$onCanvasActionEnd=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!0)},this.$onCanvasChange=t=>{const{x:i,y:s,width:n,height:a}=t.detail;this.$change(i,s,n,a),(e.hidden||0===i&&0===s&&0===n&&0===a)&&(this.hidden=!0)},rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,j,this.$onCanvasChange))}this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasChange&&(ot(t,j,this.$onCanvasChange),this.$onCanvasChange=null)),super.disconnectedCallback()}$change(t,e,i=this.width,s=this.height){return B(t)&&B(e)&&B(i)&&B(s)&&(t!==this.x||e!==this.y||i!==this.width||s!==this.height)?(this.hidden&&(this.hidden=!1),this.x=t,this.y=e,this.width=i,this.height=s,this.$render()):this}$reset(){return this.$change(0,0,0,0)}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height,outlineWidth:i.innerWidth})}}It.$name=u,It.$version="2.0.0-rc";class Rt extends At{constructor(){super(...arguments),this.$onCanvasCropEnd=null,this.$onCanvasCropStart=null,this.$style=':host{background-color:var(--theme-color);display:block}:host([action=move]),:host([action=select]){height:100%;left:0;position:absolute;top:0;width:100%}:host([action=move]){cursor:move}:host([action=select]){cursor:crosshair}:host([action$=-resize]){background-color:transparent;height:15px;position:absolute;width:15px}:host([action$=-resize]):after{background-color:var(--theme-color);content:"";display:block;height:5px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px}:host([action=n-resize]),:host([action=s-resize]){cursor:ns-resize;left:50%;transform:translateX(-50%);width:100%}:host([action=n-resize]){top:-8px}:host([action=s-resize]){bottom:-8px}:host([action=e-resize]),:host([action=w-resize]){cursor:ew-resize;height:100%;top:50%;transform:translateY(-50%)}:host([action=e-resize]){right:-8px}:host([action=w-resize]){left:-8px}:host([action=ne-resize]){cursor:nesw-resize;right:-8px;top:-8px}:host([action=nw-resize]){cursor:nwse-resize;left:-8px;top:-8px}:host([action=se-resize]){bottom:-8px;cursor:nwse-resize;right:-8px}:host([action=se-resize]):after{height:15px;width:15px}@media (pointer:coarse){:host([action=se-resize]):after{height:10px;width:10px}}@media (pointer:fine){:host([action=se-resize]):after{height:5px;width:5px}}:host([action=sw-resize]){bottom:-8px;cursor:nesw-resize;left:-8px}:host([plain]){background-color:transparent}',this.action=v,this.plain=!1,this.slottable=!1,this.themeColor="rgba(51, 153, 255, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["action","plain"])}}Rt.$name=c,Rt.$version="2.0.0-rc";const zt=new WeakMap;class Mt extends At{constructor(){super(...arguments),this.$onCanvasAction=null,this.$onCanvasActionStart=null,this.$onCanvasActionEnd=null,this.$onDocumentKeyDown=null,this.$action="",this.$actionStartTarget=null,this.$style=':host{display:block;left:0;position:relative;right:0}:host([outlined]){outline:1px solid var(--theme-color)}:host([multiple]){outline:1px dashed hsla(0,0%,100%,.5)}:host([multiple]):after{bottom:0;content:"";cursor:pointer;display:block;left:0;position:absolute;right:0;top:0}:host([multiple][active]){outline-color:var(--theme-color);z-index:1}:host([multiple])>*{visibility:hidden}:host([multiple][active])>*{visibility:visible}:host([multiple][active]):after{display:none}',this.$initialSelection={x:0,y:0,width:0,height:0},this.x=0,this.y=0,this.width=0,this.height=0,this.aspectRatio=NaN,this.initialAspectRatio=NaN,this.initialCoverage=NaN,this.active=!1,this.movable=!1,this.resizable=!1,this.zoomable=!1,this.multiple=!1,this.keyboard=!1,this.outlined=!1,this.precise=!1,this.linked=!1}set $canvas(t){zt.set(this,t)}get $canvas(){return zt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["active","aspect-ratio","height","initial-aspect-ratio","initial-coverage","keyboard","linked","movable","multiple","outlined","precise","resizable","width","x","y","zoomable"])}$propertyChangedCallback(t,e,i){if(!Object.is(i,e))switch(super.$propertyChangedCallback(t,e,i),t){case"x":case"y":case"width":case"height":this.$nextTick((()=>{this.$change(this.x,this.y,this.width,this.height,this.aspectRatio,!0)}));break;case"aspectRatio":case"initialAspectRatio":this.$nextTick((()=>{this.$initSelection()}));break;case"initialCoverage":this.$nextTick((()=>{K(i)&&i<=1&&this.$initSelection(!0,!0)}));break;case"keyboard":this.$nextTick((()=>{this.$canvas&&(i?this.$onDocumentKeyDown||(this.$onDocumentKeyDown=this.$handleKeyDown.bind(this),rt(this.ownerDocument,D,this.$onDocumentKeyDown)):this.$onDocumentKeyDown&&(ot(this.ownerDocument,D,this.$onDocumentKeyDown),this.$onDocumentKeyDown=null))}));break;case"multiple":this.$nextTick((()=>{if(this.$canvas){const t=this.$getSelections();i?(t.forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height})):(this.active=!1,t.slice(1).forEach((t=>{this.$removeSelection(t)})))}}));break;case"precise":this.$nextTick((()=>{this.$change(this.x,this.y)}))}}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));t?(this.$canvas=t,this.$setStyles({position:"absolute",transform:`translate(${this.x}px, ${this.y}px)`}),this.hidden||this.$render(),this.$initSelection(!0),this.$onCanvasActionStart=this.$handleActionStart.bind(this),this.$onCanvasActionEnd=this.$handleActionEnd.bind(this),this.$onCanvasAction=this.$handleAction.bind(this),rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionStart),rt(t,Y,this.$onCanvasAction)):this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(t,Y,this.$onCanvasAction),this.$onCanvasAction=null)),super.disconnectedCallback()}$getSelections(){let t=[];return this.parentElement&&(t=Array.from(this.parentElement.querySelectorAll(this.$getTagNameOf(d)))),t}$initSelection(t=!1,e=!1){const{initialCoverage:i,parentElement:s}=this;if(K(i)&&s){const n=this.aspectRatio||this.initialAspectRatio;let a=(e?0:this.width)||s.offsetWidth*i,o=(e?0:this.height)||s.offsetHeight*i;K(n)&&({width:a,height:o}=bt({aspectRatio:n,width:a,height:o})),this.$change(this.x,this.y,a,o),t&&this.$center(),this.$initialSelection={x:this.x,y:this.y,width:this.width,height:this.height}}}$createSelection(){const t=this.cloneNode(!0);return this.hasAttribute("id")&&t.removeAttribute("id"),this.active=!1,this.parentElement&&this.parentElement.insertBefore(t,this.nextSibling),t}$removeSelection(t=this){if(this.parentElement){const e=this.$getSelections();if(e.length>1){const i=e.indexOf(t),s=e[i+1]||e[i-1];s&&(t.active=!1,this.parentElement.removeChild(t),s.active=!0,s.$emit(j,{x:s.x,y:s.y,width:s.width,height:s.height}))}else this.$clear()}}$handleActionStart(t){var e,i;const s=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target;this.$action="",this.$actionStartTarget=s,!this.hidden&&this.multiple&&!this.active&&s===this&&this.parentElement&&(this.$getSelections().forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height}))}$handleAction(t){const{currentTarget:e,detail:i}=t;if(e&&i){const{relatedEvent:s}=i;let{action:n}=i;if(!n&&this.multiple&&(n=this.$action||(null==s?void 0:s.target.action),this.$action=n),!n||this.hidden&&n!==p||this.multiple&&!this.active&&n!==m)return;const a=i.endX-i.startX,o=i.endY-i.startY,{width:r,height:h}=this;let{aspectRatio:c}=this;switch(!K(c)&&t.shiftKey&&(c=K(r)&&K(h)?r/h:1),n){case p:{const{$canvas:t}=this,s=$t(e);(this.multiple&&!this.hidden?this.$createSelection():this).$change(i.startX-s.left,i.startY-s.top,a,o,c),n=T,a<0?o>0?n=k:o<0&&(n=A):a>0&&o<0&&(n=S),t&&(t.$action=n);break}case g:this.movable&&(this.linked||this.$actionStartTarget&&this.contains(this.$actionStartTarget))&&this.$move(a,o);break;case m:if(s&&this.zoomable&&(this.linked||this.contains(s.target))){const t=$t(e);this.$zoom(i.scale,s.pageX-t.left,s.pageY-t.top)}break;default:this.$resize(n,a,o,c)}}}$handleActionEnd(){this.$action="",this.$actionStartTarget=null}$handleKeyDown(t){if(!(this.hidden||!this.keyboard||this.multiple&&!this.active||t.defaultPrevented))switch(t.key){case"Backspace":t.metaKey&&(t.preventDefault(),this.$removeSelection());break;case"Delete":t.preventDefault(),this.$removeSelection();break;case"ArrowLeft":t.preventDefault(),this.$move(-1,0);break;case"ArrowRight":t.preventDefault(),this.$move(1,0);break;case"ArrowUp":t.preventDefault(),this.$move(0,-1);break;case"ArrowDown":t.preventDefault(),this.$move(0,1);break;case"+":t.preventDefault(),this.$zoom(.1);break;case"-":t.preventDefault(),this.$zoom(-.1)}}$center(){const{parentElement:t}=this;if(!t)return this;const e=(t.offsetWidth-this.width)/2,i=(t.offsetHeight-this.height)/2;return this.$change(e,i)}$move(t,e=t){return this.$moveTo(this.x+t,this.y+e)}$moveTo(t,e=t){return this.movable?this.$change(t,e):this}$resize(t,e=0,i=0,s=this.aspectRatio){if(!this.resizable)return this;const n=K(s),{$canvas:a}=this;let{x:o,y:r,width:h,height:c}=this;switch(t){case C:r+=i,c-=i,c<0&&(t=y,c=-c,r-=c),n&&(o+=(e=i*s)/2,h-=e,h<0&&(h=-h,o-=h));break;case w:h+=e,h<0&&(t=E,h=-h,o-=h),n&&(r-=(i=e/s)/2,c+=i,c<0&&(c=-c,r-=c));break;case y:c+=i,c<0&&(t=C,c=-c,r-=c),n&&(o-=(e=i*s)/2,h+=e,h<0&&(h=-h,o-=h));break;case E:o+=e,h-=e,h<0&&(t=w,h=-h,o-=h),n&&(r+=(i=e/s)/2,c-=i,c<0&&(c=-c,r-=c));break;case S:n&&(i=-e/s),r+=i,c-=i,h+=e,h<0&&c<0?(t=k,h=-h,c=-c,o-=h,r-=c):h<0?(t=A,h=-h,o-=h):c<0&&(t=T,c=-c,r-=c);break;case A:n&&(i=e/s),o+=e,r+=i,h-=e,c-=i,h<0&&c<0?(t=T,h=-h,c=-c,o-=h,r-=c):h<0?(t=S,h=-h,o-=h):c<0&&(t=k,c=-c,r-=c);break;case T:n&&(i=e/s),h+=e,c+=i,h<0&&c<0?(t=A,h=-h,c=-c,o-=h,r-=c):h<0?(t=k,h=-h,o-=h):c<0&&(t=S,c=-c,r-=c);break;case k:n&&(i=-e/s),o+=e,h-=e,c+=i,h<0&&c<0?(t=S,h=-h,c=-c,o-=h,r-=c):h<0?(t=T,h=-h,o-=h):c<0&&(t=A,c=-c,r-=c)}return a&&a.$setAction(t),this.$change(o,r,h,c)}$zoom(t,e,i){if(!this.zoomable||0===t)return this;t<0?t=1/(1-t):t+=1;const{width:s,height:n}=this,a=s*t,o=n*t;let r=this.x,h=this.y;return B(e)&&B(i)?(r-=(a-s)*((e-this.x)/s),h-=(o-n)*((i-this.y)/n)):(r-=(a-s)/2,h-=(o-n)/2),this.$change(r,h,a,o)}$change(t,e,i=this.width,s=this.height,n=this.aspectRatio,a=!1){return!B(t)||!B(e)||!B(i)||!B(s)||i<0||s<0?this:(K(n)&&({width:i,height:s}=bt({aspectRatio:n,width:i,height:s},"cover")),this.precise||(t=Math.round(t),e=Math.round(e),i=Math.round(i),s=Math.round(s)),t===this.x&&e===this.y&&i===this.width&&s===this.height&&Object.is(n,this.aspectRatio)&&!a?this:(this.hidden&&(this.hidden=!1),!1===this.$emit(j,{x:t,y:e,width:i,height:s})?this:(this.x=t,this.y=e,this.width=i,this.height=s,this.$render())))}$reset(){const{x:t,y:e,width:i,height:s}=this.$initialSelection;return this.$change(t,e,i,s)}$clear(){return this.$change(0,0,0,0,NaN,!0),this.hidden=!0,this}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height})}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let{width:n,height:a}=this,o=1;if(J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.width),s.width=n,s.height=a,!this.$canvas)return void e(s);const r=this.$canvas.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform(),p=-this.x,g=-this.y,m=(p*d-l*g)/(e*d-l*c),b=(g*e-c*p)/(e*d-l*c);let f=e*m+l*b+u,v=c*m+d*b+$,C=i.naturalWidth,w=i.naturalHeight;1!==o&&(f*=o,v*=o,C*=o,w*=o);const y=C/2,E=w/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(y,E),h.transform(e,c,l,d,f,v),h.translate(-y,-E),h.drawImage(i,0,0,C,w),h.restore()}e(s)})).catch(i):e(s)}))}}Mt.$name=d,Mt.$version="2.0.0-rc";class Pt extends At{constructor(){super(...arguments),this.$style=":host{display:flex;flex-direction:column;position:relative;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([bordered]){border:1px dashed var(--theme-color)}:host([covered]){bottom:0;left:0;position:absolute;right:0;top:0}:host>span{display:flex;flex:1}:host>span+span{border-top:1px dashed var(--theme-color)}:host>span>span{flex:1}:host>span>span+span{border-left:1px dashed var(--theme-color)}",this.bordered=!1,this.columns=3,this.covered=!1,this.rows=3,this.slottable=!1,this.themeColor="rgba(238, 238, 238, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["bordered","columns","covered","rows"])}$propertyChangedCallback(t,e,i){Object.is(i,e)||(super.$propertyChangedCallback(t,e,i),"rows"!==t&&"columns"!==t||this.$nextTick((()=>{this.$render()})))}connectedCallback(){super.connectedCallback(),this.$render()}$render(){const t=this.$getShadowRoot(),e=document.createDocumentFragment();for(let t=0;t{setTimeout((()=>{this.$render()}),50)})))}$handleSourceImageTransform(t){this.$render(void 0,t.detail.matrix)}$render(t,e){const{$canvas:i,$selection:s}=this;t||s.hidden||(t=s),(!t||0===t.x&&0===t.y&&0===t.width&&0===t.height)&&(t={x:0,y:0,width:i.offsetWidth,height:i.offsetHeight});const{x:n,y:a,width:o,height:r}=t,h={},{clientWidth:c,clientHeight:l}=this;let d=c,u=l,$=NaN;switch(this.resize){case"both":$=1,d=o,u=r,h.width=o,h.height=r;break;case"horizontal":$=r>0?l/r:0,d=o*$,h.width=d;break;case Xt:$=o>0?c/o:0,u=r*$,h.height=u;break;default:c>0?$=o>0?c/o:0:l>0&&($=r>0?l/r:0)}this.$scale=$,this.$setStyles(h),this.$sourceImage&&this.$transformImageByOffset(null!=e?e:this.$sourceImage.$getTransform(),-n,-a)}$transformImageByOffset(t,e,i){const{$image:s,$scale:n,$sourceImage:a}=this;if(a&&s&&n>=0){const[a,o,r,h,c,l]=t,d=(e*h-r*i)/(a*h-r*o),u=(i*a-o*e)/(a*h-r*o),$=a*d+r*u+c,p=o*d+h*u+l;s.$ready((t=>{this.$setStyles.call(s,{width:t.naturalWidth*n,height:t.naturalHeight*n})})),s.$setTransform(a,o,r,h,$*n,p*n)}}}Ht.$name=$,Ht.$version="2.0.0-rc";var jt='';const Vt=/^img|canvas$/,Ut=/<(\/?(?:script|style)[^>]*)>/gi,qt={template:jt};Tt.$define(),Dt.$define(),Pt.$define(),Rt.$define(),Ot.$define(),Mt.$define(),It.$define(),Ht.$define();class Bt{constructor(t,e){if(this.options=qt,U(t)&&(t=document.querySelector(t)),!tt(t)||!Vt.test(t.localName))throw new Error("The first argument is required and must be an or element.");this.element=t,e=Object.assign(Object.assign({},qt),e),this.options=e;const{ownerDocument:i}=t;let{container:s}=e;if(s&&(U(s)&&(s=i.querySelector(s)),!tt(s)))throw new Error("The `container` option must be an element or a valid selector.");tt(s)||(s=t.parentElement?t.parentElement:i.body),this.container=s;const n=t.localName;let a="";"img"===n?({src:a}=t):"canvas"===n&&window.HTMLCanvasElement&&(a=t.toDataURL());const{template:o}=e;if(o&&U(o)){const e=document.createElement("template"),i=document.createDocumentFragment();e.innerHTML=o.replace(Ut,"<$1>"),i.appendChild(e.content),Array.from(i.querySelectorAll(l)).forEach((e=>{e.setAttribute("src",a),e.setAttribute("alt",t.alt||"The image to crop")})),t.parentElement?(t.style.display="none",s.insertBefore(i,t.nextSibling)):s.appendChild(i)}}getCropperCanvas(){return this.container.querySelector(o)}getCropperImage(){return this.container.querySelector(l)}getCropperSelection(){return this.container.querySelector(d)}getCropperSelections(){return this.container.querySelectorAll(d)}}Bt.version="2.0.0-rc",t.ACTION_MOVE=g,t.ACTION_NONE=v,t.ACTION_RESIZE_EAST=w,t.ACTION_RESIZE_NORTH=C,t.ACTION_RESIZE_NORTHEAST=S,t.ACTION_RESIZE_NORTHWEST=A,t.ACTION_RESIZE_SOUTH=y,t.ACTION_RESIZE_SOUTHEAST=T,t.ACTION_RESIZE_SOUTHWEST=k,t.ACTION_RESIZE_WEST=E,t.ACTION_ROTATE=b,t.ACTION_SCALE=m,t.ACTION_SELECT=p,t.ACTION_TRANSFORM=f,t.ATTRIBUTE_ACTION=x,t.CROPPER_CANVAS=o,t.CROPPER_CROSSHAIR=r,t.CROPPER_GIRD=h,t.CROPPER_HANDLE=c,t.CROPPER_IMAGE=l,t.CROPPER_SELECTION=d,t.CROPPER_SHADE=u,t.CROPPER_VIEWER=$,t.CropperCanvas=Tt,t.CropperCrosshair=Dt,t.CropperElement=At,t.CropperGrid=Pt,t.CropperHandle=Rt,t.CropperImage=Ot,t.CropperSelection=Mt,t.CropperShade=It,t.CropperViewer=Ht,t.DEFAULT_TEMPLATE=jt,t.EVENT_ACTION=Y,t.EVENT_ACTION_END=L,t.EVENT_ACTION_MOVE=X,t.EVENT_ACTION_START=H,t.EVENT_CHANGE=j,t.EVENT_ERROR=P,t.EVENT_KEYDOWN=D,t.EVENT_LOAD=_,t.EVENT_POINTER_DOWN=R,t.EVENT_POINTER_MOVE=z,t.EVENT_POINTER_UP=M,t.EVENT_RESIZE="resize",t.EVENT_TOUCH_END=O,t.EVENT_TOUCH_MOVE=N,t.EVENT_TOUCH_START=I,t.EVENT_TRANSFORM=V,t.EVENT_WHEEL=W,t.HAS_POINTER_EVENT=n,t.IS_BROWSER=e,t.IS_TOUCH_DEVICE=s,t.NAMESPACE=a,t.WINDOW=i,t.default=Bt,t.emit=lt,t.getAdjustedSizes=bt,t.getOffset=$t,t.isElement=tt,t.isFunction=Q,t.isNaN=q,t.isNumber=B,t.isObject=F,t.isPlainObject=J,t.isPositiveNumber=K,t.isString=U,t.isUndefined=Z,t.multiplyMatrices=ft,t.nextTick=ut,t.off=ot,t.on=rt,t.once=ht,t.toAngleInRadian=gt,t.toCamelCase=nt,t.toKebabCase=it,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 79370b0d75..727d284c10 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -18,6 +18,7 @@ requirejs.config({ "ckeditor5-translation": "3rdParty/ckeditor/translations", "diff-match-patch": "3rdParty/diff-match-patch/diff_match_patch.min", "emoji-picker-element": "3rdParty/emoji-picker-element.min", + cropperjs: "3rdParty/cropper.min", }, packages: [ { From 29927c0486bf0ee5593a3c2a6011b51df62352d9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 5 Nov 2024 11:54:21 +0100 Subject: [PATCH 03/35] Add image cropper configuration --- .../processor/AbstractFileProcessor.class.php | 7 ++ .../file/processor/IFileProcessor.class.php | 5 ++ .../file/processor/ImageCropSize.class.php | 20 ++++++ .../ImageCropperConfiguration.class.php | 72 +++++++++++++++++++ .../file/processor/ImageCropperType.class.php | 9 +++ 5 files changed, 113 insertions(+) create mode 100644 wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php create mode 100644 wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php create mode 100644 wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php diff --git a/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php index 750edfeb35..21634bd5a0 100644 --- a/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php @@ -90,4 +90,11 @@ public function trackDownload(File $file): void { // Do not track downloads. } + + #[\Override] + public function getImageCropperConfiguration(): ?ImageCropperConfiguration + { + // Do not crop images. + return null; + } } diff --git a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php index 701d22ce1a..2682d30570 100644 --- a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php @@ -156,4 +156,9 @@ public function getUploadResponse(File $file): array; * file types that are served by the web server itself. */ public function trackDownload(File $file): void; + + /** + * Returns the image cropper configuration for this file processor. + */ + public function getImageCropperConfiguration(): ?ImageCropperConfiguration; } diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php new file mode 100644 index 0000000000..993fb20213 --- /dev/null +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php @@ -0,0 +1,20 @@ +width / $this->height; + } +} diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php new file mode 100644 index 0000000000..4929f6d75d --- /dev/null +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -0,0 +1,72 @@ +aspectRatio = $size->aspectRatio(); + + foreach ($sizes as $size) { + if ($size->aspectRatio() !== $this->aspectRatio) { + throw new \InvalidArgumentException('All sizes must have the same aspect ratio.'); + } + } + + \usort($sizes, function (ImageCropSize $a, ImageCropSize $b) { + if ($a->width > $a->height) { + return $a->width <=> $b->width; + } else { + return $a->height <=> $b->height; + } + }); + $this->sizes = $sizes; + } + + /** + * Creates an image cropper with minimum and maximum size with the same aspect ratio. + * The user can freely select, move and scale. + * However, the cropping area is limited to `$min` and `$max`. + */ + public static function createMinMax(ImageCropSize $min, ImageCropSize $max): self + { + return new self(ImageCropperType::MinMax, $min, $max); + } + + /** + * Creates an image cropper that reduces the image to a specific size + * and only allows the user to move the cropping area. + * The size is determined by `$sizes` and corresponds to the smallest side of the image that is the next smaller + * or equal size of `$sizes`. The aspect ratio of the uploaded image is retained. + * + * Example: + * `$sizes` is [128x128, 256x256] + * - Image is 100x200 + * - Image is rejected + * - Image is 200x150 + * - Image is resized to 170x128 + * - Image is 150x200 + * - Image is resized to 128x170 + * - Image is 300x300 + * - Image is resized to 256x256 + */ + public static function createExact(ImageCropSize ...$sizes): self + { + return new self(ImageCropperType::Exact, ...$sizes); + } +} diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php new file mode 100644 index 0000000000..54330f3bed --- /dev/null +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php @@ -0,0 +1,9 @@ + Date: Tue, 5 Nov 2024 12:06:31 +0100 Subject: [PATCH 04/35] Output the cropper configuration --- .../file/processor/FileProcessor.class.php | 5 +++++ .../file/processor/ImageCropSize.class.php | 11 ++++++++++- .../ImageCropperConfiguration.class.php | 12 +++++++++++- .../file/processor/ImageCropperType.class.php | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php index 8ecd6b751e..a83f45acb4 100644 --- a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php @@ -105,6 +105,8 @@ public function getHtmlElement(IFileProcessor $fileProcessor, array $context): s $maximumSize = -1; } + $cropperConfiguration = $fileProcessor->getImageCropperConfiguration(); + return \sprintf( <<<'HTML' @@ -120,6 +123,8 @@ public function getHtmlElement(IFileProcessor $fileProcessor, array $context): s StringUtil::encodeHTML(JSON::encode($context)), StringUtil::encodeHTML($allowedFileExtensions), StringUtil::encodeHTML(JSON::encode($fileProcessor->getResizeConfiguration())), + $cropperConfiguration === null ? '' + : 'data-cropper-configuration="' . StringUtil::encodeHTML(JSON::encode($cropperConfiguration)) . '"', $maximumCount, $maximumSize, ); diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php index 993fb20213..b71b12637c 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php @@ -2,7 +2,7 @@ namespace wcf\system\file\processor; -final class ImageCropSize +final class ImageCropSize implements \JsonSerializable { public function __construct( public readonly int $width, @@ -17,4 +17,13 @@ public function aspectRatio(): float { return $this->width / $this->height; } + + #[\Override] + public function jsonSerialize(): mixed + { + return [ + 'width' => $this->width, + 'height' => $this->height, + ]; + } } diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php index 4929f6d75d..7c696a02f9 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -2,7 +2,7 @@ namespace wcf\system\file\processor; -final class ImageCropperConfiguration +final class ImageCropperConfiguration implements \JsonSerializable { public readonly float $aspectRatio; @@ -38,6 +38,16 @@ public function __construct( $this->sizes = $sizes; } + #[\Override] + public function jsonSerialize(): mixed + { + return [ + 'aspectRatio' => $this->aspectRatio, + 'sizes' => $this->sizes, + 'type' => $this->type->toString(), + ]; + } + /** * Creates an image cropper with minimum and maximum size with the same aspect ratio. * The user can freely select, move and scale. diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php index 54330f3bed..bf74e95082 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php @@ -6,4 +6,20 @@ enum ImageCropperType { case MinMax; case Exact; + + public function toString(): string + { + return match ($this) { + self::MinMax => 'minMax', + self::Exact => 'exact', + }; + } + + public static function fromString(string $fileType): self + { + return match ($fileType) { + 'minMax' => self::MinMax, + 'exact' => self::Exact, + }; + } } From c6dda3e74612ec0f0cb196d36c9fc34219784bfc Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 5 Nov 2024 15:00:19 +0100 Subject: [PATCH 05/35] Open crop dialog if `cropperConfiguration` set --- ts/WoltLabSuite/Core/Component/File/Upload.ts | 20 +- .../Core/Component/Image/Cropper.ts | 263 ++++++++++++++++++ .../Core/Component/File/Upload.js | 21 +- .../Core/Component/Image/Cropper.js | 201 +++++++++++++ ...PreloadPhrasesCollectingListener.class.php | 1 + wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 7 files changed, 501 insertions(+), 7 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Component/Image/Cropper.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index 8f7fe935e4..4e6441be8f 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -11,6 +11,7 @@ import ImageResizer from "WoltLabSuite/Core/Image/Resizer"; import { AttachmentData } from "../Ckeditor/Attachment"; import { innerError } from "WoltLabSuite/Core/Dom/Util"; import { getPhrase } from "WoltLabSuite/Core/Language"; +import { cropImage, CropperConfiguration } from "WoltLabSuite/Core/Component/Image/Cropper"; export type CkeditorDropEvent = { file: File; @@ -268,9 +269,22 @@ export function setup(): void { return; } - void resizeImage(element, file).then((resizedFile) => { - void upload(element, resizedFile); - }); + if (element.dataset.cropperConfiguration) { + const cropperConfiguration = JSON.parse(element.dataset.cropperConfiguration) as CropperConfiguration; + + void cropImage(element, file, cropperConfiguration) + .then((resizedFile) => { + void upload(element, resizedFile); + }) + .catch((e) => { + //TODO handle error + console.error(e); + }); + } else { + void resizeImage(element, file).then((resizedFile) => { + void upload(element, resizedFile); + }); + } }); element.addEventListener("ckeditorDrop", (event: CustomEvent) => { diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts new file mode 100644 index 0000000000..219759408f --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -0,0 +1,263 @@ +import ImageResizer from "WoltLabSuite/Core/Image/Resizer"; +import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; +import Cropper, { CropperCanvas, CropperImage, CropperSelection } from "cropperjs"; +import type { Selection } from "@cropper/element-selection"; +import { getPhrase } from "WoltLabSuite/Core/Language"; +import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog"; +import * as ExifUtil from "WoltLabSuite/Core/Image/ExifUtil"; + +export interface CropperConfiguration { + aspectRatio: number; + type: "minMax" | "exact"; + sizes: { + width: number; + height: number; + }[]; +} + +abstract class ImageCropper { + readonly configuration: CropperConfiguration; + readonly file: File; + readonly element: WoltlabCoreFileUploadElement; + readonly resizer: ImageResizer; + protected image?: HTMLImageElement | HTMLCanvasElement; + protected cropperCanvas?: CropperCanvas | null; + protected cropperImage?: CropperImage | null; + protected cropperSelection?: CropperSelection | null; + protected dialog: WoltlabCoreDialogElement; + protected exif?: ExifUtil.Exif; + #cropper?: Cropper; + + constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { + this.configuration = configuration; + this.element = element; + this.file = file; + this.resizer = new ImageResizer(); + } + + public async showDialog(): Promise { + await this.loadImage(); + + this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ + extra: this.getDialogExtra(), + }); + this.dialog.show(getPhrase("wcf.upload.crop.image")); + + return this.createCropper(); + } + + protected async createCropper(): Promise { + this.#cropper = new Cropper(this.image!, { + template: this.getCropperTemplate(), + }); + + this.cropperCanvas = this.#cropper.getCropperCanvas(); + this.cropperImage = this.#cropper.getCropperImage(); + this.cropperSelection = this.#cropper.getCropperSelection(); + + this.setCropperStyle(); + + this.cropperImage!.$center("contain"); + this.cropperSelection!.$center(); + + this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { + // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries + const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); + const selection = event.detail as Selection; + + const maxSelection: Selection = { + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, + }; + + if ( + selection.x < maxSelection.x || + selection.y < maxSelection.y || + selection.x + selection.width > maxSelection.x + maxSelection.width || + selection.y + selection.height > maxSelection.y + maxSelection.height + ) { + event.preventDefault(); + } + }); + + this.dialog.addEventListener("extra", () => { + this.cropperImage!.$center("contain"); + this.cropperSelection!.$reset(); + }); + + return new Promise((resolve, reject) => { + this.dialog.addEventListener("primary", () => { + this.cropperSelection!.$toCanvas() + .then((canvas) => { + this.resizer + .saveFile({ exif: this.exif, image: canvas }, this.file.name, this.file.type) + .then((resizedFile) => { + resolve(resizedFile); + }) + .catch(() => { + reject(); + }); + }) + .catch(() => { + reject(); + }); + }); + }); + } + + protected setCropperStyle() { + this.cropperCanvas!.style.aspectRatio = `${this.image!.width}/${this.image!.height}`; + + if (this.image!.width > this.image!.height) { + this.cropperCanvas!.style.width = `min(70vw, ${this.image!.width}px)`; + this.cropperCanvas!.style.height = "auto"; + } else { + this.cropperCanvas!.style.height = `min(60vh, ${this.image!.height}px)`; + this.cropperCanvas!.style.width = "auto"; + } + + this.cropperSelection!.aspectRatio = this.configuration.aspectRatio; + } + + protected abstract getCropperTemplate(): string; + + protected getDialogExtra(): string | undefined { + return undefined; + } + + protected async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + } +} + +class ExactImageCropper extends ImageCropper { + #size?: { width: number; height: number }; + + public async showDialog(): Promise { + await this.loadImage(); + + // The image already has the correct size, cropping is not necessary + if ( + this.image!.width == this.#size!.width && + this.image!.height == this.#size!.height && + this.image instanceof HTMLCanvasElement + ) { + return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); + } + + this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ + extra: this.getDialogExtra(), + }); + this.dialog.show(getPhrase("wcf.upload.crop.image")); + + return this.createCropper(); + } + + protected getCropperTemplate(): string { + return ` + + + + + + + +`; + } + + protected async loadImage(): Promise { + await super.loadImage(); + + const timeout = new Promise((resolve) => { + window.setTimeout(() => resolve(this.file), 10_000); + }); + + // resize image to the largest possible size + this.configuration.sizes = this.configuration.sizes + .filter((size) => { + return size.width <= this.image!.width && size.height <= this.image!.height; + }) + .sort((a, b) => { + if (a.width >= a.height) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + + if (this.configuration.sizes.length === 0) { + // TODO show error message + throw new Error("No suitable size found"); + } + + this.#size = this.configuration.sizes[0]; + this.image = await this.resizer.resize( + this.image as HTMLImageElement, + this.image!.width >= this.image!.height ? this.image!.width : this.#size.width, + this.image!.height > this.image!.width ? this.image!.height : this.#size.height, + this.resizer.quality, + true, + timeout, + ); + } + + protected setCropperStyle() { + super.setCropperStyle(); + + this.cropperSelection!.width = this.#size!.width; + this.cropperSelection!.height = this.#size!.height; + } +} + +class MinMaxImageCropper extends ImageCropper { + protected getCropperTemplate(): string { + return ` + + + + + + + + + + + + + + + + +`; + } + + protected getDialogExtra(): string { + return getPhrase("wcf.global.button.reset"); + } + + // TODO handle resize cropper selection to min/max size +} + +export async function cropImage( + element: WoltlabCoreFileUploadElement, + file: File, + configuration: CropperConfiguration, +): Promise { + let imageCropper: ImageCropper; + switch (configuration.type) { + case "exact": + imageCropper = new ExactImageCropper(element, file, configuration); + break; + case "minMax": + imageCropper = new MinMaxImageCropper(element, file, configuration); + break; + default: + throw new Error("Invalid configuration type"); + } + + return imageCropper.showDialog(); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index 227c871344..b05ab0157a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -1,4 +1,4 @@ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1, Resizer_1, Util_1, Language_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Image/Cropper"], function (require, exports, tslib_1, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1, Resizer_1, Util_1, Language_1, Cropper_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.clearPreviousErrors = clearPreviousErrors; @@ -183,9 +183,22 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol else if (!validateFileSize(element, file)) { return; } - void resizeImage(element, file).then((resizedFile) => { - void upload(element, resizedFile); - }); + if (element.dataset.cropperConfiguration) { + const cropperConfiguration = JSON.parse(element.dataset.cropperConfiguration); + void (0, Cropper_1.cropImage)(element, file, cropperConfiguration) + .then((resizedFile) => { + void upload(element, resizedFile); + }) + .catch((e) => { + //TODO handle error + console.error(e); + }); + } + else { + void resizeImage(element, file).then((resizedFile) => { + void upload(element, resizedFile); + }); + } }); element.addEventListener("ckeditorDrop", (event) => { const { file } = event.detail; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js new file mode 100644 index 0000000000..0c9946cd24 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -0,0 +1,201 @@ +define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.cropImage = cropImage; + Resizer_1 = tslib_1.__importDefault(Resizer_1); + cropperjs_1 = tslib_1.__importDefault(cropperjs_1); + class ImageCropper { + configuration; + file; + element; + resizer; + image; + cropperCanvas; + cropperImage; + cropperSelection; + dialog; + exif; + #cropper; + constructor(element, file, configuration) { + this.configuration = configuration; + this.element = element; + this.file = file; + this.resizer = new Resizer_1.default(); + } + async showDialog() { + await this.loadImage(); + this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ + extra: this.getDialogExtra(), + }); + this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); + return this.createCropper(); + } + async createCropper() { + this.#cropper = new cropperjs_1.default(this.image, { + template: this.getCropperTemplate(), + }); + this.cropperCanvas = this.#cropper.getCropperCanvas(); + this.cropperImage = this.#cropper.getCropperImage(); + this.cropperSelection = this.#cropper.getCropperSelection(); + this.setCropperStyle(); + this.cropperImage.$center("contain"); + this.cropperSelection.$center(); + this.cropperSelection.addEventListener("change", (event) => { + // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries + const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); + const selection = event.detail; + const maxSelection = { + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, + }; + if (selection.x < maxSelection.x || + selection.y < maxSelection.y || + selection.x + selection.width > maxSelection.x + maxSelection.width || + selection.y + selection.height > maxSelection.y + maxSelection.height) { + event.preventDefault(); + } + }); + this.dialog.addEventListener("extra", () => { + this.cropperImage.$center("contain"); + this.cropperSelection.$reset(); + }); + return new Promise((resolve, reject) => { + this.dialog.addEventListener("primary", () => { + this.cropperSelection.$toCanvas() + .then((canvas) => { + this.resizer + .saveFile({ exif: this.exif, image: canvas }, this.file.name, this.file.type) + .then((resizedFile) => { + resolve(resizedFile); + }) + .catch(() => { + reject(); + }); + }) + .catch(() => { + reject(); + }); + }); + }); + } + setCropperStyle() { + this.cropperCanvas.style.aspectRatio = `${this.image.width}/${this.image.height}`; + if (this.image.width > this.image.height) { + this.cropperCanvas.style.width = `min(70vw, ${this.image.width}px)`; + this.cropperCanvas.style.height = "auto"; + } + else { + this.cropperCanvas.style.height = `min(60vh, ${this.image.height}px)`; + this.cropperCanvas.style.width = "auto"; + } + this.cropperSelection.aspectRatio = this.configuration.aspectRatio; + } + getDialogExtra() { + return undefined; + } + async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + } + } + class ExactImageCropper extends ImageCropper { + #size; + async showDialog() { + await this.loadImage(); + // The image already has the correct size, cropping is not necessary + if (this.image.width == this.#size.width && + this.image.height == this.#size.height && + this.image instanceof HTMLCanvasElement) { + return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); + } + this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ + extra: this.getDialogExtra(), + }); + this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); + return this.createCropper(); + } + getCropperTemplate() { + return ` + + + + + + + +`; + } + async loadImage() { + await super.loadImage(); + const timeout = new Promise((resolve) => { + window.setTimeout(() => resolve(this.file), 10_000); + }); + // resize image to the largest possible size + this.configuration.sizes = this.configuration.sizes + .filter((size) => { + return size.width <= this.image.width && size.height <= this.image.height; + }) + .sort((a, b) => { + if (a.width >= a.height) { + return b.width - a.width; + } + else { + return b.height - a.height; + } + }); + if (this.configuration.sizes.length === 0) { + // TODO show error message + throw new Error("No suitable size found"); + } + this.#size = this.configuration.sizes[0]; + this.image = await this.resizer.resize(this.image, this.image.width >= this.image.height ? this.image.width : this.#size.width, this.image.height > this.image.width ? this.image.height : this.#size.height, this.resizer.quality, true, timeout); + } + setCropperStyle() { + super.setCropperStyle(); + this.cropperSelection.width = this.#size.width; + this.cropperSelection.height = this.#size.height; + } + } + class MinMaxImageCropper extends ImageCropper { + getCropperTemplate() { + return ` + + + + + + + + + + + + + + + + +`; + } + getDialogExtra() { + return (0, Language_1.getPhrase)("wcf.global.button.reset"); + } + } + async function cropImage(element, file, configuration) { + let imageCropper; + switch (configuration.type) { + case "exact": + imageCropper = new ExactImageCropper(element, file, configuration); + break; + case "minMax": + imageCropper = new MinMaxImageCropper(element, file, configuration); + break; + default: + throw new Error("Invalid configuration type"); + } + return imageCropper.showDialog(); + } +}); diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index eab6ba99d9..aaef5d3cf1 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -150,6 +150,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.style.changeStyle'); + $event->preload('wcf.upload.crop.image'); $event->preload('wcf.upload.error.fileExtensionNotPermitted'); $event->preload('wcf.upload.error.fileSizeTooLarge'); $event->preload('wcf.upload.error.maximumCountReached'); diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 62793c611b..4a3fbf005c 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -5561,6 +5561,7 @@ Benachrichtigungen auf {PAGE_TITLE|phra + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index e213f89ae2..d8ed730f1d 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -5563,6 +5563,7 @@ your notifications on {PAGE_TITLE|phras + From 1163259db4fde3f7941df77ac6c3ddfd1194ba70 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 09:02:36 +0100 Subject: [PATCH 06/35] Display error message if images are too small --- ts/WoltLabSuite/Core/Component/File/Upload.ts | 5 ++- .../Core/Component/Image/Cropper.ts | 37 +++++++++++-------- .../Core/Component/File/Upload.js | 5 ++- .../Core/Component/Image/Cropper.js | 21 ++++++----- ...PreloadPhrasesCollectingListener.class.php | 1 + wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index 4e6441be8f..646af3fa3b 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -277,8 +277,9 @@ export function setup(): void { void upload(element, resizedFile); }) .catch((e) => { - //TODO handle error - console.error(e); + if (e instanceof Error) { + innerError(element, e.message); + } }); } else { void resizeImage(element, file).then((resizedFile) => { diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 219759408f..37843398e5 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -60,6 +60,7 @@ abstract class ImageCropper { this.cropperImage!.$center("contain"); this.cropperSelection!.$center(); + // Limit the selection to the canvas boundaries this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); @@ -177,24 +178,30 @@ class ExactImageCropper extends ImageCropper { }); // resize image to the largest possible size - this.configuration.sizes = this.configuration.sizes - .filter((size) => { - return size.width <= this.image!.width && size.height <= this.image!.height; - }) - .sort((a, b) => { - if (a.width >= a.height) { - return b.width - a.width; - } else { - return b.height - a.height; - } - }); + this.configuration.sizes = this.configuration.sizes.sort((a, b) => { + if (a.width >= a.height) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + + const sizes = this.configuration.sizes.filter((size) => { + return size.width <= this.image!.width && size.height <= this.image!.height; + }); - if (this.configuration.sizes.length === 0) { - // TODO show error message - throw new Error("No suitable size found"); + if (sizes.length === 0) { + const smallestSize = + this.configuration.sizes.length > 1 ? this.configuration.sizes[this.configuration.sizes.length - 1] : undefined; + throw new Error( + getPhrase("wcf.upload.error.image.tooSmall", { + width: smallestSize?.width, + height: smallestSize?.height, + }), + ); } - this.#size = this.configuration.sizes[0]; + this.#size = sizes[0]; this.image = await this.resizer.resize( this.image as HTMLImageElement, this.image!.width >= this.image!.height ? this.image!.width : this.#size.width, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index b05ab0157a..2eb2f663f1 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -190,8 +190,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol void upload(element, resizedFile); }) .catch((e) => { - //TODO handle error - console.error(e); + if (e instanceof Error) { + (0, Util_1.innerError)(element, e.message); + } }); } else { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 0c9946cd24..5e66d95f6a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -40,6 +40,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.setCropperStyle(); this.cropperImage.$center("contain"); this.cropperSelection.$center(); + // Limit the selection to the canvas boundaries this.cropperSelection.addEventListener("change", (event) => { // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); @@ -134,11 +135,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL window.setTimeout(() => resolve(this.file), 10_000); }); // resize image to the largest possible size - this.configuration.sizes = this.configuration.sizes - .filter((size) => { - return size.width <= this.image.width && size.height <= this.image.height; - }) - .sort((a, b) => { + this.configuration.sizes = this.configuration.sizes.sort((a, b) => { if (a.width >= a.height) { return b.width - a.width; } @@ -146,11 +143,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL return b.height - a.height; } }); - if (this.configuration.sizes.length === 0) { - // TODO show error message - throw new Error("No suitable size found"); + const sizes = this.configuration.sizes.filter((size) => { + return size.width <= this.image.width && size.height <= this.image.height; + }); + if (sizes.length === 0) { + const smallestSize = this.configuration.sizes.length > 1 ? this.configuration.sizes[this.configuration.sizes.length - 1] : undefined; + throw new Error((0, Language_1.getPhrase)("wcf.upload.error.image.tooSmall", { + width: smallestSize?.width, + height: smallestSize?.height, + })); } - this.#size = this.configuration.sizes[0]; + this.#size = sizes[0]; this.image = await this.resizer.resize(this.image, this.image.width >= this.image.height ? this.image.width : this.#size.width, this.image.height > this.image.width ? this.image.height : this.#size.height, this.resizer.quality, true, timeout); } setCropperStyle() { diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index aaef5d3cf1..b09a97f498 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -153,6 +153,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.upload.crop.image'); $event->preload('wcf.upload.error.fileExtensionNotPermitted'); $event->preload('wcf.upload.error.fileSizeTooLarge'); + $event->preload('wcf.upload.error.image.tooSmall'); $event->preload('wcf.upload.error.maximumCountReached'); $event->preload('wcf.user.activityPoint'); diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 4a3fbf005c..91ae548c7b 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -5570,6 +5570,7 @@ Benachrichtigungen auf {PAGE_TITLE|phra + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index d8ed730f1d..f8b67dcfda 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -5572,6 +5572,7 @@ your notifications on {PAGE_TITLE|phras + From 8bd9c70032f3dc555189c0edc296f860b05f385f Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 09:26:32 +0100 Subject: [PATCH 07/35] Add `cropperContainer` around `cropper-canvas` --- .../Core/Component/Image/Cropper.ts | 168 +++++++++--------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 37843398e5..c0a5fc35a7 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -36,52 +36,12 @@ abstract class ImageCropper { } public async showDialog(): Promise { - await this.loadImage(); - this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ extra: this.getDialogExtra(), }); this.dialog.show(getPhrase("wcf.upload.crop.image")); - return this.createCropper(); - } - - protected async createCropper(): Promise { - this.#cropper = new Cropper(this.image!, { - template: this.getCropperTemplate(), - }); - - this.cropperCanvas = this.#cropper.getCropperCanvas(); - this.cropperImage = this.#cropper.getCropperImage(); - this.cropperSelection = this.#cropper.getCropperSelection(); - - this.setCropperStyle(); - - this.cropperImage!.$center("contain"); - this.cropperSelection!.$center(); - - // Limit the selection to the canvas boundaries - this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { - // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries - const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); - const selection = event.detail as Selection; - - const maxSelection: Selection = { - x: 0, - y: 0, - width: cropperCanvasRect.width, - height: cropperCanvasRect.height, - }; - - if ( - selection.x < maxSelection.x || - selection.y < maxSelection.y || - selection.x + selection.width > maxSelection.x + maxSelection.width || - selection.y + selection.height > maxSelection.y + maxSelection.height - ) { - event.preventDefault(); - } - }); + this.createCropper(); this.dialog.addEventListener("extra", () => { this.cropperImage!.$center("contain"); @@ -108,6 +68,12 @@ abstract class ImageCropper { }); } + public async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + } + protected setCropperStyle() { this.cropperCanvas!.style.aspectRatio = `${this.image!.width}/${this.image!.height}`; @@ -128,10 +94,42 @@ abstract class ImageCropper { return undefined; } - protected async loadImage() { - const { image, exif } = await this.resizer.loadFile(this.file); - this.image = image; - this.exif = exif; + protected createCropper() { + this.#cropper = new Cropper(this.image!, { + template: this.getCropperTemplate(), + }); + + this.cropperCanvas = this.#cropper.getCropperCanvas(); + this.cropperImage = this.#cropper.getCropperImage(); + this.cropperSelection = this.#cropper.getCropperSelection(); + + this.setCropperStyle(); + + this.cropperImage!.$center("contain"); + this.cropperSelection!.$center(); + + // Limit the selection to the canvas boundaries + this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { + // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries + const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); + const selection = event.detail as Selection; + + const maxSelection: Selection = { + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, + }; + + if ( + selection.x < maxSelection.x || + selection.y < maxSelection.y || + selection.x + selection.width > maxSelection.x + maxSelection.width || + selection.y + selection.height > maxSelection.y + maxSelection.height + ) { + event.preventDefault(); + } + }); } } @@ -139,8 +137,6 @@ class ExactImageCropper extends ImageCropper { #size?: { width: number; height: number }; public async showDialog(): Promise { - await this.loadImage(); - // The image already has the correct size, cropping is not necessary if ( this.image!.width == this.#size!.width && @@ -150,27 +146,10 @@ class ExactImageCropper extends ImageCropper { return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); } - this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ - extra: this.getDialogExtra(), - }); - this.dialog.show(getPhrase("wcf.upload.crop.image")); - - return this.createCropper(); + return super.showDialog(); } - protected getCropperTemplate(): string { - return ` - - - - - - - -`; - } - - protected async loadImage(): Promise { + public async loadImage(): Promise { await super.loadImage(); const timeout = new Promise((resolve) => { @@ -212,34 +191,54 @@ class ExactImageCropper extends ImageCropper { ); } + protected getCropperTemplate(): string { + return `
+ + + + + + + + + +
`; + } + protected setCropperStyle() { super.setCropperStyle(); this.cropperSelection!.width = this.#size!.width; this.cropperSelection!.height = this.#size!.height; + + this.cropperCanvas!.style.width = `${this.image!.width}px`; + this.cropperCanvas!.style.height = `${this.image!.height}px`; + this.cropperSelection!.style.removeProperty("aspectRatio"); } } class MinMaxImageCropper extends ImageCropper { protected getCropperTemplate(): string { - return ` - - - - - - - - - - - - - - - - -`; + return `
+ + + + + + + + + + + + + + + + + + +
`; } protected getDialogExtra(): string { @@ -266,5 +265,6 @@ export async function cropImage( throw new Error("Invalid configuration type"); } + await imageCropper.loadImage(); return imageCropper.showDialog(); } From 53d65398ce1d8e067fb25ddd7a487a83cadd94cf Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 09:26:55 +0100 Subject: [PATCH 08/35] Validate the file type --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index c0a5fc35a7..d77a980f31 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -253,6 +253,18 @@ export async function cropImage( file: File, configuration: CropperConfiguration, ): Promise { + switch (file.type) { + case "image/jpeg": + case "image/png": + case "image/webp": + // Potential candidate for a resize operation. + break; + + default: + // Not an image or an unsupported file type. + return file; + } + let imageCropper: ImageCropper; switch (configuration.type) { case "exact": From c5d033fd2ba878ed9d42b460be0ebeb92697f47c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 09:27:41 +0100 Subject: [PATCH 09/35] Check whether the user has canceled the dialog manually --- ts/WoltLabSuite/Core/Component/File/Upload.ts | 5 + .../Core/Component/File/Upload.js | 4 + .../Core/Component/Image/Cropper.js | 152 ++++++++++-------- wcfsetup/install/files/style/ui/dialog.scss | 4 + 4 files changed, 95 insertions(+), 70 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index 646af3fa3b..32a00f6f1c 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -277,6 +277,11 @@ export function setup(): void { void upload(element, resizedFile); }) .catch((e) => { + if (e === undefined) { + // User closed the dialog. + return; + } + if (e instanceof Error) { innerError(element, e.message); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js index 2eb2f663f1..d27345958c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js @@ -190,6 +190,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol void upload(element, resizedFile); }) .catch((e) => { + if (e === undefined) { + // User closed the dialog. + return; + } if (e instanceof Error) { (0, Util_1.innerError)(element, e.message); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 5e66d95f6a..4139d310bf 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -23,41 +23,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.resizer = new Resizer_1.default(); } async showDialog() { - await this.loadImage(); this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ extra: this.getDialogExtra(), }); this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); - return this.createCropper(); - } - async createCropper() { - this.#cropper = new cropperjs_1.default(this.image, { - template: this.getCropperTemplate(), - }); - this.cropperCanvas = this.#cropper.getCropperCanvas(); - this.cropperImage = this.#cropper.getCropperImage(); - this.cropperSelection = this.#cropper.getCropperSelection(); - this.setCropperStyle(); - this.cropperImage.$center("contain"); - this.cropperSelection.$center(); - // Limit the selection to the canvas boundaries - this.cropperSelection.addEventListener("change", (event) => { - // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries - const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); - const selection = event.detail; - const maxSelection = { - x: 0, - y: 0, - width: cropperCanvasRect.width, - height: cropperCanvasRect.height, - }; - if (selection.x < maxSelection.x || - selection.y < maxSelection.y || - selection.x + selection.width > maxSelection.x + maxSelection.width || - selection.y + selection.height > maxSelection.y + maxSelection.height) { - event.preventDefault(); - } - }); + this.createCropper(); this.dialog.addEventListener("extra", () => { this.cropperImage.$center("contain"); this.cropperSelection.$reset(); @@ -81,6 +51,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); }); } + async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + } setCropperStyle() { this.cropperCanvas.style.aspectRatio = `${this.image.width}/${this.image.height}`; if (this.image.width > this.image.height) { @@ -96,38 +71,46 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL getDialogExtra() { return undefined; } - async loadImage() { - const { image, exif } = await this.resizer.loadFile(this.file); - this.image = image; - this.exif = exif; + createCropper() { + this.#cropper = new cropperjs_1.default(this.image, { + template: this.getCropperTemplate(), + }); + this.cropperCanvas = this.#cropper.getCropperCanvas(); + this.cropperImage = this.#cropper.getCropperImage(); + this.cropperSelection = this.#cropper.getCropperSelection(); + this.setCropperStyle(); + this.cropperImage.$center("contain"); + this.cropperSelection.$center(); + // Limit the selection to the canvas boundaries + this.cropperSelection.addEventListener("change", (event) => { + // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries + const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); + const selection = event.detail; + const maxSelection = { + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, + }; + if (selection.x < maxSelection.x || + selection.y < maxSelection.y || + selection.x + selection.width > maxSelection.x + maxSelection.width || + selection.y + selection.height > maxSelection.y + maxSelection.height) { + event.preventDefault(); + } + }); } } class ExactImageCropper extends ImageCropper { #size; async showDialog() { - await this.loadImage(); // The image already has the correct size, cropping is not necessary if (this.image.width == this.#size.width && this.image.height == this.#size.height && this.image instanceof HTMLCanvasElement) { return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); } - this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ - extra: this.getDialogExtra(), - }); - this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); - return this.createCropper(); - } - getCropperTemplate() { - return ` - - - - - - - -`; + return super.showDialog(); } async loadImage() { await super.loadImage(); @@ -156,38 +139,66 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.#size = sizes[0]; this.image = await this.resizer.resize(this.image, this.image.width >= this.image.height ? this.image.width : this.#size.width, this.image.height > this.image.width ? this.image.height : this.#size.height, this.resizer.quality, true, timeout); } + getCropperTemplate() { + return `
+ + + + + + + + + +
`; + } setCropperStyle() { super.setCropperStyle(); this.cropperSelection.width = this.#size.width; this.cropperSelection.height = this.#size.height; + this.cropperCanvas.style.width = `${this.image.width}px`; + this.cropperCanvas.style.height = `${this.image.height}px`; + this.cropperSelection.style.removeProperty("aspectRatio"); } } class MinMaxImageCropper extends ImageCropper { getCropperTemplate() { - return ` - - - - - - - - - - - - - - - - -`; + return `
+ + + + + + + + + + + + + + + + + + +
`; } getDialogExtra() { return (0, Language_1.getPhrase)("wcf.global.button.reset"); } } async function cropImage(element, file, configuration) { + switch (file.type) { + case "image/jpeg": + case "image/png": + case "image/webp": + // Potential candidate for a resize operation. + break; + default: + // Not an image or an unsupported file type. + return file; + } let imageCropper; switch (configuration.type) { case "exact": @@ -199,6 +210,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL default: throw new Error("Invalid configuration type"); } + await imageCropper.loadImage(); return imageCropper.showDialog(); } }); diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index 0a09771149..dfc88c79a2 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -465,3 +465,7 @@ html[data-color-scheme="dark"] .dialog::backdrop { min-width: 0; } } + +.dialog .cropperContainer { + overflow: scroll; +} From f93cf87d46da753cba888a20d1cccae6eb302838 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 10:04:39 +0100 Subject: [PATCH 10/35] Check whether the `cropper-selection` in `MinMaxImageCropper` limits to the minimum and maximum. --- .../Core/Component/Image/Cropper.ts | 78 ++++++++++++++----- .../Core/Component/Image/Cropper.js | 62 +++++++++++---- wcfsetup/install/files/style/ui/dialog.scss | 4 +- 3 files changed, 109 insertions(+), 35 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index d77a980f31..a449ffc0a0 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -24,7 +24,7 @@ abstract class ImageCropper { protected cropperCanvas?: CropperCanvas | null; protected cropperImage?: CropperImage | null; protected cropperSelection?: CropperSelection | null; - protected dialog: WoltlabCoreDialogElement; + protected dialog?: WoltlabCoreDialogElement; protected exif?: ExifUtil.Exif; #cropper?: Cropper; @@ -33,6 +33,14 @@ abstract class ImageCropper { this.element = element; this.file = file; this.resizer = new ImageResizer(); + + this.configuration.sizes = this.configuration.sizes.sort((a, b) => { + if (a.width >= a.height) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); } public async showDialog(): Promise { @@ -43,13 +51,8 @@ abstract class ImageCropper { this.createCropper(); - this.dialog.addEventListener("extra", () => { - this.cropperImage!.$center("contain"); - this.cropperSelection!.$reset(); - }); - return new Promise((resolve, reject) => { - this.dialog.addEventListener("primary", () => { + this.dialog!.addEventListener("primary", () => { this.cropperSelection!.$toCanvas() .then((canvas) => { this.resizer @@ -157,14 +160,6 @@ class ExactImageCropper extends ImageCropper { }); // resize image to the largest possible size - this.configuration.sizes = this.configuration.sizes.sort((a, b) => { - if (a.width >= a.height) { - return b.width - a.width; - } else { - return b.height - a.height; - } - }); - const sizes = this.configuration.sizes.filter((size) => { return size.width <= this.image!.width && size.height <= this.image!.height; }); @@ -218,13 +213,32 @@ class ExactImageCropper extends ImageCropper { } class MinMaxImageCropper extends ImageCropper { + constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { + super(element, file, configuration); + if (configuration.sizes.length !== 2) { + throw new Error("MinMaxImageCropper requires exactly two sizes"); + } + } + + get minSize() { + return this.configuration.sizes[1]; + } + + get maxSize() { + return this.configuration.sizes[0]; + } + + protected getDialogExtra(): string { + return getPhrase("wcf.global.button.reset"); + } + protected getCropperTemplate(): string { return `
- + @@ -241,11 +255,37 @@ class MinMaxImageCropper extends ImageCropper {
`; } - protected getDialogExtra(): string { - return getPhrase("wcf.global.button.reset"); + protected setCropperStyle() { + super.setCropperStyle(); + + this.cropperSelection!.width = this.minSize.width; + this.cropperSelection!.height = this.minSize.height; + this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.image!.width}px)`; + this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.image!.height}px)`; } - // TODO handle resize cropper selection to min/max size + protected createCropper() { + super.createCropper(); + + this.dialog!.addEventListener("extra", () => { + this.cropperImage!.$center("contain"); + this.cropperSelection!.$reset(); + }); + + // Limit the selection to the canvas boundaries + this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { + const selection = event.detail as Selection; + + if ( + selection.width < this.minSize.width || + selection.height < this.minSize.height || + selection.width > this.maxSize.width || + selection.height > this.maxSize.height + ) { + event.preventDefault(); + } + }); + } } export async function cropImage( diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 4139d310bf..524384bcfa 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -21,6 +21,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.element = element; this.file = file; this.resizer = new Resizer_1.default(); + this.configuration.sizes = this.configuration.sizes.sort((a, b) => { + if (a.width >= a.height) { + return b.width - a.width; + } + else { + return b.height - a.height; + } + }); } async showDialog() { this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ @@ -28,10 +36,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); this.createCropper(); - this.dialog.addEventListener("extra", () => { - this.cropperImage.$center("contain"); - this.cropperSelection.$reset(); - }); return new Promise((resolve, reject) => { this.dialog.addEventListener("primary", () => { this.cropperSelection.$toCanvas() @@ -118,14 +122,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL window.setTimeout(() => resolve(this.file), 10_000); }); // resize image to the largest possible size - this.configuration.sizes = this.configuration.sizes.sort((a, b) => { - if (a.width >= a.height) { - return b.width - a.width; - } - else { - return b.height - a.height; - } - }); const sizes = this.configuration.sizes.filter((size) => { return size.width <= this.image.width && size.height <= this.image.height; }); @@ -162,13 +158,28 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } } class MinMaxImageCropper extends ImageCropper { + constructor(element, file, configuration) { + super(element, file, configuration); + if (configuration.sizes.length !== 2) { + throw new Error("MinMaxImageCropper requires exactly two sizes"); + } + } + get minSize() { + return this.configuration.sizes[1]; + } + get maxSize() { + return this.configuration.sizes[0]; + } + getDialogExtra() { + return (0, Language_1.getPhrase)("wcf.global.button.reset"); + } getCropperTemplate() { return `
- + @@ -184,8 +195,29 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL
`; } - getDialogExtra() { - return (0, Language_1.getPhrase)("wcf.global.button.reset"); + setCropperStyle() { + super.setCropperStyle(); + this.cropperSelection.width = this.minSize.width; + this.cropperSelection.height = this.minSize.height; + this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.image.width}px)`; + this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.image.height}px)`; + } + createCropper() { + super.createCropper(); + this.dialog.addEventListener("extra", () => { + this.cropperImage.$center("contain"); + this.cropperSelection.$reset(); + }); + // Limit the selection to the canvas boundaries + this.cropperSelection.addEventListener("change", (event) => { + const selection = event.detail; + if (selection.width < this.minSize.width || + selection.height < this.minSize.height || + selection.width > this.maxSize.width || + selection.height > this.maxSize.height) { + event.preventDefault(); + } + }); } } async function cropImage(element, file, configuration) { diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index dfc88c79a2..e4a5ae0a71 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -467,5 +467,7 @@ html[data-color-scheme="dark"] .dialog::backdrop { } .dialog .cropperContainer { - overflow: scroll; + overflow: auto; + height: 100%; + width: 100%; } From a255d253ac2c03252121e48866535010f048b539 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 10:13:35 +0100 Subject: [PATCH 11/35] Remove sorting the sizes in js, server already does this --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 14 +++----------- .../WoltLabSuite/Core/Component/Image/Cropper.js | 14 +++----------- .../processor/ImageCropperConfiguration.class.php | 1 + 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index a449ffc0a0..821692e489 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -33,14 +33,6 @@ abstract class ImageCropper { this.element = element; this.file = file; this.resizer = new ImageResizer(); - - this.configuration.sizes = this.configuration.sizes.sort((a, b) => { - if (a.width >= a.height) { - return b.width - a.width; - } else { - return b.height - a.height; - } - }); } public async showDialog(): Promise { @@ -175,7 +167,7 @@ class ExactImageCropper extends ImageCropper { ); } - this.#size = sizes[0]; + this.#size = sizes[sizes.length - 1]; this.image = await this.resizer.resize( this.image as HTMLImageElement, this.image!.width >= this.image!.height ? this.image!.width : this.#size.width, @@ -221,11 +213,11 @@ class MinMaxImageCropper extends ImageCropper { } get minSize() { - return this.configuration.sizes[1]; + return this.configuration.sizes[0]; } get maxSize() { - return this.configuration.sizes[0]; + return this.configuration.sizes[1]; } protected getDialogExtra(): string { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 524384bcfa..c59d3ccf5c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -21,14 +21,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.element = element; this.file = file; this.resizer = new Resizer_1.default(); - this.configuration.sizes = this.configuration.sizes.sort((a, b) => { - if (a.width >= a.height) { - return b.width - a.width; - } - else { - return b.height - a.height; - } - }); } async showDialog() { this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ @@ -132,7 +124,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL height: smallestSize?.height, })); } - this.#size = sizes[0]; + this.#size = sizes[sizes.length - 1]; this.image = await this.resizer.resize(this.image, this.image.width >= this.image.height ? this.image.width : this.#size.width, this.image.height > this.image.width ? this.image.height : this.#size.height, this.resizer.quality, true, timeout); } getCropperTemplate() { @@ -165,10 +157,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } } get minSize() { - return this.configuration.sizes[1]; + return this.configuration.sizes[0]; } get maxSize() { - return this.configuration.sizes[0]; + return this.configuration.sizes[1]; } getDialogExtra() { return (0, Language_1.getPhrase)("wcf.global.button.reset"); diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php index 7c696a02f9..249edb2705 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -74,6 +74,7 @@ public static function createMinMax(ImageCropSize $min, ImageCropSize $max): sel * - Image is resized to 128x170 * - Image is 300x300 * - Image is resized to 256x256 + * - The image is uploaded directly without displaying the cropping dialog */ public static function createExact(ImageCropSize ...$sizes): self { From 3a494ba378dd530722af88671e658c0d1279fdc7 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 10:19:24 +0100 Subject: [PATCH 12/35] Correct comment --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 821692e489..4349c010f2 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -264,7 +264,7 @@ class MinMaxImageCropper extends ImageCropper { this.cropperSelection!.$reset(); }); - // Limit the selection to the canvas boundaries + // Limit the selection to the min/max size this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { const selection = event.detail as Selection; From 7c82a01927a3416458f23a26886a25a0500e43ee Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 10:28:49 +0100 Subject: [PATCH 13/35] Update `cropper.min.js` to the current installed version --- wcfsetup/install/files/js/3rdParty/cropper.min.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/js/3rdParty/cropper.min.js b/wcfsetup/install/files/js/3rdParty/cropper.min.js index 9bbf19b372..1ea266e1d4 100644 --- a/wcfsetup/install/files/js/3rdParty/cropper.min.js +++ b/wcfsetup/install/files/js/3rdParty/cropper.min.js @@ -1,2 +1,2 @@ -/*! Cropper.js v2.0.0-rc | (c) 2015-present Chen Fengyuan | MIT */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Cropper={})}(this,(function(t){"use strict";const e="undefined"!=typeof window&&void 0!==window.document,i=e?window:{},s=!!e&&"ontouchstart"in i.document.documentElement,n=!!e&&"PointerEvent"in i,a="cropper",o=`${a}-canvas`,r=`${a}-crosshair`,h=`${a}-grid`,c=`${a}-handle`,l=`${a}-image`,d=`${a}-selection`,u=`${a}-shade`,$=`${a}-viewer`,p="select",g="move",m="scale",b="rotate",f="transform",v="none",C="n-resize",w="e-resize",y="s-resize",E="w-resize",S="ne-resize",A="nw-resize",T="se-resize",k="sw-resize",x="action",O=s?"touchend touchcancel":"mouseup",N=s?"touchmove":"mousemove",I=s?"touchstart":"mousedown",R=n?"pointerdown":I,z=n?"pointermove":N,M=n?"pointerup pointercancel":O,P="error",D="keydown",_="load",W="wheel",Y="action",L="actionend",X="actionmove",H="actionstart",j="change",V="transform";function U(t){return"string"==typeof t}const q=Number.isNaN||i.isNaN;function B(t){return"number"==typeof t&&!q(t)}function K(t){return B(t)&&t>0&&t<1/0}function Z(t){return void 0===t}function F(t){return"object"==typeof t&&null!==t}const{hasOwnProperty:G}=Object.prototype;function J(t){if(!F(t))return!1;try{const{constructor:e}=t,{prototype:i}=e;return e&&i&&G.call(i,"isPrototypeOf")}catch(t){return!1}}function Q(t){return"function"==typeof t}function tt(t){return"object"==typeof t&&null!==t&&1===t.nodeType}const et=/([a-z\d])([A-Z])/g;function it(t){return String(t).replace(et,"$1-$2").toLowerCase()}const st=/-[A-z\d]/g;function nt(t){return t.replace(st,(t=>t.slice(1).toUpperCase()))}const at=/\s\s*/;function ot(t,e,i,s){e.trim().split(at).forEach((e=>{t.removeEventListener(e,i,s)}))}function rt(t,e,i,s){e.trim().split(at).forEach((e=>{t.addEventListener(e,i,s)}))}function ht(t,e,i,s){rt(t,e,i,Object.assign(Object.assign({},s),{once:!0}))}const ct={bubbles:!0,cancelable:!0,composed:!0};function lt(t,e,i,s){return t.dispatchEvent(new CustomEvent(e,Object.assign(Object.assign(Object.assign({},ct),{detail:i}),s)))}const dt=Promise.resolve();function ut(t,e){return e?dt.then(t?e.bind(t):e):dt}function $t(t){const{documentElement:e}=t.ownerDocument,s=t.getBoundingClientRect();return{left:s.left+(i.pageXOffset-e.clientLeft),top:s.top+(i.pageYOffset-e.clientTop)}}const pt=/deg|g?rad|turn$/i;function gt(t){const e=parseFloat(t)||0;if(0!==e){const[i="rad"]=String(t).match(pt)||[];switch(i.toLowerCase()){case"deg":return e/360*(2*Math.PI);case"grad":return e/400*(2*Math.PI);case"turn":return e*(2*Math.PI)}}return e}const mt="contain";function bt(t,e=mt){const{aspectRatio:i}=t;let{width:s,height:n}=t;const a=K(s),o=K(n);if(a&&o){const t=n*i;e===mt&&t>s||"cover"===e&&t{const e=nt(t);let i=this[e];Z(i)||this.$propertyChangedCallback(e,void 0,i),Object.defineProperty(this,e,{enumerable:!0,configurable:!0,get:()=>i,set(t){const s=i;i=t,this.$propertyChangedCallback(e,s,t)}})}));const t=this.attachShadow({mode:this.shadowRootMode||Ct});if(this.shadowRoot||wt.set(this,t),yt.set(this,this.$addStyles(this.$sharedStyle)),this.$style&&this.$addStyles(this.$style),this.$template){const e=document.createElement("template");e.innerHTML=this.$template,t.appendChild(e.content)}if(this.slottable){const e=document.createElement("slot");t.appendChild(e)}}disconnectedCallback(){yt.has(this)&&yt.delete(this),wt.has(this)&&wt.delete(this)}$getTagNameOf(t){var e;return null!==(e=Et.get(t))&&void 0!==e?e:t}$setStyles(t){return Object.keys(t).forEach((e=>{let i=t[e];B(i)&&(i=0!==i&&vt.test(e)?`${i}px`:String(i)),this.style[e]=i})),this}$getShadowRoot(){return this.shadowRoot||wt.get(this)}$addStyles(t){let e;const i=this.$getShadowRoot();return St?(e=new CSSStyleSheet,e.replaceSync(t),i.adoptedStyleSheets=i.adoptedStyleSheets.concat(e)):(e=document.createElement("style"),e.textContent=t,i.appendChild(e)),e}$emit(t,e,i){return lt(this,t,e,i)}$nextTick(t){return ut(this,t)}static $define(t,s){F(t)&&(s=t,t=""),t||(t=this.$name||this.name),t=it(t),e&&i.customElements&&!i.customElements.get(t)&&customElements.define(t,this,s)}}At.$version="2.0.0-rc";class Tt extends At{constructor(){super(...arguments),this.$onPointerDown=null,this.$onPointerMove=null,this.$onPointerUp=null,this.$onWheel=null,this.$wheeling=!1,this.$pointers=new Map,this.$style=':host{display:block;min-height:100px;min-width:200px;overflow:hidden;position:relative;touch-action:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([background]){background-color:#fff;background-image:repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc),repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc);background-image:repeating-conic-gradient(#ccc 0 25%,#fff 0 50%);background-position:0 0,.5rem .5rem;background-size:1rem 1rem}:host([disabled]){pointer-events:none}:host([disabled]):after{bottom:0;content:"";cursor:not-allowed;display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}',this.$action=v,this.background=!1,this.disabled=!1,this.scaleStep=.1,this.themeColor="#39f"}static get observedAttributes(){return super.observedAttributes.concat(["background","disabled","scale-step"])}connectedCallback(){super.connectedCallback(),this.disabled||this.$bind()}disconnectedCallback(){this.disabled||this.$unbind(),super.disconnectedCallback()}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"disabled"===t))i?this.$unbind():this.$bind()}$bind(){this.$onPointerDown||(this.$onPointerDown=this.$handlePointerDown.bind(this),rt(this,R,this.$onPointerDown)),this.$onPointerMove||(this.$onPointerMove=this.$handlePointerMove.bind(this),rt(this.ownerDocument,z,this.$onPointerMove)),this.$onPointerUp||(this.$onPointerUp=this.$handlePointerUp.bind(this),rt(this.ownerDocument,M,this.$onPointerUp)),this.$onWheel||(this.$onWheel=this.$handleWheel.bind(this),rt(this,W,this.$onWheel,{passive:!1,capture:!0}))}$unbind(){this.$onPointerDown&&(ot(this,R,this.$onPointerDown),this.$onPointerDown=null),this.$onPointerMove&&(ot(this.ownerDocument,z,this.$onPointerMove),this.$onPointerMove=null),this.$onPointerUp&&(ot(this.ownerDocument,M,this.$onPointerUp),this.$onPointerUp=null),this.$onWheel&&(ot(this,W,this.$onWheel,{capture:!0}),this.$onWheel=null)}$handlePointerDown(t){const{buttons:e,button:i,type:s}=t;if(this.disabled||("pointerdown"===s&&"mouse"===t.pointerType||"mousedown"===s)&&(B(e)&&1!==e||B(i)&&0!==i||t.ctrlKey))return;const{$pointers:n}=this;let a="";if(t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:i})=>{n.set(t,{startX:e,startY:i,endX:e,endY:i})}));else{const{pointerId:e=0,pageX:i,pageY:s}=t;n.set(e,{startX:i,startY:s,endX:i,endY:s})}n.size>1?a=f:tt(t.target)&&(a=t.target.action||t.target.getAttribute(x)||""),!1!==this.$emit(H,{action:a,relatedEvent:t})&&(t.preventDefault(),this.$action=a,this.style.willChange="transform")}$handlePointerMove(t){const{$action:e,$pointers:i}=this;if(this.disabled||e===v||0===i.size)return;if(!1===this.$emit(X,{action:e,relatedEvent:t}))return;if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:s})=>{const n=i.get(t);n&&Object.assign(n,{endX:e,endY:s})}));else{const{pointerId:e=0,pageX:s,pageY:n}=t,a=i.get(e);a&&Object.assign(a,{endX:s,endY:n})}const s={action:e,relatedEvent:t};if(e===f){const e=new Map(i);let n=0,a=0,o=0,r=0,h=t.pageX,c=t.pageY;i.forEach(((t,i)=>{e.delete(i),e.forEach((e=>{let i=e.startX-t.startX,s=e.startY-t.startY,l=e.endX-t.endX,d=e.endY-t.endY,u=0,$=0,p=0,g=0;if(0===i?s<0?p=2*Math.PI:s>0&&(p=Math.PI):i>0?p=Math.PI/2+Math.atan(s/i):i<0&&(p=1.5*Math.PI+Math.atan(s/i)),0===l?d<0?g=2*Math.PI:d>0&&(g=Math.PI):l>0?g=Math.PI/2+Math.atan(d/l):l<0&&(g=1.5*Math.PI+Math.atan(d/l)),g>0||p>0){const i=g-p,s=Math.abs(i);s>n&&(n=s,o=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}if(i=Math.abs(i),s=Math.abs(s),l=Math.abs(l),d=Math.abs(d),i>0&&s>0?u=Math.sqrt(i*i+s*s):i>0?u=i:s>0&&(u=s),l>0&&d>0?$=Math.sqrt(l*l+d*d):l>0?$=l:d>0&&($=d),u>0&&$>0){const i=($-u)/u,s=Math.abs(i);s>a&&(a=s,r=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}}))}));const l=n>0,d=a>0;l&&d?(s.rotate=o,s.scale=r,s.centerX=h,s.centerY=c):l?(s.action=b,s.rotate=o,s.centerX=h,s.centerY=c):d?(s.action=m,s.scale=r,s.centerX=h,s.centerY=c):s.action=v}else{const[t]=Array.from(i.values());Object.assign(s,t)}i.forEach((t=>{t.startX=t.endX,t.startY=t.endY})),s.action!==v&&this.$emit(Y,s,{cancelable:!1})}$handlePointerUp(t){const{$action:e,$pointers:i}=this;if(!this.disabled&&e!==v&&!1!==this.$emit(L,{action:e,relatedEvent:t})){if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t})=>{i.delete(t)}));else{const{pointerId:e=0}=t;i.delete(e)}0===i.size&&(this.style.willChange="",this.$action=v)}}$handleWheel(t){if(this.disabled)return;if(t.preventDefault(),this.$wheeling)return;this.$wheeling=!0,setTimeout((()=>{this.$wheeling=!1}),50);const e=(t.deltaY>0?-1:1)*this.scaleStep;this.$emit(Y,{action:m,scale:e,relatedEvent:t},{cancelable:!1})}$setAction(t){return U(t)&&(this.$action=t),this}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let n=this.offsetWidth,a=this.offsetHeight,o=1;J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.offsetWidth),s.width=n,s.height=a;const r=this.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform();let p=u,g=$,m=i.naturalWidth,b=i.naturalHeight;1!==o&&(p*=o,g*=o,m*=o,b*=o);const f=m/2,v=b/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(f,v),h.transform(e,c,l,d,p,g),h.translate(-f,-v),h.drawImage(i,0,0,m,b),h.restore()}e(s)})).catch(i):e(s)}))}}Tt.$name=o,Tt.$version="2.0.0-rc";const kt=new WeakMap,xt=["alt","crossorigin","decoding","importance","loading","referrerpolicy","sizes","src","srcset"];class Ot extends At{constructor(){super(...arguments),this.$matrix=[1,0,0,1,0,0],this.$onLoad=null,this.$onCanvasAction=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$actionStartTarget=null,this.$style=":host{display:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}",this.$image=new Image,this.initialCenterSize="contain",this.rotatable=!1,this.scalable=!1,this.skewable=!1,this.slottable=!1,this.translatable=!1}set $canvas(t){kt.set(this,t)}get $canvas(){return kt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(xt,["initial-center-size","rotatable","scalable","skewable","translatable"])}attributeChangedCallback(t,e,i){Object.is(i,e)||(super.attributeChangedCallback(t,e,i),xt.includes(t)&&this.$image.setAttribute(t,i))}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"initialCenterSize"===t))this.$nextTick((()=>{this.$center(i)}))}connectedCallback(){super.connectedCallback();const{$image:t}=this,e=this.closest(this.$getTagNameOf(o));e&&(this.$canvas=e,this.$setStyles({display:"block",position:"absolute"}),this.$onCanvasActionStart=t=>{var e,i;this.$actionStartTarget=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target},this.$onCanvasActionEnd=()=>{this.$actionStartTarget=null},this.$onCanvasAction=this.$handleAction.bind(this),rt(e,H,this.$onCanvasActionStart),rt(e,L,this.$onCanvasActionEnd),rt(e,Y,this.$onCanvasAction)),this.$onLoad=this.$handleLoad.bind(this),rt(t,_,this.$onLoad),this.$getShadowRoot().appendChild(t)}disconnectedCallback(){const{$image:t,$canvas:e}=this;e&&(this.$onCanvasActionStart&&(ot(e,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(e,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(e,Y,this.$onCanvasAction),this.$onCanvasAction=null)),t&&this.$onLoad&&(ot(t,_,this.$onLoad),this.$onLoad=null),this.$getShadowRoot().removeChild(t),super.disconnectedCallback()}$handleLoad(){const{$image:t}=this;this.$setStyles({width:t.naturalWidth,height:t.naturalHeight}),this.$canvas&&this.$center(this.initialCenterSize)}$handleAction(t){if(this.hidden||!(this.rotatable||this.scalable||this.translatable))return;const{$canvas:e}=this,{detail:i}=t;if(i){const{relatedEvent:t}=i;let{action:s}=i;switch(s!==f||this.rotatable&&this.scalable||(s=this.rotatable?b:this.scalable?m:v),s){case g:if(this.translatable){let s=null;t&&(s=t.target.closest(this.$getTagNameOf(d))),s||(s=e.querySelector(this.$getTagNameOf(d))),s&&s.multiple&&!s.active&&(s=e.querySelector(`${this.$getTagNameOf(d)}[active]`)),s&&!s.hidden&&s.movable&&!s.linked&&this.$actionStartTarget&&s.contains(this.$actionStartTarget)||this.$move(i.endX-i.startX,i.endY-i.startY)}break;case b:if(this.rotatable)if(t){const{x:e,y:s}=this.getBoundingClientRect();this.$rotate(i.rotate,t.clientX-e,t.clientY-s)}else this.$rotate(i.rotate);break;case m:if(this.scalable)if(t){const e=t.target.closest(this.$getTagNameOf(d));if(!e||e.linked){const{x:e,y:s}=this.getBoundingClientRect();this.$zoom(i.scale,t.clientX-e,t.clientY-s)}}else this.$zoom(i.scale);break;case f:if(this.rotatable&&this.scalable){const{rotate:e}=i;let{scale:s}=i;s<0?s=1/(1-s):s+=1;const n=Math.cos(e),a=Math.sin(e),[o,r,h,c]=[n*s,a*s,-a*s,n*s];if(t){const e=this.getBoundingClientRect(),i=t.clientX-e.x,s=t.clientY-e.y,[n,a,l,d]=this.$matrix,u=i-e.width/2,$=s-e.height/2,p=(u*d-l*$)/(n*d-l*a),g=($*n-a*u)/(n*d-l*a);this.$transform(o,r,h,c,p*(1-o)+g*h,g*(1-c)+p*r)}else this.$transform(o,r,h,c,0,0)}}}}$ready(t){const{$image:e}=this,i=new Promise(((t,i)=>{const s=new Error("Failed to load the image source");if(e.complete)e.naturalWidth>0&&e.naturalHeight>0?t(e):i(s);else{const n=()=>{ot(e,P,a),t(e)},a=()=>{ot(e,_,n),i(s)};ht(e,_,n),ht(e,P,a)}}));return Q(t)&&i.then((e=>(t(e),e))),i}$center(t){const{parentElement:e}=this;if(!e)return this;const i=e.getBoundingClientRect(),s=i.width,n=i.height,{x:a,y:o,width:r,height:h}=this.getBoundingClientRect(),c=a+r/2,l=o+h/2,d=i.x+s/2,u=i.y+n/2;if(this.$move(d-c,u-l),t&&(r!==s||h!==n)){const e=s/r,i=n/h;switch(t){case"cover":this.$scale(Math.max(e,i));break;case"contain":this.$scale(Math.min(e,i))}}return this}$move(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$translate(o,r)}return this}$moveTo(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$setTransform(i,s,n,a,o,r)}return this}$rotate(t,e,i){if(this.rotatable){const s=gt(t),n=Math.cos(s),a=Math.sin(s),[o,r,h,c]=[n,a,-a,n];if(B(e)&&B(i)){const[t,s,n,a]=this.$matrix,{width:l,height:d}=this.getBoundingClientRect(),u=e-l/2,$=i-d/2,p=(u*a-n*$)/(t*a-n*s),g=($*t-s*u)/(t*a-n*s);this.$transform(o,r,h,c,p*(1-o)-g*h,g*(1-c)-p*r)}else this.$transform(o,r,h,c,0,0)}return this}$zoom(t,e,i){if(!this.scalable||0===t)return this;if(t<0?t=1/(1-t):t+=1,B(e)&&B(i)){const[s,n,a,o]=this.$matrix,{width:r,height:h}=this.getBoundingClientRect(),c=e-r/2,l=i-h/2,d=(c*o-a*l)/(s*o-a*n),u=(l*s-n*c)/(s*o-a*n);this.$transform(t,0,0,t,d*(1-t),u*(1-t))}else this.$scale(t);return this}$scale(t,e=t){return this.scalable&&this.$transform(t,0,0,e,0,0),this}$skew(t,e=0){if(this.skewable){const i=gt(t),s=gt(e);this.$transform(1,Math.tan(s),Math.tan(i),1,0,0)}return this}$translate(t,e=t){return this.translatable&&B(t)&&B(e)&&this.$transform(1,0,0,1,t,e),this}$transform(t,e,i,s,n,a){return B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a)?this.$setTransform(ft(this.$matrix,[t,e,i,s,n,a])):this}$setTransform(t,e,i,s,n,a){if((this.rotatable||this.scalable||this.skewable||this.translatable)&&(Array.isArray(t)&&([t,e,i,s,n,a]=t),B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a))){const o=[...this.$matrix],r=[t,e,i,s,n,a];if(!1===this.$emit(V,{matrix:r,oldMatrix:o}))return this;this.$matrix=r,this.style.transform=`matrix(${r.join(", ")})`}return this}$getTransform(){return this.$matrix.slice()}$resetTransform(){return this.$setTransform([1,0,0,1,0,0])}}Ot.$name=l,Ot.$version="2.0.0-rc";const Nt=new WeakMap;class It extends At{constructor(){super(...arguments),this.$onCanvasChange=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$style=":host{display:block;height:0;left:0;outline:var(--theme-color) solid 1px;position:relative;top:0;width:0}:host([transparent]){outline-color:transparent}",this.x=0,this.y=0,this.width=0,this.height=0,this.slottable=!1,this.themeColor="rgba(0, 0, 0, 0.65)"}set $canvas(t){Nt.set(this,t)}get $canvas(){return Nt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["height","width","x","y"])}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));if(t){this.$canvas=t,this.style.position="absolute";const e=t.querySelector(this.$getTagNameOf(d));e&&(this.$onCanvasActionStart=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!1)},this.$onCanvasActionEnd=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!0)},this.$onCanvasChange=t=>{const{x:i,y:s,width:n,height:a}=t.detail;this.$change(i,s,n,a),(e.hidden||0===i&&0===s&&0===n&&0===a)&&(this.hidden=!0)},rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,j,this.$onCanvasChange))}this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasChange&&(ot(t,j,this.$onCanvasChange),this.$onCanvasChange=null)),super.disconnectedCallback()}$change(t,e,i=this.width,s=this.height){return B(t)&&B(e)&&B(i)&&B(s)&&(t!==this.x||e!==this.y||i!==this.width||s!==this.height)?(this.hidden&&(this.hidden=!1),this.x=t,this.y=e,this.width=i,this.height=s,this.$render()):this}$reset(){return this.$change(0,0,0,0)}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height,outlineWidth:i.innerWidth})}}It.$name=u,It.$version="2.0.0-rc";class Rt extends At{constructor(){super(...arguments),this.$onCanvasCropEnd=null,this.$onCanvasCropStart=null,this.$style=':host{background-color:var(--theme-color);display:block}:host([action=move]),:host([action=select]){height:100%;left:0;position:absolute;top:0;width:100%}:host([action=move]){cursor:move}:host([action=select]){cursor:crosshair}:host([action$=-resize]){background-color:transparent;height:15px;position:absolute;width:15px}:host([action$=-resize]):after{background-color:var(--theme-color);content:"";display:block;height:5px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px}:host([action=n-resize]),:host([action=s-resize]){cursor:ns-resize;left:50%;transform:translateX(-50%);width:100%}:host([action=n-resize]){top:-8px}:host([action=s-resize]){bottom:-8px}:host([action=e-resize]),:host([action=w-resize]){cursor:ew-resize;height:100%;top:50%;transform:translateY(-50%)}:host([action=e-resize]){right:-8px}:host([action=w-resize]){left:-8px}:host([action=ne-resize]){cursor:nesw-resize;right:-8px;top:-8px}:host([action=nw-resize]){cursor:nwse-resize;left:-8px;top:-8px}:host([action=se-resize]){bottom:-8px;cursor:nwse-resize;right:-8px}:host([action=se-resize]):after{height:15px;width:15px}@media (pointer:coarse){:host([action=se-resize]):after{height:10px;width:10px}}@media (pointer:fine){:host([action=se-resize]):after{height:5px;width:5px}}:host([action=sw-resize]){bottom:-8px;cursor:nesw-resize;left:-8px}:host([plain]){background-color:transparent}',this.action=v,this.plain=!1,this.slottable=!1,this.themeColor="rgba(51, 153, 255, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["action","plain"])}}Rt.$name=c,Rt.$version="2.0.0-rc";const zt=new WeakMap;class Mt extends At{constructor(){super(...arguments),this.$onCanvasAction=null,this.$onCanvasActionStart=null,this.$onCanvasActionEnd=null,this.$onDocumentKeyDown=null,this.$action="",this.$actionStartTarget=null,this.$style=':host{display:block;left:0;position:relative;right:0}:host([outlined]){outline:1px solid var(--theme-color)}:host([multiple]){outline:1px dashed hsla(0,0%,100%,.5)}:host([multiple]):after{bottom:0;content:"";cursor:pointer;display:block;left:0;position:absolute;right:0;top:0}:host([multiple][active]){outline-color:var(--theme-color);z-index:1}:host([multiple])>*{visibility:hidden}:host([multiple][active])>*{visibility:visible}:host([multiple][active]):after{display:none}',this.$initialSelection={x:0,y:0,width:0,height:0},this.x=0,this.y=0,this.width=0,this.height=0,this.aspectRatio=NaN,this.initialAspectRatio=NaN,this.initialCoverage=NaN,this.active=!1,this.movable=!1,this.resizable=!1,this.zoomable=!1,this.multiple=!1,this.keyboard=!1,this.outlined=!1,this.precise=!1,this.linked=!1}set $canvas(t){zt.set(this,t)}get $canvas(){return zt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["active","aspect-ratio","height","initial-aspect-ratio","initial-coverage","keyboard","linked","movable","multiple","outlined","precise","resizable","width","x","y","zoomable"])}$propertyChangedCallback(t,e,i){if(!Object.is(i,e))switch(super.$propertyChangedCallback(t,e,i),t){case"x":case"y":case"width":case"height":this.$nextTick((()=>{this.$change(this.x,this.y,this.width,this.height,this.aspectRatio,!0)}));break;case"aspectRatio":case"initialAspectRatio":this.$nextTick((()=>{this.$initSelection()}));break;case"initialCoverage":this.$nextTick((()=>{K(i)&&i<=1&&this.$initSelection(!0,!0)}));break;case"keyboard":this.$nextTick((()=>{this.$canvas&&(i?this.$onDocumentKeyDown||(this.$onDocumentKeyDown=this.$handleKeyDown.bind(this),rt(this.ownerDocument,D,this.$onDocumentKeyDown)):this.$onDocumentKeyDown&&(ot(this.ownerDocument,D,this.$onDocumentKeyDown),this.$onDocumentKeyDown=null))}));break;case"multiple":this.$nextTick((()=>{if(this.$canvas){const t=this.$getSelections();i?(t.forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height})):(this.active=!1,t.slice(1).forEach((t=>{this.$removeSelection(t)})))}}));break;case"precise":this.$nextTick((()=>{this.$change(this.x,this.y)}))}}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));t?(this.$canvas=t,this.$setStyles({position:"absolute",transform:`translate(${this.x}px, ${this.y}px)`}),this.hidden||this.$render(),this.$initSelection(!0),this.$onCanvasActionStart=this.$handleActionStart.bind(this),this.$onCanvasActionEnd=this.$handleActionEnd.bind(this),this.$onCanvasAction=this.$handleAction.bind(this),rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionStart),rt(t,Y,this.$onCanvasAction)):this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(t,Y,this.$onCanvasAction),this.$onCanvasAction=null)),super.disconnectedCallback()}$getSelections(){let t=[];return this.parentElement&&(t=Array.from(this.parentElement.querySelectorAll(this.$getTagNameOf(d)))),t}$initSelection(t=!1,e=!1){const{initialCoverage:i,parentElement:s}=this;if(K(i)&&s){const n=this.aspectRatio||this.initialAspectRatio;let a=(e?0:this.width)||s.offsetWidth*i,o=(e?0:this.height)||s.offsetHeight*i;K(n)&&({width:a,height:o}=bt({aspectRatio:n,width:a,height:o})),this.$change(this.x,this.y,a,o),t&&this.$center(),this.$initialSelection={x:this.x,y:this.y,width:this.width,height:this.height}}}$createSelection(){const t=this.cloneNode(!0);return this.hasAttribute("id")&&t.removeAttribute("id"),this.active=!1,this.parentElement&&this.parentElement.insertBefore(t,this.nextSibling),t}$removeSelection(t=this){if(this.parentElement){const e=this.$getSelections();if(e.length>1){const i=e.indexOf(t),s=e[i+1]||e[i-1];s&&(t.active=!1,this.parentElement.removeChild(t),s.active=!0,s.$emit(j,{x:s.x,y:s.y,width:s.width,height:s.height}))}else this.$clear()}}$handleActionStart(t){var e,i;const s=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target;this.$action="",this.$actionStartTarget=s,!this.hidden&&this.multiple&&!this.active&&s===this&&this.parentElement&&(this.$getSelections().forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height}))}$handleAction(t){const{currentTarget:e,detail:i}=t;if(e&&i){const{relatedEvent:s}=i;let{action:n}=i;if(!n&&this.multiple&&(n=this.$action||(null==s?void 0:s.target.action),this.$action=n),!n||this.hidden&&n!==p||this.multiple&&!this.active&&n!==m)return;const a=i.endX-i.startX,o=i.endY-i.startY,{width:r,height:h}=this;let{aspectRatio:c}=this;switch(!K(c)&&t.shiftKey&&(c=K(r)&&K(h)?r/h:1),n){case p:{const{$canvas:t}=this,s=$t(e);(this.multiple&&!this.hidden?this.$createSelection():this).$change(i.startX-s.left,i.startY-s.top,a,o,c),n=T,a<0?o>0?n=k:o<0&&(n=A):a>0&&o<0&&(n=S),t&&(t.$action=n);break}case g:this.movable&&(this.linked||this.$actionStartTarget&&this.contains(this.$actionStartTarget))&&this.$move(a,o);break;case m:if(s&&this.zoomable&&(this.linked||this.contains(s.target))){const t=$t(e);this.$zoom(i.scale,s.pageX-t.left,s.pageY-t.top)}break;default:this.$resize(n,a,o,c)}}}$handleActionEnd(){this.$action="",this.$actionStartTarget=null}$handleKeyDown(t){if(!(this.hidden||!this.keyboard||this.multiple&&!this.active||t.defaultPrevented))switch(t.key){case"Backspace":t.metaKey&&(t.preventDefault(),this.$removeSelection());break;case"Delete":t.preventDefault(),this.$removeSelection();break;case"ArrowLeft":t.preventDefault(),this.$move(-1,0);break;case"ArrowRight":t.preventDefault(),this.$move(1,0);break;case"ArrowUp":t.preventDefault(),this.$move(0,-1);break;case"ArrowDown":t.preventDefault(),this.$move(0,1);break;case"+":t.preventDefault(),this.$zoom(.1);break;case"-":t.preventDefault(),this.$zoom(-.1)}}$center(){const{parentElement:t}=this;if(!t)return this;const e=(t.offsetWidth-this.width)/2,i=(t.offsetHeight-this.height)/2;return this.$change(e,i)}$move(t,e=t){return this.$moveTo(this.x+t,this.y+e)}$moveTo(t,e=t){return this.movable?this.$change(t,e):this}$resize(t,e=0,i=0,s=this.aspectRatio){if(!this.resizable)return this;const n=K(s),{$canvas:a}=this;let{x:o,y:r,width:h,height:c}=this;switch(t){case C:r+=i,c-=i,c<0&&(t=y,c=-c,r-=c),n&&(o+=(e=i*s)/2,h-=e,h<0&&(h=-h,o-=h));break;case w:h+=e,h<0&&(t=E,h=-h,o-=h),n&&(r-=(i=e/s)/2,c+=i,c<0&&(c=-c,r-=c));break;case y:c+=i,c<0&&(t=C,c=-c,r-=c),n&&(o-=(e=i*s)/2,h+=e,h<0&&(h=-h,o-=h));break;case E:o+=e,h-=e,h<0&&(t=w,h=-h,o-=h),n&&(r+=(i=e/s)/2,c-=i,c<0&&(c=-c,r-=c));break;case S:n&&(i=-e/s),r+=i,c-=i,h+=e,h<0&&c<0?(t=k,h=-h,c=-c,o-=h,r-=c):h<0?(t=A,h=-h,o-=h):c<0&&(t=T,c=-c,r-=c);break;case A:n&&(i=e/s),o+=e,r+=i,h-=e,c-=i,h<0&&c<0?(t=T,h=-h,c=-c,o-=h,r-=c):h<0?(t=S,h=-h,o-=h):c<0&&(t=k,c=-c,r-=c);break;case T:n&&(i=e/s),h+=e,c+=i,h<0&&c<0?(t=A,h=-h,c=-c,o-=h,r-=c):h<0?(t=k,h=-h,o-=h):c<0&&(t=S,c=-c,r-=c);break;case k:n&&(i=-e/s),o+=e,h-=e,c+=i,h<0&&c<0?(t=S,h=-h,c=-c,o-=h,r-=c):h<0?(t=T,h=-h,o-=h):c<0&&(t=A,c=-c,r-=c)}return a&&a.$setAction(t),this.$change(o,r,h,c)}$zoom(t,e,i){if(!this.zoomable||0===t)return this;t<0?t=1/(1-t):t+=1;const{width:s,height:n}=this,a=s*t,o=n*t;let r=this.x,h=this.y;return B(e)&&B(i)?(r-=(a-s)*((e-this.x)/s),h-=(o-n)*((i-this.y)/n)):(r-=(a-s)/2,h-=(o-n)/2),this.$change(r,h,a,o)}$change(t,e,i=this.width,s=this.height,n=this.aspectRatio,a=!1){return!B(t)||!B(e)||!B(i)||!B(s)||i<0||s<0?this:(K(n)&&({width:i,height:s}=bt({aspectRatio:n,width:i,height:s},"cover")),this.precise||(t=Math.round(t),e=Math.round(e),i=Math.round(i),s=Math.round(s)),t===this.x&&e===this.y&&i===this.width&&s===this.height&&Object.is(n,this.aspectRatio)&&!a?this:(this.hidden&&(this.hidden=!1),!1===this.$emit(j,{x:t,y:e,width:i,height:s})?this:(this.x=t,this.y=e,this.width=i,this.height=s,this.$render())))}$reset(){const{x:t,y:e,width:i,height:s}=this.$initialSelection;return this.$change(t,e,i,s)}$clear(){return this.$change(0,0,0,0,NaN,!0),this.hidden=!0,this}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height})}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let{width:n,height:a}=this,o=1;if(J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.width),s.width=n,s.height=a,!this.$canvas)return void e(s);const r=this.$canvas.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform(),p=-this.x,g=-this.y,m=(p*d-l*g)/(e*d-l*c),b=(g*e-c*p)/(e*d-l*c);let f=e*m+l*b+u,v=c*m+d*b+$,C=i.naturalWidth,w=i.naturalHeight;1!==o&&(f*=o,v*=o,C*=o,w*=o);const y=C/2,E=w/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(y,E),h.transform(e,c,l,d,f,v),h.translate(-y,-E),h.drawImage(i,0,0,C,w),h.restore()}e(s)})).catch(i):e(s)}))}}Mt.$name=d,Mt.$version="2.0.0-rc";class Pt extends At{constructor(){super(...arguments),this.$style=":host{display:flex;flex-direction:column;position:relative;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([bordered]){border:1px dashed var(--theme-color)}:host([covered]){bottom:0;left:0;position:absolute;right:0;top:0}:host>span{display:flex;flex:1}:host>span+span{border-top:1px dashed var(--theme-color)}:host>span>span{flex:1}:host>span>span+span{border-left:1px dashed var(--theme-color)}",this.bordered=!1,this.columns=3,this.covered=!1,this.rows=3,this.slottable=!1,this.themeColor="rgba(238, 238, 238, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["bordered","columns","covered","rows"])}$propertyChangedCallback(t,e,i){Object.is(i,e)||(super.$propertyChangedCallback(t,e,i),"rows"!==t&&"columns"!==t||this.$nextTick((()=>{this.$render()})))}connectedCallback(){super.connectedCallback(),this.$render()}$render(){const t=this.$getShadowRoot(),e=document.createDocumentFragment();for(let t=0;t{setTimeout((()=>{this.$render()}),50)})))}$handleSourceImageTransform(t){this.$render(void 0,t.detail.matrix)}$render(t,e){const{$canvas:i,$selection:s}=this;t||s.hidden||(t=s),(!t||0===t.x&&0===t.y&&0===t.width&&0===t.height)&&(t={x:0,y:0,width:i.offsetWidth,height:i.offsetHeight});const{x:n,y:a,width:o,height:r}=t,h={},{clientWidth:c,clientHeight:l}=this;let d=c,u=l,$=NaN;switch(this.resize){case"both":$=1,d=o,u=r,h.width=o,h.height=r;break;case"horizontal":$=r>0?l/r:0,d=o*$,h.width=d;break;case Xt:$=o>0?c/o:0,u=r*$,h.height=u;break;default:c>0?$=o>0?c/o:0:l>0&&($=r>0?l/r:0)}this.$scale=$,this.$setStyles(h),this.$sourceImage&&this.$transformImageByOffset(null!=e?e:this.$sourceImage.$getTransform(),-n,-a)}$transformImageByOffset(t,e,i){const{$image:s,$scale:n,$sourceImage:a}=this;if(a&&s&&n>=0){const[a,o,r,h,c,l]=t,d=(e*h-r*i)/(a*h-r*o),u=(i*a-o*e)/(a*h-r*o),$=a*d+r*u+c,p=o*d+h*u+l;s.$ready((t=>{this.$setStyles.call(s,{width:t.naturalWidth*n,height:t.naturalHeight*n})})),s.$setTransform(a,o,r,h,$*n,p*n)}}}Ht.$name=$,Ht.$version="2.0.0-rc";var jt='';const Vt=/^img|canvas$/,Ut=/<(\/?(?:script|style)[^>]*)>/gi,qt={template:jt};Tt.$define(),Dt.$define(),Pt.$define(),Rt.$define(),Ot.$define(),Mt.$define(),It.$define(),Ht.$define();class Bt{constructor(t,e){if(this.options=qt,U(t)&&(t=document.querySelector(t)),!tt(t)||!Vt.test(t.localName))throw new Error("The first argument is required and must be an or element.");this.element=t,e=Object.assign(Object.assign({},qt),e),this.options=e;const{ownerDocument:i}=t;let{container:s}=e;if(s&&(U(s)&&(s=i.querySelector(s)),!tt(s)))throw new Error("The `container` option must be an element or a valid selector.");tt(s)||(s=t.parentElement?t.parentElement:i.body),this.container=s;const n=t.localName;let a="";"img"===n?({src:a}=t):"canvas"===n&&window.HTMLCanvasElement&&(a=t.toDataURL());const{template:o}=e;if(o&&U(o)){const e=document.createElement("template"),i=document.createDocumentFragment();e.innerHTML=o.replace(Ut,"<$1>"),i.appendChild(e.content),Array.from(i.querySelectorAll(l)).forEach((e=>{e.setAttribute("src",a),e.setAttribute("alt",t.alt||"The image to crop")})),t.parentElement?(t.style.display="none",s.insertBefore(i,t.nextSibling)):s.appendChild(i)}}getCropperCanvas(){return this.container.querySelector(o)}getCropperImage(){return this.container.querySelector(l)}getCropperSelection(){return this.container.querySelector(d)}getCropperSelections(){return this.container.querySelectorAll(d)}}Bt.version="2.0.0-rc",t.ACTION_MOVE=g,t.ACTION_NONE=v,t.ACTION_RESIZE_EAST=w,t.ACTION_RESIZE_NORTH=C,t.ACTION_RESIZE_NORTHEAST=S,t.ACTION_RESIZE_NORTHWEST=A,t.ACTION_RESIZE_SOUTH=y,t.ACTION_RESIZE_SOUTHEAST=T,t.ACTION_RESIZE_SOUTHWEST=k,t.ACTION_RESIZE_WEST=E,t.ACTION_ROTATE=b,t.ACTION_SCALE=m,t.ACTION_SELECT=p,t.ACTION_TRANSFORM=f,t.ATTRIBUTE_ACTION=x,t.CROPPER_CANVAS=o,t.CROPPER_CROSSHAIR=r,t.CROPPER_GIRD=h,t.CROPPER_HANDLE=c,t.CROPPER_IMAGE=l,t.CROPPER_SELECTION=d,t.CROPPER_SHADE=u,t.CROPPER_VIEWER=$,t.CropperCanvas=Tt,t.CropperCrosshair=Dt,t.CropperElement=At,t.CropperGrid=Pt,t.CropperHandle=Rt,t.CropperImage=Ot,t.CropperSelection=Mt,t.CropperShade=It,t.CropperViewer=Ht,t.DEFAULT_TEMPLATE=jt,t.EVENT_ACTION=Y,t.EVENT_ACTION_END=L,t.EVENT_ACTION_MOVE=X,t.EVENT_ACTION_START=H,t.EVENT_CHANGE=j,t.EVENT_ERROR=P,t.EVENT_KEYDOWN=D,t.EVENT_LOAD=_,t.EVENT_POINTER_DOWN=R,t.EVENT_POINTER_MOVE=z,t.EVENT_POINTER_UP=M,t.EVENT_RESIZE="resize",t.EVENT_TOUCH_END=O,t.EVENT_TOUCH_MOVE=N,t.EVENT_TOUCH_START=I,t.EVENT_TRANSFORM=V,t.EVENT_WHEEL=W,t.HAS_POINTER_EVENT=n,t.IS_BROWSER=e,t.IS_TOUCH_DEVICE=s,t.NAMESPACE=a,t.WINDOW=i,t.default=Bt,t.emit=lt,t.getAdjustedSizes=bt,t.getOffset=$t,t.isElement=tt,t.isFunction=Q,t.isNaN=q,t.isNumber=B,t.isObject=F,t.isPlainObject=J,t.isPositiveNumber=K,t.isString=U,t.isUndefined=Z,t.multiplyMatrices=ft,t.nextTick=ut,t.off=ot,t.on=rt,t.once=ht,t.toAngleInRadian=gt,t.toCamelCase=nt,t.toKebabCase=it,Object.defineProperty(t,"__esModule",{value:!0})})); +/*! Cropper.js v2.0.0-rc.2 | (c) 2015-present Chen Fengyuan | MIT */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Cropper={})}(this,(function(t){"use strict";const e="undefined"!=typeof window&&void 0!==window.document,i=e?window:{},s=!!e&&"ontouchstart"in i.document.documentElement,n=!!e&&"PointerEvent"in i,a="cropper",o=`${a}-canvas`,r=`${a}-crosshair`,h=`${a}-grid`,c=`${a}-handle`,l=`${a}-image`,d=`${a}-selection`,u=`${a}-shade`,$=`${a}-viewer`,p="select",g="move",m="scale",b="rotate",f="transform",v="none",C="n-resize",w="e-resize",y="s-resize",E="w-resize",S="ne-resize",A="nw-resize",T="se-resize",k="sw-resize",x="action",O=s?"touchend touchcancel":"mouseup",N=s?"touchmove":"mousemove",I=s?"touchstart":"mousedown",R=n?"pointerdown":I,z=n?"pointermove":N,M=n?"pointerup pointercancel":O,P="error",D="keydown",_="load",W="wheel",Y="action",L="actionend",X="actionmove",H="actionstart",j="change",V="transform";function U(t){return"string"==typeof t}const q=Number.isNaN||i.isNaN;function B(t){return"number"==typeof t&&!q(t)}function K(t){return B(t)&&t>0&&t<1/0}function Z(t){return void 0===t}function F(t){return"object"==typeof t&&null!==t}const{hasOwnProperty:G}=Object.prototype;function J(t){if(!F(t))return!1;try{const{constructor:e}=t,{prototype:i}=e;return e&&i&&G.call(i,"isPrototypeOf")}catch(t){return!1}}function Q(t){return"function"==typeof t}function tt(t){return"object"==typeof t&&null!==t&&1===t.nodeType}const et=/([a-z\d])([A-Z])/g;function it(t){return String(t).replace(et,"$1-$2").toLowerCase()}const st=/-[A-z\d]/g;function nt(t){return t.replace(st,(t=>t.slice(1).toUpperCase()))}const at=/\s\s*/;function ot(t,e,i,s){e.trim().split(at).forEach((e=>{t.removeEventListener(e,i,s)}))}function rt(t,e,i,s){e.trim().split(at).forEach((e=>{t.addEventListener(e,i,s)}))}function ht(t,e,i,s){rt(t,e,i,Object.assign(Object.assign({},s),{once:!0}))}const ct={bubbles:!0,cancelable:!0,composed:!0};function lt(t,e,i,s){return t.dispatchEvent(new CustomEvent(e,Object.assign(Object.assign(Object.assign({},ct),{detail:i}),s)))}const dt=Promise.resolve();function ut(t,e){return e?dt.then(t?e.bind(t):e):dt}function $t(t){const{documentElement:e}=t.ownerDocument,s=t.getBoundingClientRect();return{left:s.left+(i.pageXOffset-e.clientLeft),top:s.top+(i.pageYOffset-e.clientTop)}}const pt=/deg|g?rad|turn$/i;function gt(t){const e=parseFloat(t)||0;if(0!==e){const[i="rad"]=String(t).match(pt)||[];switch(i.toLowerCase()){case"deg":return e/360*(2*Math.PI);case"grad":return e/400*(2*Math.PI);case"turn":return e*(2*Math.PI)}}return e}const mt="contain";function bt(t,e=mt){const{aspectRatio:i}=t;let{width:s,height:n}=t;const a=K(s),o=K(n);if(a&&o){const t=n*i;e===mt&&t>s||"cover"===e&&t{const e=nt(t);let i=this[e];Z(i)||this.$propertyChangedCallback(e,void 0,i),Object.defineProperty(this,e,{enumerable:!0,configurable:!0,get:()=>i,set(t){const s=i;i=t,this.$propertyChangedCallback(e,s,t)}})}));const t=this.attachShadow({mode:this.shadowRootMode||Ct});if(this.shadowRoot||wt.set(this,t),yt.set(this,this.$addStyles(this.$sharedStyle)),this.$style&&this.$addStyles(this.$style),this.$template){const e=document.createElement("template");e.innerHTML=this.$template,t.appendChild(e.content)}if(this.slottable){const e=document.createElement("slot");t.appendChild(e)}}disconnectedCallback(){yt.has(this)&&yt.delete(this),wt.has(this)&&wt.delete(this)}$getTagNameOf(t){var e;return null!==(e=Et.get(t))&&void 0!==e?e:t}$setStyles(t){return Object.keys(t).forEach((e=>{let i=t[e];B(i)&&(i=0!==i&&vt.test(e)?`${i}px`:String(i)),this.style[e]=i})),this}$getShadowRoot(){return this.shadowRoot||wt.get(this)}$addStyles(t){let e;const i=this.$getShadowRoot();return St?(e=new CSSStyleSheet,e.replaceSync(t),i.adoptedStyleSheets=i.adoptedStyleSheets.concat(e)):(e=document.createElement("style"),e.textContent=t,i.appendChild(e)),e}$emit(t,e,i){return lt(this,t,e,i)}$nextTick(t){return ut(this,t)}static $define(t,s){F(t)&&(s=t,t=""),t||(t=this.$name||this.name),t=it(t),e&&i.customElements&&!i.customElements.get(t)&&customElements.define(t,this,s)}}At.$version="2.0.0-rc.2";class Tt extends At{constructor(){super(...arguments),this.$onPointerDown=null,this.$onPointerMove=null,this.$onPointerUp=null,this.$onWheel=null,this.$wheeling=!1,this.$pointers=new Map,this.$style=':host{display:block;min-height:100px;min-width:200px;overflow:hidden;position:relative;touch-action:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([background]){background-color:#fff;background-image:repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc),repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc);background-image:repeating-conic-gradient(#ccc 0 25%,#fff 0 50%);background-position:0 0,.5rem .5rem;background-size:1rem 1rem}:host([disabled]){pointer-events:none}:host([disabled]):after{bottom:0;content:"";cursor:not-allowed;display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}',this.$action=v,this.background=!1,this.disabled=!1,this.scaleStep=.1,this.themeColor="#39f"}static get observedAttributes(){return super.observedAttributes.concat(["background","disabled","scale-step"])}connectedCallback(){super.connectedCallback(),this.disabled||this.$bind()}disconnectedCallback(){this.disabled||this.$unbind(),super.disconnectedCallback()}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"disabled"===t))i?this.$unbind():this.$bind()}$bind(){this.$onPointerDown||(this.$onPointerDown=this.$handlePointerDown.bind(this),rt(this,R,this.$onPointerDown)),this.$onPointerMove||(this.$onPointerMove=this.$handlePointerMove.bind(this),rt(this.ownerDocument,z,this.$onPointerMove)),this.$onPointerUp||(this.$onPointerUp=this.$handlePointerUp.bind(this),rt(this.ownerDocument,M,this.$onPointerUp)),this.$onWheel||(this.$onWheel=this.$handleWheel.bind(this),rt(this,W,this.$onWheel,{passive:!1,capture:!0}))}$unbind(){this.$onPointerDown&&(ot(this,R,this.$onPointerDown),this.$onPointerDown=null),this.$onPointerMove&&(ot(this.ownerDocument,z,this.$onPointerMove),this.$onPointerMove=null),this.$onPointerUp&&(ot(this.ownerDocument,M,this.$onPointerUp),this.$onPointerUp=null),this.$onWheel&&(ot(this,W,this.$onWheel,{capture:!0}),this.$onWheel=null)}$handlePointerDown(t){const{buttons:e,button:i,type:s}=t;if(this.disabled||("pointerdown"===s&&"mouse"===t.pointerType||"mousedown"===s)&&(B(e)&&1!==e||B(i)&&0!==i||t.ctrlKey))return;const{$pointers:n}=this;let a="";if(t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:i})=>{n.set(t,{startX:e,startY:i,endX:e,endY:i})}));else{const{pointerId:e=0,pageX:i,pageY:s}=t;n.set(e,{startX:i,startY:s,endX:i,endY:s})}n.size>1?a=f:tt(t.target)&&(a=t.target.action||t.target.getAttribute(x)||""),!1!==this.$emit(H,{action:a,relatedEvent:t})&&(t.preventDefault(),this.$action=a,this.style.willChange="transform")}$handlePointerMove(t){const{$action:e,$pointers:i}=this;if(this.disabled||e===v||0===i.size)return;if(!1===this.$emit(X,{action:e,relatedEvent:t}))return;if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t,pageX:e,pageY:s})=>{const n=i.get(t);n&&Object.assign(n,{endX:e,endY:s})}));else{const{pointerId:e=0,pageX:s,pageY:n}=t,a=i.get(e);a&&Object.assign(a,{endX:s,endY:n})}const s={action:e,relatedEvent:t};if(e===f){const e=new Map(i);let n=0,a=0,o=0,r=0,h=t.pageX,c=t.pageY;i.forEach(((t,i)=>{e.delete(i),e.forEach((e=>{let i=e.startX-t.startX,s=e.startY-t.startY,l=e.endX-t.endX,d=e.endY-t.endY,u=0,$=0,p=0,g=0;if(0===i?s<0?p=2*Math.PI:s>0&&(p=Math.PI):i>0?p=Math.PI/2+Math.atan(s/i):i<0&&(p=1.5*Math.PI+Math.atan(s/i)),0===l?d<0?g=2*Math.PI:d>0&&(g=Math.PI):l>0?g=Math.PI/2+Math.atan(d/l):l<0&&(g=1.5*Math.PI+Math.atan(d/l)),g>0||p>0){const i=g-p,s=Math.abs(i);s>n&&(n=s,o=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}if(i=Math.abs(i),s=Math.abs(s),l=Math.abs(l),d=Math.abs(d),i>0&&s>0?u=Math.sqrt(i*i+s*s):i>0?u=i:s>0&&(u=s),l>0&&d>0?$=Math.sqrt(l*l+d*d):l>0?$=l:d>0&&($=d),u>0&&$>0){const i=($-u)/u,s=Math.abs(i);s>a&&(a=s,r=i,h=(t.startX+e.startX)/2,c=(t.startY+e.startY)/2)}}))}));const l=n>0,d=a>0;l&&d?(s.rotate=o,s.scale=r,s.centerX=h,s.centerY=c):l?(s.action=b,s.rotate=o,s.centerX=h,s.centerY=c):d?(s.action=m,s.scale=r,s.centerX=h,s.centerY=c):s.action=v}else{const[t]=Array.from(i.values());Object.assign(s,t)}i.forEach((t=>{t.startX=t.endX,t.startY=t.endY})),s.action!==v&&this.$emit(Y,s,{cancelable:!1})}$handlePointerUp(t){const{$action:e,$pointers:i}=this;if(!this.disabled&&e!==v&&!1!==this.$emit(L,{action:e,relatedEvent:t})){if(t.preventDefault(),t.changedTouches)Array.from(t.changedTouches).forEach((({identifier:t})=>{i.delete(t)}));else{const{pointerId:e=0}=t;i.delete(e)}0===i.size&&(this.style.willChange="",this.$action=v)}}$handleWheel(t){if(this.disabled)return;if(t.preventDefault(),this.$wheeling)return;this.$wheeling=!0,setTimeout((()=>{this.$wheeling=!1}),50);const e=(t.deltaY>0?-1:1)*this.scaleStep;this.$emit(Y,{action:m,scale:e,relatedEvent:t},{cancelable:!1})}$setAction(t){return U(t)&&(this.$action=t),this}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let n=this.offsetWidth,a=this.offsetHeight,o=1;J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.offsetWidth),s.width=n,s.height=a;const r=this.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform();let p=u,g=$,m=i.naturalWidth,b=i.naturalHeight;1!==o&&(p*=o,g*=o,m*=o,b*=o);const f=m/2,v=b/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(f,v),h.transform(e,c,l,d,p,g),h.translate(-f,-v),h.drawImage(i,0,0,m,b),h.restore()}e(s)})).catch(i):e(s)}))}}Tt.$name=o,Tt.$version="2.0.0-rc.2";const kt=new WeakMap,xt=["alt","crossorigin","decoding","importance","loading","referrerpolicy","sizes","src","srcset"];class Ot extends At{constructor(){super(...arguments),this.$matrix=[1,0,0,1,0,0],this.$onLoad=null,this.$onCanvasAction=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$actionStartTarget=null,this.$style=":host{display:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}",this.$image=new Image,this.initialCenterSize="contain",this.rotatable=!1,this.scalable=!1,this.skewable=!1,this.slottable=!1,this.translatable=!1}set $canvas(t){kt.set(this,t)}get $canvas(){return kt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(xt,["initial-center-size","rotatable","scalable","skewable","translatable"])}attributeChangedCallback(t,e,i){Object.is(i,e)||(super.attributeChangedCallback(t,e,i),xt.includes(t)&&this.$image.setAttribute(t,i))}$propertyChangedCallback(t,e,i){if(!Object.is(i,e)&&(super.$propertyChangedCallback(t,e,i),"initialCenterSize"===t))this.$nextTick((()=>{this.$center(i)}))}connectedCallback(){super.connectedCallback();const{$image:t}=this,e=this.closest(this.$getTagNameOf(o));e&&(this.$canvas=e,this.$setStyles({display:"block",position:"absolute"}),this.$onCanvasActionStart=t=>{var e,i;this.$actionStartTarget=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target},this.$onCanvasActionEnd=()=>{this.$actionStartTarget=null},this.$onCanvasAction=this.$handleAction.bind(this),rt(e,H,this.$onCanvasActionStart),rt(e,L,this.$onCanvasActionEnd),rt(e,Y,this.$onCanvasAction)),this.$onLoad=this.$handleLoad.bind(this),rt(t,_,this.$onLoad),this.$getShadowRoot().appendChild(t)}disconnectedCallback(){const{$image:t,$canvas:e}=this;e&&(this.$onCanvasActionStart&&(ot(e,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(e,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(e,Y,this.$onCanvasAction),this.$onCanvasAction=null)),t&&this.$onLoad&&(ot(t,_,this.$onLoad),this.$onLoad=null),this.$getShadowRoot().removeChild(t),super.disconnectedCallback()}$handleLoad(){const{$image:t}=this;this.$setStyles({width:t.naturalWidth,height:t.naturalHeight}),this.$canvas&&this.$center(this.initialCenterSize)}$handleAction(t){if(this.hidden||!(this.rotatable||this.scalable||this.translatable))return;const{$canvas:e}=this,{detail:i}=t;if(i){const{relatedEvent:t}=i;let{action:s}=i;switch(s!==f||this.rotatable&&this.scalable||(s=this.rotatable?b:this.scalable?m:v),s){case g:if(this.translatable){let s=null;t&&(s=t.target.closest(this.$getTagNameOf(d))),s||(s=e.querySelector(this.$getTagNameOf(d))),s&&s.multiple&&!s.active&&(s=e.querySelector(`${this.$getTagNameOf(d)}[active]`)),s&&!s.hidden&&s.movable&&!s.dynamic&&this.$actionStartTarget&&s.contains(this.$actionStartTarget)||this.$move(i.endX-i.startX,i.endY-i.startY)}break;case b:if(this.rotatable)if(t){const{x:e,y:s}=this.getBoundingClientRect();this.$rotate(i.rotate,t.clientX-e,t.clientY-s)}else this.$rotate(i.rotate);break;case m:if(this.scalable)if(t){const e=t.target.closest(this.$getTagNameOf(d));if(!e||!e.zoomable||e.zoomable&&e.dynamic){const{x:e,y:s}=this.getBoundingClientRect();this.$zoom(i.scale,t.clientX-e,t.clientY-s)}}else this.$zoom(i.scale);break;case f:if(this.rotatable&&this.scalable){const{rotate:e}=i;let{scale:s}=i;s<0?s=1/(1-s):s+=1;const n=Math.cos(e),a=Math.sin(e),[o,r,h,c]=[n*s,a*s,-a*s,n*s];if(t){const e=this.getBoundingClientRect(),i=t.clientX-e.x,s=t.clientY-e.y,[n,a,l,d]=this.$matrix,u=i-e.width/2,$=s-e.height/2,p=(u*d-l*$)/(n*d-l*a),g=($*n-a*u)/(n*d-l*a);this.$transform(o,r,h,c,p*(1-o)+g*h,g*(1-c)+p*r)}else this.$transform(o,r,h,c,0,0)}}}}$ready(t){const{$image:e}=this,i=new Promise(((t,i)=>{const s=new Error("Failed to load the image source");if(e.complete)e.naturalWidth>0&&e.naturalHeight>0?t(e):i(s);else{const n=()=>{ot(e,P,a),t(e)},a=()=>{ot(e,_,n),i(s)};ht(e,_,n),ht(e,P,a)}}));return Q(t)&&i.then((e=>(t(e),e))),i}$center(t){const{parentElement:e}=this;if(!e)return this;const i=e.getBoundingClientRect(),s=i.width,n=i.height,{x:a,y:o,width:r,height:h}=this.getBoundingClientRect(),c=a+r/2,l=o+h/2,d=i.x+s/2,u=i.y+n/2;if(this.$move(d-c,u-l),t&&(r!==s||h!==n)){const e=s/r,i=n/h;switch(t){case"cover":this.$scale(Math.max(e,i));break;case"contain":this.$scale(Math.min(e,i))}}return this}$move(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$translate(o,r)}return this}$moveTo(t,e=t){if(this.translatable&&B(t)&&B(e)){const[i,s,n,a]=this.$matrix,o=(t*a-n*e)/(i*a-n*s),r=(e*i-s*t)/(i*a-n*s);this.$setTransform(i,s,n,a,o,r)}return this}$rotate(t,e,i){if(this.rotatable){const s=gt(t),n=Math.cos(s),a=Math.sin(s),[o,r,h,c]=[n,a,-a,n];if(B(e)&&B(i)){const[t,s,n,a]=this.$matrix,{width:l,height:d}=this.getBoundingClientRect(),u=e-l/2,$=i-d/2,p=(u*a-n*$)/(t*a-n*s),g=($*t-s*u)/(t*a-n*s);this.$transform(o,r,h,c,p*(1-o)-g*h,g*(1-c)-p*r)}else this.$transform(o,r,h,c,0,0)}return this}$zoom(t,e,i){if(!this.scalable||0===t)return this;if(t<0?t=1/(1-t):t+=1,B(e)&&B(i)){const[s,n,a,o]=this.$matrix,{width:r,height:h}=this.getBoundingClientRect(),c=e-r/2,l=i-h/2,d=(c*o-a*l)/(s*o-a*n),u=(l*s-n*c)/(s*o-a*n);this.$transform(t,0,0,t,d*(1-t),u*(1-t))}else this.$scale(t);return this}$scale(t,e=t){return this.scalable&&this.$transform(t,0,0,e,0,0),this}$skew(t,e=0){if(this.skewable){const i=gt(t),s=gt(e);this.$transform(1,Math.tan(s),Math.tan(i),1,0,0)}return this}$translate(t,e=t){return this.translatable&&B(t)&&B(e)&&this.$transform(1,0,0,1,t,e),this}$transform(t,e,i,s,n,a){return B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a)?this.$setTransform(ft(this.$matrix,[t,e,i,s,n,a])):this}$setTransform(t,e,i,s,n,a){if((this.rotatable||this.scalable||this.skewable||this.translatable)&&(Array.isArray(t)&&([t,e,i,s,n,a]=t),B(t)&&B(e)&&B(i)&&B(s)&&B(n)&&B(a))){const o=[...this.$matrix],r=[t,e,i,s,n,a];if(!1===this.$emit(V,{matrix:r,oldMatrix:o}))return this;this.$matrix=r,this.style.transform=`matrix(${r.join(", ")})`}return this}$getTransform(){return this.$matrix.slice()}$resetTransform(){return this.$setTransform([1,0,0,1,0,0])}}Ot.$name=l,Ot.$version="2.0.0-rc.2";const Nt=new WeakMap;class It extends At{constructor(){super(...arguments),this.$onCanvasChange=null,this.$onCanvasActionEnd=null,this.$onCanvasActionStart=null,this.$style=":host{display:block;height:0;left:0;outline:var(--theme-color) solid 1px;position:relative;top:0;width:0}:host([transparent]){outline-color:transparent}",this.x=0,this.y=0,this.width=0,this.height=0,this.slottable=!1,this.themeColor="rgba(0, 0, 0, 0.65)"}set $canvas(t){Nt.set(this,t)}get $canvas(){return Nt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["height","width","x","y"])}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));if(t){this.$canvas=t,this.style.position="absolute";const e=t.querySelector(this.$getTagNameOf(d));e&&(this.$onCanvasActionStart=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!1)},this.$onCanvasActionEnd=t=>{e.hidden&&t.detail.action===p&&(this.hidden=!0)},this.$onCanvasChange=t=>{const{x:i,y:s,width:n,height:a}=t.detail;this.$change(i,s,n,a),(e.hidden||0===i&&0===s&&0===n&&0===a)&&(this.hidden=!0)},rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,j,this.$onCanvasChange))}this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasChange&&(ot(t,j,this.$onCanvasChange),this.$onCanvasChange=null)),super.disconnectedCallback()}$change(t,e,i=this.width,s=this.height){return B(t)&&B(e)&&B(i)&&B(s)&&(t!==this.x||e!==this.y||i!==this.width||s!==this.height)?(this.hidden&&(this.hidden=!1),this.x=t,this.y=e,this.width=i,this.height=s,this.$render()):this}$reset(){return this.$change(0,0,0,0)}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height,outlineWidth:i.innerWidth})}}It.$name=u,It.$version="2.0.0-rc.2";class Rt extends At{constructor(){super(...arguments),this.$onCanvasCropEnd=null,this.$onCanvasCropStart=null,this.$style=':host{background-color:var(--theme-color);display:block}:host([action=move]),:host([action=select]){height:100%;left:0;position:absolute;top:0;width:100%}:host([action=move]){cursor:move}:host([action=select]){cursor:crosshair}:host([action$=-resize]){background-color:transparent;height:15px;position:absolute;width:15px}:host([action$=-resize]):after{background-color:var(--theme-color);content:"";display:block;height:5px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px}:host([action=n-resize]),:host([action=s-resize]){cursor:ns-resize;left:50%;transform:translateX(-50%);width:100%}:host([action=n-resize]){top:-8px}:host([action=s-resize]){bottom:-8px}:host([action=e-resize]),:host([action=w-resize]){cursor:ew-resize;height:100%;top:50%;transform:translateY(-50%)}:host([action=e-resize]){right:-8px}:host([action=w-resize]){left:-8px}:host([action=ne-resize]){cursor:nesw-resize;right:-8px;top:-8px}:host([action=nw-resize]){cursor:nwse-resize;left:-8px;top:-8px}:host([action=se-resize]){bottom:-8px;cursor:nwse-resize;right:-8px}:host([action=se-resize]):after{height:15px;width:15px}@media (pointer:coarse){:host([action=se-resize]):after{height:10px;width:10px}}@media (pointer:fine){:host([action=se-resize]):after{height:5px;width:5px}}:host([action=sw-resize]){bottom:-8px;cursor:nesw-resize;left:-8px}:host([plain]){background-color:transparent}',this.action=v,this.plain=!1,this.slottable=!1,this.themeColor="rgba(51, 153, 255, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["action","plain"])}}Rt.$name=c,Rt.$version="2.0.0-rc.2";const zt=new WeakMap;class Mt extends At{constructor(){super(...arguments),this.$onCanvasAction=null,this.$onCanvasActionStart=null,this.$onCanvasActionEnd=null,this.$onDocumentKeyDown=null,this.$action="",this.$actionStartTarget=null,this.$changing=!1,this.$style=':host{display:block;left:0;position:relative;right:0}:host([outlined]){outline:1px solid var(--theme-color)}:host([multiple]){outline:1px dashed hsla(0,0%,100%,.5)}:host([multiple]):after{bottom:0;content:"";cursor:pointer;display:block;left:0;position:absolute;right:0;top:0}:host([multiple][active]){outline-color:var(--theme-color);z-index:1}:host([multiple])>*{visibility:hidden}:host([multiple][active])>*{visibility:visible}:host([multiple][active]):after{display:none}',this.$initialSelection={x:0,y:0,width:0,height:0},this.x=0,this.y=0,this.width=0,this.height=0,this.aspectRatio=NaN,this.initialAspectRatio=NaN,this.initialCoverage=NaN,this.active=!1,this.linked=!1,this.dynamic=!1,this.movable=!1,this.resizable=!1,this.zoomable=!1,this.multiple=!1,this.keyboard=!1,this.outlined=!1,this.precise=!1}set $canvas(t){zt.set(this,t)}get $canvas(){return zt.get(this)}static get observedAttributes(){return super.observedAttributes.concat(["active","aspect-ratio","dynamic","height","initial-aspect-ratio","initial-coverage","keyboard","linked","movable","multiple","outlined","precise","resizable","width","x","y","zoomable"])}$propertyChangedCallback(t,e,i){if(!Object.is(i,e))switch(super.$propertyChangedCallback(t,e,i),t){case"x":case"y":case"width":case"height":this.$changing||this.$nextTick((()=>{this.$change(this.x,this.y,this.width,this.height,this.aspectRatio,!0)}));break;case"aspectRatio":case"initialAspectRatio":this.$nextTick((()=>{this.$initSelection()}));break;case"initialCoverage":this.$nextTick((()=>{K(i)&&i<=1&&this.$initSelection(!0,!0)}));break;case"keyboard":this.$nextTick((()=>{this.$canvas&&(i?this.$onDocumentKeyDown||(this.$onDocumentKeyDown=this.$handleKeyDown.bind(this),rt(this.ownerDocument,D,this.$onDocumentKeyDown)):this.$onDocumentKeyDown&&(ot(this.ownerDocument,D,this.$onDocumentKeyDown),this.$onDocumentKeyDown=null))}));break;case"multiple":this.$nextTick((()=>{if(this.$canvas){const t=this.$getSelections();i?(t.forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height})):(this.active=!1,t.slice(1).forEach((t=>{this.$removeSelection(t)})))}}));break;case"precise":this.$nextTick((()=>{this.$change(this.x,this.y)}));break;case"linked":i&&(this.dynamic=!0)}}connectedCallback(){super.connectedCallback();const t=this.closest(this.$getTagNameOf(o));t?(this.$canvas=t,this.$setStyles({position:"absolute",transform:`translate(${this.x}px, ${this.y}px)`}),this.hidden||this.$render(),this.$initSelection(!0),this.$onCanvasActionStart=this.$handleActionStart.bind(this),this.$onCanvasActionEnd=this.$handleActionEnd.bind(this),this.$onCanvasAction=this.$handleAction.bind(this),rt(t,H,this.$onCanvasActionStart),rt(t,L,this.$onCanvasActionEnd),rt(t,Y,this.$onCanvasAction)):this.$render()}disconnectedCallback(){const{$canvas:t}=this;t&&(this.$onCanvasActionStart&&(ot(t,H,this.$onCanvasActionStart),this.$onCanvasActionStart=null),this.$onCanvasActionEnd&&(ot(t,L,this.$onCanvasActionEnd),this.$onCanvasActionEnd=null),this.$onCanvasAction&&(ot(t,Y,this.$onCanvasAction),this.$onCanvasAction=null)),super.disconnectedCallback()}$getSelections(){let t=[];return this.parentElement&&(t=Array.from(this.parentElement.querySelectorAll(this.$getTagNameOf(d)))),t}$initSelection(t=!1,e=!1){const{initialCoverage:i,parentElement:s}=this;if(K(i)&&s){const n=this.aspectRatio||this.initialAspectRatio;let a=(e?0:this.width)||s.offsetWidth*i,o=(e?0:this.height)||s.offsetHeight*i;K(n)&&({width:a,height:o}=bt({aspectRatio:n,width:a,height:o})),this.$change(this.x,this.y,a,o),t&&this.$center(),this.$initialSelection={x:this.x,y:this.y,width:this.width,height:this.height}}}$createSelection(){const t=this.cloneNode(!0);return this.hasAttribute("id")&&t.removeAttribute("id"),t.initialCoverage=NaN,this.active=!1,this.parentElement&&this.parentElement.insertBefore(t,this.nextSibling),t}$removeSelection(t=this){if(this.parentElement){const e=this.$getSelections();if(e.length>1){const i=e.indexOf(t),s=e[i+1]||e[i-1];s&&(t.active=!1,this.parentElement.removeChild(t),s.active=!0,s.$emit(j,{x:s.x,y:s.y,width:s.width,height:s.height}))}else this.$clear()}}$handleActionStart(t){var e,i;const s=null===(i=null===(e=t.detail)||void 0===e?void 0:e.relatedEvent)||void 0===i?void 0:i.target;this.$action="",this.$actionStartTarget=s,!this.hidden&&this.multiple&&!this.active&&s===this&&this.parentElement&&(this.$getSelections().forEach((t=>{t.active=!1})),this.active=!0,this.$emit(j,{x:this.x,y:this.y,width:this.width,height:this.height}))}$handleAction(t){const{currentTarget:e,detail:i}=t;if(!e||!i)return;const{relatedEvent:s}=i;let{action:n}=i;if(!n&&this.multiple&&(n=this.$action||(null==s?void 0:s.target.action),this.$action=n),!n||this.hidden&&n!==p||this.multiple&&!this.active&&n!==m)return;const a=i.endX-i.startX,o=i.endY-i.startY,{width:r,height:h}=this;let{aspectRatio:c}=this;switch(!K(c)&&s.shiftKey&&(c=K(r)&&K(h)?r/h:1),n){case p:if(0!==a&&0!==o){const{$canvas:t}=this,s=$t(e);(this.multiple&&!this.hidden?this.$createSelection():this).$change(i.startX-s.left,i.startY-s.top,Math.abs(a),Math.abs(o),c),a<0?o<0?n=A:o>0&&(n=k):a>0&&(o<0?n=S:o>0&&(n=T)),t&&(t.$action=n)}break;case g:this.movable&&(this.dynamic||this.$actionStartTarget&&this.contains(this.$actionStartTarget))&&this.$move(a,o);break;case m:if(s&&this.zoomable&&(this.dynamic||this.contains(s.target))){const t=$t(e);this.$zoom(i.scale,s.pageX-t.left,s.pageY-t.top)}break;default:this.$resize(n,a,o,c)}}$handleActionEnd(){this.$action="",this.$actionStartTarget=null}$handleKeyDown(t){if(this.hidden||!this.keyboard||this.multiple&&!this.active||t.defaultPrevented)return;const{activeElement:e}=document;if(!e||!["INPUT","TEXTAREA"].includes(e.tagName)&&!["true","plaintext-only"].includes(e.contentEditable))switch(t.key){case"Backspace":t.metaKey&&(t.preventDefault(),this.$removeSelection());break;case"Delete":t.preventDefault(),this.$removeSelection();break;case"ArrowLeft":t.preventDefault(),this.$move(-1,0);break;case"ArrowRight":t.preventDefault(),this.$move(1,0);break;case"ArrowUp":t.preventDefault(),this.$move(0,-1);break;case"ArrowDown":t.preventDefault(),this.$move(0,1);break;case"+":t.preventDefault(),this.$zoom(.1);break;case"-":t.preventDefault(),this.$zoom(-.1)}}$center(){const{parentElement:t}=this;if(!t)return this;const e=(t.offsetWidth-this.width)/2,i=(t.offsetHeight-this.height)/2;return this.$change(e,i)}$move(t,e=t){return this.$moveTo(this.x+t,this.y+e)}$moveTo(t,e=t){return this.movable?this.$change(t,e):this}$resize(t,e=0,i=0,s=this.aspectRatio){if(!this.resizable)return this;const n=K(s),{$canvas:a}=this;let{x:o,y:r,width:h,height:c}=this;switch(t){case C:r+=i,c-=i,c<0&&(t=y,c=-c,r-=c),n&&(o+=(e=i*s)/2,h-=e,h<0&&(h=-h,o-=h));break;case w:h+=e,h<0&&(t=E,h=-h,o-=h),n&&(r-=(i=e/s)/2,c+=i,c<0&&(c=-c,r-=c));break;case y:c+=i,c<0&&(t=C,c=-c,r-=c),n&&(o-=(e=i*s)/2,h+=e,h<0&&(h=-h,o-=h));break;case E:o+=e,h-=e,h<0&&(t=w,h=-h,o-=h),n&&(r+=(i=e/s)/2,c-=i,c<0&&(c=-c,r-=c));break;case S:n&&(i=-e/s),r+=i,c-=i,h+=e,h<0&&c<0?(t=k,h=-h,c=-c,o-=h,r-=c):h<0?(t=A,h=-h,o-=h):c<0&&(t=T,c=-c,r-=c);break;case A:n&&(i=e/s),o+=e,r+=i,h-=e,c-=i,h<0&&c<0?(t=T,h=-h,c=-c,o-=h,r-=c):h<0?(t=S,h=-h,o-=h):c<0&&(t=k,c=-c,r-=c);break;case T:n&&(i=e/s),h+=e,c+=i,h<0&&c<0?(t=A,h=-h,c=-c,o-=h,r-=c):h<0?(t=k,h=-h,o-=h):c<0&&(t=S,c=-c,r-=c);break;case k:n&&(i=-e/s),o+=e,h-=e,c+=i,h<0&&c<0?(t=S,h=-h,c=-c,o-=h,r-=c):h<0?(t=T,h=-h,o-=h):c<0&&(t=A,c=-c,r-=c)}return a&&a.$setAction(t),this.$change(o,r,h,c)}$zoom(t,e,i){if(!this.zoomable||0===t)return this;t<0?t=1/(1-t):t+=1;const{width:s,height:n}=this,a=s*t,o=n*t;let r=this.x,h=this.y;return B(e)&&B(i)?(r-=(a-s)*((e-this.x)/s),h-=(o-n)*((i-this.y)/n)):(r-=(a-s)/2,h-=(o-n)/2),this.$change(r,h,a,o)}$change(t,e,i=this.width,s=this.height,n=this.aspectRatio,a=!1){return this.$changing||!B(t)||!B(e)||!B(i)||!B(s)||i<0||s<0?this:(K(n)&&({width:i,height:s}=bt({aspectRatio:n,width:i,height:s},"cover")),this.precise||(t=Math.round(t),e=Math.round(e),i=Math.round(i),s=Math.round(s)),t===this.x&&e===this.y&&i===this.width&&s===this.height&&Object.is(n,this.aspectRatio)&&!a?this:(this.hidden&&(this.hidden=!1),!1===this.$emit(j,{x:t,y:e,width:i,height:s})?this:(this.$changing=!0,this.x=t,this.y=e,this.width=i,this.height=s,this.$changing=!1,this.$render())))}$reset(){const{x:t,y:e,width:i,height:s}=this.$initialSelection;return this.$change(t,e,i,s)}$clear(){return this.$change(0,0,0,0,NaN,!0),this.hidden=!0,this}$render(){return this.$setStyles({transform:`translate(${this.x}px, ${this.y}px)`,width:this.width,height:this.height})}$toCanvas(t){return new Promise(((e,i)=>{if(!this.isConnected)return void i(new Error("The current element is not connected to the DOM."));const s=document.createElement("canvas");let{width:n,height:a}=this,o=1;if(J(t)&&(K(t.width)||K(t.height))&&(({width:n,height:a}=bt({aspectRatio:n/a,width:t.width,height:t.height})),o=n/this.width),s.width=n,s.height=a,!this.$canvas)return void e(s);const r=this.$canvas.querySelector(this.$getTagNameOf(l));r?r.$ready().then((i=>{const h=s.getContext("2d");if(h){const[e,c,l,d,u,$]=r.$getTransform(),p=-this.x,g=-this.y,m=(p*d-l*g)/(e*d-l*c),b=(g*e-c*p)/(e*d-l*c);let f=e*m+l*b+u,v=c*m+d*b+$,C=i.naturalWidth,w=i.naturalHeight;1!==o&&(f*=o,v*=o,C*=o,w*=o);const y=C/2,E=w/2;h.fillStyle="transparent",h.fillRect(0,0,n,a),J(t)&&Q(t.beforeDraw)&&t.beforeDraw.call(this,h,s),h.save(),h.translate(y,E),h.transform(e,c,l,d,f,v),h.translate(-y,-E),h.drawImage(i,0,0,C,w),h.restore()}e(s)})).catch(i):e(s)}))}}Mt.$name=d,Mt.$version="2.0.0-rc.2";class Pt extends At{constructor(){super(...arguments),this.$style=":host{display:flex;flex-direction:column;position:relative;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([bordered]){border:1px dashed var(--theme-color)}:host([covered]){bottom:0;left:0;position:absolute;right:0;top:0}:host>span{display:flex;flex:1}:host>span+span{border-top:1px dashed var(--theme-color)}:host>span>span{flex:1}:host>span>span+span{border-left:1px dashed var(--theme-color)}",this.bordered=!1,this.columns=3,this.covered=!1,this.rows=3,this.slottable=!1,this.themeColor="rgba(238, 238, 238, 0.5)"}static get observedAttributes(){return super.observedAttributes.concat(["bordered","columns","covered","rows"])}$propertyChangedCallback(t,e,i){Object.is(i,e)||(super.$propertyChangedCallback(t,e,i),"rows"!==t&&"columns"!==t||this.$nextTick((()=>{this.$render()})))}connectedCallback(){super.connectedCallback(),this.$render()}$render(){const t=this.$getShadowRoot(),e=document.createDocumentFragment();for(let t=0;t{setTimeout((()=>{this.$render()}),50)})))}$handleSourceImageTransform(t){this.$render(void 0,t.detail.matrix)}$render(t,e){const{$canvas:i,$selection:s}=this;t||s.hidden||(t=s),(!t||0===t.x&&0===t.y&&0===t.width&&0===t.height)&&(t={x:0,y:0,width:i.offsetWidth,height:i.offsetHeight});const{x:n,y:a,width:o,height:r}=t,h={},{clientWidth:c,clientHeight:l}=this;let d=c,u=l,$=NaN;switch(this.resize){case"both":$=1,d=o,u=r,h.width=o,h.height=r;break;case"horizontal":$=r>0?l/r:0,d=o*$,h.width=d;break;case Xt:$=o>0?c/o:0,u=r*$,h.height=u;break;default:c>0?$=o>0?c/o:0:l>0&&($=r>0?l/r:0)}this.$scale=$,this.$setStyles(h),this.$sourceImage&&this.$transformImageByOffset(null!=e?e:this.$sourceImage.$getTransform(),-n,-a)}$transformImageByOffset(t,e,i){const{$image:s,$scale:n,$sourceImage:a}=this;if(a&&s&&n>=0){const[a,o,r,h,c,l]=t,d=(e*h-r*i)/(a*h-r*o),u=(i*a-o*e)/(a*h-r*o),$=a*d+r*u+c,p=o*d+h*u+l;s.$ready((t=>{this.$setStyles.call(s,{width:t.naturalWidth*n,height:t.naturalHeight*n})})),s.$setTransform(a,o,r,h,$*n,p*n)}}}Ht.$name=$,Ht.$version="2.0.0-rc.2";var jt='';const Vt=/^img|canvas$/,Ut=/<(\/?(?:script|style)[^>]*)>/gi,qt={template:jt};Tt.$define(),Dt.$define(),Pt.$define(),Rt.$define(),Ot.$define(),Mt.$define(),It.$define(),Ht.$define();class Bt{constructor(t,e){if(this.options=qt,U(t)&&(t=document.querySelector(t)),!tt(t)||!Vt.test(t.localName))throw new Error("The first argument is required and must be an or element.");this.element=t,e=Object.assign(Object.assign({},qt),e),this.options=e;const{ownerDocument:i}=t;let{container:s}=e;if(s&&(U(s)&&(s=i.querySelector(s)),!tt(s)))throw new Error("The `container` option must be an element or a valid selector.");tt(s)||(s=t.parentElement?t.parentElement:i.body),this.container=s;const n=t.localName;let a="";"img"===n?({src:a}=t):"canvas"===n&&window.HTMLCanvasElement&&(a=t.toDataURL());const{template:o}=e;if(o&&U(o)){const e=document.createElement("template"),i=document.createDocumentFragment();e.innerHTML=o.replace(Ut,"<$1>"),i.appendChild(e.content),Array.from(i.querySelectorAll(l)).forEach((e=>{e.setAttribute("src",a),e.setAttribute("alt",t.alt||"The image to crop")})),t.parentElement?(t.style.display="none",s.insertBefore(i,t.nextSibling)):s.appendChild(i)}}getCropperCanvas(){return this.container.querySelector(o)}getCropperImage(){return this.container.querySelector(l)}getCropperSelection(){return this.container.querySelector(d)}getCropperSelections(){return this.container.querySelectorAll(d)}}Bt.version="2.0.0-rc.2",t.ACTION_MOVE=g,t.ACTION_NONE=v,t.ACTION_RESIZE_EAST=w,t.ACTION_RESIZE_NORTH=C,t.ACTION_RESIZE_NORTHEAST=S,t.ACTION_RESIZE_NORTHWEST=A,t.ACTION_RESIZE_SOUTH=y,t.ACTION_RESIZE_SOUTHEAST=T,t.ACTION_RESIZE_SOUTHWEST=k,t.ACTION_RESIZE_WEST=E,t.ACTION_ROTATE=b,t.ACTION_SCALE=m,t.ACTION_SELECT=p,t.ACTION_TRANSFORM=f,t.ATTRIBUTE_ACTION=x,t.CROPPER_CANVAS=o,t.CROPPER_CROSSHAIR=r,t.CROPPER_GIRD=h,t.CROPPER_HANDLE=c,t.CROPPER_IMAGE=l,t.CROPPER_SELECTION=d,t.CROPPER_SHADE=u,t.CROPPER_VIEWER=$,t.CropperCanvas=Tt,t.CropperCrosshair=Dt,t.CropperElement=At,t.CropperGrid=Pt,t.CropperHandle=Rt,t.CropperImage=Ot,t.CropperSelection=Mt,t.CropperShade=It,t.CropperViewer=Ht,t.DEFAULT_TEMPLATE=jt,t.EVENT_ACTION=Y,t.EVENT_ACTION_END=L,t.EVENT_ACTION_MOVE=X,t.EVENT_ACTION_START=H,t.EVENT_CHANGE=j,t.EVENT_ERROR=P,t.EVENT_KEYDOWN=D,t.EVENT_LOAD=_,t.EVENT_POINTER_DOWN=R,t.EVENT_POINTER_MOVE=z,t.EVENT_POINTER_UP=M,t.EVENT_RESIZE="resize",t.EVENT_TOUCH_END=O,t.EVENT_TOUCH_MOVE=N,t.EVENT_TOUCH_START=I,t.EVENT_TRANSFORM=V,t.EVENT_WHEEL=W,t.HAS_POINTER_EVENT=n,t.IS_BROWSER=e,t.IS_TOUCH_DEVICE=s,t.NAMESPACE=a,t.WINDOW=i,t.default=Bt,t.emit=lt,t.getAdjustedSizes=bt,t.getOffset=$t,t.isElement=tt,t.isFunction=Q,t.isNaN=q,t.isNumber=B,t.isObject=F,t.isPlainObject=J,t.isPositiveNumber=K,t.isString=U,t.isUndefined=Z,t.multiplyMatrices=ft,t.nextTick=ut,t.off=ot,t.on=rt,t.once=ht,t.toAngleInRadian=gt,t.toCamelCase=nt,t.toKebabCase=it,Object.defineProperty(t,"__esModule",{value:!0})})); From 5a38babc85bb20f9f13d79daf6a6bd11fe8e0141 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 10:34:48 +0100 Subject: [PATCH 14/35] Run tsc --- .../files/js/WoltLabSuite/Core/Component/Image/Cropper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index c59d3ccf5c..14f3dd227f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -200,7 +200,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.cropperImage.$center("contain"); this.cropperSelection.$reset(); }); - // Limit the selection to the canvas boundaries + // Limit the selection to the min/max size this.cropperSelection.addEventListener("change", (event) => { const selection = event.detail; if (selection.width < this.minSize.width || From b88331c37d8ca69b7ffd543f538ea796ba8412d9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 13:19:48 +0100 Subject: [PATCH 15/35] Add since information --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 9 +++++++++ .../lib/system/file/processor/IFileProcessor.class.php | 2 ++ .../lib/system/file/processor/ImageCropSize.class.php | 6 ++++++ .../file/processor/ImageCropperConfiguration.class.php | 8 ++++++++ .../lib/system/file/processor/ImageCropperType.class.php | 6 ++++++ 5 files changed, 31 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 4349c010f2..d5a305530f 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -1,3 +1,12 @@ +/** + * An image cropper that allows the user to crop an image before uploading it. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ + import ImageResizer from "WoltLabSuite/Core/Image/Resizer"; import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; import Cropper, { CropperCanvas, CropperImage, CropperSelection } from "cropperjs"; diff --git a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php index 2682d30570..7bffa60c92 100644 --- a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php @@ -159,6 +159,8 @@ public function trackDownload(File $file): void; /** * Returns the image cropper configuration for this file processor. + * + * @since 6.2 */ public function getImageCropperConfiguration(): ?ImageCropperConfiguration; } diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php index b71b12637c..f26c9d1ba2 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropSize.class.php @@ -2,6 +2,12 @@ namespace wcf\system\file\processor; +/** + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ final class ImageCropSize implements \JsonSerializable { public function __construct( diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php index 249edb2705..cfe5c7205b 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -2,6 +2,14 @@ namespace wcf\system\file\processor; +/** + * The configuration for the image cropper. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ final class ImageCropperConfiguration implements \JsonSerializable { public readonly float $aspectRatio; diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php index bf74e95082..3df5db1847 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperType.class.php @@ -2,6 +2,12 @@ namespace wcf\system\file\processor; +/** + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ enum ImageCropperType { case MinMax; From 648fa4853b7ceb588decf7a3bd65f9f47e927766 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 6 Nov 2024 13:20:55 +0100 Subject: [PATCH 16/35] Run `tsc` --- .../files/js/WoltLabSuite/Core/Component/Image/Cropper.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 14f3dd227f..2543eda160 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -1,3 +1,11 @@ +/** + * An image cropper that allows the user to crop an image before uploading it. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); From 2792414e61bec9d0adc0704bfec19048c9f92dbc Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 29 Nov 2024 09:42:13 +0100 Subject: [PATCH 17/35] Implement `FileRuntimeCache`, which also loads the thumbnails --- .../data/attachment/AttachmentList.class.php | 17 ++------- .../files/lib/data/file/FileEditor.class.php | 9 +---- .../files/lib/data/file/FileList.class.php | 24 +++++++++++++ .../cache/runtime/FileRuntimeCache.class.php | 35 +++++++++++++++++++ 4 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/cache/runtime/FileRuntimeCache.class.php diff --git a/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php b/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php index f4b35143c1..77636895d6 100644 --- a/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php +++ b/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php @@ -3,8 +3,7 @@ namespace wcf\data\attachment; use wcf\data\DatabaseObjectList; -use wcf\data\file\FileList; -use wcf\data\file\thumbnail\FileThumbnailList; +use wcf\system\cache\runtime\FileRuntimeCache; /** * Represents a list of attachments. @@ -51,20 +50,10 @@ private function loadFiles(): void return; } - $fileList = new FileList(); - $fileList->getConditionBuilder()->add("fileID IN (?)", [$fileIDs]); - $fileList->readObjects(); - $files = $fileList->getObjects(); - - $thumbnailList = new FileThumbnailList(); - $thumbnailList->getConditionBuilder()->add("fileID IN (?)", [$fileIDs]); - $thumbnailList->readObjects(); - foreach ($thumbnailList as $thumbnail) { - $files[$thumbnail->fileID]->addThumbnail($thumbnail); - } + FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); foreach ($this->objects as $attachment) { - $file = $files[$attachment->fileID] ?? null; + $file = FileRuntimeCache::getInstance()->getObject($attachment->fileID) ?? null; if ($file !== null) { $attachment->setFile($file); } diff --git a/wcfsetup/install/files/lib/data/file/FileEditor.class.php b/wcfsetup/install/files/lib/data/file/FileEditor.class.php index 8318937018..50564a6e8d 100644 --- a/wcfsetup/install/files/lib/data/file/FileEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/FileEditor.class.php @@ -5,7 +5,6 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\file\temporary\FileTemporary; use wcf\data\file\thumbnail\FileThumbnailEditor; -use wcf\data\file\thumbnail\FileThumbnailList; use wcf\system\file\processor\FileProcessor; use wcf\system\image\ImageHandler; use wcf\util\ExifUtil; @@ -41,6 +40,7 @@ public function deleteFiles(): void public static function deleteAll(array $objectIDs = []) { $fileList = new FileList(); + $fileList->loadThumbnails = true; $fileList->getConditionBuilder()->add("fileID IN (?)", [$objectIDs]); $fileList->readObjects(); $files = $fileList->getObjects(); @@ -48,13 +48,6 @@ public static function deleteAll(array $objectIDs = []) return 0; } - $thumbnailList = new FileThumbnailList(); - $thumbnailList->getConditionBuilder()->add("fileID IN (?)", [$objectIDs]); - $thumbnailList->readObjects(); - foreach ($thumbnailList as $thumbnail) { - $files[$thumbnail->fileID]->addThumbnail($thumbnail); - } - foreach ($files as $file) { (new FileEditor($file))->deleteFiles(); } diff --git a/wcfsetup/install/files/lib/data/file/FileList.class.php b/wcfsetup/install/files/lib/data/file/FileList.class.php index 9dca494aef..22b16c0a77 100644 --- a/wcfsetup/install/files/lib/data/file/FileList.class.php +++ b/wcfsetup/install/files/lib/data/file/FileList.class.php @@ -3,6 +3,7 @@ namespace wcf\data\file; use wcf\data\DatabaseObjectList; +use wcf\data\file\thumbnail\FileThumbnailList; /** * @author Alexander Ebert @@ -18,4 +19,27 @@ class FileList extends DatabaseObjectList { public $className = File::class; + public bool $loadThumbnails = false; + + #[\Override] + public function readObjects() + { + parent::readObjects(); + + $this->loadThumbnails(); + } + + public function loadThumbnails(): void + { + if (!$this->loadThumbnails || $this->getObjectIDs() === []) { + return; + } + + $thumbnailList = new FileThumbnailList(); + $thumbnailList->getConditionBuilder()->add("fileID IN (?)", [$this->getObjectIDs()]); + $thumbnailList->readObjects(); + foreach ($thumbnailList as $thumbnail) { + $this->objects[$thumbnail->fileID]->addThumbnail($thumbnail); + } + } } diff --git a/wcfsetup/install/files/lib/system/cache/runtime/FileRuntimeCache.class.php b/wcfsetup/install/files/lib/system/cache/runtime/FileRuntimeCache.class.php new file mode 100644 index 0000000000..d08713019a --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/runtime/FileRuntimeCache.class.php @@ -0,0 +1,35 @@ + + * @since 6.2 + * + * @method File[] getCachedObjects() + * @method File|null getObject($objectID) + * @method File[] getObjects(array $objectIDs) + */ +class FileRuntimeCache extends AbstractRuntimeCache +{ + /** + * @inheritDoc + */ + protected $listClassName = FileList::class; + + #[\Override] + protected function getObjectList() + { + $fileList = new FileList(); + $fileList->loadThumbnails = true; + + return $fileList; + } +} From 9dc5693eceeb18d78c393dbc214a95716e578b77 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 29 Nov 2024 13:19:23 +0100 Subject: [PATCH 18/35] Add `exifreader` --- .github/workflows/javascript.yml | 3 + package-lock.json | 21 +++++ package.json | 11 +++ .../Core/Component/Image/Cropper.ts | 94 ++++++++++++++----- .../install/files/js/3rdParty/exif-reader.js | 2 + .../Core/Component/Image/Cropper.js | 78 +++++++++++---- 6 files changed, 166 insertions(+), 43 deletions(-) create mode 100644 wcfsetup/install/files/js/3rdParty/exif-reader.js diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index de632bffcf..19c11cd78d 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -74,3 +74,6 @@ jobs: - name: "Check 'cropperjs'" run: | diff -wu wcfsetup/install/files/js/3rdParty/cropper.min.js node_modules/cropperjs/dist/cropper.min.js + - name: "Check 'exifreader'" + run: | + diff -wu wcfsetup/install/files/js/3rdParty/exif-reader.js node_modules/exifreader/dist/exif-reader.js diff --git a/package-lock.json b/package-lock.json index a8628b9d68..ef80fae99e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#5b582b24e437f1883ccad3c37dae7c3c5f1e7da3", "cropperjs": "2.0.0-rc.2", "emoji-picker-element": "^1.23.0", + "exifreader": "^4.25.0", "focus-trap": "^7.6.1", "html-parsed-element": "^0.4.1", "perfect-scrollbar": "^1.5.6", @@ -2198,6 +2199,16 @@ "@types/zxcvbn": "^4.4.1" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.5.tgz", + "integrity": "sha512-6g1EwSs8cr8JhP1iBxzyVAWM6BIDvx9Y3FZRIQiMDzgG43Pxi8YkWOZ0nQj2NHgNzgXDZbJewFx/n+YAvMZrfg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.6" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -2908,6 +2919,16 @@ "node": ">=0.8.x" } }, + "node_modules/exifreader": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.25.0.tgz", + "integrity": "sha512-lPyPXWTUuYgoKdKf3rw2EDoE9Zl7xHoy/ehPNeQ4gFVNLzfLyNMP4oEI+sP0/Czp5r/2i7cFhqg5MHsl4FYtyw==", + "hasInstallScript": true, + "license": "MPL-2.0", + "optionalDependencies": { + "@xmldom/xmldom": "^0.9.4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index ab8d4762bf..7af19efd29 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#5b582b24e437f1883ccad3c37dae7c3c5f1e7da3", "cropperjs": "2.0.0-rc.2", "emoji-picker-element": "^1.23.0", + "exifreader": "^4.25.0", "focus-trap": "^7.6.1", "html-parsed-element": "^0.4.1", "perfect-scrollbar": "^1.5.6", @@ -37,5 +38,15 @@ "tabbable": "^6.2.0", "tslib": "^2.8.1", "webpack-cli": "^5.1.4" + }, + "exifreader": { + "include": { + "jpeg": true, + "png": true, + "webp": true, + "exif": [ + "Orientation" + ] + } } } diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index d5a305530f..48c54bb115 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -14,6 +14,7 @@ import type { Selection } from "@cropper/element-selection"; import { getPhrase } from "WoltLabSuite/Core/Language"; import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog"; import * as ExifUtil from "WoltLabSuite/Core/Image/ExifUtil"; +import ExifReader from "exifreader"; export interface CropperConfiguration { aspectRatio: number; @@ -35,6 +36,7 @@ abstract class ImageCropper { protected cropperSelection?: CropperSelection | null; protected dialog?: WoltlabCoreDialogElement; protected exif?: ExifUtil.Exif; + protected orientation?: number; #cropper?: Cropper; constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { @@ -44,6 +46,26 @@ abstract class ImageCropper { this.resizer = new ImageResizer(); } + protected get width() { + switch (this.orientation) { + case 90: + case 270: + return this.image!.height; + default: + return this.image!.width; + } + } + + protected get height() { + switch (this.orientation) { + case 90: + case 270: + return this.image!.width; + default: + return this.image!.height; + } + } + public async showDialog(): Promise { this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ extra: this.getDialogExtra(), @@ -57,7 +79,11 @@ abstract class ImageCropper { this.cropperSelection!.$toCanvas() .then((canvas) => { this.resizer - .saveFile({ exif: this.exif, image: canvas }, this.file.name, this.file.type) + .saveFile( + { exif: this.orientation ? undefined : this.exif, image: canvas }, + this.file.name, + this.file.type, + ) .then((resizedFile) => { resolve(resizedFile); }) @@ -76,28 +102,43 @@ abstract class ImageCropper { const { image, exif } = await this.resizer.loadFile(this.file); this.image = image; this.exif = exif; + const tags = await ExifReader.load(this.file); + if (tags.Orientation) { + switch (tags.Orientation.value) { + case 3: + this.orientation = 180; + break; + case 6: + this.orientation = 90; + break; + case 8: + this.orientation = 270; + break; + // Any other rotation is unsupported. + } + } + } + + protected abstract getCropperTemplate(): string; + + protected getDialogExtra(): string | undefined { + return undefined; } protected setCropperStyle() { - this.cropperCanvas!.style.aspectRatio = `${this.image!.width}/${this.image!.height}`; + this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`; - if (this.image!.width > this.image!.height) { - this.cropperCanvas!.style.width = `min(70vw, ${this.image!.width}px)`; + if (this.width > this.height) { + this.cropperCanvas!.style.width = `min(70vw, ${this.width}px)`; this.cropperCanvas!.style.height = "auto"; } else { - this.cropperCanvas!.style.height = `min(60vh, ${this.image!.height}px)`; + this.cropperCanvas!.style.height = `min(60vh, ${this.height}px)`; this.cropperCanvas!.style.width = "auto"; } this.cropperSelection!.aspectRatio = this.configuration.aspectRatio; } - protected abstract getCropperTemplate(): string; - - protected getDialogExtra(): string | undefined { - return undefined; - } - protected createCropper() { this.#cropper = new Cropper(this.image!, { template: this.getCropperTemplate(), @@ -109,6 +150,9 @@ abstract class ImageCropper { this.setCropperStyle(); + if (this.orientation) { + this.cropperImage!.$rotate(`${this.orientation}deg`); + } this.cropperImage!.$center("contain"); this.cropperSelection!.$center(); @@ -143,11 +187,15 @@ class ExactImageCropper extends ImageCropper { public async showDialog(): Promise { // The image already has the correct size, cropping is not necessary if ( - this.image!.width == this.#size!.width && - this.image!.height == this.#size!.height && + this.width == this.#size!.width && + this.height == this.#size!.height && this.image instanceof HTMLCanvasElement ) { - return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); + return this.resizer.saveFile( + { exif: this.orientation ? undefined : this.exif, image: this.image }, + this.file.name, + this.file.type, + ); } return super.showDialog(); @@ -162,7 +210,7 @@ class ExactImageCropper extends ImageCropper { // resize image to the largest possible size const sizes = this.configuration.sizes.filter((size) => { - return size.width <= this.image!.width && size.height <= this.image!.height; + return size.width <= this.width && size.height <= this.height; }); if (sizes.length === 0) { @@ -179,8 +227,8 @@ class ExactImageCropper extends ImageCropper { this.#size = sizes[sizes.length - 1]; this.image = await this.resizer.resize( this.image as HTMLImageElement, - this.image!.width >= this.image!.height ? this.image!.width : this.#size.width, - this.image!.height > this.image!.width ? this.image!.height : this.#size.height, + this.width >= this.height ? this.width : this.#size.width, + this.height > this.width ? this.height : this.#size.height, this.resizer.quality, true, timeout, @@ -190,7 +238,7 @@ class ExactImageCropper extends ImageCropper { protected getCropperTemplate(): string { return `
- + @@ -207,8 +255,8 @@ class ExactImageCropper extends ImageCropper { this.cropperSelection!.width = this.#size!.width; this.cropperSelection!.height = this.#size!.height; - this.cropperCanvas!.style.width = `${this.image!.width}px`; - this.cropperCanvas!.style.height = `${this.image!.height}px`; + this.cropperCanvas!.style.width = `${this.width}px`; + this.cropperCanvas!.style.height = `${this.height}px`; this.cropperSelection!.style.removeProperty("aspectRatio"); } } @@ -236,7 +284,7 @@ class MinMaxImageCropper extends ImageCropper { protected getCropperTemplate(): string { return `
- + @@ -261,8 +309,8 @@ class MinMaxImageCropper extends ImageCropper { this.cropperSelection!.width = this.minSize.width; this.cropperSelection!.height = this.minSize.height; - this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.image!.width}px)`; - this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.image!.height}px)`; + this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; + this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; } protected createCropper() { diff --git a/wcfsetup/install/files/js/3rdParty/exif-reader.js b/wcfsetup/install/files/js/3rdParty/exif-reader.js new file mode 100644 index 0000000000..01f41d181a --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/exif-reader.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ExifReader=t():e.ExifReader=t()}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={d:function(t,n){for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:1,get:n[r]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r:function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:1})}},t={};function n(e,t,n){for(var i=[],o=0;o=T&&n<=x||n===P||n===y||n===h||n===m||n===b||n===A||n===S}function O(e,t){return e.getUint16(t)===U}var k="‰PNG\r\n\n",B=4,C=4,_=0,D=B,R=B+C,j="tEXt",M="iTXt",N="zTXt",G="pHYs",z="tIME",F="eXIf";function V(e,t,r){var i=n(e,t+D,C);return i===j||i===M||i===N&&r}function H(e,t){return n(e,t+D,C)===F}function X(e,t){var r=[G,z],i=n(e,t+D,C);return r.includes(i)}var Y={parseAppMarkers:function(e,t){if(function(e){return!!e&&e.byteLength>=s&&e.getUint16(0)===d}(e))return W(function(e){for(var t,n,r=p;r+g+5<=e.byteLength;){if(L(e,r))t=e.getUint16(r+l),n=r+v;else{if(!I(e,r)){if(O(e,r)){r++;continue}break}t=e.getUint16(r+l)}r+=l+t}return{hasAppMarkers:r>p,fileDataOffset:void 0,jfifDataOffset:void 0,tiffHeaderOffset:n,iptcDataOffset:void 0,xmpChunks:void 0,iccChunks:void 0,mpfDataOffset:void 0}}(e),"jpeg","JPEG");if(function(e){return!!e&&n(e,0,k.length)===k}(e))return W(function(e,t){for(var r={hasAppMarkers:0},i=k.length;i+B+C<=e.byteLength;){if(V(e,i,t)){r.hasAppMarkers=1;var o=n(e,i+D,C);r.pngTextChunks||(r.pngTextChunks=[]),r.pngTextChunks.push({length:e.getUint32(i+_),type:o,offset:i+R})}else H(e,i)?(r.hasAppMarkers=1,r.tiffHeaderOffset=i+R):X(e,i)&&(r.hasAppMarkers=1,r.pngChunkOffsets||(r.pngChunkOffsets=[]),r.pngChunkOffsets.push(i+_));i+=e.getUint32(i+_)+B+C+4}return r}(e,t),"png","PNG");if(function(e){return!!e&&"RIFF"===n(e,0,4)&&"WEBP"===n(e,8,4)}(e))return W(function(e){for(var t,r,i=12,o=0;i+8e.byteLength);c++){var s=de(e,t,n,r,i,o);void 0!==s&&(u[s.name]={id:s.id,value:s.value,description:s.description},"MakerNote"===s.name&&(u[s.name].__offset=s.__offset)),r+=12}return u}function de(e,t,n,r,i,o){var f,u,a=oe.getTypeSize("SHORT"),c=a+oe.getTypeSize("SHORT"),s=c+oe.getTypeSize("LONG"),d=oe.getShortAt(e,r,i),p=oe.getShortAt(e,r+a,i),g=oe.getLongAt(e,r+c,i);if(void 0!==oe.typeSizes[p]&&(o||void 0!==ne[t][d])){f=function(e,t){return oe.typeSizes[e]*t<=oe.getTypeSize("LONG")}(p,g)?pe(e,u=r+s,p,g,i):function(e,t,n,r,i){return t+n+oe.typeSizes[r]*i<=e.byteLength}(e,n,u=oe.getLongAt(e,r+s,i),p,g)?pe(e,n+u,p,g,i,33723===d):"",p===oe.tagTypes.ASCII&&(f=function(e){try{return e.map((function(e){return decodeURIComponent(escape(e))}))}catch(t){return e}}(f=function(e){for(var t=[],n=0,r=0;r5&&void 0!==arguments[5]&&arguments[5]&&(r*=oe.typeSizes[n],n=oe.tagTypes.BYTE);for(var f=0;f0?Promise.all(o):void 0}}},xe="STATE_KEYWORD",Pe="STATE_COMPRESSION",Ue="STATE_LANG",Ee="STATE_TRANSLATED_KEYWORD",Le="STATE_TEXT",Ie=1,Oe=1,ke=6;function Be(e,t,n,r,i){for(var f,u=[],a=[],c=[],s=xe,d=o,p=0;p3&&void 0!==arguments[3]?arguments[3]:"string";if(0===t&&"function"==typeof DecompressionStream){var i=new DecompressionStream("deflate"),o=new Blob([e]).stream().pipeThrough(i);return"dataview"===r?new Response(o).arrayBuffer().then((function(e){return new DataView(e)})):new Response(o).arrayBuffer().then((function(e){return new TextDecoder(n).decode(e)}))}return void 0!==t?Promise.reject("Unknown compression method ".concat(t,".")):e}(f,d,function(e){return e===j||e===N?"latin1":"utf-8"}(r));return l instanceof Promise?l.then((function(e){return De(e,r,a,u)})).catch((function(){return De("".split(""),r,a,u)})):De(l,r,a,u)}function Ce(e){var t=e.type,n=e.dataView,r=e.offset;if(t===M){if(n.getUint8(r)===Oe)return n.getUint8(r+1)}else if(t===N)return n.getUint8(r);return o}function _e(e,t){return t===xe&&[M,N].includes(e)?Pe:t===Pe?e===M?Ue:Le:t===Ue?Ee:Le}function De(e,t,r,i){var o=function(e){return e instanceof DataView?n(e,0,e.byteLength):e}(e);return{name:Re(t,r,i),value:o,description:t===M?je(e):o}}function Re(e,t,n){var i=r(n);if(e===j||0===t.length)return i;var o=r(t);return"".concat(i," (").concat(o,")")}function je(e){return Se("UTF-8",e)}function Me(e,t){return"raw profile type exif"===e.toLowerCase()&&"exif"===t.substring(1,5)}function Ne(e,t){return"raw profile type iptc"===e.toLowerCase()&&"iptc"===t.substring(1,5)}function Ge(e){return function(e){for(var t=new DataView(new ArrayBuffer(e.length/2)),n=0;n1&&void 0!==arguments[1]?arguments[1]:{};return function(e){return"string"==typeof e}(e)?(n.async=1,function(e,t){return/^\w+:\/\//.test(e)?"undefined"!=typeof fetch?function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).length,n={method:"GET"};return Number.isInteger(t)&&t>=0&&(n.headers={range:"bytes=0-".concat(t-1)}),fetch(e,n).then((function(e){return e.arrayBuffer()}))}(e,t):function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).length;return new Promise((function(n,r){var i={};Number.isInteger(t)&&t>=0&&(i.headers={range:"bytes=0-".concat(t-1)});var o=function(e){return/^https:\/\//.test(e)?require("https").get:require("http").get}(e);o(e,i,(function(e){if(e.statusCode>=200&&e.statusCode<=299){var t=[];e.on("data",(function(e){return t.push(Buffer.from(e))})),e.on("error",(function(e){return r(e)})),e.on("end",(function(){return n(Buffer.concat(t))}))}else r("Could not fetch file: ".concat(e.statusCode," ").concat(e.statusMessage)),e.resume()})).on("error",(function(e){return r(e)}))}))}(e,t):function(e){return/^data:[^;,]*(;base64)?,/.test(e)}(e)?Promise.resolve(function(e){var t=e.substring(e.indexOf(",")+1);if(-1!==e.indexOf(";base64")){if("undefined"!=typeof atob)return Uint8Array.from(atob(t),(function(e){return e.charCodeAt(0)})).buffer;if("undefined"==typeof Buffer)return;return"undefined"!=typeof Buffer.from?Buffer.from(t,"base64"):new Buffer(t,"base64")}var n=decodeURIComponent(t);return"undefined"!=typeof Buffer?"undefined"!=typeof Buffer.from?Buffer.from(n):new Buffer(n):Uint8Array.from(n,(function(e){return e.charCodeAt(0)})).buffer}(e)):function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).length;return new Promise((function(n,r){var i=function(){try{return require("fs")}catch(e){return}}();i.open(e,(function(o,f){o?r(o):i.stat(e,(function(o,u){if(o)r(o);else{var a=Math.min(u.size,void 0!==t?t:u.size),c=Buffer.alloc(a),s={buffer:c,length:a};i.read(f,s,(function(t){t?r(t):i.close(f,(function(t){t&&console.warn("Could not close file ".concat(e,":"),t),n(c)}))}))}}))}))}))}(e,t)}(e,n).then((function(e){return rt(e,n)}))):function(e){return"undefined"!=typeof window&&"undefined"!=typeof File&&e instanceof File}(e)?(n.async=1,(t=e,new Promise((function(e,n){var r=new FileReader;r.onload=function(t){return e(t.target.result)},r.onerror=function(){return n(r.error)},r.readAsArrayBuffer(t)}))).then((function(e){return rt(e,n)}))):rt(e,n)}function rt(e,t){return function(e){try{return Buffer.isBuffer(e)}catch(e){return 0}}(e)&&(e=new Uint8Array(e).buffer),it(function(e){try{return new DataView(e)}catch(t){return new a(e)}}(e),t)}function it(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{expanded:0,async:0,includeUnknown:0},n=t.expanded,r=void 0===n?0:n,o=t.async,f=void 0===o?0:o,u=t.includeUnknown,a=void 0===u?0:u,s=0,d={},p=[],g=Y.parseAppMarkers(e,f),l=g.fileType,v=(g.fileDataOffset,g.jfifDataOffset,g.tiffHeaderOffset),y=(g.iptcDataOffset,g.xmpChunks,g.iccChunks,g.mpfDataOffset,g.pngHeaderOffset,g.pngTextChunks),h=g.pngChunkOffsets,m=g.vp8xChunkOffset;if(g.gifHeaderOffset,function(e){return void 0!==e}(v)){s=1;var b=he.read(e,v,a),A=b.tags;b.byteOrder,A.Thumbnail&&(d.Thumbnail=A.Thumbnail,delete A.Thumbnail),r?(d.exif=A,function(e){if(e.exif){if(e.exif.GPSLatitude&&e.exif.GPSLatitudeRef)try{e.gps=e.gps||{},e.gps.Latitude=c(e.exif.GPSLatitude.value),"S"===e.exif.GPSLatitudeRef.value.join("")&&(e.gps.Latitude=-e.gps.Latitude)}catch(e){}if(e.exif.GPSLongitude&&e.exif.GPSLongitudeRef)try{e.gps=e.gps||{},e.gps.Longitude=c(e.exif.GPSLongitude.value),"W"===e.exif.GPSLongitudeRef.value.join("")&&(e.gps.Longitude=-e.gps.Longitude)}catch(e){}if(e.exif.GPSAltitude&&e.exif.GPSAltitudeRef)try{e.gps=e.gps||{},e.gps.Altitude=e.exif.GPSAltitude.value[0]/e.exif.GPSAltitude.value[1],1===e.exif.GPSAltitudeRef.value&&(e.gps.Altitude=-e.gps.Altitude)}catch(e){}}}(d)):d=i({},d,A),A.MakerNote&&delete A.MakerNote.__offset}if(function(e){return void 0!==e}(y)){s=1;var S=we.read(e,y,f,a),T=S.readTags,w=S.readTagsPromise;U(T),w&&p.push(w.then((function(e){return e.forEach(U)})))}if(function(e){return void 0!==e}(h)){s=1;var x=ze.read(e,h);r?d.png=d.png?i({},d.png,x):x:d=i({},d,x)}if(function(e){return void 0!==e}(m)){s=1;var P=qe.read(e,m);r?d.riff=d.riff?i({},d.riff,P):P:d=i({},d,P)}if(delete d.Thumbnail,l&&(r?(d.file||(d.file={}),d.file.FileType=l):d.FileType=l,s=1),!s)throw new Ze.MetadataMissingError;return f?Promise.all(p).then((function(){return d})):d;function U(e){if(r){for(var t=0,n=["exif","iptc"];t * @since 6.2 */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language", "exifreader"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1, exifreader_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.cropImage = cropImage; Resizer_1 = tslib_1.__importDefault(Resizer_1); cropperjs_1 = tslib_1.__importDefault(cropperjs_1); + exifreader_1 = tslib_1.__importDefault(exifreader_1); class ImageCropper { configuration; file; @@ -23,6 +24,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL cropperSelection; dialog; exif; + orientation; #cropper; constructor(element, file, configuration) { this.configuration = configuration; @@ -30,6 +32,24 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.file = file; this.resizer = new Resizer_1.default(); } + get width() { + switch (this.orientation) { + case 90: + case 270: + return this.image.height; + default: + return this.image.width; + } + } + get height() { + switch (this.orientation) { + case 90: + case 270: + return this.image.width; + default: + return this.image.height; + } + } async showDialog() { this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ extra: this.getDialogExtra(), @@ -41,7 +61,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.cropperSelection.$toCanvas() .then((canvas) => { this.resizer - .saveFile({ exif: this.exif, image: canvas }, this.file.name, this.file.type) + .saveFile({ exif: this.orientation ? undefined : this.exif, image: canvas }, this.file.name, this.file.type) .then((resizedFile) => { resolve(resizedFile); }) @@ -59,22 +79,37 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL const { image, exif } = await this.resizer.loadFile(this.file); this.image = image; this.exif = exif; + const tags = await exifreader_1.default.load(this.file); + if (tags.Orientation) { + switch (tags.Orientation.value) { + case 3: + this.orientation = 180; + break; + case 6: + this.orientation = 90; + break; + case 8: + this.orientation = 270; + break; + // Any other rotation is unsupported. + } + } + } + getDialogExtra() { + return undefined; } setCropperStyle() { - this.cropperCanvas.style.aspectRatio = `${this.image.width}/${this.image.height}`; - if (this.image.width > this.image.height) { - this.cropperCanvas.style.width = `min(70vw, ${this.image.width}px)`; + this.cropperCanvas.style.aspectRatio = `${this.width}/${this.height}`; + if (this.width > this.height) { + this.cropperCanvas.style.width = `min(70vw, ${this.width}px)`; this.cropperCanvas.style.height = "auto"; } else { - this.cropperCanvas.style.height = `min(60vh, ${this.image.height}px)`; + this.cropperCanvas.style.height = `min(60vh, ${this.height}px)`; this.cropperCanvas.style.width = "auto"; } this.cropperSelection.aspectRatio = this.configuration.aspectRatio; } - getDialogExtra() { - return undefined; - } createCropper() { this.#cropper = new cropperjs_1.default(this.image, { template: this.getCropperTemplate(), @@ -83,6 +118,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.cropperImage = this.#cropper.getCropperImage(); this.cropperSelection = this.#cropper.getCropperSelection(); this.setCropperStyle(); + if (this.orientation) { + this.cropperImage.$rotate(`${this.orientation}deg`); + } this.cropperImage.$center("contain"); this.cropperSelection.$center(); // Limit the selection to the canvas boundaries @@ -109,10 +147,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL #size; async showDialog() { // The image already has the correct size, cropping is not necessary - if (this.image.width == this.#size.width && - this.image.height == this.#size.height && + if (this.width == this.#size.width && + this.height == this.#size.height && this.image instanceof HTMLCanvasElement) { - return this.resizer.saveFile({ exif: this.exif, image: this.image }, this.file.name, this.file.type); + return this.resizer.saveFile({ exif: this.orientation ? undefined : this.exif, image: this.image }, this.file.name, this.file.type); } return super.showDialog(); } @@ -123,7 +161,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); // resize image to the largest possible size const sizes = this.configuration.sizes.filter((size) => { - return size.width <= this.image.width && size.height <= this.image.height; + return size.width <= this.width && size.height <= this.height; }); if (sizes.length === 0) { const smallestSize = this.configuration.sizes.length > 1 ? this.configuration.sizes[this.configuration.sizes.length - 1] : undefined; @@ -133,12 +171,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL })); } this.#size = sizes[sizes.length - 1]; - this.image = await this.resizer.resize(this.image, this.image.width >= this.image.height ? this.image.width : this.#size.width, this.image.height > this.image.width ? this.image.height : this.#size.height, this.resizer.quality, true, timeout); + this.image = await this.resizer.resize(this.image, this.width >= this.height ? this.width : this.#size.width, this.height > this.width ? this.height : this.#size.height, this.resizer.quality, true, timeout); } getCropperTemplate() { return `
- + @@ -152,8 +190,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL super.setCropperStyle(); this.cropperSelection.width = this.#size.width; this.cropperSelection.height = this.#size.height; - this.cropperCanvas.style.width = `${this.image.width}px`; - this.cropperCanvas.style.height = `${this.image.height}px`; + this.cropperCanvas.style.width = `${this.width}px`; + this.cropperCanvas.style.height = `${this.height}px`; this.cropperSelection.style.removeProperty("aspectRatio"); } } @@ -176,7 +214,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL getCropperTemplate() { return `
- + @@ -199,8 +237,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL super.setCropperStyle(); this.cropperSelection.width = this.minSize.width; this.cropperSelection.height = this.minSize.height; - this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.image.width}px)`; - this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.image.height}px)`; + this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; + this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; } createCropper() { super.createCropper(); From bb9d061cdb324e5f665ef204cd14bc99af55691f Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 29 Nov 2024 13:27:45 +0100 Subject: [PATCH 19/35] Add exifreader to `require.config.js` --- wcfsetup/install/files/js/require.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 26f9c7fb07..0842d35f70 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -19,6 +19,7 @@ requirejs.config({ "diff-match-patch": "3rdParty/diff-match-patch/diff_match_patch.min", "emoji-picker-element": "3rdParty/emoji-picker-element.min", cropperjs: "3rdParty/cropper.min", + exifreader: "3rdParty/exif-reader", }, packages: [ { From e976c8df725f87c6ede52599bad54dd061a2e6e9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 4 Dec 2024 13:39:02 +0100 Subject: [PATCH 20/35] Center the cropper selection in the canvas --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 1 + .../files/js/WoltLabSuite/Core/Component/Image/Cropper.js | 1 + 2 files changed, 2 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 48c54bb115..f556ff997b 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -155,6 +155,7 @@ abstract class ImageCropper { } this.cropperImage!.$center("contain"); this.cropperSelection!.$center(); + this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); // Limit the selection to the canvas boundaries this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 5c375ab647..ab2688dbca 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -123,6 +123,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } this.cropperImage.$center("contain"); this.cropperSelection.$center(); + this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); // Limit the selection to the canvas boundaries this.cropperSelection.addEventListener("change", (event) => { // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries From dcb129b663f204c8c69555a2027bcb14fc0a70ef Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 4 Dec 2024 13:40:17 +0100 Subject: [PATCH 21/35] Change the construct to private --- .../file/processor/ImageCropperConfiguration.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php index cfe5c7205b..8cb30fe084 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -19,7 +19,7 @@ final class ImageCropperConfiguration implements \JsonSerializable */ public readonly array $sizes; - public function __construct( + private function __construct( public readonly ImageCropperType $type, ImageCropSize ...$sizes ) { @@ -61,7 +61,7 @@ public function jsonSerialize(): mixed * The user can freely select, move and scale. * However, the cropping area is limited to `$min` and `$max`. */ - public static function createMinMax(ImageCropSize $min, ImageCropSize $max): self + public static function forMinMax(ImageCropSize $min, ImageCropSize $max): self { return new self(ImageCropperType::MinMax, $min, $max); } @@ -84,7 +84,7 @@ public static function createMinMax(ImageCropSize $min, ImageCropSize $max): sel * - Image is resized to 256x256 * - The image is uploaded directly without displaying the cropping dialog */ - public static function createExact(ImageCropSize ...$sizes): self + public static function forExact(ImageCropSize ...$sizes): self { return new self(ImageCropperType::Exact, ...$sizes); } From 7faa35e24b3cf40532d866c70dec5db8208d8fae Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Wed, 4 Dec 2024 14:29:18 +0100 Subject: [PATCH 22/35] Disable zoom Set default max width and height for the selection --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 7 ++++--- .../files/js/WoltLabSuite/Core/Component/Image/Cropper.js | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index f556ff997b..f85cf2dc8f 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -288,7 +288,7 @@ class MinMaxImageCropper extends ImageCropper { - + @@ -308,8 +308,8 @@ class MinMaxImageCropper extends ImageCropper { protected setCropperStyle() { super.setCropperStyle(); - this.cropperSelection!.width = this.minSize.width; - this.cropperSelection!.height = this.minSize.height; + this.cropperSelection!.width = Math.min(this.width, this.maxSize.width); + this.cropperSelection!.height = Math.min(this.height, this.maxSize.height); this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; } @@ -320,6 +320,7 @@ class MinMaxImageCropper extends ImageCropper { this.dialog!.addEventListener("extra", () => { this.cropperImage!.$center("contain"); this.cropperSelection!.$reset(); + this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); }); // Limit the selection to the min/max size diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index ab2688dbca..8441b7e135 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -218,7 +218,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL - + @@ -236,8 +236,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } setCropperStyle() { super.setCropperStyle(); - this.cropperSelection.width = this.minSize.width; - this.cropperSelection.height = this.minSize.height; + this.cropperSelection.width = Math.min(this.width, this.maxSize.width); + this.cropperSelection.height = Math.min(this.height, this.maxSize.height); this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; } @@ -246,6 +246,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.dialog.addEventListener("extra", () => { this.cropperImage.$center("contain"); this.cropperSelection.$reset(); + this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); }); // Limit the selection to the min/max size this.cropperSelection.addEventListener("change", (event) => { From b140a08497acde836cb69970746b0c5c2de311a3 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 5 Dec 2024 11:37:49 +0100 Subject: [PATCH 23/35] Set the default selection to the maximum width --- .../Core/Component/Image/Cropper.ts | 72 +++++++++++++------ .../Core/Component/Image/Cropper.js | 56 +++++++++------ 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index f85cf2dc8f..7ac8922f28 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -25,6 +25,15 @@ export interface CropperConfiguration { }[]; } +function inSelection(selection: Selection, maxSelection: Selection): boolean { + return ( + selection.x >= maxSelection.x && + selection.y >= maxSelection.y && + selection.x + selection.width <= maxSelection.x + maxSelection.width && + selection.y + selection.height <= maxSelection.y + maxSelection.height + ); +} + abstract class ImageCropper { readonly configuration: CropperConfiguration; readonly file: File; @@ -128,7 +137,7 @@ abstract class ImageCropper { protected setCropperStyle() { this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`; - if (this.width > this.height) { + if (this.width >= this.height) { this.cropperCanvas!.style.width = `min(70vw, ${this.width}px)`; this.cropperCanvas!.style.height = "auto"; } else { @@ -153,9 +162,8 @@ abstract class ImageCropper { if (this.orientation) { this.cropperImage!.$rotate(`${this.orientation}deg`); } - this.cropperImage!.$center("contain"); - this.cropperSelection!.$center(); - this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); + + this.centerSelection(); // Limit the selection to the canvas boundaries this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { @@ -163,23 +171,25 @@ abstract class ImageCropper { const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); const selection = event.detail as Selection; + const cropperImageRect = this.cropperImage!.getBoundingClientRect(); const maxSelection: Selection = { - x: 0, - y: 0, - width: cropperCanvasRect.width, - height: cropperCanvasRect.height, + x: cropperImageRect.left - cropperCanvasRect.left, + y: cropperImageRect.top - cropperCanvasRect.top, + width: cropperImageRect.width, + height: cropperImageRect.height, }; - if ( - selection.x < maxSelection.x || - selection.y < maxSelection.y || - selection.x + selection.width > maxSelection.x + maxSelection.width || - selection.y + selection.height > maxSelection.y + maxSelection.height - ) { + if (!inSelection(selection, maxSelection)) { event.preventDefault(); } }); } + + protected centerSelection(): void { + this.cropperImage!.$center("contain"); + this.cropperSelection!.$center(); + this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); + } } class ExactImageCropper extends ImageCropper { @@ -284,10 +294,10 @@ class MinMaxImageCropper extends ImageCropper { protected getCropperTemplate(): string { return `
- + - + @@ -308,19 +318,18 @@ class MinMaxImageCropper extends ImageCropper { protected setCropperStyle() { super.setCropperStyle(); - this.cropperSelection!.width = Math.min(this.width, this.maxSize.width); - this.cropperSelection!.height = Math.min(this.height, this.maxSize.height); - this.cropperCanvas!.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; - this.cropperCanvas!.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; + if (this.width >= this.height) { + this.cropperCanvas!.style.width = `${Math.min(this.maxSize.width, this.width)}px`; + } else { + this.cropperCanvas!.style.height = `${Math.min(this.maxSize.height, this.height)}px`; + } } protected createCropper() { super.createCropper(); this.dialog!.addEventListener("extra", () => { - this.cropperImage!.$center("contain"); - this.cropperSelection!.$reset(); - this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); + this.centerSelection(); }); // Limit the selection to the min/max size @@ -337,6 +346,23 @@ class MinMaxImageCropper extends ImageCropper { } }); } + + protected centerSelection(): void { + this.cropperImage!.$center("contain"); + + const { width: imageWidth } = this.cropperImage!.getBoundingClientRect(); + + this.cropperSelection!.$change( + 0, + 0, + imageWidth, + 0, + this.configuration.aspectRatio, + true, + ); + this.cropperSelection!.$center(); + this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); + } } export async function cropImage( diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 8441b7e135..86794834c6 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -13,6 +13,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL Resizer_1 = tslib_1.__importDefault(Resizer_1); cropperjs_1 = tslib_1.__importDefault(cropperjs_1); exifreader_1 = tslib_1.__importDefault(exifreader_1); + function inSelection(selection, maxSelection) { + return (selection.x >= maxSelection.x && + selection.y >= maxSelection.y && + selection.x + selection.width <= maxSelection.x + maxSelection.width && + selection.y + selection.height <= maxSelection.y + maxSelection.height); + } class ImageCropper { configuration; file; @@ -100,7 +106,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } setCropperStyle() { this.cropperCanvas.style.aspectRatio = `${this.width}/${this.height}`; - if (this.width > this.height) { + if (this.width >= this.height) { this.cropperCanvas.style.width = `min(70vw, ${this.width}px)`; this.cropperCanvas.style.height = "auto"; } @@ -121,28 +127,29 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL if (this.orientation) { this.cropperImage.$rotate(`${this.orientation}deg`); } - this.cropperImage.$center("contain"); - this.cropperSelection.$center(); - this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); + this.centerSelection(); // Limit the selection to the canvas boundaries this.cropperSelection.addEventListener("change", (event) => { // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); const selection = event.detail; + const cropperImageRect = this.cropperImage.getBoundingClientRect(); const maxSelection = { - x: 0, - y: 0, - width: cropperCanvasRect.width, - height: cropperCanvasRect.height, + x: cropperImageRect.left - cropperCanvasRect.left, + y: cropperImageRect.top - cropperCanvasRect.top, + width: cropperImageRect.width, + height: cropperImageRect.height, }; - if (selection.x < maxSelection.x || - selection.y < maxSelection.y || - selection.x + selection.width > maxSelection.x + maxSelection.width || - selection.y + selection.height > maxSelection.y + maxSelection.height) { + if (!inSelection(selection, maxSelection)) { event.preventDefault(); } }); } + centerSelection() { + this.cropperImage.$center("contain"); + this.cropperSelection.$center(); + this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); + } } class ExactImageCropper extends ImageCropper { #size; @@ -214,10 +221,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } getCropperTemplate() { return `
- + - + @@ -236,17 +243,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } setCropperStyle() { super.setCropperStyle(); - this.cropperSelection.width = Math.min(this.width, this.maxSize.width); - this.cropperSelection.height = Math.min(this.height, this.maxSize.height); - this.cropperCanvas.style.minWidth = `min(${this.maxSize.width}px, ${this.width}px)`; - this.cropperCanvas.style.minHeight = `min(${this.maxSize.height}px, ${this.height}px)`; + if (this.width >= this.height) { + this.cropperCanvas.style.width = `${Math.min(this.maxSize.width, this.width)}px`; + } + else { + this.cropperCanvas.style.height = `${Math.min(this.maxSize.height, this.height)}px`; + } } createCropper() { super.createCropper(); this.dialog.addEventListener("extra", () => { - this.cropperImage.$center("contain"); - this.cropperSelection.$reset(); - this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); + this.centerSelection(); }); // Limit the selection to the min/max size this.cropperSelection.addEventListener("change", (event) => { @@ -259,6 +266,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } }); } + centerSelection() { + this.cropperImage.$center("contain"); + const { width: imageWidth } = this.cropperImage.getBoundingClientRect(); + this.cropperSelection.$change(0, 0, imageWidth, 0, this.configuration.aspectRatio, true); + this.cropperSelection.$center(); + this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); + } } async function cropImage(element, file, configuration) { switch (file.type) { From 552d037518c0b1542670532380344db2f24cc4c9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 5 Dec 2024 11:48:06 +0100 Subject: [PATCH 24/35] Round the numbers --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 7ac8922f28..72479511ca 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -173,10 +173,10 @@ abstract class ImageCropper { const cropperImageRect = this.cropperImage!.getBoundingClientRect(); const maxSelection: Selection = { - x: cropperImageRect.left - cropperCanvasRect.left, - y: cropperImageRect.top - cropperCanvasRect.top, - width: cropperImageRect.width, - height: cropperImageRect.height, + x: Math.round(cropperImageRect.left - cropperCanvasRect.left), + y: Math.round(cropperImageRect.top - cropperCanvasRect.top), + width: Math.round(cropperImageRect.width), + height: Math.round(cropperImageRect.height), }; if (!inSelection(selection, maxSelection)) { From 983dc4d4fd72771ccff5f315f8085fddce8bfca0 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 5 Dec 2024 11:48:58 +0100 Subject: [PATCH 25/35] Run `tsc` --- .../files/js/WoltLabSuite/Core/Component/Image/Cropper.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 86794834c6..7c87962d7e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -135,10 +135,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL const selection = event.detail; const cropperImageRect = this.cropperImage.getBoundingClientRect(); const maxSelection = { - x: cropperImageRect.left - cropperCanvasRect.left, - y: cropperImageRect.top - cropperCanvasRect.top, - width: cropperImageRect.width, - height: cropperImageRect.height, + x: Math.round(cropperImageRect.left - cropperCanvasRect.left), + y: Math.round(cropperImageRect.top - cropperCanvasRect.top), + width: Math.round(cropperImageRect.width), + height: Math.round(cropperImageRect.height), }; if (!inSelection(selection, maxSelection)) { event.preventDefault(); From 9ad04760677b126326eeebe2a6d2c38c8c1dba79 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 6 Dec 2024 10:23:58 +0100 Subject: [PATCH 26/35] Calculate the size of the cropper selection in relation to the window size --- .../Core/Component/Image/Cropper.ts | 144 ++++++++++-------- .../Core/Component/Image/Cropper.js | 127 ++++++++------- wcfsetup/install/files/style/ui/dialog.scss | 6 - 3 files changed, 151 insertions(+), 126 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 72479511ca..e57d3a1892 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -29,8 +29,8 @@ function inSelection(selection: Selection, maxSelection: Selection): boolean { return ( selection.x >= maxSelection.x && selection.y >= maxSelection.y && - selection.x + selection.width <= maxSelection.x + maxSelection.width && - selection.y + selection.height <= maxSelection.y + maxSelection.height + Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && + Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height) ); } @@ -85,7 +85,7 @@ abstract class ImageCropper { return new Promise((resolve, reject) => { this.dialog!.addEventListener("primary", () => { - this.cropperSelection!.$toCanvas() + void this.getCanvas() .then((canvas) => { this.resizer .saveFile( @@ -107,6 +107,10 @@ abstract class ImageCropper { }); } + protected getCanvas(): Promise { + return this.cropperSelection!.$toCanvas(); + } + public async loadImage() { const { image, exif } = await this.resizer.loadFile(this.file); this.image = image; @@ -138,11 +142,9 @@ abstract class ImageCropper { this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`; if (this.width >= this.height) { - this.cropperCanvas!.style.width = `min(70vw, ${this.width}px)`; - this.cropperCanvas!.style.height = "auto"; + this.cropperCanvas!.style.maxHeight = "100%"; } else { - this.cropperCanvas!.style.height = `min(60vh, ${this.height}px)`; - this.cropperCanvas!.style.width = "auto"; + this.cropperCanvas!.style.maxWidth = "100%"; } this.cropperSelection!.aspectRatio = this.configuration.aspectRatio; @@ -171,12 +173,11 @@ abstract class ImageCropper { const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); const selection = event.detail as Selection; - const cropperImageRect = this.cropperImage!.getBoundingClientRect(); const maxSelection: Selection = { - x: Math.round(cropperImageRect.left - cropperCanvasRect.left), - y: Math.round(cropperImageRect.top - cropperCanvasRect.top), - width: Math.round(cropperImageRect.width), - height: Math.round(cropperImageRect.height), + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, }; if (!inSelection(selection, maxSelection)) { @@ -247,17 +248,15 @@ class ExactImageCropper extends ImageCropper { } protected getCropperTemplate(): string { - return `
- - - - - - - - - -
`; + return ` + + + + + + + +`; } protected setCropperStyle() { @@ -273,6 +272,7 @@ class ExactImageCropper extends ImageCropper { } class MinMaxImageCropper extends ImageCropper { + #cropperCanvasRect?: DOMRect; constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { super(element, file, configuration); if (configuration.sizes.length !== 2) { @@ -292,39 +292,40 @@ class MinMaxImageCropper extends ImageCropper { return getPhrase("wcf.global.button.reset"); } - protected getCropperTemplate(): string { - return `
- - - - - - - - - - - - - - - - - - -
`; - } - - protected setCropperStyle() { - super.setCropperStyle(); + public async loadImage(): Promise { + await super.loadImage(); - if (this.width >= this.height) { - this.cropperCanvas!.style.width = `${Math.min(this.maxSize.width, this.width)}px`; - } else { - this.cropperCanvas!.style.height = `${Math.min(this.maxSize.height, this.height)}px`; + if (this.image!.width < this.minSize.width || this.image!.height < this.minSize.height) { + throw new Error( + getPhrase("wcf.upload.error.image.tooSmall", { + width: this.minSize.width, + height: this.minSize.height, + }), + ); } } + protected getCropperTemplate(): string { + return ` + + + + + + + + + + + + + + + + +`; + } + protected createCropper() { super.createCropper(); @@ -335,31 +336,46 @@ class MinMaxImageCropper extends ImageCropper { // Limit the selection to the min/max size this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { const selection = event.detail as Selection; + this.#cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); + + const maxImageWidth = Math.min(this.image!.width, this.maxSize.width); + const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; + + const minWidth = this.minSize.width * widthRatio; + const maxWidth = this.maxSize.width * widthRatio; + const minHeight = minWidth / this.configuration.aspectRatio; + const maxHeight = maxWidth / this.configuration.aspectRatio; if ( - selection.width < this.minSize.width || - selection.height < this.minSize.height || - selection.width > this.maxSize.width || - selection.height > this.maxSize.height + selection.width < minWidth || + selection.height < minHeight || + selection.width > maxWidth || + selection.height > maxHeight ) { event.preventDefault(); } }); } + protected getCanvas(): Promise { + // Calculate the size of the image in relation to the window size + const maxImageWidth = Math.min(this.image!.width, this.maxSize.width); + const widthRatio = this.#cropperCanvasRect!.width / maxImageWidth; + const width = this.cropperSelection!.width / widthRatio; + const height = width / this.configuration.aspectRatio; + + return this.cropperSelection!.$toCanvas({ + width: Math.max(Math.min(Math.ceil(width), this.maxSize.width), this.minSize.width), + height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), + }); + } + protected centerSelection(): void { this.cropperImage!.$center("contain"); const { width: imageWidth } = this.cropperImage!.getBoundingClientRect(); - this.cropperSelection!.$change( - 0, - 0, - imageWidth, - 0, - this.configuration.aspectRatio, - true, - ); + this.cropperSelection!.$change(0, 0, imageWidth, 0, this.configuration.aspectRatio, true); this.cropperSelection!.$center(); this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 7c87962d7e..4ad2427089 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -16,8 +16,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL function inSelection(selection, maxSelection) { return (selection.x >= maxSelection.x && selection.y >= maxSelection.y && - selection.x + selection.width <= maxSelection.x + maxSelection.width && - selection.y + selection.height <= maxSelection.y + maxSelection.height); + Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && + Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height)); } class ImageCropper { configuration; @@ -64,7 +64,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.createCropper(); return new Promise((resolve, reject) => { this.dialog.addEventListener("primary", () => { - this.cropperSelection.$toCanvas() + void this.getCanvas() .then((canvas) => { this.resizer .saveFile({ exif: this.orientation ? undefined : this.exif, image: canvas }, this.file.name, this.file.type) @@ -81,6 +81,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); }); } + getCanvas() { + return this.cropperSelection.$toCanvas(); + } async loadImage() { const { image, exif } = await this.resizer.loadFile(this.file); this.image = image; @@ -107,12 +110,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL setCropperStyle() { this.cropperCanvas.style.aspectRatio = `${this.width}/${this.height}`; if (this.width >= this.height) { - this.cropperCanvas.style.width = `min(70vw, ${this.width}px)`; - this.cropperCanvas.style.height = "auto"; + this.cropperCanvas.style.maxHeight = "100%"; } else { - this.cropperCanvas.style.height = `min(60vh, ${this.height}px)`; - this.cropperCanvas.style.width = "auto"; + this.cropperCanvas.style.maxWidth = "100%"; } this.cropperSelection.aspectRatio = this.configuration.aspectRatio; } @@ -133,12 +134,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL // see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries const cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); const selection = event.detail; - const cropperImageRect = this.cropperImage.getBoundingClientRect(); const maxSelection = { - x: Math.round(cropperImageRect.left - cropperCanvasRect.left), - y: Math.round(cropperImageRect.top - cropperCanvasRect.top), - width: Math.round(cropperImageRect.width), - height: Math.round(cropperImageRect.height), + x: 0, + y: 0, + width: cropperCanvasRect.width, + height: cropperCanvasRect.height, }; if (!inSelection(selection, maxSelection)) { event.preventDefault(); @@ -182,17 +182,15 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.image = await this.resizer.resize(this.image, this.width >= this.height ? this.width : this.#size.width, this.height > this.width ? this.height : this.#size.height, this.resizer.quality, true, timeout); } getCropperTemplate() { - return `
- - - - - - - - - -
`; + return ` + + + + + + + +`; } setCropperStyle() { super.setCropperStyle(); @@ -204,6 +202,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } } class MinMaxImageCropper extends ImageCropper { + #cropperCanvasRect; constructor(element, file, configuration) { super(element, file, configuration); if (configuration.sizes.length !== 2) { @@ -219,37 +218,35 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL getDialogExtra() { return (0, Language_1.getPhrase)("wcf.global.button.reset"); } - getCropperTemplate() { - return `
- - - - - - - - - - - - - - - - - - -
`; - } - setCropperStyle() { - super.setCropperStyle(); - if (this.width >= this.height) { - this.cropperCanvas.style.width = `${Math.min(this.maxSize.width, this.width)}px`; - } - else { - this.cropperCanvas.style.height = `${Math.min(this.maxSize.height, this.height)}px`; + async loadImage() { + await super.loadImage(); + if (this.image.width < this.minSize.width || this.image.height < this.minSize.height) { + throw new Error((0, Language_1.getPhrase)("wcf.upload.error.image.tooSmall", { + width: this.minSize.width, + height: this.minSize.height, + })); } } + getCropperTemplate() { + return ` + + + + + + + + + + + + + + + + +`; + } createCropper() { super.createCropper(); this.dialog.addEventListener("extra", () => { @@ -258,14 +255,32 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL // Limit the selection to the min/max size this.cropperSelection.addEventListener("change", (event) => { const selection = event.detail; - if (selection.width < this.minSize.width || - selection.height < this.minSize.height || - selection.width > this.maxSize.width || - selection.height > this.maxSize.height) { + this.#cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); + const maxImageWidth = Math.min(this.image.width, this.maxSize.width); + const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; + const minWidth = this.minSize.width * widthRatio; + const maxWidth = this.maxSize.width * widthRatio; + const minHeight = minWidth / this.configuration.aspectRatio; + const maxHeight = maxWidth / this.configuration.aspectRatio; + if (selection.width < minWidth || + selection.height < minHeight || + selection.width > maxWidth || + selection.height > maxHeight) { event.preventDefault(); } }); } + getCanvas() { + // Calculate the size of the image in relation to the window size + const maxImageWidth = Math.min(this.image.width, this.maxSize.width); + const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; + const width = this.cropperSelection.width / widthRatio; + const height = width / this.configuration.aspectRatio; + return this.cropperSelection.$toCanvas({ + width: Math.max(Math.min(Math.ceil(width), this.maxSize.width), this.minSize.width), + height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), + }); + } centerSelection() { this.cropperImage.$center("contain"); const { width: imageWidth } = this.cropperImage.getBoundingClientRect(); diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index e4a5ae0a71..0a09771149 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -465,9 +465,3 @@ html[data-color-scheme="dark"] .dialog::backdrop { min-width: 0; } } - -.dialog .cropperContainer { - overflow: auto; - height: 100%; - width: 100%; -} From a9d93bfa2f0c6bc624895f953b41cf8afcc6f30f Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 6 Dec 2024 11:00:06 +0100 Subject: [PATCH 27/35] Fixes the problem when the aspect ratio is less than 1, that the selection has an incorrect resolution --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 9 ++++++--- .../js/WoltLabSuite/Core/Component/Image/Cropper.js | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index e57d3a1892..c6bc19ec80 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -372,10 +372,13 @@ class MinMaxImageCropper extends ImageCropper { protected centerSelection(): void { this.cropperImage!.$center("contain"); + this.#cropperCanvasRect = this.cropperImage!.getBoundingClientRect(); - const { width: imageWidth } = this.cropperImage!.getBoundingClientRect(); - - this.cropperSelection!.$change(0, 0, imageWidth, 0, this.configuration.aspectRatio, true); + if (this.configuration.aspectRatio >= 1.0) { + this.cropperSelection!.$change(0, 0, this.#cropperCanvasRect.width, 0, this.configuration.aspectRatio, true); + } else { + this.cropperSelection!.$change(0, 0, 0, this.#cropperCanvasRect.height, this.configuration.aspectRatio, true); + } this.cropperSelection!.$center(); this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 4ad2427089..46bc1e8dac 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -283,8 +283,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } centerSelection() { this.cropperImage.$center("contain"); - const { width: imageWidth } = this.cropperImage.getBoundingClientRect(); - this.cropperSelection.$change(0, 0, imageWidth, 0, this.configuration.aspectRatio, true); + this.#cropperCanvasRect = this.cropperImage.getBoundingClientRect(); + if (this.configuration.aspectRatio >= 1.0) { + this.cropperSelection.$change(0, 0, this.#cropperCanvasRect.width, 0, this.configuration.aspectRatio, true); + } + else { + this.cropperSelection.$change(0, 0, 0, this.#cropperCanvasRect.height, this.configuration.aspectRatio, true); + } this.cropperSelection.$center(); this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); } From 58dce4dff0e785b55b4c1bbeb6aff49b521df207 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 9 Dec 2024 12:46:15 +0100 Subject: [PATCH 28/35] Recalculate cropper height on window resize. Set height on container(parent) height --- .../Core/Component/Image/Cropper.ts | 26 +++++++++++++++++-- .../Core/Component/Image/Cropper.js | 20 +++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index c6bc19ec80..9f3a22de7a 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -15,6 +15,7 @@ import { getPhrase } from "WoltLabSuite/Core/Language"; import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog"; import * as ExifUtil from "WoltLabSuite/Core/Image/ExifUtil"; import ExifReader from "exifreader"; +import DomUtil from "WoltLabSuite/Core/Dom/Util"; export interface CropperConfiguration { aspectRatio: number; @@ -27,8 +28,8 @@ export interface CropperConfiguration { function inSelection(selection: Selection, maxSelection: Selection): boolean { return ( - selection.x >= maxSelection.x && - selection.y >= maxSelection.y && + Math.ceil(selection.x) >= maxSelection.x && + Math.ceil(selection.y) >= maxSelection.y && Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height) ); @@ -83,6 +84,21 @@ abstract class ImageCropper { this.createCropper(); + const resize = () => { + this.centerSelection(); + }; + + window.addEventListener("resize", resize, { passive: true }); + this.dialog.addEventListener( + "afterClose", + () => { + window.removeEventListener("resize", resize); + }, + { + once: true, + }, + ); + return new Promise((resolve, reject) => { this.dialog!.addEventListener("primary", () => { void this.getCanvas() @@ -371,6 +387,12 @@ class MinMaxImageCropper extends ImageCropper { } protected centerSelection(): void { + // Reset to get the maximum available height + this.cropperCanvas!.style.height = ""; + + const dimensions = DomUtil.outerDimensions(this.cropperCanvas!.parentElement!); + this.cropperCanvas!.style.height = `${dimensions.height}px`; + this.cropperImage!.$center("contain"); this.#cropperCanvasRect = this.cropperImage!.getBoundingClientRect(); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 46bc1e8dac..7631ca3083 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -6,16 +6,17 @@ * @license GNU Lesser General Public License * @since 6.2 */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language", "exifreader"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1, exifreader_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltLabSuite/Core/Component/Dialog", "cropperjs", "WoltLabSuite/Core/Language", "exifreader", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, Resizer_1, Dialog_1, cropperjs_1, Language_1, exifreader_1, Util_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.cropImage = cropImage; Resizer_1 = tslib_1.__importDefault(Resizer_1); cropperjs_1 = tslib_1.__importDefault(cropperjs_1); exifreader_1 = tslib_1.__importDefault(exifreader_1); + Util_1 = tslib_1.__importDefault(Util_1); function inSelection(selection, maxSelection) { - return (selection.x >= maxSelection.x && - selection.y >= maxSelection.y && + return (Math.ceil(selection.x) >= maxSelection.x && + Math.ceil(selection.y) >= maxSelection.y && Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height)); } @@ -62,6 +63,15 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); this.dialog.show((0, Language_1.getPhrase)("wcf.upload.crop.image")); this.createCropper(); + const resize = () => { + this.centerSelection(); + }; + window.addEventListener("resize", resize, { passive: true }); + this.dialog.addEventListener("afterClose", () => { + window.removeEventListener("resize", resize); + }, { + once: true, + }); return new Promise((resolve, reject) => { this.dialog.addEventListener("primary", () => { void this.getCanvas() @@ -282,6 +292,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); } centerSelection() { + // Reset to get the maximum available height + this.cropperCanvas.style.height = ""; + const dimensions = Util_1.default.outerDimensions(this.cropperCanvas.parentElement); + this.cropperCanvas.style.height = `${dimensions.height}px`; this.cropperImage.$center("contain"); this.#cropperCanvasRect = this.cropperImage.getBoundingClientRect(); if (this.configuration.aspectRatio >= 1.0) { From 243135e0ce4a319ac716002a87dd2b6ec75d40a8 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 9 Dec 2024 12:47:42 +0100 Subject: [PATCH 29/35] Center `cropper-canvas`. Fix, if the image on a side has a large dimension that a white area is displayed at the top and/or bottom --- wcfsetup/install/files/style/ui/dialog.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index 0a09771149..c573f0ab76 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -465,3 +465,13 @@ html[data-color-scheme="dark"] .dialog::backdrop { min-width: 0; } } + +.dialog cropper-canvas { + margin-left: auto; + margin-right: auto; +} + +/* If the height of the image is many times greater than the width, a white area would be displayed at the bottom and/or top. */ +.dialog cropper-shade { + outline-width: max(100vh, 100vw) !important; +} From 12a1c7419a9dcf1089778a0e7c8c2b6afac33df5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 10 Dec 2024 09:52:34 +0100 Subject: [PATCH 30/35] Add helper function to get the inner dimensions for an element --- ts/WoltLabSuite/Core/Dom/Util.ts | 36 +++++++++++++++++++ .../files/js/WoltLabSuite/Core/Dom/Util.js | 28 +++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/ts/WoltLabSuite/Core/Dom/Util.ts b/ts/WoltLabSuite/Core/Dom/Util.ts index a346947100..66c3f53837 100644 --- a/ts/WoltLabSuite/Core/Dom/Util.ts +++ b/ts/WoltLabSuite/Core/Dom/Util.ts @@ -96,6 +96,42 @@ const DomUtil = { return id; }, + /** + * Returns the inner height of an element including paddings. + */ + innerHeight(element: HTMLElement, styles?: CSSStyleDeclaration): number { + styles = styles || window.getComputedStyle(element); + + let height = element.clientHeight; + height += ~~styles.paddingTop + ~~styles.paddingBottom; + + return height; + }, + + /** + * Returns the inner width of an element including paddings. + */ + innerWidth(element: HTMLElement, styles?: CSSStyleDeclaration): number { + styles = styles || window.getComputedStyle(element); + + let width = element.clientWidth; + width += ~~styles.paddingLeft + ~~styles.paddingRight; + + return width; + }, + + /** + * Returns the inner dimensions of an element including paddings. + */ + innerDimensions(element: HTMLElement): Dimensions { + const styles = window.getComputedStyle(element); + + return { + height: DomUtil.innerHeight(element, styles), + width: DomUtil.innerWidth(element, styles), + }; + }, + /** * Returns the outer height of an element including margins. */ diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js index c5a472f2b5..f981c4f49a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js @@ -81,6 +81,34 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo } return id; }, + /** + * Returns the inner height of an element including paddings. + */ + innerHeight(element, styles) { + styles = styles || window.getComputedStyle(element); + let height = element.clientHeight; + height += ~~styles.paddingTop + ~~styles.paddingBottom; + return height; + }, + /** + * Returns the inner width of an element including paddings. + */ + innerWidth(element, styles) { + styles = styles || window.getComputedStyle(element); + let width = element.clientWidth; + width += ~~styles.paddingLeft + ~~styles.paddingRight; + return width; + }, + /** + * Returns the inner dimensions of an element including paddings. + */ + innerDimensions(element) { + const styles = window.getComputedStyle(element); + return { + height: DomUtil.innerHeight(element, styles), + width: DomUtil.innerWidth(element, styles), + }; + }, /** * Returns the outer height of an element including margins. */ From 3575389d971a6a875a6b2c015f59890e69d0cfbe Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 10 Dec 2024 09:53:51 +0100 Subject: [PATCH 31/35] overwrites default values for `min-width` and `min-height` --- wcfsetup/install/files/style/ui/dialog.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index c573f0ab76..cca59d6c5b 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -469,6 +469,9 @@ html[data-color-scheme="dark"] .dialog::backdrop { .dialog cropper-canvas { margin-left: auto; margin-right: auto; + /* overwrites the default values of `min-height: 100px` and `min-width: 200px` */ + min-height: 1px; + min-width: 1px; } /* If the height of the image is many times greater than the width, a white area would be displayed at the bottom and/or top. */ From 1c6b8050b82bbbe477e2c13e105b69cd5d5c5b63 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 10 Dec 2024 09:55:23 +0100 Subject: [PATCH 32/35] Also note the maximum width that is available --- .../Core/Component/Image/Cropper.ts | 55 ++++++++++++------- .../Core/Component/Image/Cropper.js | 40 +++++++------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 9f3a22de7a..9d9b942ce3 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -28,10 +28,10 @@ export interface CropperConfiguration { function inSelection(selection: Selection, maxSelection: Selection): boolean { return ( - Math.ceil(selection.x) >= maxSelection.x && - Math.ceil(selection.y) >= maxSelection.y && - Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && - Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height) + Math.round(selection.x) >= maxSelection.x && + Math.round(selection.y) >= maxSelection.y && + Math.round(selection.x + selection.width) <= Math.round(maxSelection.x + maxSelection.width) && + Math.round(selection.y + selection.height) <= Math.round(maxSelection.y + maxSelection.height) ); } @@ -355,18 +355,22 @@ class MinMaxImageCropper extends ImageCropper { this.#cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); const maxImageWidth = Math.min(this.image!.width, this.maxSize.width); - const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; + const maxImageHeight = Math.min(this.image!.height, this.maxSize.height); + const selectionRatio = Math.min( + this.#cropperCanvasRect.width / maxImageWidth, + this.#cropperCanvasRect.height / maxImageHeight, + ); - const minWidth = this.minSize.width * widthRatio; - const maxWidth = this.maxSize.width * widthRatio; + const minWidth = this.minSize.width * selectionRatio; + const maxWidth = this.maxSize.width * selectionRatio; const minHeight = minWidth / this.configuration.aspectRatio; const maxHeight = maxWidth / this.configuration.aspectRatio; if ( - selection.width < minWidth || - selection.height < minHeight || - selection.width > maxWidth || - selection.height > maxHeight + Math.round(selection.width) < minWidth || + Math.round(selection.height) < minHeight || + Math.round(selection.width) > maxWidth || + Math.round(selection.height) > maxHeight ) { event.preventDefault(); } @@ -387,20 +391,33 @@ class MinMaxImageCropper extends ImageCropper { } protected centerSelection(): void { - // Reset to get the maximum available height + // Reset to get the maximum available height and width this.cropperCanvas!.style.height = ""; + this.cropperCanvas!.style.width = ""; + + const dimension = DomUtil.innerDimensions(this.cropperCanvas!.parentElement!); + const ratio = Math.min(dimension.width / this.image!.width, dimension.height / this.image!.height); - const dimensions = DomUtil.outerDimensions(this.cropperCanvas!.parentElement!); - this.cropperCanvas!.style.height = `${dimensions.height}px`; + this.cropperCanvas!.style.height = `${this.image!.height * ratio}px`; + this.cropperCanvas!.style.width = `${this.image!.width * ratio}px`; this.cropperImage!.$center("contain"); this.#cropperCanvasRect = this.cropperImage!.getBoundingClientRect(); - if (this.configuration.aspectRatio >= 1.0) { - this.cropperSelection!.$change(0, 0, this.#cropperCanvasRect.width, 0, this.configuration.aspectRatio, true); - } else { - this.cropperSelection!.$change(0, 0, 0, this.#cropperCanvasRect.height, this.configuration.aspectRatio, true); - } + const selectionRatio = Math.min( + this.#cropperCanvasRect.width / this.maxSize.width, + this.#cropperCanvasRect.height / this.maxSize.height, + ); + + this.cropperSelection!.$change( + 0, + 0, + this.maxSize.width * selectionRatio, + this.maxSize.height * selectionRatio, + this.configuration.aspectRatio, + true, + ); + this.cropperSelection!.$center(); this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 7631ca3083..60bd94b1b2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -15,10 +15,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL exifreader_1 = tslib_1.__importDefault(exifreader_1); Util_1 = tslib_1.__importDefault(Util_1); function inSelection(selection, maxSelection) { - return (Math.ceil(selection.x) >= maxSelection.x && - Math.ceil(selection.y) >= maxSelection.y && - Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) && - Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height)); + return (Math.round(selection.x) >= maxSelection.x && + Math.round(selection.y) >= maxSelection.y && + Math.round(selection.x + selection.width) <= Math.round(maxSelection.x + maxSelection.width) && + Math.round(selection.y + selection.height) <= Math.round(maxSelection.y + maxSelection.height)); } class ImageCropper { configuration; @@ -267,15 +267,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL const selection = event.detail; this.#cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); const maxImageWidth = Math.min(this.image.width, this.maxSize.width); - const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; - const minWidth = this.minSize.width * widthRatio; - const maxWidth = this.maxSize.width * widthRatio; + const maxImageHeight = Math.min(this.image.height, this.maxSize.height); + const selectionRatio = Math.min(this.#cropperCanvasRect.width / maxImageWidth, this.#cropperCanvasRect.height / maxImageHeight); + const minWidth = this.minSize.width * selectionRatio; + const maxWidth = this.maxSize.width * selectionRatio; const minHeight = minWidth / this.configuration.aspectRatio; const maxHeight = maxWidth / this.configuration.aspectRatio; - if (selection.width < minWidth || - selection.height < minHeight || - selection.width > maxWidth || - selection.height > maxHeight) { + if (Math.round(selection.width) < minWidth || + Math.round(selection.height) < minHeight || + Math.round(selection.width) > maxWidth || + Math.round(selection.height) > maxHeight) { event.preventDefault(); } }); @@ -292,18 +293,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); } centerSelection() { - // Reset to get the maximum available height + // Reset to get the maximum available height and width this.cropperCanvas.style.height = ""; - const dimensions = Util_1.default.outerDimensions(this.cropperCanvas.parentElement); - this.cropperCanvas.style.height = `${dimensions.height}px`; + this.cropperCanvas.style.width = ""; + const dimension = Util_1.default.innerDimensions(this.cropperCanvas.parentElement); + const ratio = Math.min(dimension.width / this.image.width, dimension.height / this.image.height); + this.cropperCanvas.style.height = `${this.image.height * ratio}px`; + this.cropperCanvas.style.width = `${this.image.width * ratio}px`; this.cropperImage.$center("contain"); this.#cropperCanvasRect = this.cropperImage.getBoundingClientRect(); - if (this.configuration.aspectRatio >= 1.0) { - this.cropperSelection.$change(0, 0, this.#cropperCanvasRect.width, 0, this.configuration.aspectRatio, true); - } - else { - this.cropperSelection.$change(0, 0, 0, this.#cropperCanvasRect.height, this.configuration.aspectRatio, true); - } + const selectionRatio = Math.min(this.#cropperCanvasRect.width / this.maxSize.width, this.#cropperCanvasRect.height / this.maxSize.height); + this.cropperSelection.$change(0, 0, this.maxSize.width * selectionRatio, this.maxSize.height * selectionRatio, this.configuration.aspectRatio, true); this.cropperSelection.$center(); this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); } From 50bed09cf1f39a088de14b94be5f6f4b3bf4f674 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 16 Dec 2024 12:51:46 +0100 Subject: [PATCH 33/35] Resize the image to the maximum that the browser can display in the dialog. --- .../Core/Component/Image/Cropper.ts | 334 ++++++++---------- ts/WoltLabSuite/Core/Dom/Util.ts | 4 +- .../Core/Component/Image/Cropper.js | 242 ++++++------- .../files/js/WoltLabSuite/Core/Dom/Util.js | 4 +- .../ImageCropperConfiguration.class.php | 7 +- wcfsetup/install/files/style/ui/dialog.scss | 3 + 6 files changed, 272 insertions(+), 322 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 9d9b942ce3..0df7481033 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -47,6 +47,7 @@ abstract class ImageCropper { protected dialog?: WoltlabCoreDialogElement; protected exif?: ExifUtil.Exif; protected orientation?: number; + protected cropperCanvasRect?: DOMRect; #cropper?: Cropper; constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { @@ -76,6 +77,31 @@ abstract class ImageCropper { } } + abstract get minSize(): { width: number; height: number }; + + abstract get maxSize(): { width: number; height: number }; + + public async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + const tags = await ExifReader.load(this.file); + if (tags.Orientation) { + switch (tags.Orientation.value) { + case 3: + this.orientation = 180; + break; + case 6: + this.orientation = 90; + break; + case 8: + this.orientation = 270; + break; + // Any other rotation is unsupported. + } + } + } + public async showDialog(): Promise { this.dialog = dialogFactory().fromElement(this.image!).asPrompt({ extra: this.getDialogExtra(), @@ -89,18 +115,21 @@ abstract class ImageCropper { }; window.addEventListener("resize", resize, { passive: true }); - this.dialog.addEventListener( - "afterClose", - () => { - window.removeEventListener("resize", resize); - }, - { - once: true, - }, - ); return new Promise((resolve, reject) => { + let callReject = true; + this.dialog!.addEventListener("afterClose", () => { + window.removeEventListener("resize", resize); + + // If the dialog is closed without confirming, reject the promise to trigger a cancel event. + if (callReject) { + reject(); + } + }); + this.dialog!.addEventListener("primary", () => { + callReject = false; + void this.getCanvas() .then((canvas) => { this.resizer @@ -123,47 +152,23 @@ abstract class ImageCropper { }); } - protected getCanvas(): Promise { - return this.cropperSelection!.$toCanvas(); - } - - public async loadImage() { - const { image, exif } = await this.resizer.loadFile(this.file); - this.image = image; - this.exif = exif; - const tags = await ExifReader.load(this.file); - if (tags.Orientation) { - switch (tags.Orientation.value) { - case 3: - this.orientation = 180; - break; - case 6: - this.orientation = 90; - break; - case 8: - this.orientation = 270; - break; - // Any other rotation is unsupported. - } - } - } - - protected abstract getCropperTemplate(): string; - protected getDialogExtra(): string | undefined { return undefined; } - protected setCropperStyle() { - this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`; - - if (this.width >= this.height) { - this.cropperCanvas!.style.maxHeight = "100%"; - } else { - this.cropperCanvas!.style.maxWidth = "100%"; - } + protected getCanvas(): Promise { + // Calculate the size of the image in relation to the window size + const selectionRatio = Math.min( + this.cropperCanvasRect!.width / this.width, + this.cropperCanvasRect!.height / this.height, + ); + const width = this.cropperSelection!.width / selectionRatio; + const height = width / this.configuration.aspectRatio; - this.cropperSelection!.aspectRatio = this.configuration.aspectRatio; + return this.cropperSelection!.$toCanvas({ + width: Math.max(Math.min(Math.floor(width), this.maxSize.width), this.minSize.width), + height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), + }); } protected createCropper() { @@ -200,23 +205,108 @@ abstract class ImageCropper { event.preventDefault(); } }); + + // Limit the selection to the min/max size + this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { + const selection = event.detail as Selection; + this.cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); + + const selectionRatio = Math.min( + this.cropperCanvasRect.width / this.width, + this.cropperCanvasRect.height / this.height, + ); + + const minWidth = this.minSize.width * selectionRatio; + const maxWidth = this.cropperCanvasRect.width; + const minHeight = minWidth / this.configuration.aspectRatio; + const maxHeight = maxWidth / this.configuration.aspectRatio; + + if ( + Math.round(selection.width) < minWidth || + Math.round(selection.height) < minHeight || + Math.round(selection.width) > maxWidth || + Math.round(selection.height) > maxHeight + ) { + event.preventDefault(); + } + }); + } + + protected setCropperStyle() { + this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`; + + this.cropperSelection!.aspectRatio = this.configuration.aspectRatio; } protected centerSelection(): void { + // Set to the maximum size + this.cropperCanvas!.style.width = `${this.width}px`; + this.cropperCanvas!.style.height = `${this.height}px`; + + const dimension = DomUtil.innerDimensions(this.cropperCanvas!.parentElement!); + const ratio = Math.min(dimension.width / this.width, dimension.height / this.height); + + this.cropperCanvas!.style.height = `${this.height * ratio}px`; + this.cropperCanvas!.style.width = `${this.width * ratio}px`; + this.cropperImage!.$center("contain"); + this.cropperCanvasRect = this.cropperImage!.getBoundingClientRect(); + + const selectionRatio = Math.min( + this.cropperCanvasRect.width / this.maxSize.width, + this.cropperCanvasRect.height / this.maxSize.height, + ); + + this.cropperSelection!.$change( + 0, + 0, + this.maxSize.width * selectionRatio, + this.maxSize.height * selectionRatio, + this.configuration.aspectRatio, + true, + ); + this.cropperSelection!.$center(); this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); } + + protected getCropperTemplate(): string { + return ` + + + + + + + + + + + + + + + + +`; + } } class ExactImageCropper extends ImageCropper { - #size?: { width: number; height: number }; + get minSize() { + return this.configuration.sizes[0]; + } + + get maxSize() { + return this.configuration.sizes[this.configuration.sizes.length - 1]; + } public async showDialog(): Promise { // The image already has the correct size, cropping is not necessary if ( - this.width == this.#size!.width && - this.height == this.#size!.height && + this.configuration.sizes.filter((size) => { + return size.width == this.width && size.height == this.height; + }).length > 0 && this.image instanceof HTMLCanvasElement ) { return this.resizer.saveFile( @@ -232,14 +322,17 @@ class ExactImageCropper extends ImageCropper { public async loadImage(): Promise { await super.loadImage(); - const timeout = new Promise((resolve) => { - window.setTimeout(() => resolve(this.file), 10_000); - }); - - // resize image to the largest possible size - const sizes = this.configuration.sizes.filter((size) => { - return size.width <= this.width && size.height <= this.height; - }); + const sizes = this.configuration.sizes + .filter((size) => { + return size.width <= this.width && size.height <= this.height; + }) + .sort((a, b) => { + if (this.configuration.aspectRatio >= 1) { + return a.width - b.width; + } else { + return a.height - b.height; + } + }); if (sizes.length === 0) { const smallestSize = @@ -252,43 +345,11 @@ class ExactImageCropper extends ImageCropper { ); } - this.#size = sizes[sizes.length - 1]; - this.image = await this.resizer.resize( - this.image as HTMLImageElement, - this.width >= this.height ? this.width : this.#size.width, - this.height > this.width ? this.height : this.#size.height, - this.resizer.quality, - true, - timeout, - ); - } - - protected getCropperTemplate(): string { - return ` - - - - - - - -`; - } - - protected setCropperStyle() { - super.setCropperStyle(); - - this.cropperSelection!.width = this.#size!.width; - this.cropperSelection!.height = this.#size!.height; - - this.cropperCanvas!.style.width = `${this.width}px`; - this.cropperCanvas!.style.height = `${this.height}px`; - this.cropperSelection!.style.removeProperty("aspectRatio"); + this.configuration.sizes = sizes; } } class MinMaxImageCropper extends ImageCropper { - #cropperCanvasRect?: DOMRect; constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) { super(element, file, configuration); if (configuration.sizes.length !== 2) { @@ -311,7 +372,7 @@ class MinMaxImageCropper extends ImageCropper { public async loadImage(): Promise { await super.loadImage(); - if (this.image!.width < this.minSize.width || this.image!.height < this.minSize.height) { + if (this.width < this.minSize.width || this.height < this.minSize.height) { throw new Error( getPhrase("wcf.upload.error.image.tooSmall", { width: this.minSize.width, @@ -321,105 +382,12 @@ class MinMaxImageCropper extends ImageCropper { } } - protected getCropperTemplate(): string { - return ` - - - - - - - - - - - - - - - - -`; - } - protected createCropper() { super.createCropper(); this.dialog!.addEventListener("extra", () => { this.centerSelection(); }); - - // Limit the selection to the min/max size - this.cropperSelection!.addEventListener("change", (event: CustomEvent) => { - const selection = event.detail as Selection; - this.#cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect(); - - const maxImageWidth = Math.min(this.image!.width, this.maxSize.width); - const maxImageHeight = Math.min(this.image!.height, this.maxSize.height); - const selectionRatio = Math.min( - this.#cropperCanvasRect.width / maxImageWidth, - this.#cropperCanvasRect.height / maxImageHeight, - ); - - const minWidth = this.minSize.width * selectionRatio; - const maxWidth = this.maxSize.width * selectionRatio; - const minHeight = minWidth / this.configuration.aspectRatio; - const maxHeight = maxWidth / this.configuration.aspectRatio; - - if ( - Math.round(selection.width) < minWidth || - Math.round(selection.height) < minHeight || - Math.round(selection.width) > maxWidth || - Math.round(selection.height) > maxHeight - ) { - event.preventDefault(); - } - }); - } - - protected getCanvas(): Promise { - // Calculate the size of the image in relation to the window size - const maxImageWidth = Math.min(this.image!.width, this.maxSize.width); - const widthRatio = this.#cropperCanvasRect!.width / maxImageWidth; - const width = this.cropperSelection!.width / widthRatio; - const height = width / this.configuration.aspectRatio; - - return this.cropperSelection!.$toCanvas({ - width: Math.max(Math.min(Math.ceil(width), this.maxSize.width), this.minSize.width), - height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), - }); - } - - protected centerSelection(): void { - // Reset to get the maximum available height and width - this.cropperCanvas!.style.height = ""; - this.cropperCanvas!.style.width = ""; - - const dimension = DomUtil.innerDimensions(this.cropperCanvas!.parentElement!); - const ratio = Math.min(dimension.width / this.image!.width, dimension.height / this.image!.height); - - this.cropperCanvas!.style.height = `${this.image!.height * ratio}px`; - this.cropperCanvas!.style.width = `${this.image!.width * ratio}px`; - - this.cropperImage!.$center("contain"); - this.#cropperCanvasRect = this.cropperImage!.getBoundingClientRect(); - - const selectionRatio = Math.min( - this.#cropperCanvasRect.width / this.maxSize.width, - this.#cropperCanvasRect.height / this.maxSize.height, - ); - - this.cropperSelection!.$change( - 0, - 0, - this.maxSize.width * selectionRatio, - this.maxSize.height * selectionRatio, - this.configuration.aspectRatio, - true, - ); - - this.cropperSelection!.$center(); - this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" }); } } diff --git a/ts/WoltLabSuite/Core/Dom/Util.ts b/ts/WoltLabSuite/Core/Dom/Util.ts index 66c3f53837..6f1052d80a 100644 --- a/ts/WoltLabSuite/Core/Dom/Util.ts +++ b/ts/WoltLabSuite/Core/Dom/Util.ts @@ -103,7 +103,7 @@ const DomUtil = { styles = styles || window.getComputedStyle(element); let height = element.clientHeight; - height += ~~styles.paddingTop + ~~styles.paddingBottom; + height -= ~~styles.paddingTop + ~~styles.paddingBottom; return height; }, @@ -115,7 +115,7 @@ const DomUtil = { styles = styles || window.getComputedStyle(element); let width = element.clientWidth; - width += ~~styles.paddingLeft + ~~styles.paddingRight; + width -= ~~parseInt(styles.paddingLeft) + ~~parseInt(styles.paddingRight); return width; }, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 60bd94b1b2..046b13b6cd 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -32,6 +32,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL dialog; exif; orientation; + cropperCanvasRect; #cropper; constructor(element, file, configuration) { this.configuration = configuration; @@ -57,6 +58,26 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL return this.image.height; } } + async loadImage() { + const { image, exif } = await this.resizer.loadFile(this.file); + this.image = image; + this.exif = exif; + const tags = await exifreader_1.default.load(this.file); + if (tags.Orientation) { + switch (tags.Orientation.value) { + case 3: + this.orientation = 180; + break; + case 6: + this.orientation = 90; + break; + case 8: + this.orientation = 270; + break; + // Any other rotation is unsupported. + } + } + } async showDialog() { this.dialog = (0, Dialog_1.dialogFactory)().fromElement(this.image).asPrompt({ extra: this.getDialogExtra(), @@ -67,13 +88,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.centerSelection(); }; window.addEventListener("resize", resize, { passive: true }); - this.dialog.addEventListener("afterClose", () => { - window.removeEventListener("resize", resize); - }, { - once: true, - }); return new Promise((resolve, reject) => { + let callReject = true; + this.dialog.addEventListener("afterClose", () => { + window.removeEventListener("resize", resize); + // If the dialog is closed without confirming, reject the promise to trigger a cancel event. + if (callReject) { + reject(); + } + }); this.dialog.addEventListener("primary", () => { + callReject = false; void this.getCanvas() .then((canvas) => { this.resizer @@ -91,41 +116,18 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL }); }); } - getCanvas() { - return this.cropperSelection.$toCanvas(); - } - async loadImage() { - const { image, exif } = await this.resizer.loadFile(this.file); - this.image = image; - this.exif = exif; - const tags = await exifreader_1.default.load(this.file); - if (tags.Orientation) { - switch (tags.Orientation.value) { - case 3: - this.orientation = 180; - break; - case 6: - this.orientation = 90; - break; - case 8: - this.orientation = 270; - break; - // Any other rotation is unsupported. - } - } - } getDialogExtra() { return undefined; } - setCropperStyle() { - this.cropperCanvas.style.aspectRatio = `${this.width}/${this.height}`; - if (this.width >= this.height) { - this.cropperCanvas.style.maxHeight = "100%"; - } - else { - this.cropperCanvas.style.maxWidth = "100%"; - } - this.cropperSelection.aspectRatio = this.configuration.aspectRatio; + getCanvas() { + // Calculate the size of the image in relation to the window size + const selectionRatio = Math.min(this.cropperCanvasRect.width / this.width, this.cropperCanvasRect.height / this.height); + const width = this.cropperSelection.width / selectionRatio; + const height = width / this.configuration.aspectRatio; + return this.cropperSelection.$toCanvas({ + width: Math.max(Math.min(Math.floor(width), this.maxSize.width), this.minSize.width), + height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), + }); } createCropper() { this.#cropper = new cropperjs_1.default(this.image, { @@ -154,19 +156,75 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL event.preventDefault(); } }); + // Limit the selection to the min/max size + this.cropperSelection.addEventListener("change", (event) => { + const selection = event.detail; + this.cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); + const selectionRatio = Math.min(this.cropperCanvasRect.width / this.width, this.cropperCanvasRect.height / this.height); + const minWidth = this.minSize.width * selectionRatio; + const maxWidth = this.cropperCanvasRect.width; + const minHeight = minWidth / this.configuration.aspectRatio; + const maxHeight = maxWidth / this.configuration.aspectRatio; + if (Math.round(selection.width) < minWidth || + Math.round(selection.height) < minHeight || + Math.round(selection.width) > maxWidth || + Math.round(selection.height) > maxHeight) { + event.preventDefault(); + } + }); + } + setCropperStyle() { + this.cropperCanvas.style.aspectRatio = `${this.width}/${this.height}`; + this.cropperSelection.aspectRatio = this.configuration.aspectRatio; } centerSelection() { + // Set to the maximum size + this.cropperCanvas.style.width = `${this.width}px`; + this.cropperCanvas.style.height = `${this.height}px`; + const dimension = Util_1.default.innerDimensions(this.cropperCanvas.parentElement); + const ratio = Math.min(dimension.width / this.width, dimension.height / this.height); + this.cropperCanvas.style.height = `${this.height * ratio}px`; + this.cropperCanvas.style.width = `${this.width * ratio}px`; this.cropperImage.$center("contain"); + this.cropperCanvasRect = this.cropperImage.getBoundingClientRect(); + const selectionRatio = Math.min(this.cropperCanvasRect.width / this.maxSize.width, this.cropperCanvasRect.height / this.maxSize.height); + this.cropperSelection.$change(0, 0, this.maxSize.width * selectionRatio, this.maxSize.height * selectionRatio, this.configuration.aspectRatio, true); this.cropperSelection.$center(); this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); } + getCropperTemplate() { + return ` + + + + + + + + + + + + + + + + +`; + } } class ExactImageCropper extends ImageCropper { - #size; + get minSize() { + return this.configuration.sizes[0]; + } + get maxSize() { + return this.configuration.sizes[this.configuration.sizes.length - 1]; + } async showDialog() { // The image already has the correct size, cropping is not necessary - if (this.width == this.#size.width && - this.height == this.#size.height && + if (this.configuration.sizes.filter((size) => { + return size.width == this.width && size.height == this.height; + }).length > 0 && this.image instanceof HTMLCanvasElement) { return this.resizer.saveFile({ exif: this.orientation ? undefined : this.exif, image: this.image }, this.file.name, this.file.type); } @@ -174,12 +232,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } async loadImage() { await super.loadImage(); - const timeout = new Promise((resolve) => { - window.setTimeout(() => resolve(this.file), 10_000); - }); - // resize image to the largest possible size - const sizes = this.configuration.sizes.filter((size) => { + const sizes = this.configuration.sizes + .filter((size) => { return size.width <= this.width && size.height <= this.height; + }) + .sort((a, b) => { + if (this.configuration.aspectRatio >= 1) { + return a.width - b.width; + } + else { + return a.height - b.height; + } }); if (sizes.length === 0) { const smallestSize = this.configuration.sizes.length > 1 ? this.configuration.sizes[this.configuration.sizes.length - 1] : undefined; @@ -188,31 +251,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL height: smallestSize?.height, })); } - this.#size = sizes[sizes.length - 1]; - this.image = await this.resizer.resize(this.image, this.width >= this.height ? this.width : this.#size.width, this.height > this.width ? this.height : this.#size.height, this.resizer.quality, true, timeout); - } - getCropperTemplate() { - return ` - - - - - - - -`; - } - setCropperStyle() { - super.setCropperStyle(); - this.cropperSelection.width = this.#size.width; - this.cropperSelection.height = this.#size.height; - this.cropperCanvas.style.width = `${this.width}px`; - this.cropperCanvas.style.height = `${this.height}px`; - this.cropperSelection.style.removeProperty("aspectRatio"); + this.configuration.sizes = sizes; } } class MinMaxImageCropper extends ImageCropper { - #cropperCanvasRect; constructor(element, file, configuration) { super(element, file, configuration); if (configuration.sizes.length !== 2) { @@ -230,82 +272,18 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } async loadImage() { await super.loadImage(); - if (this.image.width < this.minSize.width || this.image.height < this.minSize.height) { + if (this.width < this.minSize.width || this.height < this.minSize.height) { throw new Error((0, Language_1.getPhrase)("wcf.upload.error.image.tooSmall", { width: this.minSize.width, height: this.minSize.height, })); } } - getCropperTemplate() { - return ` - - - - - - - - - - - - - - - - -`; - } createCropper() { super.createCropper(); this.dialog.addEventListener("extra", () => { this.centerSelection(); }); - // Limit the selection to the min/max size - this.cropperSelection.addEventListener("change", (event) => { - const selection = event.detail; - this.#cropperCanvasRect = this.cropperCanvas.getBoundingClientRect(); - const maxImageWidth = Math.min(this.image.width, this.maxSize.width); - const maxImageHeight = Math.min(this.image.height, this.maxSize.height); - const selectionRatio = Math.min(this.#cropperCanvasRect.width / maxImageWidth, this.#cropperCanvasRect.height / maxImageHeight); - const minWidth = this.minSize.width * selectionRatio; - const maxWidth = this.maxSize.width * selectionRatio; - const minHeight = minWidth / this.configuration.aspectRatio; - const maxHeight = maxWidth / this.configuration.aspectRatio; - if (Math.round(selection.width) < minWidth || - Math.round(selection.height) < minHeight || - Math.round(selection.width) > maxWidth || - Math.round(selection.height) > maxHeight) { - event.preventDefault(); - } - }); - } - getCanvas() { - // Calculate the size of the image in relation to the window size - const maxImageWidth = Math.min(this.image.width, this.maxSize.width); - const widthRatio = this.#cropperCanvasRect.width / maxImageWidth; - const width = this.cropperSelection.width / widthRatio; - const height = width / this.configuration.aspectRatio; - return this.cropperSelection.$toCanvas({ - width: Math.max(Math.min(Math.ceil(width), this.maxSize.width), this.minSize.width), - height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height), - }); - } - centerSelection() { - // Reset to get the maximum available height and width - this.cropperCanvas.style.height = ""; - this.cropperCanvas.style.width = ""; - const dimension = Util_1.default.innerDimensions(this.cropperCanvas.parentElement); - const ratio = Math.min(dimension.width / this.image.width, dimension.height / this.image.height); - this.cropperCanvas.style.height = `${this.image.height * ratio}px`; - this.cropperCanvas.style.width = `${this.image.width * ratio}px`; - this.cropperImage.$center("contain"); - this.#cropperCanvasRect = this.cropperImage.getBoundingClientRect(); - const selectionRatio = Math.min(this.#cropperCanvasRect.width / this.maxSize.width, this.#cropperCanvasRect.height / this.maxSize.height); - this.cropperSelection.$change(0, 0, this.maxSize.width * selectionRatio, this.maxSize.height * selectionRatio, this.configuration.aspectRatio, true); - this.cropperSelection.$center(); - this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); } } async function cropImage(element, file, configuration) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js index f981c4f49a..736f5015fa 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js @@ -87,7 +87,7 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo innerHeight(element, styles) { styles = styles || window.getComputedStyle(element); let height = element.clientHeight; - height += ~~styles.paddingTop + ~~styles.paddingBottom; + height -= ~~styles.paddingTop + ~~styles.paddingBottom; return height; }, /** @@ -96,7 +96,7 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo innerWidth(element, styles) { styles = styles || window.getComputedStyle(element); let width = element.clientWidth; - width += ~~styles.paddingLeft + ~~styles.paddingRight; + width -= ~~parseInt(styles.paddingLeft) + ~~parseInt(styles.paddingRight); return width; }, /** diff --git a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php index 8cb30fe084..73b928e0a4 100644 --- a/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/ImageCropperConfiguration.class.php @@ -77,11 +77,12 @@ public static function forMinMax(ImageCropSize $min, ImageCropSize $max): self * - Image is 100x200 * - Image is rejected * - Image is 200x150 - * - Image is resized to 170x128 + * - Uploaded image is 128x128 * - Image is 150x200 - * - Image is resized to 128x170 + * - Uploaded image is 128x128 * - Image is 300x300 - * - Image is resized to 256x256 + * - Uploaded can image is 128x128 or 256x256, depending on cropping selection from the user + * - Image is 256x256 * - The image is uploaded directly without displaying the cropping dialog */ public static function forExact(ImageCropSize ...$sizes): self diff --git a/wcfsetup/install/files/style/ui/dialog.scss b/wcfsetup/install/files/style/ui/dialog.scss index cca59d6c5b..627afc6bda 100644 --- a/wcfsetup/install/files/style/ui/dialog.scss +++ b/wcfsetup/install/files/style/ui/dialog.scss @@ -469,6 +469,9 @@ html[data-color-scheme="dark"] .dialog::backdrop { .dialog cropper-canvas { margin-left: auto; margin-right: auto; + max-height: 100%; + max-width: 100%; + /* overwrites the default values of `min-height: 100px` and `min-width: 200px` */ min-height: 1px; min-width: 1px; From 4c6e55c07618b7bcb258a0edd7d0d22fd90a407c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 09:01:47 +0100 Subject: [PATCH 34/35] Remove round Limit to max width and height --- ts/WoltLabSuite/Core/Component/Image/Cropper.ts | 12 ++++++------ .../js/WoltLabSuite/Core/Component/Image/Cropper.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 0df7481033..8212edbefe 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -222,10 +222,10 @@ abstract class ImageCropper { const maxHeight = maxWidth / this.configuration.aspectRatio; if ( - Math.round(selection.width) < minWidth || - Math.round(selection.height) < minHeight || - Math.round(selection.width) > maxWidth || - Math.round(selection.height) > maxHeight + selection.width < minWidth || + selection.height < minHeight || + selection.width > maxWidth || + selection.height > maxHeight ) { event.preventDefault(); } @@ -260,8 +260,8 @@ abstract class ImageCropper { this.cropperSelection!.$change( 0, 0, - this.maxSize.width * selectionRatio, - this.maxSize.height * selectionRatio, + Math.min(this.cropperCanvasRect.width, this.maxSize.width * selectionRatio), + Math.min(this.cropperCanvasRect.height, this.maxSize.height * selectionRatio), this.configuration.aspectRatio, true, ); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 046b13b6cd..06c2cf3796 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -165,10 +165,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL const maxWidth = this.cropperCanvasRect.width; const minHeight = minWidth / this.configuration.aspectRatio; const maxHeight = maxWidth / this.configuration.aspectRatio; - if (Math.round(selection.width) < minWidth || - Math.round(selection.height) < minHeight || - Math.round(selection.width) > maxWidth || - Math.round(selection.height) > maxHeight) { + if (selection.width < minWidth || + selection.height < minHeight || + selection.width > maxWidth || + selection.height > maxHeight) { event.preventDefault(); } }); @@ -188,7 +188,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL this.cropperImage.$center("contain"); this.cropperCanvasRect = this.cropperImage.getBoundingClientRect(); const selectionRatio = Math.min(this.cropperCanvasRect.width / this.maxSize.width, this.cropperCanvasRect.height / this.maxSize.height); - this.cropperSelection.$change(0, 0, this.maxSize.width * selectionRatio, this.maxSize.height * selectionRatio, this.configuration.aspectRatio, true); + this.cropperSelection.$change(0, 0, Math.min(this.cropperCanvasRect.width, this.maxSize.width * selectionRatio), Math.min(this.cropperCanvasRect.height, this.maxSize.height * selectionRatio), this.configuration.aspectRatio, true); this.cropperSelection.$center(); this.cropperSelection.scrollIntoView({ block: "center", inline: "center" }); } From c9726adcce8c7c5e400ecff8dc83323cdddf8ef9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 11:59:15 +0100 Subject: [PATCH 35/35] Find the exact size for the image or use the minimum size for the image --- .../Core/Component/Image/Cropper.ts | 23 +++++++++++++++++++ .../Core/Component/Image/Cropper.js | 16 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts index 8212edbefe..5ff0cb49bd 100644 --- a/ts/WoltLabSuite/Core/Component/Image/Cropper.ts +++ b/ts/WoltLabSuite/Core/Component/Image/Cropper.ts @@ -319,6 +319,29 @@ class ExactImageCropper extends ImageCropper { return super.showDialog(); } + protected getCanvas(): Promise { + // Calculate the size of the image in relation to the window size + const selectionRatio = Math.min( + this.cropperCanvasRect!.width / this.width, + this.cropperCanvasRect!.height / this.height, + ); + const width = this.cropperSelection!.width / selectionRatio; + const height = width / this.configuration.aspectRatio; + + const sizes = this.configuration.sizes + .filter((size) => { + return width >= size.width && height >= size.height; + }) + .reverse(); + + const size = sizes.length > 0 ? sizes[0] : this.minSize; + + return this.cropperSelection!.$toCanvas({ + width: size.width, + height: size.height, + }); + } + public async loadImage(): Promise { await super.loadImage(); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js index 06c2cf3796..4fd299bde8 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js @@ -230,6 +230,22 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Image/Resizer", "WoltL } return super.showDialog(); } + getCanvas() { + // Calculate the size of the image in relation to the window size + const selectionRatio = Math.min(this.cropperCanvasRect.width / this.width, this.cropperCanvasRect.height / this.height); + const width = this.cropperSelection.width / selectionRatio; + const height = width / this.configuration.aspectRatio; + const sizes = this.configuration.sizes + .filter((size) => { + return width >= size.width && height >= size.height; + }) + .reverse(); + const size = sizes.length > 0 ? sizes[0] : this.minSize; + return this.cropperSelection.$toCanvas({ + width: size.width, + height: size.height, + }); + } async loadImage() { await super.loadImage(); const sizes = this.configuration.sizes