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 6262df7b4..dfcff9654 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 37f75f584..70cfd3ba1 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 dc0774af9..7711c004b 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