@midscene/web 0.0.1
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/dist/es/index.js +442 -0
- package/dist/lib/index.js +473 -0
- package/dist/script/htmlElement.js +272 -0
- package/dist/script/types/htmlElement.d.ts +26 -0
- package/dist/types/index.d.ts +66 -0
- package/modern.config.ts +13 -0
- package/modern.inspect.config.ts +20 -0
- package/package.json +85 -0
- package/playwright.config.ts +42 -0
- package/src/html-element/constants.ts +10 -0
- package/src/html-element/debug.ts +3 -0
- package/src/html-element/dom-util.ts +11 -0
- package/src/html-element/extractInfo.ts +168 -0
- package/src/html-element/index.ts +1 -0
- package/src/html-element/util.ts +160 -0
- package/src/img/img.ts +132 -0
- package/src/img/util.ts +28 -0
- package/src/index.ts +2 -0
- package/src/playwright/actions.ts +276 -0
- package/src/playwright/cdp.ts +322 -0
- package/src/playwright/element.ts +74 -0
- package/src/playwright/index.ts +120 -0
- package/src/playwright/utils.ts +88 -0
- package/src/puppeteer/element.ts +49 -0
- package/src/puppeteer/index.ts +6 -0
- package/src/puppeteer/utils.ts +116 -0
- package/tests/e2e/ai-auto-todo.spec.ts +24 -0
- package/tests/e2e/ai-xicha.spec.ts +34 -0
- package/tests/e2e/fixture.ts +6 -0
- package/tests/e2e/generate-test-data.spec.ts +60 -0
- package/tests/e2e/todo-app-midscene.spec.ts +98 -0
- package/tests/e2e/tool.ts +63 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getNodeAttributes,
|
|
3
|
+
getPseudoElementContent,
|
|
4
|
+
logger,
|
|
5
|
+
setDataForNode,
|
|
6
|
+
validTextNodeContent,
|
|
7
|
+
visibleRect,
|
|
8
|
+
} from './util';
|
|
9
|
+
import { NodeType, TEXT_SIZE_THRESHOLD } from './constants';
|
|
10
|
+
import { isButtonElement, isImgElement, isInputElement } from './dom-util';
|
|
11
|
+
|
|
12
|
+
interface NodeDescriptor {
|
|
13
|
+
node: Node;
|
|
14
|
+
childrens: NodeDescriptor[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ElementInfo {
|
|
18
|
+
id: string;
|
|
19
|
+
locator: string | void;
|
|
20
|
+
attributes: {
|
|
21
|
+
nodeType: NodeType;
|
|
22
|
+
[key: string]: string;
|
|
23
|
+
};
|
|
24
|
+
content: string;
|
|
25
|
+
rect: { left: number; top: number; width: number; height: number };
|
|
26
|
+
center: [number, number];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const container: HTMLElement = document.body;
|
|
30
|
+
|
|
31
|
+
function generateId(numberId: number) {
|
|
32
|
+
// const letters = 'ABCDEFGHIJKLMNPRSTUVXYZ';
|
|
33
|
+
// const numbers = '0123456789';
|
|
34
|
+
// const randomLetter = letters.charAt(Math.floor(Math.random() * letters.length)).toUpperCase();
|
|
35
|
+
// const randomNumber = numbers.charAt(Math.floor(Math.random() * numbers.length));
|
|
36
|
+
// return randomLetter + numberId;
|
|
37
|
+
return `${numberId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extractTextWithPositionDFS(initNode: Node = container): ElementInfo[] {
|
|
41
|
+
const elementInfoArray: ElementInfo[] = [];
|
|
42
|
+
const nodeMapTree: NodeDescriptor = { node: initNode, childrens: [] };
|
|
43
|
+
let nodeIndex = 1;
|
|
44
|
+
|
|
45
|
+
function dfs(node: Node, parentNode: NodeDescriptor | null = null): void {
|
|
46
|
+
if (!node) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const currentNodeDes: NodeDescriptor = { node, childrens: [] };
|
|
51
|
+
if (parentNode?.childrens) {
|
|
52
|
+
parentNode.childrens.push(currentNodeDes);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
collectElementInfo(node);
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
58
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
59
|
+
logger('will dfs', node.childNodes[i]);
|
|
60
|
+
dfs(node.childNodes[i], currentNodeDes);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectElementInfo(node: Node) {
|
|
65
|
+
const rect = visibleRect(node);
|
|
66
|
+
if (!rect) {
|
|
67
|
+
logger('Element is not visible', node);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isInputElement(node)) {
|
|
72
|
+
const attributes = getNodeAttributes(node);
|
|
73
|
+
const selector = setDataForNode(node, nodeIndex);
|
|
74
|
+
elementInfoArray.push({
|
|
75
|
+
id: generateId(nodeIndex++),
|
|
76
|
+
locator: selector,
|
|
77
|
+
attributes: {
|
|
78
|
+
...attributes,
|
|
79
|
+
nodeType: NodeType.INPUT,
|
|
80
|
+
},
|
|
81
|
+
content: attributes.placeholder || '',
|
|
82
|
+
rect,
|
|
83
|
+
center: [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)],
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isButtonElement(node)) {
|
|
89
|
+
const attributes = getNodeAttributes(node);
|
|
90
|
+
const pseudo = getPseudoElementContent(node);
|
|
91
|
+
const selector = setDataForNode(node, nodeIndex);
|
|
92
|
+
elementInfoArray.push({
|
|
93
|
+
id: generateId(nodeIndex++),
|
|
94
|
+
locator: selector,
|
|
95
|
+
attributes: {
|
|
96
|
+
...attributes,
|
|
97
|
+
nodeType: NodeType.BUTTON,
|
|
98
|
+
},
|
|
99
|
+
content: node.innerText || pseudo.before || pseudo.after || '',
|
|
100
|
+
rect,
|
|
101
|
+
center: [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)],
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isImgElement(node)) {
|
|
107
|
+
const attributes = getNodeAttributes(node);
|
|
108
|
+
const selector = setDataForNode(node, nodeIndex);
|
|
109
|
+
elementInfoArray.push({
|
|
110
|
+
id: generateId(nodeIndex++),
|
|
111
|
+
locator: selector,
|
|
112
|
+
attributes: {
|
|
113
|
+
...attributes,
|
|
114
|
+
nodeType: NodeType.IMG,
|
|
115
|
+
},
|
|
116
|
+
content: '',
|
|
117
|
+
rect,
|
|
118
|
+
center: [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)],
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// if (node instanceof HTMLElement && hasOverflowY(node)) {
|
|
124
|
+
// const rect = visibleRect(node);
|
|
125
|
+
// if (!rect || rect.height < 100) {
|
|
126
|
+
// logger('Element is not visible', node);
|
|
127
|
+
// } else {
|
|
128
|
+
// const attributes = getNodeAttributes(node);
|
|
129
|
+
// const selector = setDataForNode(node, nodeIndex);
|
|
130
|
+
// elementInfoArray.push({
|
|
131
|
+
// id: nodeIndex++,
|
|
132
|
+
// nodeType: 'ScrollContainer Node',
|
|
133
|
+
// locator: selector!,
|
|
134
|
+
// parentIndex,
|
|
135
|
+
// attributes,
|
|
136
|
+
// content: "",
|
|
137
|
+
// rect,
|
|
138
|
+
// center: [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)],
|
|
139
|
+
// });
|
|
140
|
+
// }
|
|
141
|
+
// }
|
|
142
|
+
|
|
143
|
+
const text = validTextNodeContent(node);
|
|
144
|
+
if (text) {
|
|
145
|
+
if (rect.width < TEXT_SIZE_THRESHOLD || rect.height < TEXT_SIZE_THRESHOLD) {
|
|
146
|
+
logger('Element is too small', text);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const attributes = getNodeAttributes(node);
|
|
150
|
+
const selector = setDataForNode(node, nodeIndex);
|
|
151
|
+
elementInfoArray.push({
|
|
152
|
+
id: generateId(nodeIndex++),
|
|
153
|
+
attributes: {
|
|
154
|
+
...attributes,
|
|
155
|
+
nodeType: NodeType.TEXT,
|
|
156
|
+
},
|
|
157
|
+
locator: selector,
|
|
158
|
+
center: [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)],
|
|
159
|
+
// attributes,
|
|
160
|
+
content: text,
|
|
161
|
+
rect,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dfs(initNode, nodeMapTree);
|
|
167
|
+
return elementInfoArray;
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { extractTextWithPositionDFS } from './extractInfo';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// import { TEXT_MAX_SIZE } from './constants';
|
|
2
|
+
|
|
3
|
+
export function logger(...msg: any[]): void {
|
|
4
|
+
// console.log(...msg);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// const nodeIndexCounter = 0;
|
|
8
|
+
|
|
9
|
+
const taskIdKey = '_midscene_retrieve_task_id';
|
|
10
|
+
// const nodeDataIdKey = 'data-midscene-task-';
|
|
11
|
+
// const nodeIndexKey = '_midscene_retrieve_node_index';
|
|
12
|
+
|
|
13
|
+
function selectorForValue(val: number): string {
|
|
14
|
+
return `[${taskIdKey}='${val}']`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setDataForNode(node: HTMLElement | Node, nodeIndex: number): string {
|
|
18
|
+
const taskId = taskIdKey;
|
|
19
|
+
if (!(node instanceof HTMLElement)) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
if (!taskId) {
|
|
23
|
+
console.error('No task id found');
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const selector = selectorForValue(nodeIndex);
|
|
28
|
+
node.setAttribute(taskIdKey, nodeIndex.toString());
|
|
29
|
+
return selector;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getPseudoElementContent(element: Node): { before: string; after: string } {
|
|
33
|
+
if (!(element instanceof HTMLElement)) {
|
|
34
|
+
return { before: '', after: '' };
|
|
35
|
+
}
|
|
36
|
+
const beforeContent = window.getComputedStyle(element, '::before').getPropertyValue('content');
|
|
37
|
+
const afterContent = window.getComputedStyle(element, '::after').getPropertyValue('content');
|
|
38
|
+
return {
|
|
39
|
+
before: beforeContent === 'none' ? '' : beforeContent.replace(/"/g, ''),
|
|
40
|
+
after: afterContent === 'none' ? '' : afterContent.replace(/"/g, ''),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasOverflowY(element: HTMLElement): boolean {
|
|
45
|
+
const style = window.getComputedStyle(element);
|
|
46
|
+
return style.overflowY === 'scroll' || style.overflowY === 'auto' || style.overflowY === 'hidden';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function visibleRect(
|
|
50
|
+
el: HTMLElement | Node | null,
|
|
51
|
+
): { left: number; top: number; width: number; height: number } | false {
|
|
52
|
+
if (!el) {
|
|
53
|
+
logger('Element is not in the DOM hierarchy');
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!(el instanceof HTMLElement)) {
|
|
58
|
+
logger('Element is not in the DOM hierarchy');
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const style = window.getComputedStyle(el);
|
|
63
|
+
if (
|
|
64
|
+
style.display === 'none' ||
|
|
65
|
+
style.visibility === 'hidden' ||
|
|
66
|
+
(style.opacity === '0' && el.tagName !== 'INPUT')
|
|
67
|
+
) {
|
|
68
|
+
logger('Element is hidden');
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rect = el.getBoundingClientRect();
|
|
73
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
74
|
+
logger('Element has no size');
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
79
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
80
|
+
const isInViewport =
|
|
81
|
+
rect.top >= 0 + scrollTop &&
|
|
82
|
+
rect.left >= 0 + scrollLeft &&
|
|
83
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + scrollTop &&
|
|
84
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth) + scrollLeft;
|
|
85
|
+
|
|
86
|
+
if (!isInViewport) {
|
|
87
|
+
logger('Element is not in the viewport');
|
|
88
|
+
logger(rect, window.innerHeight, window.innerWidth, scrollTop, scrollLeft);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let parent: HTMLElement | null = el;
|
|
93
|
+
while (parent && parent !== document.body) {
|
|
94
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
95
|
+
if (parentStyle.overflow === 'hidden') {
|
|
96
|
+
const parentRect = parent.getBoundingClientRect();
|
|
97
|
+
const tolerance = 10;
|
|
98
|
+
if (
|
|
99
|
+
rect.top < parentRect.top - tolerance ||
|
|
100
|
+
rect.left < parentRect.left - tolerance ||
|
|
101
|
+
rect.bottom > parentRect.bottom + tolerance ||
|
|
102
|
+
rect.right > parentRect.right + tolerance
|
|
103
|
+
) {
|
|
104
|
+
logger('Element is clipped by an ancestor', parent, rect, parentRect);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
parent = parent.parentElement;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
left: Math.round(rect.left - scrollLeft),
|
|
113
|
+
top: Math.round(rect.top - scrollTop),
|
|
114
|
+
width: Math.round(rect.width),
|
|
115
|
+
height: Math.round(rect.height),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function validTextNodeContent(node: Node): string | false {
|
|
120
|
+
if (!node) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
console.log('node', node);
|
|
124
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const everyChildNodeIsText = Array.from(node.childNodes).findIndex(
|
|
129
|
+
(child) => child.nodeType === Node.TEXT_NODE,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (everyChildNodeIsText === -1) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const content = node.textContent || (node as HTMLElement).innerText;
|
|
137
|
+
if (content && !/^\s*$/.test(content)) {
|
|
138
|
+
return content.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getNodeAttributes(node: HTMLElement | Node): Record<string, string> {
|
|
145
|
+
if (!node || !(node instanceof HTMLElement) || !node.attributes) {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const attributesList = Array.from(node.attributes).map((attr) => {
|
|
150
|
+
if (attr.name === 'class') {
|
|
151
|
+
return [attr.name, `.${attr.value.split(' ').join('.')}`];
|
|
152
|
+
}
|
|
153
|
+
if (!attr.value) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
return [attr.name, attr.value];
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return Object.fromEntries(attributesList);
|
|
160
|
+
}
|
package/src/img/img.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { NodeType } from '@/html-element/constants';
|
|
5
|
+
|
|
6
|
+
// Define picture path
|
|
7
|
+
type ElementType = {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
label: string;
|
|
13
|
+
attributes: {
|
|
14
|
+
[key: string]: string;
|
|
15
|
+
nodeType: NodeType;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const createSvgOverlay = (elements: Array<ElementType>, imageWidth: number, imageHeight: number) => {
|
|
20
|
+
let svgContent = `<svg width="${imageWidth}" height="${imageHeight}" xmlns="http://www.w3.org/2000/svg">`;
|
|
21
|
+
|
|
22
|
+
// Define color array
|
|
23
|
+
const colors = [
|
|
24
|
+
{ rect: 'blue', text: 'white' },
|
|
25
|
+
{ rect: 'green', text: 'white' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Define clipping path
|
|
29
|
+
svgContent += `<defs>`;
|
|
30
|
+
elements.forEach((element, index) => {
|
|
31
|
+
svgContent += `
|
|
32
|
+
<clipPath id="clip${index}">
|
|
33
|
+
<rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}" />
|
|
34
|
+
</clipPath>
|
|
35
|
+
`;
|
|
36
|
+
});
|
|
37
|
+
svgContent += `</defs>`;
|
|
38
|
+
|
|
39
|
+
elements.forEach((element, index) => {
|
|
40
|
+
// Calculate the width and height of the text
|
|
41
|
+
const textWidth = element.label.length * 8; // Assume that each character is 8px wide
|
|
42
|
+
const textHeight = 12; // Assume that the text height is 20px
|
|
43
|
+
|
|
44
|
+
// Calculates the position of the initial color block so that it wraps and centers the text
|
|
45
|
+
const rectWidth = textWidth + 5;
|
|
46
|
+
const rectHeight = textHeight + 4;
|
|
47
|
+
let rectX = element.x - rectWidth;
|
|
48
|
+
let rectY = element.y + element.height / 2 - textHeight / 2 - 2;
|
|
49
|
+
|
|
50
|
+
// Initial text position
|
|
51
|
+
let textX = rectX + rectWidth / 2;
|
|
52
|
+
let textY = rectY + rectHeight / 2 + 6;
|
|
53
|
+
|
|
54
|
+
// Check to see if it's obscured by the left
|
|
55
|
+
if (rectX < 0) {
|
|
56
|
+
rectX = element.x;
|
|
57
|
+
rectY = element.y - rectHeight;
|
|
58
|
+
textX = rectX + rectWidth / 2;
|
|
59
|
+
textY = rectY + rectHeight / 2 + 6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Choose color
|
|
63
|
+
const color = colors[index % colors.length];
|
|
64
|
+
|
|
65
|
+
// Draw boxes and text
|
|
66
|
+
svgContent += `
|
|
67
|
+
<rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}"
|
|
68
|
+
style="fill:none;stroke:${color.rect};stroke-width:4" clip-path="url(#clip${index})" />
|
|
69
|
+
<rect x="${rectX}" y="${rectY}" width="${rectWidth}" height="${rectHeight}" style="fill:${color.rect};" />
|
|
70
|
+
<text x="${textX}" y="${textY}"
|
|
71
|
+
text-anchor="middle" dominant-baseline="middle" style="fill:${color.text};font-size:12px;font-weight:bold;">
|
|
72
|
+
${element.label}
|
|
73
|
+
</text>
|
|
74
|
+
`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
svgContent += `</svg>`;
|
|
78
|
+
return Buffer.from(svgContent);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const processImageElementInfo = async (options: {
|
|
82
|
+
inputImgBase64: string;
|
|
83
|
+
elementsPostionInfo: Array<ElementType>;
|
|
84
|
+
elementsPostionInfoWithoutText: Array<ElementType>;
|
|
85
|
+
}) => {
|
|
86
|
+
// Get the size of the original image
|
|
87
|
+
const base64Image = options.inputImgBase64.split(';base64,').pop();
|
|
88
|
+
assert(base64Image, 'base64Image is undefined');
|
|
89
|
+
|
|
90
|
+
const imageBuffer = Buffer.from(base64Image, 'base64');
|
|
91
|
+
const metadata = await sharp(imageBuffer).metadata();
|
|
92
|
+
const { width, height } = metadata;
|
|
93
|
+
|
|
94
|
+
if (width && height) {
|
|
95
|
+
// Create svg overlay
|
|
96
|
+
const svgOverlay = createSvgOverlay(options.elementsPostionInfo, width, height);
|
|
97
|
+
const svgOverlayWithoutText = createSvgOverlay(options.elementsPostionInfoWithoutText, width, height);
|
|
98
|
+
|
|
99
|
+
// Composite picture
|
|
100
|
+
const compositeElementInfoImgBase64 = await sharp(imageBuffer)
|
|
101
|
+
// .resize(newDimensions.width, newDimensions.height)
|
|
102
|
+
.composite([{ input: svgOverlay, blend: 'over' }])
|
|
103
|
+
.toBuffer()
|
|
104
|
+
.then((data) => {
|
|
105
|
+
// Convert image data to base64 encoding
|
|
106
|
+
return data.toString('base64');
|
|
107
|
+
})
|
|
108
|
+
.catch((err) => {
|
|
109
|
+
throw err;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Composite picture withtoutText
|
|
113
|
+
const compositeElementInfoImgWithoutTextBase64 = await sharp(imageBuffer)
|
|
114
|
+
// .resize(newDimensions.width, newDimensions.height)
|
|
115
|
+
.composite([{ input: svgOverlayWithoutText, blend: 'over' }])
|
|
116
|
+
.toBuffer()
|
|
117
|
+
.then((data) => {
|
|
118
|
+
// Convert image data to base64 encoding
|
|
119
|
+
return data.toString('base64');
|
|
120
|
+
})
|
|
121
|
+
.catch((err) => {
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
compositeElementInfoImgBase64,
|
|
127
|
+
compositeElementInfoImgWithoutTextBase64,
|
|
128
|
+
};
|
|
129
|
+
} else {
|
|
130
|
+
throw Error('Image processing failed because width or height is undefined');
|
|
131
|
+
}
|
|
132
|
+
};
|
package/src/img/util.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getElementInfosFromPage } from '../playwright/utils';
|
|
2
|
+
import { NodeType } from '@/html-element/constants';
|
|
3
|
+
import { ElementInfo } from '@/html-element/extractInfo';
|
|
4
|
+
|
|
5
|
+
export async function getElementInfos(page: any) {
|
|
6
|
+
const captureElementSnapshot: Array<ElementInfo> = await getElementInfosFromPage(page);
|
|
7
|
+
const elementsPostionInfo = captureElementSnapshot.map((elementInfo) => {
|
|
8
|
+
return {
|
|
9
|
+
label: elementInfo.id.toString(),
|
|
10
|
+
x: elementInfo.rect.left,
|
|
11
|
+
y: elementInfo.rect.top,
|
|
12
|
+
width: elementInfo.rect.width,
|
|
13
|
+
height: elementInfo.rect.height,
|
|
14
|
+
attributes: elementInfo.attributes,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
const elementsPostionInfoWithoutText = elementsPostionInfo.filter((elementInfo) => {
|
|
18
|
+
if (elementInfo.attributes.nodeType === NodeType.TEXT) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
elementsPostionInfo,
|
|
25
|
+
captureElementSnapshot,
|
|
26
|
+
elementsPostionInfoWithoutText,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/index.ts
ADDED