202 lines
6.7 KiB
JavaScript
202 lines
6.7 KiB
JavaScript
/**
|
|
* @author j433866 [j433866@gmail.com]
|
|
* @copyright Crown Copyright 2019
|
|
* @license Apache-2.0
|
|
*/
|
|
|
|
import Operation from "../Operation.mjs";
|
|
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";
|
|
|
|
/**
|
|
* 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 = "ArrayBuffer";
|
|
this.outputType = "ArrayBuffer";
|
|
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 {ArrayBuffer} 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(input);
|
|
} catch (err) {
|
|
throw new OperationError(`Error loading image. (${err})`);
|
|
}
|
|
|
|
try {
|
|
if (isWorkerEnvironment())
|
|
self.sendStatusMessage("Sharpening image... (Cloning image)");
|
|
const blurMask = image.clone();
|
|
|
|
if (isWorkerEnvironment())
|
|
self.sendStatusMessage(
|
|
"Sharpening image... (Blurring cloned image)",
|
|
);
|
|
const blurImage = image.clone().gaussian(radius);
|
|
|
|
if (isWorkerEnvironment())
|
|
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 (isWorkerEnvironment())
|
|
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;
|
|
}
|
|
},
|
|
);
|
|
|
|
let imageBuffer;
|
|
if (image.mime === "image/gif") {
|
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
|
} else {
|
|
imageBuffer = await image.getBuffer(image.mime);
|
|
}
|
|
return imageBuffer.buffer;
|
|
} catch (err) {
|
|
throw new OperationError(`Error sharpening image. (${err})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays the sharpened image using HTML for web apps
|
|
* @param {ArrayBuffer} data
|
|
* @returns {html}
|
|
*/
|
|
present(data) {
|
|
if (!data.byteLength) return "";
|
|
const dataArray = new Uint8Array(data);
|
|
|
|
const type = isImage(dataArray);
|
|
if (!type) {
|
|
throw new OperationError("Invalid file type.");
|
|
}
|
|
|
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
|
}
|
|
}
|
|
|
|
export default SharpenImage;
|