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); } }