@@ -29,8 +29,8 @@ function inSelection(selection: Selection, maxSelection: Selection): boolean {
2929 return (
3030 selection . x >= maxSelection . x &&
3131 selection . y >= maxSelection . y &&
32- selection . x + selection . width <= maxSelection . x + maxSelection . width &&
33- selection . y + selection . height <= maxSelection . y + maxSelection . height
32+ Math . ceil ( selection . x + selection . width ) <= Math . ceil ( maxSelection . x + maxSelection . width ) &&
33+ Math . ceil ( selection . y + selection . height ) <= Math . ceil ( maxSelection . y + maxSelection . height )
3434 ) ;
3535}
3636
@@ -85,7 +85,7 @@ abstract class ImageCropper {
8585
8686 return new Promise < File > ( ( resolve , reject ) => {
8787 this . dialog ! . addEventListener ( "primary" , ( ) => {
88- this . cropperSelection ! . $toCanvas ( )
88+ void this . getCanvas ( )
8989 . then ( ( canvas ) => {
9090 this . resizer
9191 . saveFile (
@@ -107,6 +107,10 @@ abstract class ImageCropper {
107107 } ) ;
108108 }
109109
110+ protected getCanvas ( ) : Promise < HTMLCanvasElement > {
111+ return this . cropperSelection ! . $toCanvas ( ) ;
112+ }
113+
110114 public async loadImage ( ) {
111115 const { image, exif } = await this . resizer . loadFile ( this . file ) ;
112116 this . image = image ;
@@ -138,11 +142,9 @@ abstract class ImageCropper {
138142 this . cropperCanvas ! . style . aspectRatio = `${ this . width } /${ this . height } ` ;
139143
140144 if ( this . width >= this . height ) {
141- this . cropperCanvas ! . style . width = `min(70vw, ${ this . width } px)` ;
142- this . cropperCanvas ! . style . height = "auto" ;
145+ this . cropperCanvas ! . style . maxHeight = "100%" ;
143146 } else {
144- this . cropperCanvas ! . style . height = `min(60vh, ${ this . height } px)` ;
145- this . cropperCanvas ! . style . width = "auto" ;
147+ this . cropperCanvas ! . style . maxWidth = "100%" ;
146148 }
147149
148150 this . cropperSelection ! . aspectRatio = this . configuration . aspectRatio ;
@@ -171,12 +173,11 @@ abstract class ImageCropper {
171173 const cropperCanvasRect = this . cropperCanvas ! . getBoundingClientRect ( ) ;
172174 const selection = event . detail as Selection ;
173175
174- const cropperImageRect = this . cropperImage ! . getBoundingClientRect ( ) ;
175176 const maxSelection : Selection = {
176- x : Math . round ( cropperImageRect . left - cropperCanvasRect . left ) ,
177- y : Math . round ( cropperImageRect . top - cropperCanvasRect . top ) ,
178- width : Math . round ( cropperImageRect . width ) ,
179- height : Math . round ( cropperImageRect . height ) ,
177+ x : 0 ,
178+ y : 0 ,
179+ width : cropperCanvasRect . width ,
180+ height : cropperCanvasRect . height ,
180181 } ;
181182
182183 if ( ! inSelection ( selection , maxSelection ) ) {
@@ -247,17 +248,15 @@ class ExactImageCropper extends ImageCropper {
247248 }
248249
249250 protected getCropperTemplate ( ) : string {
250- return `<div class="cropperContainer">
251- <cropper-canvas background>
252- <cropper-image rotatable></cropper-image>
253- <cropper-shade hidden></cropper-shade>
254- <cropper-selection movable outlined keyboard>
255- <cropper-grid role="grid" bordered covered></cropper-grid>
256- <cropper-crosshair centered></cropper-crosshair>
257- <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
258- </cropper-selection>
259- </cropper-canvas>
260- </div>` ;
251+ return `<cropper-canvas background>
252+ <cropper-image rotatable></cropper-image>
253+ <cropper-shade hidden></cropper-shade>
254+ <cropper-selection movable outlined keyboard>
255+ <cropper-grid role="grid" bordered covered></cropper-grid>
256+ <cropper-crosshair centered></cropper-crosshair>
257+ <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
258+ </cropper-selection>
259+ </cropper-canvas>` ;
261260 }
262261
263262 protected setCropperStyle ( ) {
@@ -273,6 +272,7 @@ class ExactImageCropper extends ImageCropper {
273272}
274273
275274class MinMaxImageCropper extends ImageCropper {
275+ #cropperCanvasRect?: DOMRect ;
276276 constructor ( element : WoltlabCoreFileUploadElement , file : File , configuration : CropperConfiguration ) {
277277 super ( element , file , configuration ) ;
278278 if ( configuration . sizes . length !== 2 ) {
@@ -292,39 +292,40 @@ class MinMaxImageCropper extends ImageCropper {
292292 return getPhrase ( "wcf.global.button.reset" ) ;
293293 }
294294
295- protected getCropperTemplate ( ) : string {
296- return `<div class="cropperContainer">
297- <cropper-canvas background scale-step="0.0">
298- <cropper-image skewable scalable translatable rotatable></cropper-image>
299- <cropper-shade hidden></cropper-shade>
300- <cropper-handle action="scale" hidden disabled></cropper-handle>
301- <cropper-selection movable resizable outlined>
302- <cropper-grid role="grid" bordered covered></cropper-grid>
303- <cropper-crosshair centered></cropper-crosshair>
304- <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
305- <cropper-handle action="n-resize"></cropper-handle>
306- <cropper-handle action="e-resize"></cropper-handle>
307- <cropper-handle action="s-resize"></cropper-handle>
308- <cropper-handle action="w-resize"></cropper-handle>
309- <cropper-handle action="ne-resize"></cropper-handle>
310- <cropper-handle action="nw-resize"></cropper-handle>
311- <cropper-handle action="se-resize"></cropper-handle>
312- <cropper-handle action="sw-resize"></cropper-handle>
313- </cropper-selection>
314- </cropper-canvas>
315- </div>` ;
316- }
317-
318- protected setCropperStyle ( ) {
319- super . setCropperStyle ( ) ;
295+ public async loadImage ( ) : Promise < void > {
296+ await super . loadImage ( ) ;
320297
321- if ( this . width >= this . height ) {
322- this . cropperCanvas ! . style . width = `${ Math . min ( this . maxSize . width , this . width ) } px` ;
323- } else {
324- this . cropperCanvas ! . style . height = `${ Math . min ( this . maxSize . height , this . height ) } px` ;
298+ if ( this . image ! . width < this . minSize . width || this . image ! . height < this . minSize . height ) {
299+ throw new Error (
300+ getPhrase ( "wcf.upload.error.image.tooSmall" , {
301+ width : this . minSize . width ,
302+ height : this . minSize . height ,
303+ } ) ,
304+ ) ;
325305 }
326306 }
327307
308+ protected getCropperTemplate ( ) : string {
309+ return `<cropper-canvas background scale-step="0.0">
310+ <cropper-image skewable scalable translatable rotatable></cropper-image>
311+ <cropper-shade hidden></cropper-shade>
312+ <cropper-handle action="scale" hidden disabled></cropper-handle>
313+ <cropper-selection precise movable resizable outlined>
314+ <cropper-grid role="grid" bordered covered></cropper-grid>
315+ <cropper-crosshair centered></cropper-crosshair>
316+ <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
317+ <cropper-handle action="n-resize"></cropper-handle>
318+ <cropper-handle action="e-resize"></cropper-handle>
319+ <cropper-handle action="s-resize"></cropper-handle>
320+ <cropper-handle action="w-resize"></cropper-handle>
321+ <cropper-handle action="ne-resize"></cropper-handle>
322+ <cropper-handle action="nw-resize"></cropper-handle>
323+ <cropper-handle action="se-resize"></cropper-handle>
324+ <cropper-handle action="sw-resize"></cropper-handle>
325+ </cropper-selection>
326+ </cropper-canvas>` ;
327+ }
328+
328329 protected createCropper ( ) {
329330 super . createCropper ( ) ;
330331
@@ -335,31 +336,46 @@ class MinMaxImageCropper extends ImageCropper {
335336 // Limit the selection to the min/max size
336337 this . cropperSelection ! . addEventListener ( "change" , ( event : CustomEvent ) => {
337338 const selection = event . detail as Selection ;
339+ this . #cropperCanvasRect = this . cropperCanvas ! . getBoundingClientRect ( ) ;
340+
341+ const maxImageWidth = Math . min ( this . image ! . width , this . maxSize . width ) ;
342+ const widthRatio = this . #cropperCanvasRect. width / maxImageWidth ;
343+
344+ const minWidth = this . minSize . width * widthRatio ;
345+ const maxWidth = this . maxSize . width * widthRatio ;
346+ const minHeight = minWidth / this . configuration . aspectRatio ;
347+ const maxHeight = maxWidth / this . configuration . aspectRatio ;
338348
339349 if (
340- selection . width < this . minSize . width ||
341- selection . height < this . minSize . height ||
342- selection . width > this . maxSize . width ||
343- selection . height > this . maxSize . height
350+ selection . width < minWidth ||
351+ selection . height < minHeight ||
352+ selection . width > maxWidth ||
353+ selection . height > maxHeight
344354 ) {
345355 event . preventDefault ( ) ;
346356 }
347357 } ) ;
348358 }
349359
360+ protected getCanvas ( ) : Promise < HTMLCanvasElement > {
361+ // Calculate the size of the image in relation to the window size
362+ const maxImageWidth = Math . min ( this . image ! . width , this . maxSize . width ) ;
363+ const widthRatio = this . #cropperCanvasRect! . width / maxImageWidth ;
364+ const width = this . cropperSelection ! . width / widthRatio ;
365+ const height = width / this . configuration . aspectRatio ;
366+
367+ return this . cropperSelection ! . $toCanvas ( {
368+ width : Math . max ( Math . min ( Math . ceil ( width ) , this . maxSize . width ) , this . minSize . width ) ,
369+ height : Math . max ( Math . min ( Math . ceil ( height ) , this . maxSize . height ) , this . minSize . height ) ,
370+ } ) ;
371+ }
372+
350373 protected centerSelection ( ) : void {
351374 this . cropperImage ! . $center ( "contain" ) ;
352375
353376 const { width : imageWidth } = this . cropperImage ! . getBoundingClientRect ( ) ;
354377
355- this . cropperSelection ! . $change (
356- 0 ,
357- 0 ,
358- imageWidth ,
359- 0 ,
360- this . configuration . aspectRatio ,
361- true ,
362- ) ;
378+ this . cropperSelection ! . $change ( 0 , 0 , imageWidth , 0 , this . configuration . aspectRatio , true ) ;
363379 this . cropperSelection ! . $center ( ) ;
364380 this . cropperSelection ! . scrollIntoView ( { block : "center" , inline : "center" } ) ;
365381 }
0 commit comments