@@ -47,6 +47,7 @@ abstract class ImageCropper {
4747 protected dialog ?: WoltlabCoreDialogElement ;
4848 protected exif ?: ExifUtil . Exif ;
4949 protected orientation ?: number ;
50+ protected cropperCanvasRect ?: DOMRect ;
5051 #cropper?: Cropper ;
5152
5253 constructor ( element : WoltlabCoreFileUploadElement , file : File , configuration : CropperConfiguration ) {
@@ -76,6 +77,31 @@ abstract class ImageCropper {
7677 }
7778 }
7879
80+ abstract get minSize ( ) : { width : number ; height : number } ;
81+
82+ abstract get maxSize ( ) : { width : number ; height : number } ;
83+
84+ public async loadImage ( ) {
85+ const { image, exif } = await this . resizer . loadFile ( this . file ) ;
86+ this . image = image ;
87+ this . exif = exif ;
88+ const tags = await ExifReader . load ( this . file ) ;
89+ if ( tags . Orientation ) {
90+ switch ( tags . Orientation . value ) {
91+ case 3 :
92+ this . orientation = 180 ;
93+ break ;
94+ case 6 :
95+ this . orientation = 90 ;
96+ break ;
97+ case 8 :
98+ this . orientation = 270 ;
99+ break ;
100+ // Any other rotation is unsupported.
101+ }
102+ }
103+ }
104+
79105 public async showDialog ( ) : Promise < File > {
80106 this . dialog = dialogFactory ( ) . fromElement ( this . image ! ) . asPrompt ( {
81107 extra : this . getDialogExtra ( ) ,
@@ -126,47 +152,23 @@ abstract class ImageCropper {
126152 } ) ;
127153 }
128154
129- protected getCanvas ( ) : Promise < HTMLCanvasElement > {
130- return this . cropperSelection ! . $toCanvas ( ) ;
131- }
132-
133- public async loadImage ( ) {
134- const { image, exif } = await this . resizer . loadFile ( this . file ) ;
135- this . image = image ;
136- this . exif = exif ;
137- const tags = await ExifReader . load ( this . file ) ;
138- if ( tags . Orientation ) {
139- switch ( tags . Orientation . value ) {
140- case 3 :
141- this . orientation = 180 ;
142- break ;
143- case 6 :
144- this . orientation = 90 ;
145- break ;
146- case 8 :
147- this . orientation = 270 ;
148- break ;
149- // Any other rotation is unsupported.
150- }
151- }
152- }
153-
154- protected abstract getCropperTemplate ( ) : string ;
155-
156155 protected getDialogExtra ( ) : string | undefined {
157156 return undefined ;
158157 }
159158
160- protected setCropperStyle ( ) {
161- this . cropperCanvas ! . style . aspectRatio = ` ${ this . width } / ${ this . height } ` ;
162-
163- if ( this . width >= this . height ) {
164- this . cropperCanvas ! . style . maxHeight = "100%" ;
165- } else {
166- this . cropperCanvas ! . style . maxWidth = "100%" ;
167- }
159+ protected getCanvas ( ) : Promise < HTMLCanvasElement > {
160+ // Calculate the size of the image in relation to the window size
161+ const selectionRatio = Math . min (
162+ this . cropperCanvasRect ! . width / this . width ,
163+ this . cropperCanvasRect ! . height / this . height ,
164+ ) ;
165+ const width = this . cropperSelection ! . width / selectionRatio ;
166+ const height = width / this . configuration . aspectRatio ;
168167
169- this . cropperSelection ! . aspectRatio = this . configuration . aspectRatio ;
168+ return this . cropperSelection ! . $toCanvas ( {
169+ width : Math . max ( Math . min ( Math . floor ( width ) , this . maxSize . width ) , this . minSize . width ) ,
170+ height : Math . max ( Math . min ( Math . ceil ( height ) , this . maxSize . height ) , this . minSize . height ) ,
171+ } ) ;
170172 }
171173
172174 protected createCropper ( ) {
@@ -203,23 +205,108 @@ abstract class ImageCropper {
203205 event . preventDefault ( ) ;
204206 }
205207 } ) ;
208+
209+ // Limit the selection to the min/max size
210+ this . cropperSelection ! . addEventListener ( "change" , ( event : CustomEvent ) => {
211+ const selection = event . detail as Selection ;
212+ this . cropperCanvasRect = this . cropperCanvas ! . getBoundingClientRect ( ) ;
213+
214+ const selectionRatio = Math . min (
215+ this . cropperCanvasRect . width / this . width ,
216+ this . cropperCanvasRect . height / this . height ,
217+ ) ;
218+
219+ const minWidth = this . minSize . width * selectionRatio ;
220+ const maxWidth = this . cropperCanvasRect . width ;
221+ const minHeight = minWidth / this . configuration . aspectRatio ;
222+ const maxHeight = maxWidth / this . configuration . aspectRatio ;
223+
224+ if (
225+ Math . round ( selection . width ) < minWidth ||
226+ Math . round ( selection . height ) < minHeight ||
227+ Math . round ( selection . width ) > maxWidth ||
228+ Math . round ( selection . height ) > maxHeight
229+ ) {
230+ event . preventDefault ( ) ;
231+ }
232+ } ) ;
233+ }
234+
235+ protected setCropperStyle ( ) {
236+ this . cropperCanvas ! . style . aspectRatio = `${ this . width } /${ this . height } ` ;
237+
238+ this . cropperSelection ! . aspectRatio = this . configuration . aspectRatio ;
206239 }
207240
208241 protected centerSelection ( ) : void {
242+ // Set to the maximum size
243+ this . cropperCanvas ! . style . width = `${ this . width } px` ;
244+ this . cropperCanvas ! . style . height = `${ this . height } px` ;
245+
246+ const dimension = DomUtil . innerDimensions ( this . cropperCanvas ! . parentElement ! ) ;
247+ const ratio = Math . min ( dimension . width / this . width , dimension . height / this . height ) ;
248+
249+ this . cropperCanvas ! . style . height = `${ this . height * ratio } px` ;
250+ this . cropperCanvas ! . style . width = `${ this . width * ratio } px` ;
251+
209252 this . cropperImage ! . $center ( "contain" ) ;
253+ this . cropperCanvasRect = this . cropperImage ! . getBoundingClientRect ( ) ;
254+
255+ const selectionRatio = Math . min (
256+ this . cropperCanvasRect . width / this . maxSize . width ,
257+ this . cropperCanvasRect . height / this . maxSize . height ,
258+ ) ;
259+
260+ this . cropperSelection ! . $change (
261+ 0 ,
262+ 0 ,
263+ this . maxSize . width * selectionRatio ,
264+ this . maxSize . height * selectionRatio ,
265+ this . configuration . aspectRatio ,
266+ true ,
267+ ) ;
268+
210269 this . cropperSelection ! . $center ( ) ;
211270 this . cropperSelection ! . scrollIntoView ( { block : "center" , inline : "center" } ) ;
212271 }
272+
273+ protected getCropperTemplate ( ) : string {
274+ return `<cropper-canvas background scale-step="0.0">
275+ <cropper-image skewable scalable translatable rotatable></cropper-image>
276+ <cropper-shade hidden></cropper-shade>
277+ <cropper-handle action="scale" hidden disabled></cropper-handle>
278+ <cropper-selection precise movable resizable outlined>
279+ <cropper-grid role="grid" bordered covered></cropper-grid>
280+ <cropper-crosshair centered></cropper-crosshair>
281+ <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
282+ <cropper-handle action="n-resize"></cropper-handle>
283+ <cropper-handle action="e-resize"></cropper-handle>
284+ <cropper-handle action="s-resize"></cropper-handle>
285+ <cropper-handle action="w-resize"></cropper-handle>
286+ <cropper-handle action="ne-resize"></cropper-handle>
287+ <cropper-handle action="nw-resize"></cropper-handle>
288+ <cropper-handle action="se-resize"></cropper-handle>
289+ <cropper-handle action="sw-resize"></cropper-handle>
290+ </cropper-selection>
291+ </cropper-canvas>` ;
292+ }
213293}
214294
215295class ExactImageCropper extends ImageCropper {
216- #size?: { width : number ; height : number } ;
296+ get minSize ( ) {
297+ return this . configuration . sizes [ 0 ] ;
298+ }
299+
300+ get maxSize ( ) {
301+ return this . configuration . sizes [ this . configuration . sizes . length - 1 ] ;
302+ }
217303
218304 public async showDialog ( ) : Promise < File > {
219305 // The image already has the correct size, cropping is not necessary
220306 if (
221- this . width == this . #size! . width &&
222- this . height == this . #size! . height &&
307+ this . configuration . sizes . filter ( ( size ) => {
308+ return size . width == this . width && size . height == this . height ;
309+ } ) . length > 0 &&
223310 this . image instanceof HTMLCanvasElement
224311 ) {
225312 return this . resizer . saveFile (
@@ -235,14 +322,17 @@ class ExactImageCropper extends ImageCropper {
235322 public async loadImage ( ) : Promise < void > {
236323 await super . loadImage ( ) ;
237324
238- const timeout = new Promise < File > ( ( resolve ) => {
239- window . setTimeout ( ( ) => resolve ( this . file ) , 10_000 ) ;
240- } ) ;
241-
242- // resize image to the largest possible size
243- const sizes = this . configuration . sizes . filter ( ( size ) => {
244- return size . width <= this . width && size . height <= this . height ;
245- } ) ;
325+ const sizes = this . configuration . sizes
326+ . filter ( ( size ) => {
327+ return size . width <= this . width && size . height <= this . height ;
328+ } )
329+ . sort ( ( a , b ) => {
330+ if ( this . configuration . aspectRatio >= 1 ) {
331+ return a . width - b . width ;
332+ } else {
333+ return a . height - b . height ;
334+ }
335+ } ) ;
246336
247337 if ( sizes . length === 0 ) {
248338 const smallestSize =
@@ -255,43 +345,11 @@ class ExactImageCropper extends ImageCropper {
255345 ) ;
256346 }
257347
258- this . #size = sizes [ sizes . length - 1 ] ;
259- this . image = await this . resizer . resize (
260- this . image as HTMLImageElement ,
261- this . width >= this . height ? this . width : this . #size. width ,
262- this . height > this . width ? this . height : this . #size. height ,
263- this . resizer . quality ,
264- true ,
265- timeout ,
266- ) ;
267- }
268-
269- protected getCropperTemplate ( ) : string {
270- return `<cropper-canvas background>
271- <cropper-image rotatable></cropper-image>
272- <cropper-shade hidden></cropper-shade>
273- <cropper-selection movable outlined keyboard>
274- <cropper-grid role="grid" bordered covered></cropper-grid>
275- <cropper-crosshair centered></cropper-crosshair>
276- <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
277- </cropper-selection>
278- </cropper-canvas>` ;
279- }
280-
281- protected setCropperStyle ( ) {
282- super . setCropperStyle ( ) ;
283-
284- this . cropperSelection ! . width = this . #size! . width ;
285- this . cropperSelection ! . height = this . #size! . height ;
286-
287- this . cropperCanvas ! . style . width = `${ this . width } px` ;
288- this . cropperCanvas ! . style . height = `${ this . height } px` ;
289- this . cropperSelection ! . style . removeProperty ( "aspectRatio" ) ;
348+ this . configuration . sizes = sizes ;
290349 }
291350}
292351
293352class MinMaxImageCropper extends ImageCropper {
294- #cropperCanvasRect?: DOMRect ;
295353 constructor ( element : WoltlabCoreFileUploadElement , file : File , configuration : CropperConfiguration ) {
296354 super ( element , file , configuration ) ;
297355 if ( configuration . sizes . length !== 2 ) {
@@ -314,7 +372,7 @@ class MinMaxImageCropper extends ImageCropper {
314372 public async loadImage ( ) : Promise < void > {
315373 await super . loadImage ( ) ;
316374
317- if ( this . image ! . width < this . minSize . width || this . image ! . height < this . minSize . height ) {
375+ if ( this . width < this . minSize . width || this . height < this . minSize . height ) {
318376 throw new Error (
319377 getPhrase ( "wcf.upload.error.image.tooSmall" , {
320378 width : this . minSize . width ,
@@ -324,105 +382,12 @@ class MinMaxImageCropper extends ImageCropper {
324382 }
325383 }
326384
327- protected getCropperTemplate ( ) : string {
328- return `<cropper-canvas background scale-step="0.0">
329- <cropper-image skewable scalable translatable rotatable></cropper-image>
330- <cropper-shade hidden></cropper-shade>
331- <cropper-handle action="scale" hidden disabled></cropper-handle>
332- <cropper-selection precise movable resizable outlined>
333- <cropper-grid role="grid" bordered covered></cropper-grid>
334- <cropper-crosshair centered></cropper-crosshair>
335- <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
336- <cropper-handle action="n-resize"></cropper-handle>
337- <cropper-handle action="e-resize"></cropper-handle>
338- <cropper-handle action="s-resize"></cropper-handle>
339- <cropper-handle action="w-resize"></cropper-handle>
340- <cropper-handle action="ne-resize"></cropper-handle>
341- <cropper-handle action="nw-resize"></cropper-handle>
342- <cropper-handle action="se-resize"></cropper-handle>
343- <cropper-handle action="sw-resize"></cropper-handle>
344- </cropper-selection>
345- </cropper-canvas>` ;
346- }
347-
348385 protected createCropper ( ) {
349386 super . createCropper ( ) ;
350387
351388 this . dialog ! . addEventListener ( "extra" , ( ) => {
352389 this . centerSelection ( ) ;
353390 } ) ;
354-
355- // Limit the selection to the min/max size
356- this . cropperSelection ! . addEventListener ( "change" , ( event : CustomEvent ) => {
357- const selection = event . detail as Selection ;
358- this . #cropperCanvasRect = this . cropperCanvas ! . getBoundingClientRect ( ) ;
359-
360- const maxImageWidth = Math . min ( this . image ! . width , this . maxSize . width ) ;
361- const maxImageHeight = Math . min ( this . image ! . height , this . maxSize . height ) ;
362- const selectionRatio = Math . min (
363- this . #cropperCanvasRect. width / maxImageWidth ,
364- this . #cropperCanvasRect. height / maxImageHeight ,
365- ) ;
366-
367- const minWidth = this . minSize . width * selectionRatio ;
368- const maxWidth = this . maxSize . width * selectionRatio ;
369- const minHeight = minWidth / this . configuration . aspectRatio ;
370- const maxHeight = maxWidth / this . configuration . aspectRatio ;
371-
372- if (
373- Math . round ( selection . width ) < minWidth ||
374- Math . round ( selection . height ) < minHeight ||
375- Math . round ( selection . width ) > maxWidth ||
376- Math . round ( selection . height ) > maxHeight
377- ) {
378- event . preventDefault ( ) ;
379- }
380- } ) ;
381- }
382-
383- protected getCanvas ( ) : Promise < HTMLCanvasElement > {
384- // Calculate the size of the image in relation to the window size
385- const maxImageWidth = Math . min ( this . image ! . width , this . maxSize . width ) ;
386- const widthRatio = this . #cropperCanvasRect! . width / maxImageWidth ;
387- const width = this . cropperSelection ! . width / widthRatio ;
388- const height = width / this . configuration . aspectRatio ;
389-
390- return this . cropperSelection ! . $toCanvas ( {
391- width : Math . max ( Math . min ( Math . ceil ( width ) , this . maxSize . width ) , this . minSize . width ) ,
392- height : Math . max ( Math . min ( Math . ceil ( height ) , this . maxSize . height ) , this . minSize . height ) ,
393- } ) ;
394- }
395-
396- protected centerSelection ( ) : void {
397- // Reset to get the maximum available height and width
398- this . cropperCanvas ! . style . height = "" ;
399- this . cropperCanvas ! . style . width = "" ;
400-
401- const dimension = DomUtil . innerDimensions ( this . cropperCanvas ! . parentElement ! ) ;
402- const ratio = Math . min ( dimension . width / this . image ! . width , dimension . height / this . image ! . height ) ;
403-
404- this . cropperCanvas ! . style . height = `${ this . image ! . height * ratio } px` ;
405- this . cropperCanvas ! . style . width = `${ this . image ! . width * ratio } px` ;
406-
407- this . cropperImage ! . $center ( "contain" ) ;
408- this . #cropperCanvasRect = this . cropperImage ! . getBoundingClientRect ( ) ;
409-
410- const selectionRatio = Math . min (
411- this . #cropperCanvasRect. width / this . maxSize . width ,
412- this . #cropperCanvasRect. height / this . maxSize . height ,
413- ) ;
414-
415- this . cropperSelection ! . $change (
416- 0 ,
417- 0 ,
418- this . maxSize . width * selectionRatio ,
419- this . maxSize . height * selectionRatio ,
420- this . configuration . aspectRatio ,
421- true ,
422- ) ;
423-
424- this . cropperSelection ! . $center ( ) ;
425- this . cropperSelection ! . scrollIntoView ( { block : "center" , inline : "center" } ) ;
426391 }
427392}
428393
0 commit comments