diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs
index 8e69b020..21ed6885 100755
--- a/src/core/Utils.mjs
+++ b/src/core/Utils.mjs
@@ -529,6 +529,22 @@ class Utils {
}
+ /**
+ * Converts a string to an ArrayBuffer.
+ *
+ * @param {string} string
+ */
+ static strToArrayBuffer(string) {
+ const arrayBuffer = new ArrayBuffer(string.length * 2);
+ const arrayBufferView = new Uint8Array(arrayBuffer);
+ for (let i = 0; i < string.length; i++) {
+ arrayBufferView[i] = string.charCodeAt(i);
+ }
+
+ return arrayBuffer;
+ }
+
+
/**
* Parses CSV data and returns it as a two dimensional array or strings.
*
diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs
index f0b55857..d66b6f93 100644
--- a/src/core/lib/Magic.mjs
+++ b/src/core/lib/Magic.mjs
@@ -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();
diff --git a/src/core/lib/QRCode.mjs b/src/core/lib/QRCode.mjs
new file mode 100644
index 00000000..709aafa2
--- /dev/null
+++ b/src/core/lib/QRCode.mjs
@@ -0,0 +1,91 @@
+/**
+ * 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();
+ }
+ } 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.");
+ }
+}
diff --git a/src/core/operations/GenerateQRCode.mjs b/src/core/operations/GenerateQRCode.mjs
index ac7e5c5c..5231d750 100644
--- a/src/core/operations/GenerateQRCode.mjs
+++ b/src/core/operations/GenerateQRCode.mjs
@@ -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.
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 ``;
+ return ``;
}
- return Utils.byteArrayToChars(data);
+ return Utils.arrayBufferToStr(data);
}
}
diff --git a/src/core/operations/ParseQRCode.mjs b/src/core/operations/ParseQRCode.mjs
index 4c1e0fee..73ffec93 100644
--- a/src/core/operations/ParseQRCode.mjs
+++ b/src/core/operations/ParseQRCode.mjs
@@ -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
@@ -34,6 +33,14 @@ 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
+ }
+ ];
}
/**
@@ -44,59 +51,10 @@ class ParseQRCode extends Operation {
async run(input, args) {
const [normalise] = args;
- // Make sure that the input is an image
- if (!isImage(new Uint8Array(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(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(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);
}
}