diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 115e8436..2edc50ab 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -374,7 +374,8 @@ "Image Filter", "Contain Image", "Cover Image", - "Image Hue/Saturation/Lightness" + "Image Hue/Saturation/Lightness", + "Sharpen Image" ] }, { diff --git a/src/core/operations/SharpenImage.mjs b/src/core/operations/SharpenImage.mjs new file mode 100644 index 00000000..db0e7bb7 --- /dev/null +++ b/src/core/operations/SharpenImage.mjs @@ -0,0 +1,161 @@ +/** + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import { isImage } from "../lib/FileType"; +import { toBase64 } from "../lib/Base64"; +import jimp from "jimp"; + +/** + * Sharpen Image operation + */ +class SharpenImage extends Operation { + + /** + * SharpenImage constructor + */ + constructor() { + super(); + + this.name = "Sharpen Image"; + this.module = "Image"; + this.description = "Sharpens an image (Unsharp mask)"; + this.infoURL = "https://wikipedia.org/wiki/Unsharp_masking"; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + name: "Radius", + type: "number", + value: 2, + min: 1 + }, + { + name: "Amount", + type: "number", + value: 1, + min: 0, + step: 0.1 + }, + { + name: "Threshold", + type: "number", + value: 10, + min: 0, + max: 100 + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + async run(input, args) { + const [radius, amount, threshold] = args; + + if (!isImage(input)){ + throw new OperationError("Invalid file type."); + } + + let image; + try { + image = await jimp.read(Buffer.from(input)); + } catch (err) { + throw new OperationError(`Error loading image. (${err})`); + } + + try { + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Sharpening image... (Cloning image)"); + const blurImage = image.clone(); + const blurMask = image.clone(); + + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Sharpening image... (Blurring cloned image)"); + blurImage.gaussian(radius); + + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Sharpening image... (Creating unsharp mask)"); + blurMask.scan(0, 0, blurMask.bitmap.width, blurMask.bitmap.height, function(x, y, idx) { + const blurRed = blurImage.bitmap.data[idx]; + const blurGreen = blurImage.bitmap.data[idx + 1]; + const blurBlue = blurImage.bitmap.data[idx + 2]; + + const normalRed = this.bitmap.data[idx]; + const normalGreen = this.bitmap.data[idx + 1]; + const normalBlue = this.bitmap.data[idx + 2]; + + // Subtract blurred pixel value from normal image + this.bitmap.data[idx] = (normalRed > blurRed) ? normalRed - blurRed : 0; + this.bitmap.data[idx + 1] = (normalGreen > blurGreen) ? normalGreen - blurGreen : 0; + this.bitmap.data[idx + 2] = (normalBlue > blurBlue) ? normalBlue - blurBlue : 0; + }); + + if (ENVIRONMENT_IS_WORKER()) + self.sendStatusMessage("Sharpening image... (Merging with unsharp mask)"); + image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) { + let maskRed = blurMask.bitmap.data[idx]; + let maskGreen = blurMask.bitmap.data[idx + 1]; + let maskBlue = blurMask.bitmap.data[idx + 2]; + + const normalRed = this.bitmap.data[idx]; + const normalGreen = this.bitmap.data[idx + 1]; + const normalBlue = this.bitmap.data[idx + 2]; + + // Calculate luminance + const maskLuminance = (0.2126 * maskRed + 0.7152 * maskGreen + 0.0722 * maskBlue); + const normalLuminance = (0.2126 * normalRed + 0.7152 * normalGreen + 0.0722 * normalBlue); + + let luminanceDiff; + if (maskLuminance > normalLuminance) { + luminanceDiff = maskLuminance - normalLuminance; + } else { + luminanceDiff = normalLuminance - maskLuminance; + } + + // Scale mask colours by amount + maskRed = maskRed * amount; + maskGreen = maskGreen * amount; + maskBlue = maskBlue * amount; + + // Only change pixel value if the difference is higher than threshold + if ((luminanceDiff / 255) * 100 >= threshold) { + this.bitmap.data[idx] = (normalRed + maskRed) <= 255 ? normalRed + maskRed : 255; + this.bitmap.data[idx + 1] = (normalGreen + maskGreen) <= 255 ? normalGreen + maskGreen : 255; + this.bitmap.data[idx + 2] = (normalBlue + maskBlue) <= 255 ? normalBlue + maskBlue : 255; + } + }); + + const imageBuffer = await image.getBufferAsync(jimp.AUTO); + return [...imageBuffer]; + } catch (err) { + throw new OperationError(`Error sharpening image. (${err})`); + } + } + + /** + * Displays the sharpened image using HTML for web apps + * @param {byteArray} data + * @returns {html} + */ + present(data) { + if (!data.length) return ""; + + const type = isImage(data); + if (!type) { + throw new OperationError("Invalid image type."); + } + + return ``; + } + +} + +export default SharpenImage;