feat(Multimedia): add WebP input support for Convert Image Format

Adds the @jimp/wasm-webp plugin to support WebP images in both browser and Node.js environments. The implementation retains Canvas-based transcoding as a priority optimization in browsers while falling back to Jimp-based decoding when necessary.
This commit is contained in:
Reuben Percival 2026-03-15 09:10:48 +00:00
parent d195a51e2e
commit 84ccd2f867
No known key found for this signature in database
GPG key ID: C7D5099A6D762DFB
2 changed files with 97 additions and 5 deletions

View file

@ -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"
}
}
}

View file

@ -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:<br><ul><li>Joint Photographic Experts Group (JPEG)</li><li>Portable Network Graphics (PNG)</li><li>Bitmap (BMP)</li><li>Tagged Image File Format (TIFF)</li></ul><br>Note: GIF files are supported for input, but cannot be outputted.";
"Converts an image between different formats. Supported output formats:<br><ul><li>Joint Photographic Experts Group (JPEG)</li><li>Portable Network Graphics (PNG)</li><li>Bitmap (BMP)</li><li>Tagged Image File Format (TIFF)</li></ul><br>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);