From ab37c1e562dbee0495ed32876ecbb8225282af25 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 21 Jun 2024 14:12:28 +0100 Subject: [PATCH] Fixed Optical Character Recognition and added tests --- .../OpticalCharacterRecognition.mjs | 21 +++++++----- tests/browser/02_ops.js | 32 +++++++++++------- tests/browser/browserUtils.js | 5 +-- tests/samples/files/testocr.png | Bin 0 -> 23359 bytes 4 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 tests/samples/files/testocr.png diff --git a/src/core/operations/OpticalCharacterRecognition.mjs b/src/core/operations/OpticalCharacterRecognition.mjs index 6262df7b..dfcff965 100644 --- a/src/core/operations/OpticalCharacterRecognition.mjs +++ b/src/core/operations/OpticalCharacterRecognition.mjs @@ -12,9 +12,10 @@ import { isImage } from "../lib/FileType.mjs"; import { toBase64 } from "../lib/Base64.mjs"; import { isWorkerEnvironment } from "../Utils.mjs"; -import process from "process"; import { createWorker } from "tesseract.js"; +const OEM_MODES = ["Tesseract only", "LSTM only", "Tesseract/LSTM Combined"]; + /** * Optical Character Recognition operation */ @@ -37,6 +38,12 @@ class OpticalCharacterRecognition extends Operation { name: "Show confidence", type: "boolean", value: true + }, + { + name: "OCR Engine Mode", + type: "option", + value: OEM_MODES, + defaultIndex: 1 } ]; } @@ -47,7 +54,7 @@ class OpticalCharacterRecognition extends Operation { * @returns {string} */ async run(input, args) { - const [showConfidence] = args; + const [showConfidence, oemChoice] = args; if (!isWorkerEnvironment()) throw new OperationError("This operation only works in a browser"); @@ -56,12 +63,13 @@ class OpticalCharacterRecognition extends Operation { throw new OperationError("Unsupported file type (supported: jpg,png,pbm,bmp) or no file provided"); } - const assetDir = isWorkerEnvironment() ? `${self.docURL}/assets/` : `${process.cwd()}/src/core/vendor/`; + const assetDir = `${self.docURL}/assets/`; + const oem = OEM_MODES.indexOf(oemChoice); try { self.sendStatusMessage("Spinning up Tesseract worker..."); const image = `data:${type};base64,${toBase64(input)}`; - const worker = createWorker({ + const worker = await createWorker("eng", oem, { workerPath: `${assetDir}tesseract/worker.min.js`, langPath: `${assetDir}tesseract/lang-data`, corePath: `${assetDir}tesseract/tesseract-core.wasm.js`, @@ -71,11 +79,6 @@ class OpticalCharacterRecognition extends Operation { } } }); - await worker.load(); - self.sendStatusMessage(`Loading English language pack...`); - await worker.loadLanguage("eng"); - self.sendStatusMessage("Intialising Tesseract API..."); - await worker.initialize("eng"); self.sendStatusMessage("Finding text..."); const result = await worker.recognize(image); diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 37f75f58..70cfd3ba 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -236,7 +236,7 @@ module.exports = { // testOp(browser, "OR", "test input", "test_output"); // testOp(browser, "Object Identifier to Hex", "test input", "test_output"); testOpHtml(browser, "Offset checker", "test input\n\nbest input", ".hl5", "est input"); - // testOp(browser, "Optical Character Recognition", "test input", "test_output"); + testOpFile(browser, "Optical Character Recognition", "files/testocr.png", false, /This is a lot of 12 point text to test the/, [], 10000); // testOp(browser, "PEM to Hex", "test input", "test_output"); // testOp(browser, "PGP Decrypt", "test input", "test_output"); // testOp(browser, "PGP Decrypt and Verify", "test input", "test_output"); @@ -408,7 +408,7 @@ module.exports = { * @param {Browser} browser - Nightwatch client * @param {string|Array} opName - name of operation to be tested, array for multiple ops * @param {string} input - input text for test - * @param {Array|Array>} args - arguments, nested if multiple ops + * @param {Array|Array>} [args=[]] - arguments, nested if multiple ops */ function bakeOp(browser, opName, input, args=[]) { browser.perform(function() { @@ -425,8 +425,8 @@ function bakeOp(browser, opName, input, args=[]) { * @param {Browser} browser - Nightwatch client * @param {string|Array} opName - name of operation to be tested, array for multiple ops * @param {string} input - input text - * @param {string} output - expected output - * @param {Array|Array>} args - arguments, nested if multiple ops + * @param {string|RegExp} output - expected output + * @param {Array|Array>} [args=[]] - arguments, nested if multiple ops */ function testOp(browser, opName, input, output, args=[]) { bakeOp(browser, opName, input, args); @@ -440,8 +440,8 @@ function testOp(browser, opName, input, output, args=[]) { * @param {string|Array} opName - name of operation to be tested array for multiple ops * @param {string} input - input text * @param {string} cssSelector - CSS selector for HTML output - * @param {string} output - expected output - * @param {Array|Array>} args - arguments, nested if multiple ops + * @param {string|RegExp} output - expected output + * @param {Array|Array>} [args=[]] - arguments, nested if multiple ops */ function testOpHtml(browser, opName, input, cssSelector, output, args=[]) { bakeOp(browser, opName, input, args); @@ -459,9 +459,9 @@ function testOpHtml(browser, opName, input, cssSelector, output, args=[]) { * @param {Browser} browser - Nightwatch client * @param {string|Array} opName - name of operation to be tested array for multiple ops * @param {string} filename - filename of image file from samples directory - * @param {Array|Array>} args - arguments, nested if multiple ops + * @param {Array|Array>} [args=[]] - arguments, nested if multiple ops */ -function testOpImage(browser, opName, filename, args) { +function testOpImage(browser, opName, filename, args=[]) { browser.perform(function() { console.log(`Current test: ${opName}`); }); @@ -481,11 +481,12 @@ function testOpImage(browser, opName, filename, args) { * @param {Browser} browser - Nightwatch client * @param {string|Array} opName - name of operation to be tested array for multiple ops * @param {string} filename - filename of file from samples directory - * @param {string} cssSelector - CSS selector for HTML output - * @param {string} output - expected output - * @param {Array|Array>} args - arguments, nested if multiple ops + * @param {string|boolean} cssSelector - CSS selector for HTML output or false for normal text output + * @param {string|RegExp} output - expected output + * @param {Array|Array>} [args=[]] - arguments, nested if multiple ops + * @param {number} [waitWindow=1000] - The number of milliseconds to wait for the output to be correct */ -function testOpFile(browser, opName, filename, cssSelector, output, args) { +function testOpFile(browser, opName, filename, cssSelector, output, args=[], waitWindow=1000) { browser.perform(function() { console.log(`Current test: ${opName}`); }); @@ -494,9 +495,14 @@ function testOpFile(browser, opName, filename, cssSelector, output, args) { browser.pause(100).waitForElementVisible("#stale-indicator", 5000); utils.bake(browser); - if (typeof output === "string") { + if (!cssSelector) { + // Text output + utils.expectOutput(browser, output, true, waitWindow); + } else if (typeof output === "string") { + // HTML output - string match browser.expect.element("#output-html " + cssSelector).text.that.equals(output); } else if (output instanceof RegExp) { + // HTML output - RegEx match browser.expect.element("#output-html " + cssSelector).text.that.matches(output); } } diff --git a/tests/browser/browserUtils.js b/tests/browser/browserUtils.js index dc0774af..7711c004 100644 --- a/tests/browser/browserUtils.js +++ b/tests/browser/browserUtils.js @@ -180,15 +180,16 @@ function loadRecipe(browser, opName, input, args) { * @param {Browser} browser - Nightwatch client * @param {string|RegExp} expected - The expected output value * @param {boolean} [waitNotNull=false] - Wait for the output to not be empty before testing the value + * @param {number} [waitWindow=1000] - The number of milliseconds to wait for the output to be correct */ -function expectOutput(browser, expected, waitNotNull=false) { +function expectOutput(browser, expected, waitNotNull=false, waitWindow=1000) { if (waitNotNull && expected !== "") { browser.waitUntil(async function() { const output = await this.execute(function() { return window.app.manager.output.outputEditorView.state.doc.toString(); }); return output.length; - }, 1000); + }, waitWindow); } browser.execute(expected => { diff --git a/tests/samples/files/testocr.png b/tests/samples/files/testocr.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8d0e78b5d4cdb46258b479fb7def468e0040b6 GIT binary patch literal 23359 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 literal 0 HcmV?d00001