@midscene/shared 0.4.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.
@@ -0,0 +1,111 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { NodeType } from './constants.js';
3
+
4
+ interface Size {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * Retrieves the dimensions of an image asynchronously
10
+ *
11
+ * @param image - The image data, which can be a string path or a buffer
12
+ * @returns A Promise that resolves to an object containing the width and height of the image
13
+ * @throws Error if the image data is invalid
14
+ */
15
+ declare function imageInfo(image: string | Buffer): Promise<Size>;
16
+ /**
17
+ * Retrieves the dimensions of an image from a base64-encoded string
18
+ *
19
+ * @param imageBase64 - The base64-encoded image data
20
+ * @returns A Promise that resolves to an object containing the width and height of the image
21
+ * @throws Error if the image data is invalid
22
+ */
23
+ declare function imageInfoOfBase64(imageBase64: string): Promise<Size>;
24
+ /**
25
+ * Encodes an image file to a base64 encoded string
26
+ *
27
+ * @param image The path of the image file
28
+ * @param withHeader Determine whether to return data including the file header information, the default is true
29
+ *
30
+ * @returns The base64 encoded string of the image file, which may or may not include header information depending on the withHeader parameter
31
+ *
32
+ * @throws When the image type is not supported, an error will be thrown
33
+ */
34
+ declare function base64Encoded(image: string, withHeader?: boolean): string;
35
+
36
+ /**
37
+ * Saves a Base64-encoded image to a file
38
+ *
39
+ * @param options - An object containing the Base64-encoded image data and the output file path
40
+ * @param options.base64Data - The Base64-encoded image data
41
+ * @param options.outputPath - The path where the image will be saved
42
+ * @throws Error if there is an error during the saving process
43
+ */
44
+ declare function saveBase64Image(options: {
45
+ base64Data: string;
46
+ outputPath: string;
47
+ }): Promise<void>;
48
+ /**
49
+ * Transforms an image path into a base64-encoded string
50
+ * @param inputPath - The path of the image file to be encoded
51
+ * @returns A Promise that resolves to a base64-encoded string representing the image file
52
+ */
53
+ declare function transformImgPathToBase64(inputPath: string): Promise<string>;
54
+ /**
55
+ * Resizes an image from a base64-encoded string
56
+ *
57
+ * @param base64Data - A base64-encoded string representing the image
58
+ * @returns A Promise that resolves to a base64-encoded string representing the resized image
59
+ * @throws An error if the width or height cannot be determined from the metadata
60
+ */
61
+ declare function resizeImg(base64Data: string): Promise<string>;
62
+ /**
63
+ * Calculates new dimensions for an image while maintaining its aspect ratio.
64
+ *
65
+ * This function is designed to resize an image to fit within a specified maximum width and height
66
+ * while maintaining the original aspect ratio. If the original width or height exceeds the maximum
67
+ * dimensions, the image will be scaled down to fit.
68
+ *
69
+ * @param {number} originalWidth - The original width of the image.
70
+ * @param {number} originalHeight - The original height of the image.
71
+ * @returns {Object} An object containing the new width and height.
72
+ * @throws {Error} Throws an error if the width or height is not a positive number.
73
+ */
74
+ declare function calculateNewDimensions(originalWidth: number, originalHeight: number): {
75
+ width: number;
76
+ height: number;
77
+ };
78
+ /**
79
+ * Trims an image and returns the trimming information, including the offset from the left and top edges, and the trimmed width and height
80
+ *
81
+ * @param image - The image to be trimmed. This can be a file path or a Buffer object containing the image data
82
+ * @returns A Promise that resolves to an object containing the trimming information. If the image does not need to be trimmed, this object will be null
83
+ */
84
+ declare function trimImage(image: string | Buffer): Promise<{
85
+ trimOffsetLeft: number;
86
+ trimOffsetTop: number;
87
+ width: number;
88
+ height: number;
89
+ } | null>;
90
+
91
+ type ElementType = {
92
+ x: number;
93
+ y: number;
94
+ width: number;
95
+ height: number;
96
+ label: string;
97
+ attributes: {
98
+ [key: string]: string;
99
+ nodeType: NodeType;
100
+ };
101
+ };
102
+ declare const processImageElementInfo: (options: {
103
+ inputImgBase64: string;
104
+ elementsPositionInfo: Array<ElementType>;
105
+ elementsPositionInfoWithoutText: Array<ElementType>;
106
+ }) => Promise<{
107
+ compositeElementInfoImgBase64: string;
108
+ compositeElementInfoImgWithoutTextBase64: string;
109
+ }>;
110
+
111
+ export { base64Encoded, calculateNewDimensions, imageInfo, imageInfoOfBase64, processImageElementInfo, resizeImg, saveBase64Image, transformImgPathToBase64, trimImage };
@@ -0,0 +1,3 @@
1
+ declare function export_default(): string;
2
+
3
+ export { export_default as default };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@midscene/shared",
3
+ "version": "0.4.0",
4
+ "types": "./dist/types/index.d.ts",
5
+ "main": "./dist/lib/index.js",
6
+ "module": "./dist/es/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/types/index.d.ts",
10
+ "import": "./dist/es/index.js",
11
+ "require": "./dist/lib/index.js"
12
+ },
13
+ "./constants": {
14
+ "types": "./dist/types/constants.d.ts",
15
+ "import": "./dist/es/constants.js",
16
+ "require": "./dist/lib/constants.js"
17
+ },
18
+ "./img": {
19
+ "types": "./dist/types/img.d.ts",
20
+ "import": "./dist/es/img.js",
21
+ "require": "./dist/lib/img.js"
22
+ }
23
+ },
24
+ "typesVersions": {
25
+ "*": {
26
+ ".": [
27
+ "./dist/types/index.d.ts"
28
+ ],
29
+ "constants": [
30
+ "./dist/types/constants.d.ts"
31
+ ],
32
+ "img": [
33
+ "./dist/types/img.d.ts"
34
+ ]
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "jimp": "0.22.12"
44
+ },
45
+ "peerDependencies": {},
46
+ "devDependencies": {
47
+ "@modern-js/module-tools": "2.58.2",
48
+ "typescript": "~5.0.4",
49
+ "@types/node": "^18.0.0",
50
+ "rimraf": "~3.0.2",
51
+ "vitest": "^1.6.0"
52
+ },
53
+ "sideEffects": [],
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "registry": "https://registry.npmjs.org/"
57
+ },
58
+ "scripts": {
59
+ "dev": "modern dev",
60
+ "build": "modern build",
61
+ "build:watch": "modern build -w",
62
+ "reset": "rimraf ./**/node_modules",
63
+ "lint": "modern lint",
64
+ "bump": "modern bump",
65
+ "pre": "modern pre",
66
+ "change-status": "modern change-status",
67
+ "gen-release-note": "modern gen-release-note",
68
+ "release": "modern release",
69
+ "new": "modern new",
70
+ "upgrade": "modern upgrade",
71
+ "test": "vitest --run",
72
+ "test:ai": "AITEST=true npm run test"
73
+ }
74
+ }
@@ -0,0 +1,10 @@
1
+ export const TEXT_SIZE_THRESHOLD = 9;
2
+
3
+ export const TEXT_MAX_SIZE = 40;
4
+
5
+ export enum NodeType {
6
+ FORM_ITEM = 'FORM_ITEM Node',
7
+ BUTTON = 'BUTTON Node',
8
+ IMG = 'IMG Node',
9
+ TEXT = 'TEXT Node',
10
+ }
@@ -0,0 +1,179 @@
1
+ import assert from 'node:assert';
2
+ import { Buffer } from 'node:buffer';
3
+ import Jimp from 'jimp';
4
+ import type { NodeType } from '../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 = (
20
+ elements: Array<ElementType>,
21
+ imageWidth: number,
22
+ imageHeight: number,
23
+ ) => {
24
+ const createPngOverlay = async (
25
+ elements: Array<ElementType>,
26
+ imageWidth: number,
27
+ imageHeight: number,
28
+ ) => {
29
+ const image = new Jimp(imageWidth, imageHeight, 0x00000000);
30
+
31
+ // Define color array
32
+ const colors = [
33
+ { rect: 0x0000ffff, text: 0xffffffff }, // blue, white
34
+ { rect: 0x8b4513ff, text: 0xffffffff }, // brown, white
35
+ ];
36
+
37
+ for (let index = 0; index < elements.length; index++) {
38
+ const element = elements[index];
39
+ const color = colors[index % colors.length];
40
+
41
+ // Draw rectangle
42
+ image.scan(
43
+ element.x,
44
+ element.y,
45
+ element.width,
46
+ element.height,
47
+ function (x, y, idx) {
48
+ if (
49
+ x === element.x ||
50
+ x === element.x + element.width - 1 ||
51
+ y === element.y ||
52
+ y === element.y + element.height - 1
53
+ ) {
54
+ this.bitmap.data[idx + 0] = (color.rect >> 24) & 0xff; // R
55
+ this.bitmap.data[idx + 1] = (color.rect >> 16) & 0xff; // G
56
+ this.bitmap.data[idx + 2] = (color.rect >> 8) & 0xff; // B
57
+ this.bitmap.data[idx + 3] = color.rect & 0xff; // A
58
+ }
59
+ },
60
+ );
61
+
62
+ // Calculate text position
63
+ const textWidth = element.label.length * 8;
64
+ const textHeight = 12;
65
+ const rectWidth = textWidth + 5;
66
+ const rectHeight = textHeight + 4;
67
+ let rectX = element.x - rectWidth;
68
+ let rectY = element.y + element.height / 2 - textHeight / 2 - 2;
69
+
70
+ // Check if obscured by the left
71
+ if (rectX < 0) {
72
+ rectX = element.x;
73
+ rectY = element.y - rectHeight;
74
+ }
75
+
76
+ // Draw text background
77
+ image.scan(rectX, rectY, rectWidth, rectHeight, function (x, y, idx) {
78
+ this.bitmap.data[idx + 0] = (color.rect >> 24) & 0xff; // R
79
+ this.bitmap.data[idx + 1] = (color.rect >> 16) & 0xff; // G
80
+ this.bitmap.data[idx + 2] = (color.rect >> 8) & 0xff; // B
81
+ this.bitmap.data[idx + 3] = color.rect & 0xff; // A
82
+ });
83
+ // Draw text (simplified, as Jimp doesn't have built-in text drawing)
84
+ const font = await Jimp.loadFont(Jimp.FONT_SANS_16_WHITE);
85
+ image.print(
86
+ font,
87
+ rectX,
88
+ rectY,
89
+ {
90
+ text: element.label,
91
+ alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
92
+ alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE,
93
+ },
94
+ rectWidth,
95
+ rectHeight,
96
+ );
97
+ }
98
+
99
+ return image.getBufferAsync(Jimp.MIME_PNG);
100
+ };
101
+
102
+ return createPngOverlay(elements, imageWidth, imageHeight);
103
+ };
104
+
105
+ export const processImageElementInfo = async (options: {
106
+ inputImgBase64: string;
107
+ elementsPositionInfo: Array<ElementType>;
108
+ elementsPositionInfoWithoutText: Array<ElementType>;
109
+ }) => {
110
+ // Get the size of the original image
111
+ const base64Image = options.inputImgBase64.split(';base64,').pop();
112
+ assert(base64Image, 'base64Image is undefined');
113
+
114
+ const imageBuffer = Buffer.from(base64Image, 'base64');
115
+ const image = await Jimp.read(imageBuffer);
116
+ const { width, height } = image.bitmap;
117
+
118
+ if (width && height) {
119
+ // Create svg overlay
120
+ const svgOverlay = await createSvgOverlay(
121
+ options.elementsPositionInfo,
122
+ width,
123
+ height,
124
+ );
125
+ const svgOverlayWithoutText = await createSvgOverlay(
126
+ options.elementsPositionInfoWithoutText,
127
+ width,
128
+ height,
129
+ );
130
+
131
+ // Composite picture
132
+ const compositeElementInfoImgBase64 = await Jimp.read(imageBuffer)
133
+ .then(async (image: Jimp) => {
134
+ const svgImage = await Jimp.read(svgOverlay);
135
+ return image.composite(svgImage, 0, 0, {
136
+ mode: Jimp.BLEND_SOURCE_OVER,
137
+ opacitySource: 1,
138
+ opacityDest: 1,
139
+ });
140
+ })
141
+ .then((compositeImage: Jimp) => {
142
+ return compositeImage.getBufferAsync(Jimp.MIME_PNG);
143
+ })
144
+ .then((buffer: Buffer) => {
145
+ return buffer.toString('base64');
146
+ })
147
+ .catch((error: unknown) => {
148
+ throw error;
149
+ });
150
+
151
+ // Composite picture withoutText
152
+ const compositeElementInfoImgWithoutTextBase64 = await Jimp.read(
153
+ imageBuffer,
154
+ )
155
+ .then(async (image: Jimp) => {
156
+ const svgImage = await Jimp.read(svgOverlayWithoutText);
157
+ return image.composite(svgImage, 0, 0, {
158
+ mode: Jimp.BLEND_SOURCE_OVER,
159
+ opacitySource: 1,
160
+ opacityDest: 1,
161
+ });
162
+ })
163
+ .then((compositeImage: Jimp) => {
164
+ return compositeImage.getBufferAsync(Jimp.MIME_PNG);
165
+ })
166
+ .then((buffer: Buffer) => {
167
+ return buffer.toString('base64');
168
+ })
169
+ .catch((error: unknown) => {
170
+ throw error;
171
+ });
172
+
173
+ return {
174
+ compositeElementInfoImgBase64,
175
+ compositeElementInfoImgWithoutTextBase64,
176
+ };
177
+ }
178
+ throw Error('Image processing failed because width or height is undefined');
179
+ };
@@ -0,0 +1,9 @@
1
+ export { imageInfo, imageInfoOfBase64, base64Encoded } from './info';
2
+ export {
3
+ trimImage,
4
+ calculateNewDimensions,
5
+ resizeImg,
6
+ transformImgPathToBase64,
7
+ saveBase64Image,
8
+ } from './transform';
9
+ export { processImageElementInfo } from './box-select';
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert';
2
+ import { Buffer } from 'node:buffer';
3
+ import { readFileSync } from 'node:fs';
4
+ import Jimp from 'jimp';
5
+
6
+ export interface Size {
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ /**
12
+ * Retrieves the dimensions of an image asynchronously
13
+ *
14
+ * @param image - The image data, which can be a string path or a buffer
15
+ * @returns A Promise that resolves to an object containing the width and height of the image
16
+ * @throws Error if the image data is invalid
17
+ */
18
+ export async function imageInfo(image: string | Buffer): Promise<Size> {
19
+ let jimpImage: Jimp;
20
+ if (typeof image === 'string') {
21
+ jimpImage = await Jimp.read(image);
22
+ } else if (Buffer.isBuffer(image)) {
23
+ jimpImage = await Jimp.read(image);
24
+ } else {
25
+ throw new Error('Invalid image input: must be a string path or a Buffer');
26
+ }
27
+ const { width, height } = jimpImage.bitmap;
28
+ assert(
29
+ width && height,
30
+ `Invalid image: ${typeof image === 'string' ? image : 'Buffer'}`,
31
+ );
32
+ return { width, height };
33
+ }
34
+
35
+ /**
36
+ * Retrieves the dimensions of an image from a base64-encoded string
37
+ *
38
+ * @param imageBase64 - The base64-encoded image data
39
+ * @returns A Promise that resolves to an object containing the width and height of the image
40
+ * @throws Error if the image data is invalid
41
+ */
42
+ export async function imageInfoOfBase64(imageBase64: string): Promise<Size> {
43
+ const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
44
+ // Call the imageInfo function to get the dimensions of the image
45
+ return imageInfo(Buffer.from(base64Data, 'base64'));
46
+ }
47
+
48
+ /**
49
+ * Encodes an image file to a base64 encoded string
50
+ *
51
+ * @param image The path of the image file
52
+ * @param withHeader Determine whether to return data including the file header information, the default is true
53
+ *
54
+ * @returns The base64 encoded string of the image file, which may or may not include header information depending on the withHeader parameter
55
+ *
56
+ * @throws When the image type is not supported, an error will be thrown
57
+ */
58
+ export function base64Encoded(image: string, withHeader = true) {
59
+ // get base64 encoded image
60
+ const imageBuffer = readFileSync(image);
61
+ if (!withHeader) {
62
+ return imageBuffer.toString('base64');
63
+ }
64
+ if (image.endsWith('png')) {
65
+ return `data:image/png;base64,${imageBuffer.toString('base64')}`;
66
+ }
67
+ if (image.endsWith('jpg') || image.endsWith('jpeg')) {
68
+ return `data:image/jpeg;base64,${imageBuffer.toString('base64')}`;
69
+ }
70
+ throw new Error('unsupported image type');
71
+ }