import WoltlabCoreFileElement from "./woltlab-core-file";
import { Response as UploadChunkResponse, uploadChunk } from "WoltLabSuite/Core/Api/Files/Chunk/Chunk";
import { generateThumbnails } from "WoltLabSuite/Core/Api/Files/GenerateThumbnails";
+import ImageResizer from "WoltLabSuite/Core/Image/Resizer";
export type ThumbnailsGenerated = {
data: GenerateThumbnailsResponse;
type GenerateThumbnailsResponse = ThumbnailData[];
+type ResizeConfiguration = {
+ maxWidth: number;
+ maxHeight: number;
+ fileType: "image/jpeg" | "image/webp" | "keep";
+ quality: number;
+};
+
async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
const typeName = element.dataset.typeName!;
.join("");
}
+function clearPreviousErrors(element: WoltlabCoreFileUploadElement): void {
+ element.parentElement?.querySelectorAll(".innerError").forEach((x) => x.remove());
+}
+
+async function resizeImage(element: WoltlabCoreFileUploadElement, file: File): Promise<File> {
+ switch (file.type) {
+ case "image/jpeg":
+ case "image/png":
+ case "image/webp":
+ // Potential candidate for a resize operation.
+ break;
+
+ default:
+ // Not an image or an unsupported file type.
+ return file;
+ }
+
+ const timeout = new Promise<File>((resolve) => {
+ window.setTimeout(() => resolve(file), 10_000);
+ });
+
+ const resizeConfiguration = JSON.parse(element.dataset.resizeConfiguration!) as ResizeConfiguration;
+
+ const resizer = new ImageResizer();
+ const { image, exif } = await resizer.loadFile(file);
+
+ const maxHeight = resizeConfiguration.maxHeight;
+ let maxWidth = resizeConfiguration.maxWidth;
+ if (window.devicePixelRatio >= 2) {
+ const actualWidth = window.screen.width * window.devicePixelRatio;
+ const actualHeight = window.screen.height * window.devicePixelRatio;
+
+ // If the dimensions are equal then this is a screenshot from a HiDPI
+ // device, thus we downscale this to the “natural” dimensions.
+ if (actualWidth === image.width && actualHeight === image.height) {
+ maxWidth = Math.min(maxWidth, window.screen.width);
+ }
+ }
+
+ const canvas = await resizer.resize(image, maxWidth, maxHeight, resizeConfiguration.quality, true, timeout);
+ if (canvas === undefined) {
+ // The resize operation failed, timed out or there was no need to perform
+ // any scaling whatsoever.
+ return file;
+ }
+
+ let fileType: string = resizeConfiguration.fileType;
+ if (fileType === "image/jpeg" || fileType === "image/webp") {
+ fileType = "image/webp";
+ } else {
+ fileType = file.type;
+ }
+
+ const resizedFile = await resizer.saveFile(
+ {
+ exif,
+ image: canvas,
+ },
+ file.name,
+ fileType,
+ resizeConfiguration.quality,
+ );
+
+ return resizedFile;
+}
+
export function setup(): void {
- wheneverFirstSeen("woltlab-core-file-upload", (element) => {
+ wheneverFirstSeen("woltlab-core-file-upload", (element: WoltlabCoreFileUploadElement) => {
element.addEventListener("upload", (event: CustomEvent<File>) => {
- // TODO: Add some pipeline logic here.
+ const file = event.detail;
- // TODO: Add a canvas based resize to the pipeline.
+ clearPreviousErrors(element);
- // TODO: We need to pass around the file using some dedicated type of
- // data structure because it can be modified somehow.
- void upload(element, event.detail);
+ void resizeImage(element, file).then((resizedFile) => {
+ void upload(element, resizedFile);
+ });
});
});
}
-define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails"], function (require, exports, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1) {
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload", "WoltLabSuite/Core/Api/Files/Chunk/Chunk", "WoltLabSuite/Core/Api/Files/GenerateThumbnails", "WoltLabSuite/Core/Image/Resizer"], function (require, exports, tslib_1, Selector_1, Upload_1, Chunk_1, GenerateThumbnails_1, Resizer_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = void 0;
+ Resizer_1 = tslib_1.__importDefault(Resizer_1);
async function upload(element, file) {
const typeName = element.dataset.typeName;
const fileHash = await getSha256Hash(await file.arrayBuffer());
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
+ function clearPreviousErrors(element) {
+ element.parentElement?.querySelectorAll(".innerError").forEach((x) => x.remove());
+ }
+ async function resizeImage(element, file) {
+ switch (file.type) {
+ case "image/jpeg":
+ case "image/png":
+ case "image/webp":
+ // Potential candidate for a resize operation.
+ break;
+ default:
+ // Not an image or an unsupported file type.
+ return file;
+ }
+ const timeout = new Promise((resolve) => {
+ window.setTimeout(() => resolve(file), 10000);
+ });
+ const resizeConfiguration = JSON.parse(element.dataset.resizeConfiguration);
+ const resizer = new Resizer_1.default();
+ const { image, exif } = await resizer.loadFile(file);
+ const maxHeight = resizeConfiguration.maxHeight;
+ let maxWidth = resizeConfiguration.maxWidth;
+ if (window.devicePixelRatio >= 2) {
+ const actualWidth = window.screen.width * window.devicePixelRatio;
+ const actualHeight = window.screen.height * window.devicePixelRatio;
+ // If the dimensions are equal then this is a screenshot from a HiDPI
+ // device, thus we downscale this to the “natural” dimensions.
+ if (actualWidth === image.width && actualHeight === image.height) {
+ maxWidth = Math.min(maxWidth, window.screen.width);
+ }
+ }
+ const canvas = await resizer.resize(image, maxWidth, maxHeight, resizeConfiguration.quality, true, timeout);
+ if (canvas === undefined) {
+ // The resize operation failed, timed out or there was no need to perform
+ // any scaling whatsoever.
+ return file;
+ }
+ let fileType = resizeConfiguration.fileType;
+ if (fileType === "image/jpeg" || fileType === "image/webp") {
+ fileType = "image/webp";
+ }
+ else {
+ fileType = file.type;
+ }
+ const resizedFile = await resizer.saveFile({
+ exif,
+ image: canvas,
+ }, file.name, fileType, resizeConfiguration.quality);
+ return resizedFile;
+ }
function setup() {
(0, Selector_1.wheneverFirstSeen)("woltlab-core-file-upload", (element) => {
element.addEventListener("upload", (event) => {
- void upload(element, event.detail);
+ const file = event.detail;
+ clearPreviousErrors(element);
+ void resizeImage(element, file).then((resizedFile) => {
+ void upload(element, resizedFile);
+ });
});
});
}
);
}
+ #[\Override]
+ public function getResizeConfiguration(): ResizeConfiguration
+ {
+ if (!\ATTACHMENT_IMAGE_AUTOSCALE) {
+ return ResizeConfiguration::unbounded();
+ }
+
+ return new ResizeConfiguration(
+ \ATTACHMENT_IMAGE_AUTOSCALE_MAX_WIDTH,
+ \ATTACHMENT_IMAGE_AUTOSCALE_MAX_HEIGHT,
+ ResizeFileType::fromString(\ATTACHMENT_IMAGE_AUTOSCALE_FILE_TYPE),
+ \ATTACHMENT_IMAGE_AUTOSCALE_QUALITY
+ );
+ }
+
#[\Override]
public function getThumbnailFormats(): array
{
data-type-name="%s"
data-context="%s"
data-file-extensions="%s"
+ data-resize-configuration="%s"
></woltlab-core-file-upload>
HTML,
StringUtil::encodeHTML($fileProcessor->getTypeName()),
StringUtil::encodeHTML(JSON::encode($context)),
StringUtil::encodeHTML($allowedFileExtensions),
+ StringUtil::encodeHTML(JSON::encode($fileProcessor->getResizeConfiguration())),
);
}
public function getAllowedFileExtensions(array $context): array;
+ public function getResizeConfiguration(): ResizeConfiguration;
+
public function getTypeName(): string;
public function getUploadResponse(File $file): array;
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor;
+
+final class ResizeConfiguration implements \JsonSerializable
+{
+ public function __construct(
+ public readonly int $maxWidth,
+ public readonly int $maxHeight,
+ public readonly ResizeFileType $fileType,
+ public readonly int $quality,
+ ) {
+ if ($quality <= 0 || $quality > 100) {
+ throw new \OutOfRangeException("The quality value must be larger than 0 and less than or equal to 100.");
+ }
+ }
+
+ #[\Override]
+ public function jsonSerialize(): mixed
+ {
+ return [
+ 'maxWidth' => $this->maxWidth,
+ 'maxHeight' => $this->maxHeight,
+ 'fileType' => $this->fileType->toString(),
+ 'quality' => $this->quality,
+ ];
+ }
+
+ public static function unbounded(): self
+ {
+ return new self(
+ -1,
+ -1,
+ ResizeFileType::Keep,
+ 100
+ );
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor;
+
+enum ResizeFileType
+{
+ case Jpeg;
+ case Keep;
+ case Webp;
+
+ public function toString(): string
+ {
+ return match ($this) {
+ self::Jpeg => 'image/jpeg',
+ self::Keep => 'keep',
+ self::Webp => 'image/webp',
+ };
+ }
+
+ public static function fromString(string $fileType): self
+ {
+ return match ($fileType) {
+ 'image/jpeg' => self::Jpeg,
+ 'keep' => self::Keep,
+ 'image/webp' => self::Webp,
+ };
+ }
+}