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%; -}