Skip to content

Commit 41f636d

Browse files
committed
Merge branch 'file-upload-image-crop' into avatar-file-processor
# Conflicts: # wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php
2 parents 0eb36d5 + bb9d061 commit 41f636d

File tree

8 files changed

+168
-44
lines changed

8 files changed

+168
-44
lines changed

.github/workflows/javascript.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ jobs:
7474
- name: "Check 'cropperjs'"
7575
run: |
7676
diff -wu wcfsetup/install/files/js/3rdParty/cropper.min.js node_modules/cropperjs/dist/cropper.min.js
77+
- name: "Check 'exifreader'"
78+
run: |
79+
diff -wu wcfsetup/install/files/js/3rdParty/exif-reader.js node_modules/exifreader/dist/exif-reader.js

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#5b582b24e437f1883ccad3c37dae7c3c5f1e7da3",
3030
"cropperjs": "2.0.0-rc.2",
3131
"emoji-picker-element": "^1.23.0",
32+
"exifreader": "^4.25.0",
3233
"focus-trap": "^7.6.1",
3334
"html-parsed-element": "^0.4.1",
3435
"perfect-scrollbar": "^1.5.6",
@@ -37,5 +38,15 @@
3738
"tabbable": "^6.2.0",
3839
"tslib": "^2.8.1",
3940
"webpack-cli": "^5.1.4"
41+
},
42+
"exifreader": {
43+
"include": {
44+
"jpeg": true,
45+
"png": true,
46+
"webp": true,
47+
"exif": [
48+
"Orientation"
49+
]
50+
}
4051
}
4152
}

ts/WoltLabSuite/Core/Component/Image/Cropper.ts

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { Selection } from "@cropper/element-selection";
1414
import { getPhrase } from "WoltLabSuite/Core/Language";
1515
import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog";
1616
import * as ExifUtil from "WoltLabSuite/Core/Image/ExifUtil";
17+
import ExifReader from "exifreader";
1718

1819
export 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() {

wcfsetup/install/files/js/3rdParty/exif-reader.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)