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/lib/Jimp.mjs b/src/core/lib/Jimp.mjs
new file mode 100644
index 000000000..e6290f72d
--- /dev/null
+++ b/src/core/lib/Jimp.mjs
@@ -0,0 +1,21 @@
+/**
+ * Jimp image library with additional plugins.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import { Jimp as BaseJimp, JimpMime, PNGFilterType, ResizeStrategy, EdgeAction } from "jimp";
+import webp from "@jimp/wasm-webp";
+
+/**
+ * Configure Jimp with WebP support
+ */
+const Jimp = new BaseJimp({
+ plugins: [webp],
+ formats: [webp]
+});
+
+export { Jimp, JimpMime, PNGFilterType, ResizeStrategy, EdgeAction };
+export default Jimp;
diff --git a/src/core/operations/AddTextToImage.mjs b/src/core/operations/AddTextToImage.mjs
index c137b4926..389acab74 100644
--- a/src/core/operations/AddTextToImage.mjs
+++ b/src/core/operations/AddTextToImage.mjs
@@ -16,7 +16,7 @@ import {
measureText,
measureTextHeight,
loadFont,
-} from "jimp";
+} from "../lib/Jimp.mjs";
/**
* Add Text To Image operation
diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs
index ba46080f2..e9fb44a4e 100644
--- a/src/core/operations/BlurImage.mjs
+++ b/src/core/operations/BlurImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Blur Image operation
diff --git a/src/core/operations/ContainImage.mjs b/src/core/operations/ContainImage.mjs
index 0304733ba..8e04f5282 100644
--- a/src/core/operations/ContainImage.mjs
+++ b/src/core/operations/ContainImage.mjs
@@ -15,7 +15,7 @@ import {
ResizeStrategy,
HorizontalAlign,
VerticalAlign,
-} from "jimp";
+} from "../lib/Jimp.mjs";
/**
* Contain Image operation
diff --git a/src/core/operations/ConvertImageFormat.mjs b/src/core/operations/ConvertImageFormat.mjs
index 5a8cb6f4a..0963a6512 100644
--- a/src/core/operations/ConvertImageFormat.mjs
+++ b/src/core/operations/ConvertImageFormat.mjs
@@ -8,7 +8,73 @@ 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, JimpMime, PNGFilterType } from "../lib/Jimp.mjs";
+
+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" || outputMime === "image/webp") {
+ 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 +89,7 @@ class ConvertImageFormat extends Operation {
this.name = "Convert Image Format";
this.module = "Image";
this.description =
- "Converts an image between different formats. Supported formats:
- Joint Photographic Experts Group (JPEG)
- Portable Network Graphics (PNG)
- Bitmap (BMP)
- Tagged Image File Format (TIFF)
Note: GIF files are supported for input, but cannot be outputted.";
+ "Converts an image between different formats. Supported output formats:
- Joint Photographic Experts Group (JPEG)
- Portable Network Graphics (PNG)
- Bitmap (BMP)
- Tagged Image File Format (TIFF)
- WebP
Note: GIF files are supported for input, but cannot be outputted.";
this.infoURL = "https://wikipedia.org/wiki/Image_file_formats";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
@@ -32,10 +98,10 @@ class ConvertImageFormat extends Operation {
{
name: "Output Format",
type: "option",
- value: ["JPEG", "PNG", "BMP", "TIFF"],
+ value: ["JPEG", "PNG", "BMP", "TIFF", "WEBP"],
},
{
- name: "JPEG Quality",
+ name: "Quality (JPEG/WebP)",
type: "number",
value: 80,
min: 1,
@@ -62,12 +128,13 @@ class ConvertImageFormat extends Operation {
* @returns {byteArray}
*/
async run(input, args) {
- const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
+ const [format, quality, pngFilterType, pngDeflateLevel] = args;
const formatMap = {
JPEG: JimpMime.jpeg,
PNG: JimpMime.png,
BMP: JimpMime.bmp,
TIFF: JimpMime.tiff,
+ WEBP: "image/webp",
};
const pngFilterMap = {
@@ -81,9 +148,28 @@ 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 {
+ let outputMime;
+ if (mime === JimpMime.jpeg) outputMime = "image/jpeg";
+ else if (mime === "image/webp") outputMime = "image/webp";
+ else outputMime = "image/png";
+
+ 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);
@@ -94,8 +180,9 @@ class ConvertImageFormat extends Operation {
let buffer;
switch (mime) {
case JimpMime.jpeg:
+ case "image/webp":
buffer = await image.getBuffer(mime, {
- quality: jpegQuality,
+ quality: quality,
});
break;
case JimpMime.png:
diff --git a/src/core/operations/CoverImage.mjs b/src/core/operations/CoverImage.mjs
index bf9a9bd1d..640d9e065 100644
--- a/src/core/operations/CoverImage.mjs
+++ b/src/core/operations/CoverImage.mjs
@@ -15,7 +15,7 @@ import {
ResizeStrategy,
HorizontalAlign,
VerticalAlign,
-} from "jimp";
+} from "../lib/Jimp.mjs";
/**
* Cover Image operation
diff --git a/src/core/operations/CropImage.mjs b/src/core/operations/CropImage.mjs
index 6b5047bcb..e796277f3 100644
--- a/src/core/operations/CropImage.mjs
+++ b/src/core/operations/CropImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Crop Image operation
diff --git a/src/core/operations/DitherImage.mjs b/src/core/operations/DitherImage.mjs
index f21c1f883..e0146a231 100644
--- a/src/core/operations/DitherImage.mjs
+++ b/src/core/operations/DitherImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Image Dither operation
diff --git a/src/core/operations/ExtractLSB.mjs b/src/core/operations/ExtractLSB.mjs
index e64831b12..a3e2d7e4d 100644
--- a/src/core/operations/ExtractLSB.mjs
+++ b/src/core/operations/ExtractLSB.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { fromBinary } from "../lib/Binary.mjs";
import { isImage } from "../lib/FileType.mjs";
-import { Jimp } from "jimp";
+import { Jimp } from "../lib/Jimp.mjs";
/**
* Extract LSB operation
diff --git a/src/core/operations/ExtractRGBA.mjs b/src/core/operations/ExtractRGBA.mjs
index b0fe3888e..d17633ab8 100644
--- a/src/core/operations/ExtractRGBA.mjs
+++ b/src/core/operations/ExtractRGBA.mjs
@@ -7,7 +7,7 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
-import { Jimp } from "jimp";
+import { Jimp } from "../lib/Jimp.mjs";
import { RGBA_DELIM_OPTIONS } from "../lib/Delim.mjs";
diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs
index cf9c747f9..bd02e08a4 100644
--- a/src/core/operations/FlipImage.mjs
+++ b/src/core/operations/FlipImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Flip Image operation
diff --git a/src/core/operations/GenerateImage.mjs b/src/core/operations/GenerateImage.mjs
index 053e4ba11..558c9fa8d 100644
--- a/src/core/operations/GenerateImage.mjs
+++ b/src/core/operations/GenerateImage.mjs
@@ -10,7 +10,7 @@ import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime, ResizeStrategy, rgbaToInt } from "jimp";
+import { Jimp, JimpMime, ResizeStrategy, rgbaToInt } from "../lib/Jimp.mjs";
/**
* Generate Image operation
diff --git a/src/core/operations/ImageBrightnessContrast.mjs b/src/core/operations/ImageBrightnessContrast.mjs
index b15503d48..827a4789c 100644
--- a/src/core/operations/ImageBrightnessContrast.mjs
+++ b/src/core/operations/ImageBrightnessContrast.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Image Brightness / Contrast operation
diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs
index c7c4dde29..67fb09e97 100644
--- a/src/core/operations/ImageFilter.mjs
+++ b/src/core/operations/ImageFilter.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Image Filter operation
diff --git a/src/core/operations/ImageHueSaturationLightness.mjs b/src/core/operations/ImageHueSaturationLightness.mjs
index e0a1910a8..a258a10cc 100644
--- a/src/core/operations/ImageHueSaturationLightness.mjs
+++ b/src/core/operations/ImageHueSaturationLightness.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Image Hue/Saturation/Lightness operation
diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs
index 4650b8d66..b483bd65d 100644
--- a/src/core/operations/ImageOpacity.mjs
+++ b/src/core/operations/ImageOpacity.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Image Opacity operation
diff --git a/src/core/operations/InvertImage.mjs b/src/core/operations/InvertImage.mjs
index cbf46748d..73de6901b 100644
--- a/src/core/operations/InvertImage.mjs
+++ b/src/core/operations/InvertImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Invert Image operation
diff --git a/src/core/operations/NormaliseImage.mjs b/src/core/operations/NormaliseImage.mjs
index 9773761b0..5423a0f54 100644
--- a/src/core/operations/NormaliseImage.mjs
+++ b/src/core/operations/NormaliseImage.mjs
@@ -8,7 +8,7 @@ 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 } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Normalise Image operation
diff --git a/src/core/operations/RandomizeColourPalette.mjs b/src/core/operations/RandomizeColourPalette.mjs
index 186023c2e..6573e293a 100644
--- a/src/core/operations/RandomizeColourPalette.mjs
+++ b/src/core/operations/RandomizeColourPalette.mjs
@@ -10,7 +10,7 @@ import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { runHash } from "../lib/Hash.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import { Jimp } from "jimp";
+import { Jimp } from "../lib/Jimp.mjs";
/**
* Randomize Colour Palette operation
diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs
index bec07c4e8..7c91a273b 100644
--- a/src/core/operations/ResizeImage.mjs
+++ b/src/core/operations/ResizeImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime, ResizeStrategy } from "jimp";
+import { Jimp, JimpMime, ResizeStrategy } from "../lib/Jimp.mjs";
/**
* Resize Image operation
diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs
index 5a96ffabe..2413dddcf 100644
--- a/src/core/operations/RotateImage.mjs
+++ b/src/core/operations/RotateImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Rotate Image operation
diff --git a/src/core/operations/SharpenImage.mjs b/src/core/operations/SharpenImage.mjs
index 1f5461f21..d9f35b4a9 100644
--- a/src/core/operations/SharpenImage.mjs
+++ b/src/core/operations/SharpenImage.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Sharpen Image operation
diff --git a/src/core/operations/SplitColourChannels.mjs b/src/core/operations/SplitColourChannels.mjs
index 3a33b78cd..40246b734 100644
--- a/src/core/operations/SplitColourChannels.mjs
+++ b/src/core/operations/SplitColourChannels.mjs
@@ -8,7 +8,7 @@ import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
-import { Jimp, JimpMime } from "jimp";
+import { Jimp, JimpMime } from "../lib/Jimp.mjs";
/**
* Split Colour Channels operation
diff --git a/src/core/operations/ViewBitPlane.mjs b/src/core/operations/ViewBitPlane.mjs
index 3740c10d7..3db31c4b4 100644
--- a/src/core/operations/ViewBitPlane.mjs
+++ b/src/core/operations/ViewBitPlane.mjs
@@ -9,7 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import { Jimp } from "jimp";
+import { Jimp } from "../lib/Jimp.mjs";
/**
* View Bit Plane operation