diff --git a/package.json b/package.json index bcbcd48b7..7c2abfa3b 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,8 @@ "vkbeautify": "^0.99.3", "xpath": "0.0.34", "xregexp": "^5.1.2", - "zlibjs": "^0.3.1" + "zlibjs": "^0.3.1", + "@jimp/wasm-webp": "^1.6.0" }, "scripts": { "start": "npx grunt dev", @@ -208,4 +209,4 @@ "getheapsize": "node -e 'console.log(`node heap limit = ${require(\"v8\").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'", "setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048" } -} +} \ No newline at end of file diff --git a/src/core/operations/ConvertImageFormat.mjs b/src/core/operations/ConvertImageFormat.mjs index 5a8cb6f4a..8cb8bade3 100644 --- a/src/core/operations/ConvertImageFormat.mjs +++ b/src/core/operations/ConvertImageFormat.mjs @@ -8,7 +8,82 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; import { isImage } from "../lib/FileType.mjs"; import { toBase64 } from "../lib/Base64.mjs"; -import { Jimp, JimpMime, PNGFilterType } from "jimp"; +import { Jimp as BaseJimp, JimpMime, PNGFilterType } from "jimp"; +import webp from "@jimp/wasm-webp"; + +/** + * Configure Jimp with WebP support + */ +const Jimp = new BaseJimp({ + plugins: [webp], + formats: [webp] +}); + +function canTranscodeViaCanvas() { + return ( + typeof globalThis !== "undefined" && + typeof globalThis.Blob !== "undefined" && + typeof globalThis.createImageBitmap === "function" && + (typeof globalThis.OffscreenCanvas !== "undefined" || + (typeof globalThis.document !== "undefined" && + typeof globalThis.document.createElement === "function")) + ); +} + +async function transcodeViaCanvas(input, inputMime, outputMime, quality) { + const {Blob: BlobCtor, createImageBitmap: createImageBitmapFn, OffscreenCanvas: OffscreenCanvasCtor, document} = + globalThis; + + const inputBytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input; + const inputBlob = new BlobCtor([inputBytes], { type: inputMime }); + + const bitmap = await createImageBitmapFn(inputBlob); + try { + let canvas; + if (typeof OffscreenCanvasCtor !== "undefined") { + canvas = new OffscreenCanvasCtor(bitmap.width, bitmap.height); + } else { + if (!document) throw new Error("Canvas API not available"); + canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + } + + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Unable to initialise canvas context"); + + if (outputMime === "image/jpeg") { + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, bitmap.width, bitmap.height); + } + + ctx.drawImage(bitmap, 0, 0); + + let outputBlob; + if (typeof canvas.convertToBlob === "function") { + outputBlob = await canvas.convertToBlob({ + type: outputMime, + quality: quality ? quality / 100 : undefined, + }); + } else if (typeof canvas.toBlob === "function") { + outputBlob = await new Promise((resolve, reject) => { + canvas.toBlob( + blob => (blob ? resolve(blob) : reject(new Error("Canvas encode failed"))), + outputMime, + quality ? quality / 100 : undefined + ); + }); + } else { + throw new Error("Canvas encoding not supported"); + } + + return await outputBlob.arrayBuffer(); + } finally { + if (bitmap && typeof bitmap.close === "function") { + bitmap.close(); + } + } +} /** * Convert Image Format operation @@ -23,7 +98,7 @@ class ConvertImageFormat extends Operation { this.name = "Convert Image Format"; this.module = "Image"; this.description = - "Converts an image between different formats. Supported formats:

Note: GIF files are supported for input, but cannot be outputted."; + "Converts an image between different formats. Supported output formats:

Note: GIF and WebP files are supported for input, but cannot be outputted."; this.infoURL = "https://wikipedia.org/wiki/Image_file_formats"; this.inputType = "ArrayBuffer"; this.outputType = "ArrayBuffer"; @@ -81,9 +156,25 @@ class ConvertImageFormat extends Operation { const mime = formatMap[format]; - if (!isImage(input)) { + const inputMime = isImage(input); + if (!inputMime) { throw new OperationError("Invalid file format."); } + + if ( + inputMime === "image/webp" && + (mime === JimpMime.jpeg || mime === JimpMime.png) && + canTranscodeViaCanvas() + ) { + try { + const outputMime = mime === JimpMime.jpeg ? "image/jpeg" : "image/png"; + const quality = outputMime === "image/jpeg" ? jpegQuality : undefined; + return await transcodeViaCanvas(input, inputMime, outputMime, quality); + } catch (err) { + // If canvas fails, we can fall back to Jimp + } + } + let image; try { image = await Jimp.read(input);