Merge remote-tracking branch 'upstream/master' into multiple-input-files

This commit is contained in:
j433866 2019-07-03 13:20:21 +01:00
commit a7eefa88fa
41 changed files with 2971 additions and 273 deletions

View file

@ -384,6 +384,9 @@
"Contain Image",
"Cover Image",
"Image Hue/Saturation/Lightness",
"Sharpen Image",
"Convert Image Format",
"Add Text To Image",
"Hex Density chart",
"Scatter chart",
"Series chart",

View file

@ -0,0 +1,251 @@
/**
* Image manipulation resources
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
/**
* Gaussian blurs an image.
*
* @param {jimp} input
* @param {number} radius
* @param {boolean} fast
* @returns {jimp}
*/
export function gaussianBlur (input, radius) {
try {
// From http://blog.ivank.net/fastest-gaussian-blur.html
const boxes = boxesForGauss(radius, 3);
for (let i = 0; i < 3; i++) {
input = boxBlur(input, (boxes[i] - 1) / 2);
}
} catch (err) {
throw new OperationError(`Error blurring image. (${err})`);
}
return input;
}
/**
*
* @param {number} radius
* @param {number} numBoxes
* @returns {Array}
*/
function boxesForGauss(radius, numBoxes) {
const idealWidth = Math.sqrt((12 * radius * radius / numBoxes) + 1);
let wl = Math.floor(idealWidth);
if (wl % 2 === 0) {
wl--;
}
const wu = wl + 2;
const mIdeal = (12 * radius * radius - numBoxes * wl * wl - 4 * numBoxes * wl - 3 * numBoxes) / (-4 * wl - 4);
const m = Math.round(mIdeal);
const sizes = [];
for (let i = 0; i < numBoxes; i++) {
sizes.push(i < m ? wl : wu);
}
return sizes;
}
/**
* Applies a box blur effect to the image
*
* @param {jimp} source
* @param {number} radius
* @returns {jimp}
*/
function boxBlur (source, radius) {
const width = source.bitmap.width;
const height = source.bitmap.height;
let output = source.clone();
output = boxBlurH(source, output, width, height, radius);
source = boxBlurV(output, source, width, height, radius);
return source;
}
/**
* Applies the horizontal blur
*
* @param {jimp} source
* @param {jimp} output
* @param {number} width
* @param {number} height
* @param {number} radius
* @returns {jimp}
*/
function boxBlurH (source, output, width, height, radius) {
const iarr = 1 / (radius + radius + 1);
for (let i = 0; i < height; i++) {
let ti = 0,
li = ti,
ri = ti + radius;
const idx = source.getPixelIndex(ti, i);
const firstValRed = source.bitmap.data[idx],
firstValGreen = source.bitmap.data[idx + 1],
firstValBlue = source.bitmap.data[idx + 2],
firstValAlpha = source.bitmap.data[idx + 3];
const lastIdx = source.getPixelIndex(width - 1, i),
lastValRed = source.bitmap.data[lastIdx],
lastValGreen = source.bitmap.data[lastIdx + 1],
lastValBlue = source.bitmap.data[lastIdx + 2],
lastValAlpha = source.bitmap.data[lastIdx + 3];
let red = (radius + 1) * firstValRed;
let green = (radius + 1) * firstValGreen;
let blue = (radius + 1) * firstValBlue;
let alpha = (radius + 1) * firstValAlpha;
for (let j = 0; j < radius; j++) {
const jIdx = source.getPixelIndex(ti + j, i);
red += source.bitmap.data[jIdx];
green += source.bitmap.data[jIdx + 1];
blue += source.bitmap.data[jIdx + 2];
alpha += source.bitmap.data[jIdx + 3];
}
for (let j = 0; j <= radius; j++) {
const jIdx = source.getPixelIndex(ri++, i);
red += source.bitmap.data[jIdx] - firstValRed;
green += source.bitmap.data[jIdx + 1] - firstValGreen;
blue += source.bitmap.data[jIdx + 2] - firstValBlue;
alpha += source.bitmap.data[jIdx + 3] - firstValAlpha;
const tiIdx = source.getPixelIndex(ti++, i);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
for (let j = radius + 1; j < width - radius; j++) {
const riIdx = source.getPixelIndex(ri++, i);
const liIdx = source.getPixelIndex(li++, i);
red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
const tiIdx = source.getPixelIndex(ti++, i);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
for (let j = width - radius; j < width; j++) {
const liIdx = source.getPixelIndex(li++, i);
red += lastValRed - source.bitmap.data[liIdx];
green += lastValGreen - source.bitmap.data[liIdx + 1];
blue += lastValBlue - source.bitmap.data[liIdx + 2];
alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
const tiIdx = source.getPixelIndex(ti++, i);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
}
return output;
}
/**
* Applies the vertical blur
*
* @param {jimp} source
* @param {jimp} output
* @param {number} width
* @param {number} height
* @param {number} radius
* @returns {jimp}
*/
function boxBlurV (source, output, width, height, radius) {
const iarr = 1 / (radius + radius + 1);
for (let i = 0; i < width; i++) {
let ti = 0,
li = ti,
ri = ti + radius;
const idx = source.getPixelIndex(i, ti);
const firstValRed = source.bitmap.data[idx],
firstValGreen = source.bitmap.data[idx + 1],
firstValBlue = source.bitmap.data[idx + 2],
firstValAlpha = source.bitmap.data[idx + 3];
const lastIdx = source.getPixelIndex(i, height - 1),
lastValRed = source.bitmap.data[lastIdx],
lastValGreen = source.bitmap.data[lastIdx + 1],
lastValBlue = source.bitmap.data[lastIdx + 2],
lastValAlpha = source.bitmap.data[lastIdx + 3];
let red = (radius + 1) * firstValRed;
let green = (radius + 1) * firstValGreen;
let blue = (radius + 1) * firstValBlue;
let alpha = (radius + 1) * firstValAlpha;
for (let j = 0; j < radius; j++) {
const jIdx = source.getPixelIndex(i, ti + j);
red += source.bitmap.data[jIdx];
green += source.bitmap.data[jIdx + 1];
blue += source.bitmap.data[jIdx + 2];
alpha += source.bitmap.data[jIdx + 3];
}
for (let j = 0; j <= radius; j++) {
const riIdx = source.getPixelIndex(i, ri++);
red += source.bitmap.data[riIdx] - firstValRed;
green += source.bitmap.data[riIdx + 1] - firstValGreen;
blue += source.bitmap.data[riIdx + 2] - firstValBlue;
alpha += source.bitmap.data[riIdx + 3] - firstValAlpha;
const tiIdx = source.getPixelIndex(i, ti++);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
for (let j = radius + 1; j < height - radius; j++) {
const riIdx = source.getPixelIndex(i, ri++);
const liIdx = source.getPixelIndex(i, li++);
red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
const tiIdx = source.getPixelIndex(i, ti++);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
for (let j = height - radius; j < height; j++) {
const liIdx = source.getPixelIndex(i, li++);
red += lastValRed - source.bitmap.data[liIdx];
green += lastValGreen - source.bitmap.data[liIdx + 1];
blue += lastValBlue - source.bitmap.data[liIdx + 2];
alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
const tiIdx = source.getPixelIndex(i, ti++);
output.bitmap.data[tiIdx] = Math.round(red * iarr);
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
}
}
return output;
}

View file

@ -312,6 +312,11 @@ class Magic {
return;
}
// If the recipe returned an empty buffer, do not continue
if (_buffersEqual(output, new ArrayBuffer())) {
return;
}
const magic = new Magic(output, this.opPatterns),
speculativeResults = await magic.speculativeExecution(
depth-1, extLang, intensive, [...recipeConfig, opConfig], op.useful, crib);
@ -395,7 +400,12 @@ class Magic {
const recipe = new Recipe(recipeConfig);
try {
await recipe.execute(dish);
return dish.get(Dish.ARRAY_BUFFER);
// Return an empty buffer if the recipe did not run to completion
if (recipe.lastRunOp === recipe.opList[recipe.opList.length - 1]) {
return dish.get(Dish.ARRAY_BUFFER);
} else {
return new ArrayBuffer();
}
} catch (err) {
// If there are errors, return an empty buffer
return new ArrayBuffer();

93
src/core/lib/QRCode.mjs Normal file
View file

@ -0,0 +1,93 @@
/**
* QR code resources
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
import jsQR from "jsqr";
import qr from "qr-image";
import jimp from "jimp";
import Utils from "../Utils";
/**
* Parses a QR code image from an image
*
* @param {ArrayBuffer} input
* @param {boolean} normalise
* @returns {string}
*/
export async function parseQrCode(input, normalise) {
let image;
try {
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image. (${err})`);
}
try {
if (normalise) {
image.rgba(false);
image.background(0xFFFFFFFF);
image.normalize();
image.greyscale();
image = await image.getBufferAsync(jimp.MIME_JPEG);
image = await jimp.read(image);
}
} catch (err) {
throw new OperationError(`Error normalising iamge. (${err})`);
}
const qrData = jsQR(image.bitmap.data, image.getWidth(), image.getHeight());
if (qrData) {
return qrData.data;
} else {
throw new OperationError("Could not read a QR code from the image.");
}
}
/**
* Generates a QR code from the input string
*
* @param {string} input
* @param {string} format
* @param {number} moduleSize
* @param {number} margin
* @param {string} errorCorrection
* @returns {ArrayBuffer}
*/
export function generateQrCode(input, format, moduleSize, margin, errorCorrection) {
const formats = ["SVG", "EPS", "PDF", "PNG"];
if (!formats.includes(format.toUpperCase())) {
throw new OperationError("Unsupported QR code format.");
}
let qrImage;
try {
qrImage = qr.imageSync(input, {
type: format,
size: moduleSize,
margin: margin,
"ec_level": errorCorrection.charAt(0).toUpperCase()
});
} catch (err) {
throw new OperationError(`Error generating QR code. (${err})`);
}
if (!qrImage) {
throw new OperationError("Error generating QR code.");
}
switch (format) {
case "SVG":
case "EPS":
case "PDF":
return Utils.strToArrayBuffer(qrImage);
case "PNG":
return qrImage.buffer;
default:
throw new OperationError("Unsupported QR code format.");
}
}

View file

@ -0,0 +1,266 @@
/**
* @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";
/**
* Add Text To Image operation
*/
class AddTextToImage extends Operation {
/**
* AddTextToImage constructor
*/
constructor() {
super();
this.name = "Add Text To Image";
this.module = "Image";
this.description = "Adds text onto an image.<br><br>Text can be horizontally or vertically aligned, or the position can be manually specified.<br>Variants of the Roboto font face are available in any size or colour.";
this.infoURL = "";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
name: "Text",
type: "string",
value: ""
},
{
name: "Horizontal align",
type: "option",
value: ["None", "Left", "Center", "Right"]
},
{
name: "Vertical align",
type: "option",
value: ["None", "Top", "Middle", "Bottom"]
},
{
name: "X position",
type: "number",
value: 0
},
{
name: "Y position",
type: "number",
value: 0
},
{
name: "Size",
type: "number",
value: 32,
min: 8
},
{
name: "Font face",
type: "option",
value: [
"Roboto",
"Roboto Black",
"Roboto Mono",
"Roboto Slab"
]
},
{
name: "Red",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Green",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Blue",
type: "number",
value: 255,
min: 0,
max: 255
},
{
name: "Alpha",
type: "number",
value: 255,
min: 0,
max: 255
}
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const text = args[0],
hAlign = args[1],
vAlign = args[2],
size = args[5],
fontFace = args[6],
red = args[7],
green = args[8],
blue = args[9],
alpha = args[10];
let xPos = args[3],
yPos = args[4];
if (!isImage(new Uint8Array(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 (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Adding text to image...");
const fontsMap = {};
const fonts = [
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
];
await Promise.all(fonts)
.then(fonts => {
fontsMap.Roboto = fonts[0];
fontsMap["Roboto Black"] = fonts[1];
fontsMap["Roboto Mono"] = fonts[2];
fontsMap["Roboto Slab"] = fonts[3];
});
// Make Webpack load the png font images
await Promise.all([
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"),
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png")
]);
const font = fontsMap[fontFace];
// LoadFont needs an absolute url, so append the font name to self.docURL
const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);
jimpFont.pages.forEach(function(page) {
if (page.bitmap) {
// Adjust the RGB values of the image pages to change the font colour.
const pageWidth = page.bitmap.width;
const pageHeight = page.bitmap.height;
for (let ix = 0; ix < pageWidth; ix++) {
for (let iy = 0; iy < pageHeight; iy++) {
const idx = (iy * pageWidth + ix) << 2;
const newRed = page.bitmap.data[idx] - (255 - red);
const newGreen = page.bitmap.data[idx + 1] - (255 - green);
const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);
// Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
}
}
}
});
// Create a temporary image to hold the rendered text
const textImage = new jimp(jimp.measureText(jimpFont, text), jimp.measureTextHeight(jimpFont, text));
textImage.print(jimpFont, 0, 0, text);
// Scale the rendered text image to the correct size
const scaleFactor = size / 72;
if (size !== 1) {
// Use bicubic for decreasing size
if (size > 1) {
textImage.scale(scaleFactor, jimp.RESIZE_BICUBIC);
} else {
textImage.scale(scaleFactor, jimp.RESIZE_BILINEAR);
}
}
// If using the alignment options, calculate the pixel values AFTER the image has been scaled
switch (hAlign) {
case "Left":
xPos = 0;
break;
case "Center":
xPos = (image.getWidth() / 2) - (textImage.getWidth() / 2);
break;
case "Right":
xPos = image.getWidth() - textImage.getWidth();
break;
}
switch (vAlign) {
case "Top":
yPos = 0;
break;
case "Middle":
yPos = (image.getHeight() / 2) - (textImage.getHeight() / 2);
break;
case "Bottom":
yPos = image.getHeight() - textImage.getHeight();
break;
}
// Blit the rendered text image onto the original source image
image.blit(textImage, xPos, yPos);
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error adding text to image. (${err})`);
}
}
/**
* Displays the blurred 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 AddTextToImage;

View file

@ -9,6 +9,7 @@ import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
import { gaussianBlur } from "../lib/ImageManipulation";
/**
* Blur Image operation
@ -25,8 +26,8 @@ class BlurImage extends Operation {
this.module = "Image";
this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -44,37 +45,44 @@ class BlurImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [blurAmount, blurType] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
switch (blurType){
case "Fast":
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Fast blurring image...");
image.blur(blurAmount);
break;
case "Gaussian":
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Gaussian blurring image. This may take a while...");
image.gaussian(blurAmount);
self.sendStatusMessage("Gaussian blurring image...");
image = gaussianBlur(image, blurAmount);
break;
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error blurring image. (${err})`);
}
@ -83,18 +91,19 @@ class BlurImage extends Operation {
/**
* Displays the blurred image using HTML for web apps
*
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class ContainImage extends Operation {
this.module = "Image";
this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -72,17 +72,22 @@ class ContainImage extends Operation {
"Bezier"
],
defaultIndex: 1
},
{
name: "Opaque background",
type: "boolean",
value: true
}
];
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [width, height, hAlign, vAlign, alg] = args;
const [width, height, hAlign, vAlign, alg, opaqueBg] = args;
const resizeMap = {
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
@ -101,13 +106,13 @@ class ContainImage extends Operation {
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
};
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -115,8 +120,20 @@ class ContainImage extends Operation {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Containing image...");
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
if (opaqueBg) {
const newImage = await jimp.read(width, height, 0x000000FF);
newImage.blit(image, 0, 0);
image = newImage;
}
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error containing image. (${err})`);
}
@ -124,18 +141,19 @@ class ContainImage extends Operation {
/**
* Displays the contained image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -0,0 +1,143 @@
/**
* @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";
/**
* Convert Image Format operation
*/
class ConvertImageFormat extends Operation {
/**
* ConvertImageFormat constructor
*/
constructor() {
super();
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.";
this.infoURL = "https://wikipedia.org/wiki/Image_file_formats";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
name: "Output Format",
type: "option",
value: [
"JPEG",
"PNG",
"BMP",
"TIFF"
]
},
{
name: "JPEG Quality",
type: "number",
value: 80,
min: 1,
max: 100
},
{
name: "PNG Filter Type",
type: "option",
value: [
"Auto",
"None",
"Sub",
"Up",
"Average",
"Paeth"
]
},
{
name: "PNG Deflate Level",
type: "number",
value: 9,
min: 0,
max: 9
}
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
const formatMap = {
"JPEG": jimp.MIME_JPEG,
"PNG": jimp.MIME_PNG,
"BMP": jimp.MIME_BMP,
"TIFF": jimp.MIME_TIFF
};
const pngFilterMap = {
"Auto": jimp.PNG_FILTER_AUTO,
"None": jimp.PNG_FILTER_NONE,
"Sub": jimp.PNG_FILTER_SUB,
"Up": jimp.PNG_FILTER_UP,
"Average": jimp.PNG_FILTER_AVERAGE,
"Paeth": jimp.PNG_FILTER_PATH // Incorrect spelling in Jimp library
};
const mime = formatMap[format];
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file format.");
}
let image;
try {
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image file. (${err})`);
}
try {
switch (format) {
case "JPEG":
image.quality(jpegQuality);
break;
case "PNG":
image.filterType(pngFilterMap[pngFilterType]);
image.deflateLevel(pngDeflateLevel);
break;
}
const imageBuffer = await image.getBufferAsync(mime);
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error converting image format. (${err})`);
}
}
/**
* Displays the converted 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 ConvertImageFormat;

View file

@ -25,8 +25,8 @@ class CoverImage extends Operation {
this.module = "Image";
this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -77,7 +77,7 @@ class CoverImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
@ -101,13 +101,13 @@ class CoverImage extends Operation {
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
};
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -115,8 +115,13 @@ class CoverImage extends Operation {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Covering image...");
image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error covering image. (${err})`);
}
@ -124,18 +129,19 @@ class CoverImage extends Operation {
/**
* Displays the covered image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class CropImage extends Operation {
this.module = "Image";
this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -86,19 +86,19 @@ class CropImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -116,8 +116,13 @@ class CropImage extends Operation {
image.crop(xPos, yPos, width, height);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error cropping image. (${err})`);
}
@ -125,18 +130,19 @@ class CropImage extends Operation {
/**
* Displays the cropped image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,25 +25,25 @@ class DitherImage extends Operation {
this.module = "Image";
this.description = "Apply a dither effect to an image.";
this.infoURL = "https://wikipedia.org/wiki/Dither";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -51,8 +51,14 @@ class DitherImage extends Operation {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Applying dither to image...");
image.dither565();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error applying dither to image. (${err})`);
}
@ -60,18 +66,19 @@ class DitherImage extends Operation {
/**
* Displays the dithered image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class FlipImage extends Operation {
this.module = "Image";
this.description = "Flips an image along its X or Y axis.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -38,19 +38,19 @@ class FlipImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [flipAxis] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid input file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -66,8 +66,13 @@ class FlipImage extends Operation {
break;
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error flipping image. (${err})`);
}
@ -75,18 +80,19 @@ class FlipImage extends Operation {
/**
* Displays the flipped image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -6,7 +6,7 @@
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import qr from "qr-image";
import { generateQrCode } from "../lib/QRCode";
import { toBase64 } from "../lib/Base64";
import { isImage } from "../lib/FileType";
import Utils from "../Utils";
@ -27,7 +27,7 @@ class GenerateQRCode extends Operation {
this.description = "Generates a Quick Response (QR) code from the input text.<br><br>A QR code is a type of matrix barcode (or two-dimensional barcode) first designed in 1994 for the automotive industry in Japan. A barcode is a machine-readable optical label that contains information about the item to which it is attached.";
this.infoURL = "https://wikipedia.org/wiki/QR_code";
this.inputType = "string";
this.outputType = "byteArray";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -38,12 +38,14 @@ class GenerateQRCode extends Operation {
{
"name": "Module size (px)",
"type": "number",
"value": 5
"value": 5,
"min": 1
},
{
"name": "Margin (num modules)",
"type": "number",
"value": 2
"value": 2,
"min": 0
},
{
"name": "Error correction",
@ -57,61 +59,34 @@ class GenerateQRCode extends Operation {
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
* @returns {ArrayBuffer}
*/
run(input, args) {
const [format, size, margin, errorCorrection] = args;
// Create new QR image from the input data, and convert it to a buffer
const qrImage = qr.imageSync(input, {
type: format,
size: size,
margin: margin,
"ec_level": errorCorrection.charAt(0).toUpperCase()
});
if (qrImage == null) {
throw new OperationError("Error generating QR code.");
}
switch (format) {
case "SVG":
case "EPS":
case "PDF":
return [...Buffer.from(qrImage)];
case "PNG":
// Return the QR image buffer as a byte array
return [...qrImage];
default:
throw new OperationError("Unsupported QR code format.");
}
return generateQrCode(input, format, size, margin, errorCorrection);
}
/**
* Displays the QR image using HTML for web apps
*
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data, args) {
if (!data.length) return "";
const [format] = args;
if (!data.byteLength && !data.length) return "";
const dataArray = new Uint8Array(data),
[format] = args;
if (format === "PNG") {
let dataURI = "data:";
const mime = isImage(data);
if (mime){
dataURI += mime + ";";
} else {
throw new OperationError("Invalid PNG file generated by QR image");
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
dataURI += "base64," + toBase64(data);
return `<img src="${dataURI}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
return Utils.byteArrayToChars(data);
return Utils.arrayBufferToStr(data);
}
}

View file

@ -25,8 +25,8 @@ class ImageBrightnessContrast extends Operation {
this.module = "Image";
this.description = "Adjust the brightness or contrast of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -47,19 +47,19 @@ class ImageBrightnessContrast extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [brightness, contrast] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -75,8 +75,13 @@ class ImageBrightnessContrast extends Operation {
image.contrast(contrast / 100);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
}
@ -84,18 +89,19 @@ class ImageBrightnessContrast extends Operation {
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class ImageFilter extends Operation {
this.module = "Image";
this.description = "Applies a greyscale or sepia filter to an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -41,19 +41,19 @@ class ImageFilter extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [filterType] = args;
if (!isImage(input)){
if (!isImage(new Uint8Array(input))){
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -66,8 +66,13 @@ class ImageFilter extends Operation {
image.sepia();
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error applying filter to image. (${err})`);
}
@ -75,18 +80,19 @@ class ImageFilter extends Operation {
/**
* Displays the blurred image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class ImageHueSaturationLightness extends Operation {
this.module = "Image";
this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -54,20 +54,20 @@ class ImageHueSaturationLightness extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [hue, saturation, lightness] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -102,8 +102,14 @@ class ImageHueSaturationLightness extends Operation {
}
]);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
}
@ -111,18 +117,19 @@ class ImageHueSaturationLightness extends Operation {
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class ImageOpacity extends Operation {
this.module = "Image";
this.description = "Adjust the opacity of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -40,19 +40,19 @@ class ImageOpacity extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [opacity] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -61,8 +61,13 @@ class ImageOpacity extends Operation {
self.sendStatusMessage("Changing image opacity...");
image.opacity(opacity / 100);
const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error changing image opacity. (${err})`);
}
@ -70,18 +75,19 @@ class ImageOpacity extends Operation {
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,25 +25,25 @@ class InvertImage extends Operation {
this.module = "Image";
this.description = "Invert the colours of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid input file format.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -51,8 +51,14 @@ class InvertImage extends Operation {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Inverting image...");
image.invert();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error inverting image. (${err})`);
}
@ -60,18 +66,19 @@ class InvertImage extends Operation {
/**
* Displays the inverted image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,44 +25,59 @@ class NormaliseImage extends Operation {
this.module = "Image";
this.description = "Normalise the image colours.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType= "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
const image = await jimp.read(Buffer.from(input));
let image;
try {
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image file. (${err})`);
}
image.normalize();
try {
image.normalize();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error normalising image. (${err})`);
}
}
/**
* Displays the normalised image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -93,7 +93,7 @@ class PGPDecryptAndVerify extends Operation {
text += `${signer.username} `;
}
if (signer.comment) {
text += `${signer.comment} `;
text += `(${signer.comment}) `;
}
if (signer.email) {
text += `<${signer.email}>`;
@ -101,8 +101,9 @@ class PGPDecryptAndVerify extends Operation {
text += "\n";
}
text += [
`PGP key ID: ${km.get_pgp_short_key_id()}`,
`PGP fingerprint: ${km.get_pgp_fingerprint().toString("hex")}`,
`Signed on ${new Date(ds.sig.hashed_subpackets[0].time * 1000).toUTCString()}`,
`Signed on ${new Date(ds.sig.when_generated() * 1000).toUTCString()}`,
"----------------------------------\n"
].join("\n");
text += unboxedLiterals.toString();

View file

@ -0,0 +1,111 @@
/**
* @author Matt C [me@mitt.dev]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import kbpgp from "kbpgp";
import { ASP, importPublicKey } from "../lib/PGP";
import * as es6promisify from "es6-promisify";
const promisify = es6promisify.default ? es6promisify.default.promisify : es6promisify.promisify;
/**
* PGP Verify operation
*/
class PGPVerify extends Operation {
/**
* PGPVerify constructor
*/
constructor() {
super();
this.name = "PGP Verify";
this.module = "PGP";
this.description = [
"Input: the ASCII-armoured encrypted PGP message you want to verify.",
"<br><br>",
"Argument: the ASCII-armoured PGP public key of the signer",
"<br><br>",
"This operation uses PGP to decrypt a clearsigned message.",
"<br><br>",
"Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
"<br><br>",
"This function uses the Keybase implementation of PGP.",
].join("\n");
this.infoURL = "https://wikipedia.org/wiki/Pretty_Good_Privacy";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Public key of signer",
"type": "text",
"value": ""
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
async run(input, args) {
const signedMessage = input,
[publicKey] = args,
keyring = new kbpgp.keyring.KeyRing();
let unboxedLiterals;
if (!publicKey) throw new OperationError("Enter the public key of the signer.");
const pubKey = await importPublicKey(publicKey);
keyring.add_key_manager(pubKey);
try {
unboxedLiterals = await promisify(kbpgp.unbox)({
armored: signedMessage,
keyfetch: keyring,
asp: ASP
});
const ds = unboxedLiterals[0].get_data_signer();
if (ds) {
const km = ds.get_key_manager();
if (km) {
const signer = km.get_userids_mark_primary()[0].components;
let text = "Signed by ";
if (signer.email || signer.username || signer.comment) {
if (signer.username) {
text += `${signer.username} `;
}
if (signer.comment) {
text += `(${signer.comment}) `;
}
if (signer.email) {
text += `<${signer.email}>`;
}
text += "\n";
}
text += [
`PGP key ID: ${km.get_pgp_short_key_id()}`,
`PGP fingerprint: ${km.get_pgp_fingerprint().toString("hex")}`,
`Signed on ${new Date(ds.sig.when_generated() * 1000).toUTCString()}`,
"----------------------------------\n"
].join("\n");
text += unboxedLiterals.toString();
return text.trim();
} else {
throw new OperationError("Could not identify a key manager.");
}
} else {
throw new OperationError("The data does not appear to be signed.");
}
} catch (err) {
throw new OperationError(`Couldn't verify message: ${err}`);
}
}
}
export default PGPVerify;

View file

@ -6,9 +6,8 @@
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import jsqr from "jsqr";
import jimp from "jimp";
import { isImage } from "../lib/FileType.mjs";
import { parseQrCode } from "../lib/QRCode";
/**
* Parse QR Code operation
@ -25,7 +24,7 @@ class ParseQRCode extends Operation {
this.module = "Image";
this.description = "Reads an image file and attempts to detect and read a Quick Response (QR) code from the image.<br><br><u>Normalise Image</u><br>Attempts to normalise the image before parsing it to improve detection of a QR code.";
this.infoURL = "https://wikipedia.org/wiki/QR_code";
this.inputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [
{
@ -34,69 +33,28 @@ class ParseQRCode extends Operation {
"value": false
}
];
this.patterns = [
{
"match": "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)",
"flags": "",
"args": [false],
"useful": true
}
];
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {string}
*/
async run(input, args) {
const [normalise] = args;
// Make sure that the input is an image
if (!isImage(input)) throw new OperationError("Invalid file type.");
let image = input;
if (normalise) {
// Process the image to be easier to read by jsqr
// Disables the alpha channel
// Sets the image default background to white
// Normalises the image colours
// Makes the image greyscale
// Converts image to a JPEG
image = await new Promise((resolve, reject) => {
jimp.read(Buffer.from(input))
.then(image => {
image
.rgba(false)
.background(0xFFFFFFFF)
.normalize()
.greyscale()
.getBuffer(jimp.MIME_JPEG, (error, result) => {
resolve(result);
});
})
.catch(err => {
reject(new OperationError("Error reading the image file."));
});
});
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
if (image instanceof OperationError) {
throw image;
}
return new Promise((resolve, reject) => {
jimp.read(Buffer.from(image))
.then(image => {
if (image.bitmap != null) {
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
if (qrData != null) {
resolve(qrData.data);
} else {
reject(new OperationError("Couldn't read a QR code from the image."));
}
} else {
reject(new OperationError("Error reading the image file."));
}
})
.catch(err => {
reject(new OperationError("Error reading the image file."));
});
});
return await parseQrCode(input, normalise);
}
}

View file

@ -25,8 +25,8 @@ class ResizeImage extends Operation {
this.module = "Image";
this.description = "Resizes an image to the specified width and height values.";
this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -67,7 +67,7 @@ class ResizeImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
@ -86,13 +86,13 @@ class ResizeImage extends Operation {
"Bezier": jimp.RESIZE_BEZIER
};
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -110,8 +110,13 @@ class ResizeImage extends Operation {
image.resize(width, height, resizeMap[resizeAlg]);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error resizing image. (${err})`);
}
@ -119,18 +124,19 @@ class ResizeImage extends Operation {
/**
* Displays the resized image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -25,8 +25,8 @@ class RotateImage extends Operation {
this.module = "Image";
this.description = "Rotates an image by the specified number of degrees.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.presentType = "html";
this.args = [
{
@ -38,20 +38,20 @@ class RotateImage extends Operation {
}
/**
* @param {byteArray} input
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [degrees] = args;
if (!isImage(input)) {
if (!isImage(new Uint8Array(input))) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
image = await jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@ -59,8 +59,14 @@ class RotateImage extends Operation {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Rotating image...");
image.rotate(degrees);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error rotating image. (${err})`);
}
@ -68,18 +74,19 @@ class RotateImage extends Operation {
/**
* Displays the rotated image using HTML for web apps
* @param {byteArray} data
* @param {ArrayBuffer} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
if (!data.byteLength) return "";
const dataArray = new Uint8Array(data);
const type = isImage(data);
const type = isImage(dataArray);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
}
}

View file

@ -0,0 +1,168 @@
/**
* @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 { gaussianBlur } from "../lib/ImageManipulation";
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 = "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(new Uint8Array(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 (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Sharpening image... (Cloning image)");
const blurMask = image.clone();
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Sharpening image... (Blurring cloned image)");
const blurImage = gaussianBlur(image.clone(), radius, 3);
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;
}
});
let imageBuffer;
if (image.getMIME() === "image/gif") {
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
} else {
imageBuffer = await image.getBufferAsync(jimp.AUTO);
}
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;