@@ -14,6 +14,7 @@ import type { Selection } from "@cropper/element-selection";
1414import { getPhrase } from "WoltLabSuite/Core/Language" ;
1515import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog" ;
1616import * as ExifUtil from "WoltLabSuite/Core/Image/ExifUtil" ;
17+ import ExifReader from "exifreader" ;
1718
1819export interface CropperConfiguration {
1920 aspectRatio : number ;
@@ -35,6 +36,7 @@ abstract class ImageCropper {
3536 protected cropperSelection ?: CropperSelection | null ;
3637 protected dialog ?: WoltlabCoreDialogElement ;
3738 protected exif ?: ExifUtil . Exif ;
39+ protected orientation ?: number ;
3840 #cropper?: Cropper ;
3941
4042 constructor ( element : WoltlabCoreFileUploadElement , file : File , configuration : CropperConfiguration ) {
@@ -44,6 +46,26 @@ abstract class ImageCropper {
4446 this . resizer = new ImageResizer ( ) ;
4547 }
4648
49+ protected get width ( ) {
50+ switch ( this . orientation ) {
51+ case 90 :
52+ case 270 :
53+ return this . image ! . height ;
54+ default :
55+ return this . image ! . width ;
56+ }
57+ }
58+
59+ protected get height ( ) {
60+ switch ( this . orientation ) {
61+ case 90 :
62+ case 270 :
63+ return this . image ! . width ;
64+ default :
65+ return this . image ! . height ;
66+ }
67+ }
68+
4769 public async showDialog ( ) : Promise < File > {
4870 this . dialog = dialogFactory ( ) . fromElement ( this . image ! ) . asPrompt ( {
4971 extra : this . getDialogExtra ( ) ,
@@ -57,7 +79,11 @@ abstract class ImageCropper {
5779 this . cropperSelection ! . $toCanvas ( )
5880 . then ( ( canvas ) => {
5981 this . resizer
60- . saveFile ( { exif : this . exif , image : canvas } , this . file . name , this . file . type )
82+ . saveFile (
83+ { exif : this . orientation ? undefined : this . exif , image : canvas } ,
84+ this . file . name ,
85+ this . file . type ,
86+ )
6187 . then ( ( resizedFile ) => {
6288 resolve ( resizedFile ) ;
6389 } )
@@ -76,28 +102,43 @@ abstract class ImageCropper {
76102 const { image, exif } = await this . resizer . loadFile ( this . file ) ;
77103 this . image = image ;
78104 this . exif = exif ;
105+ const tags = await ExifReader . load ( this . file ) ;
106+ if ( tags . Orientation ) {
107+ switch ( tags . Orientation . value ) {
108+ case 3 :
109+ this . orientation = 180 ;
110+ break ;
111+ case 6 :
112+ this . orientation = 90 ;
113+ break ;
114+ case 8 :
115+ this . orientation = 270 ;
116+ break ;
117+ // Any other rotation is unsupported.
118+ }
119+ }
120+ }
121+
122+ protected abstract getCropperTemplate ( ) : string ;
123+
124+ protected getDialogExtra ( ) : string | undefined {
125+ return undefined ;
79126 }
80127
81128 protected setCropperStyle ( ) {
82- this . cropperCanvas ! . style . aspectRatio = `${ this . image ! . width } /${ this . image ! . height } ` ;
129+ this . cropperCanvas ! . style . aspectRatio = `${ this . width } /${ this . height } ` ;
83130
84- if ( this . image ! . width > this . image ! . height ) {
85- this . cropperCanvas ! . style . width = `min(70vw, ${ this . image ! . width } px)` ;
131+ if ( this . width > this . height ) {
132+ this . cropperCanvas ! . style . width = `min(70vw, ${ this . width } px)` ;
86133 this . cropperCanvas ! . style . height = "auto" ;
87134 } else {
88- this . cropperCanvas ! . style . height = `min(60vh, ${ this . image ! . height } px)` ;
135+ this . cropperCanvas ! . style . height = `min(60vh, ${ this . height } px)` ;
89136 this . cropperCanvas ! . style . width = "auto" ;
90137 }
91138
92139 this . cropperSelection ! . aspectRatio = this . configuration . aspectRatio ;
93140 }
94141
95- protected abstract getCropperTemplate ( ) : string ;
96-
97- protected getDialogExtra ( ) : string | undefined {
98- return undefined ;
99- }
100-
101142 protected createCropper ( ) {
102143 this . #cropper = new Cropper ( this . image ! , {
103144 template : this . getCropperTemplate ( ) ,
@@ -109,6 +150,9 @@ abstract class ImageCropper {
109150
110151 this . setCropperStyle ( ) ;
111152
153+ if ( this . orientation ) {
154+ this . cropperImage ! . $rotate ( `${ this . orientation } deg` ) ;
155+ }
112156 this . cropperImage ! . $center ( "contain" ) ;
113157 this . cropperSelection ! . $center ( ) ;
114158
@@ -143,11 +187,15 @@ class ExactImageCropper extends ImageCropper {
143187 public async showDialog ( ) : Promise < File > {
144188 // The image already has the correct size, cropping is not necessary
145189 if (
146- this . image ! . width == this . #size! . width &&
147- this . image ! . height == this . #size! . height &&
190+ this . width == this . #size! . width &&
191+ this . height == this . #size! . height &&
148192 this . image instanceof HTMLCanvasElement
149193 ) {
150- return this . resizer . saveFile ( { exif : this . exif , image : this . image } , this . file . name , this . file . type ) ;
194+ return this . resizer . saveFile (
195+ { exif : this . orientation ? undefined : this . exif , image : this . image } ,
196+ this . file . name ,
197+ this . file . type ,
198+ ) ;
151199 }
152200
153201 return super . showDialog ( ) ;
@@ -162,7 +210,7 @@ class ExactImageCropper extends ImageCropper {
162210
163211 // resize image to the largest possible size
164212 const sizes = this . configuration . sizes . filter ( ( size ) => {
165- return size . width <= this . image ! . width && size . height <= this . image ! . height ;
213+ return size . width <= this . width && size . height <= this . height ;
166214 } ) ;
167215
168216 if ( sizes . length === 0 ) {
@@ -179,8 +227,8 @@ class ExactImageCropper extends ImageCropper {
179227 this . #size = sizes [ sizes . length - 1 ] ;
180228 this . image = await this . resizer . resize (
181229 this . image as HTMLImageElement ,
182- this . image ! . width >= this . image ! . height ? this . image ! . width : this . #size. width ,
183- this . image ! . height > this . image ! . width ? this . image ! . height : this . #size. height ,
230+ this . width >= this . height ? this . width : this . #size. width ,
231+ this . height > this . width ? this . height : this . #size. height ,
184232 this . resizer . quality ,
185233 true ,
186234 timeout ,
@@ -190,7 +238,7 @@ class ExactImageCropper extends ImageCropper {
190238 protected getCropperTemplate ( ) : string {
191239 return `<div class="cropperContainer">
192240 <cropper-canvas background>
193- <cropper-image></cropper-image>
241+ <cropper-image rotatable ></cropper-image>
194242 <cropper-shade hidden></cropper-shade>
195243 <cropper-selection movable outlined keyboard>
196244 <cropper-grid role="grid" bordered covered></cropper-grid>
@@ -207,8 +255,8 @@ class ExactImageCropper extends ImageCropper {
207255 this . cropperSelection ! . width = this . #size! . width ;
208256 this . cropperSelection ! . height = this . #size! . height ;
209257
210- this . cropperCanvas ! . style . width = `${ this . image ! . width } px` ;
211- this . cropperCanvas ! . style . height = `${ this . image ! . height } px` ;
258+ this . cropperCanvas ! . style . width = `${ this . width } px` ;
259+ this . cropperCanvas ! . style . height = `${ this . height } px` ;
212260 this . cropperSelection ! . style . removeProperty ( "aspectRatio" ) ;
213261 }
214262}
@@ -236,7 +284,7 @@ class MinMaxImageCropper extends ImageCropper {
236284 protected getCropperTemplate ( ) : string {
237285 return `<div class="cropperContainer">
238286 <cropper-canvas background>
239- <cropper-image skewable scalable translatable></cropper-image>
287+ <cropper-image skewable scalable translatable rotatable ></cropper-image>
240288 <cropper-shade hidden></cropper-shade>
241289 <cropper-handle action="move" plain></cropper-handle>
242290 <cropper-selection movable zoomable resizable outlined>
@@ -261,8 +309,8 @@ class MinMaxImageCropper extends ImageCropper {
261309
262310 this . cropperSelection ! . width = this . minSize . width ;
263311 this . cropperSelection ! . height = this . minSize . height ;
264- this . cropperCanvas ! . style . minWidth = `min(${ this . maxSize . width } px, ${ this . image ! . width } px)` ;
265- this . cropperCanvas ! . style . minHeight = `min(${ this . maxSize . height } px, ${ this . image ! . height } px)` ;
312+ this . cropperCanvas ! . style . minWidth = `min(${ this . maxSize . width } px, ${ this . width } px)` ;
313+ this . cropperCanvas ! . style . minHeight = `min(${ this . maxSize . height } px, ${ this . height } px)` ;
266314 }
267315
268316 protected createCropper ( ) {
0 commit comments