@wdio/ocr-service 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/assets/target.png +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/ocrClickOnText.d.ts +3 -0
- package/dist/commands/ocrClickOnText.d.ts.map +1 -0
- package/dist/commands/ocrClickOnText.js +27 -0
- package/dist/commands/ocrGetElementPositionByText.d.ts +3 -0
- package/dist/commands/ocrGetElementPositionByText.d.ts.map +1 -0
- package/dist/commands/ocrGetElementPositionByText.js +46 -0
- package/dist/commands/ocrGetText.d.ts +3 -0
- package/dist/commands/ocrGetText.d.ts.map +1 -0
- package/dist/commands/ocrGetText.js +5 -0
- package/dist/commands/ocrSetValue.d.ts +3 -0
- package/dist/commands/ocrSetValue.d.ts.map +1 -0
- package/dist/commands/ocrSetValue.js +38 -0
- package/dist/commands/ocrWaitForTextDisplayed.d.ts +3 -0
- package/dist/commands/ocrWaitForTextDisplayed.d.ts.map +1 -0
- package/dist/commands/ocrWaitForTextDisplayed.js +8 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/service.d.ts +15 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +97 -0
- package/dist/types.d.ts +331 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/constants.d.ts +67 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +66 -0
- package/dist/utils/fuzzySearch.d.ts +3 -0
- package/dist/utils/fuzzySearch.d.ts.map +1 -0
- package/dist/utils/fuzzySearch.js +29 -0
- package/dist/utils/getData.d.ts +3 -0
- package/dist/utils/getData.d.ts.map +1 -0
- package/dist/utils/getData.js +71 -0
- package/dist/utils/getTextPositions.d.ts +3 -0
- package/dist/utils/getTextPositions.d.ts.map +1 -0
- package/dist/utils/getTextPositions.js +13 -0
- package/dist/utils/imageProcessing.d.ts +5 -0
- package/dist/utils/imageProcessing.d.ts.map +1 -0
- package/dist/utils/imageProcessing.js +73 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +46 -0
- package/dist/utils/sendKeys.d.ts +2 -0
- package/dist/utils/sendKeys.d.ts.map +1 -0
- package/dist/utils/sendKeys.js +21 -0
- package/dist/utils/tesseract.d.ts +13 -0
- package/dist/utils/tesseract.d.ts.map +1 -0
- package/dist/utils/tesseract.js +210 -0
- package/package.json +41 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import logger from '@wdio/logger';
|
|
2
|
+
import Jimp from 'jimp';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const log = logger('@wdio/ocr-service:imageProcessing');
|
|
7
|
+
export async function processImage({ contrast, elementRectangles, isAndroid, isIOS, ocrImagesPath, screenshot }) {
|
|
8
|
+
const image = await Jimp.read(Buffer.from(screenshot, 'base64'));
|
|
9
|
+
image.greyscale();
|
|
10
|
+
image.contrast(contrast);
|
|
11
|
+
const timestamp = new Date().getTime();
|
|
12
|
+
const platform = isAndroid ? 'android' : isIOS ? 'ios' : 'desktop';
|
|
13
|
+
let fileName = `${platform}-${timestamp}.png`;
|
|
14
|
+
let filePath = join(ocrImagesPath, fileName);
|
|
15
|
+
const screenshotSize = { width: image.bitmap.width, height: image.bitmap.height };
|
|
16
|
+
if (elementRectangles) {
|
|
17
|
+
// Make sure it doesn't crop outside of the screenshot
|
|
18
|
+
const cropX = Math.max(0, elementRectangles.x);
|
|
19
|
+
const cropY = Math.max(0, elementRectangles.y);
|
|
20
|
+
const cropWidth = Math.min(elementRectangles.width, screenshotSize.width - cropX);
|
|
21
|
+
const cropHeight = Math.min(elementRectangles.height, screenshotSize.height - cropY);
|
|
22
|
+
image.crop(cropX, cropY, cropWidth, cropHeight);
|
|
23
|
+
fileName = `${platform}-${timestamp}-cropped.png`;
|
|
24
|
+
filePath = join(ocrImagesPath, fileName);
|
|
25
|
+
}
|
|
26
|
+
await image.writeAsync(filePath);
|
|
27
|
+
return { filePath };
|
|
28
|
+
}
|
|
29
|
+
export async function drawTarget({ filePath, targetX, targetY }) {
|
|
30
|
+
try {
|
|
31
|
+
const targetImagePath = join(__dirname, '..', '..', 'assets', 'target.png');
|
|
32
|
+
const [sourceImage, targetImage] = await Promise.all([
|
|
33
|
+
Jimp.read(filePath),
|
|
34
|
+
Jimp.read(targetImagePath)
|
|
35
|
+
]);
|
|
36
|
+
const xPosition = targetX - (targetImage.bitmap.width / 2);
|
|
37
|
+
const yPosition = targetY - (targetImage.bitmap.height / 2);
|
|
38
|
+
sourceImage.composite(targetImage, xPosition, yPosition);
|
|
39
|
+
await sourceImage.writeAsync(filePath);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log.error('Failed to draw target on image:', error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function drawHighlightedWords({ filePath, highlights }) {
|
|
46
|
+
try {
|
|
47
|
+
const image = await Jimp.read(filePath);
|
|
48
|
+
const highlightColor = { r: 57, g: 170, b: 86, a: 0.5 };
|
|
49
|
+
highlights.forEach(({ left, right, top, bottom }) => {
|
|
50
|
+
const width = right - left;
|
|
51
|
+
const height = bottom - top;
|
|
52
|
+
// Apply the semi-transparent highlight
|
|
53
|
+
for (let y = top; y < top + height; y++) {
|
|
54
|
+
for (let x = left; x < left + width; x++) {
|
|
55
|
+
// Get the current pixel color
|
|
56
|
+
const currentColor = image.getPixelColor(x, y);
|
|
57
|
+
const rgba = Jimp.intToRGBA(currentColor);
|
|
58
|
+
// Calculate new color values using simple alpha blending
|
|
59
|
+
const newR = (highlightColor.r * highlightColor.a) + (rgba.r * (1 - highlightColor.a));
|
|
60
|
+
const newG = (highlightColor.g * highlightColor.a) + (rgba.g * (1 - highlightColor.a));
|
|
61
|
+
const newB = (highlightColor.b * highlightColor.a) + (rgba.b * (1 - highlightColor.a));
|
|
62
|
+
const newA = rgba.a; // Use original alpha to maintain image integrity
|
|
63
|
+
// Set the new pixel color
|
|
64
|
+
image.setPixelColor(Jimp.rgbaToInt(newR, newG, newB, newA), x, y);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
await image.writeAsync(filePath);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
log.error('Failed to highlight words on image:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="./standalone.js" />
|
|
2
|
+
import type { ClickPoint, DetermineClickPointOptions, Rectangles, RectReturn, ScreenshotSize } from '../types.js';
|
|
3
|
+
export declare function getScreenshotSize(screenshot: string): ScreenshotSize;
|
|
4
|
+
export declare function getDprPositions(values: Rectangles, dpr: number): Rectangles;
|
|
5
|
+
/**
|
|
6
|
+
* Determine the click point
|
|
7
|
+
*/
|
|
8
|
+
export declare function determineClickPoint(options: DetermineClickPointOptions): ClickPoint;
|
|
9
|
+
/**
|
|
10
|
+
* Adjust the Element Bbox
|
|
11
|
+
*/
|
|
12
|
+
export declare function adjustElementBbox(bbox: Rectangles, elementRect: RectReturn): Rectangles;
|
|
13
|
+
export declare function createOcrDir(imagesPath: string): string;
|
|
14
|
+
export declare function isRectanglesObject(obj: WebdriverIO.Element | ChainablePromiseElement | RectReturn): boolean;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,0BAA0B,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAKpE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAO3E;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,UAAU,CAMnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,GAAG,UAAU,CAOvF;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,CAAC,OAAO,GAAG,uBAAuB,GAAG,UAAU,GAAG,OAAO,CAM3G"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
export function getScreenshotSize(screenshot) {
|
|
4
|
+
return {
|
|
5
|
+
height: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(20)),
|
|
6
|
+
width: Math.round(Buffer.from(screenshot, 'base64').readUInt32BE(16)),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function getDprPositions(values, dpr) {
|
|
10
|
+
Object.keys({ ...values }).map((value) => {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
values[value] /= dpr;
|
|
13
|
+
});
|
|
14
|
+
return values;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Determine the click point
|
|
18
|
+
*/
|
|
19
|
+
export function determineClickPoint(options) {
|
|
20
|
+
const { rectangles: { left, right, top, bottom } } = options;
|
|
21
|
+
const x = Math.round(left + (right - left) / 2);
|
|
22
|
+
const y = Math.round(top + (bottom - top) / 2);
|
|
23
|
+
return { x, y };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adjust the Element Bbox
|
|
27
|
+
*/
|
|
28
|
+
export function adjustElementBbox(bbox, elementRect) {
|
|
29
|
+
return {
|
|
30
|
+
left: Math.round(bbox.left + elementRect.x),
|
|
31
|
+
top: Math.round(bbox.top + elementRect.y),
|
|
32
|
+
right: Math.round(bbox.right + elementRect.x),
|
|
33
|
+
bottom: Math.round(bbox.bottom + elementRect.y),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function createOcrDir(imagesPath) {
|
|
37
|
+
const ocrDir = join(imagesPath, 'ocr');
|
|
38
|
+
mkdirSync(ocrDir, { recursive: true });
|
|
39
|
+
return ocrDir;
|
|
40
|
+
}
|
|
41
|
+
export function isRectanglesObject(obj) {
|
|
42
|
+
const properties = ['x', 'y', 'width', 'height'];
|
|
43
|
+
return obj !== null &&
|
|
44
|
+
typeof obj === 'object' &&
|
|
45
|
+
properties.every(prop => typeof obj[prop] === 'number' && !isNaN(obj[prop]) && obj[prop] >= 0);
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendKeys.d.ts","sourceRoot":"","sources":["../../src/utils/sendKeys.ts"],"names":[],"mappings":"AAEA,wBAA8B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBzF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Key } from 'webdriverio';
|
|
2
|
+
export default async function sendKeys(value, submitValue) {
|
|
3
|
+
const actionChain = browser.action('key');
|
|
4
|
+
[...value].forEach((char) => {
|
|
5
|
+
actionChain.down(char);
|
|
6
|
+
});
|
|
7
|
+
[...value].forEach((char) => {
|
|
8
|
+
actionChain.up(char);
|
|
9
|
+
});
|
|
10
|
+
if (submitValue) {
|
|
11
|
+
/**
|
|
12
|
+
* XCTest API only allows to send keypresses (e.g. keydown+keyup).
|
|
13
|
+
*/
|
|
14
|
+
if (!browser.isIOS) {
|
|
15
|
+
actionChain.pause(50);
|
|
16
|
+
}
|
|
17
|
+
actionChain.down(Key.Enter);
|
|
18
|
+
actionChain.up(Key.Enter);
|
|
19
|
+
}
|
|
20
|
+
await actionChain.perform();
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GetOcrData, ParseWordData, TessaractDataOptions } from '../types.js';
|
|
2
|
+
export declare function isSystemTesseractAvailable(tesseractName?: string): boolean;
|
|
3
|
+
export declare function parseWordDataFromText(attributes: string[]): ParseWordData;
|
|
4
|
+
export declare function extractTextFromAlto(altoContent: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Handle the OCR with Tesseract with pure JS
|
|
7
|
+
*/
|
|
8
|
+
export declare function getNodeOcrData(options: TessaractDataOptions): Promise<GetOcrData>;
|
|
9
|
+
/**
|
|
10
|
+
* Handle the OCR with Tesseract with the system installed version
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSystemOcrData(options: TessaractDataOptions): Promise<GetOcrData>;
|
|
13
|
+
//# sourceMappingURL=tesseract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tesseract.d.ts","sourceRoot":"","sources":["../../src/utils/tesseract.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAQ,aAAa,EAAc,oBAAoB,EAAyD,MAAM,aAAa,CAAA;AAE3J,wBAAgB,0BAA0B,CAAC,aAAa,GAAE,MAAW,GAAG,OAAO,CAW9E;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,aAAa,CA4BzE;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8FvF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8EzF"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { createWorker, PSM } from 'tesseract.js';
|
|
3
|
+
import { recognize } from 'node-tesseract-ocr';
|
|
4
|
+
import { parseString } from 'xml2js';
|
|
5
|
+
export function isSystemTesseractAvailable(tesseractName = '') {
|
|
6
|
+
const binary = tesseractName || 'tesseract';
|
|
7
|
+
const command = [binary, '--version'].join(' ');
|
|
8
|
+
try {
|
|
9
|
+
execSync(command);
|
|
10
|
+
}
|
|
11
|
+
catch (ign) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function parseWordDataFromText(attributes) {
|
|
17
|
+
let bbox = {
|
|
18
|
+
left: 0,
|
|
19
|
+
top: 0,
|
|
20
|
+
right: 0,
|
|
21
|
+
bottom: 0,
|
|
22
|
+
};
|
|
23
|
+
let wc = 0;
|
|
24
|
+
attributes.forEach((attribute) => {
|
|
25
|
+
if (attribute.includes('bbox')) {
|
|
26
|
+
const bboxValues = attribute.replace('bbox ', '').split(' ');
|
|
27
|
+
bbox = {
|
|
28
|
+
left: Number(bboxValues[0]),
|
|
29
|
+
top: Number(bboxValues[1]),
|
|
30
|
+
right: Number(bboxValues[2]),
|
|
31
|
+
bottom: Number(bboxValues[3]),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else if (attribute.includes('x_wconf')) {
|
|
35
|
+
const score = attribute.replace('x_wconf ', '');
|
|
36
|
+
wc = Number(score) / 100;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
...{ bbox },
|
|
41
|
+
wc,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function extractTextFromAlto(altoContent) {
|
|
45
|
+
const regex = /<String[^>]*CONTENT="([^"]+)"[^>]*>/g;
|
|
46
|
+
let match;
|
|
47
|
+
const textArray = [];
|
|
48
|
+
while ((match = regex.exec(altoContent)) !== null) {
|
|
49
|
+
textArray.push(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return textArray.join(' ');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Handle the OCR with Tesseract with pure JS
|
|
55
|
+
*/
|
|
56
|
+
export async function getNodeOcrData(options) {
|
|
57
|
+
let worker;
|
|
58
|
+
try {
|
|
59
|
+
const { filePath, language } = options;
|
|
60
|
+
const jsonSingleWords = [];
|
|
61
|
+
const jsonWordStrings = [];
|
|
62
|
+
let composedBlocks = [];
|
|
63
|
+
worker = await createWorker(language);
|
|
64
|
+
await worker.setParameters({
|
|
65
|
+
tessedit_pageseg_mode: PSM.AUTO,
|
|
66
|
+
tessjs_create_tsv: '0',
|
|
67
|
+
tessjs_create_box: '0',
|
|
68
|
+
tessjs_create_unlv: '0',
|
|
69
|
+
tessjs_create_osd: '0',
|
|
70
|
+
});
|
|
71
|
+
const { data: { text, hocr } } = await worker.recognize(filePath);
|
|
72
|
+
const formatedText = text.replace(/[\r\n]{2,}/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
73
|
+
await worker.terminate();
|
|
74
|
+
if (!hocr) {
|
|
75
|
+
throw Error(`No hocr data was found for the OCR, please verify image at: ${filePath}.`);
|
|
76
|
+
}
|
|
77
|
+
parseString(hocr, (error, data) => {
|
|
78
|
+
if (error) {
|
|
79
|
+
throw Error(`An error happened when parsing the getNodeOcrData, see: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
composedBlocks = data.div.div;
|
|
82
|
+
});
|
|
83
|
+
if (!composedBlocks || composedBlocks.length === 0) {
|
|
84
|
+
throw Error('No text was found for the OCR, please verify the stored image.');
|
|
85
|
+
}
|
|
86
|
+
composedBlocks.forEach((block) => {
|
|
87
|
+
if (!block.p || !Array.isArray(block.p)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
block.p.forEach((paragraph) => {
|
|
91
|
+
if (!paragraph.span || !Array.isArray(paragraph.span)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
paragraph.span.forEach((line) => {
|
|
95
|
+
if (!line.span || !Array.isArray(line.span)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const lineData = {
|
|
99
|
+
text: '',
|
|
100
|
+
bbox: { bottom: 0, left: 0, right: 0, top: 0 },
|
|
101
|
+
};
|
|
102
|
+
line.span.forEach(word => {
|
|
103
|
+
if (!word._ || !word.$) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const text = word._;
|
|
107
|
+
const title = word.$.title;
|
|
108
|
+
const attributeValue = `; ${title}`.split('; ');
|
|
109
|
+
const { bbox, wc } = parseWordDataFromText(attributeValue);
|
|
110
|
+
jsonSingleWords.push({
|
|
111
|
+
text,
|
|
112
|
+
bbox,
|
|
113
|
+
wc,
|
|
114
|
+
});
|
|
115
|
+
lineData.text = `${lineData.text} ${text}`.trim();
|
|
116
|
+
lineData.bbox = bbox;
|
|
117
|
+
});
|
|
118
|
+
if (lineData.text !== '') {
|
|
119
|
+
jsonWordStrings.push(lineData);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
lines: jsonWordStrings,
|
|
126
|
+
words: jsonSingleWords,
|
|
127
|
+
text: formatedText,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (worker) {
|
|
132
|
+
await worker.terminate();
|
|
133
|
+
}
|
|
134
|
+
throw Error(`An error happened when parsing the getNodeOcrData, see: ${error}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Handle the OCR with Tesseract with the system installed version
|
|
139
|
+
*/
|
|
140
|
+
export async function getSystemOcrData(options) {
|
|
141
|
+
try {
|
|
142
|
+
const { filePath, language } = options;
|
|
143
|
+
const jsonSingleWords = [];
|
|
144
|
+
const jsonWordStrings = [];
|
|
145
|
+
let composedBlocks = [];
|
|
146
|
+
const result = await recognize(filePath, {
|
|
147
|
+
lang: language,
|
|
148
|
+
oem: 1,
|
|
149
|
+
// https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc
|
|
150
|
+
psm: 3,
|
|
151
|
+
presets: ['alto'],
|
|
152
|
+
});
|
|
153
|
+
parseString(result, (error, data) => {
|
|
154
|
+
if (error) {
|
|
155
|
+
throw Error(`An error happened when parsing the getSystemOcrData, see: ${error}`);
|
|
156
|
+
}
|
|
157
|
+
composedBlocks = data.alto.Layout[0].Page[0].PrintSpace[0].ComposedBlock;
|
|
158
|
+
});
|
|
159
|
+
const text = extractTextFromAlto(result);
|
|
160
|
+
if (!composedBlocks || composedBlocks.length === 0) {
|
|
161
|
+
throw Error('No text was found for the OCR, please verify the stored image.');
|
|
162
|
+
}
|
|
163
|
+
composedBlocks.forEach(composedBlock => {
|
|
164
|
+
composedBlock.TextBlock?.forEach(textBlock => {
|
|
165
|
+
textBlock.TextLine?.forEach(textLine => {
|
|
166
|
+
let lineText = '';
|
|
167
|
+
let lineBbox = null;
|
|
168
|
+
if (textLine.$) {
|
|
169
|
+
lineBbox = {
|
|
170
|
+
left: Number(textLine.$.HPOS),
|
|
171
|
+
top: Number(textLine.$.VPOS),
|
|
172
|
+
right: Number(textLine.$.HPOS) + Number(textLine.$.WIDTH),
|
|
173
|
+
bottom: Number(textLine.$.VPOS) + Number(textLine.$.HEIGHT),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
textLine.String?.forEach(string => {
|
|
177
|
+
const { CONTENT, HPOS, VPOS, WIDTH, HEIGHT, WC } = string.$;
|
|
178
|
+
if (!CONTENT || !HPOS || !VPOS || !WIDTH || !HEIGHT || !WC) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const word = {
|
|
182
|
+
text: CONTENT,
|
|
183
|
+
bbox: {
|
|
184
|
+
left: Number(HPOS),
|
|
185
|
+
top: Number(VPOS),
|
|
186
|
+
right: Number(HPOS) + Number(WIDTH),
|
|
187
|
+
bottom: Number(VPOS) + Number(HEIGHT),
|
|
188
|
+
},
|
|
189
|
+
wc: Number(WC),
|
|
190
|
+
};
|
|
191
|
+
jsonSingleWords.push(word);
|
|
192
|
+
lineText += `${CONTENT} `;
|
|
193
|
+
});
|
|
194
|
+
lineText = lineText.trim();
|
|
195
|
+
if (lineText !== '' && lineBbox) {
|
|
196
|
+
jsonWordStrings.push({ text: lineText, bbox: lineBbox });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
lines: jsonWordStrings,
|
|
203
|
+
words: jsonSingleWords,
|
|
204
|
+
text: text,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw Error(`An error happened when parsing the getSystemOcrData, see: ${error}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wdio/ocr-service",
|
|
3
|
+
"author": "Wim Selles - wswebcreation",
|
|
4
|
+
"description": "A WebdriverIO service that is using Tesseract OCR for Desktop/Mobile Web and Mobile Native App tests.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://webdriver.io/docs/visual-testing",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/webdriverio/visual-testing.git"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"ocr-service": "./dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"webdriverio",
|
|
17
|
+
"tesseract",
|
|
18
|
+
"ocr",
|
|
19
|
+
"image"
|
|
20
|
+
],
|
|
21
|
+
"exports": "./dist/index.js",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@wdio/globals": "^8.36.1",
|
|
26
|
+
"@wdio/logger": "^8.28.0",
|
|
27
|
+
"@wdio/types": "^8.36.1",
|
|
28
|
+
"fuse.js": "^7.0.0",
|
|
29
|
+
"inquirer": "^9.2.22",
|
|
30
|
+
"jimp": "^0.22.12",
|
|
31
|
+
"node-tesseract-ocr": "^2.2.1",
|
|
32
|
+
"tesseract.js": "^5.1.0",
|
|
33
|
+
"xml2js": "^0.6.2"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "run-s clean build:*",
|
|
37
|
+
"build:tsc": "tsc --project ./tsconfig.json",
|
|
38
|
+
"clean": "rimraf coverage build .tmp",
|
|
39
|
+
"watch": "pnpm run build:tsc -w"
|
|
40
|
+
}
|
|
41
|
+
}
|