@rpascene/shared 0.30.8
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/README.md +9 -0
- package/dist/es/baseDB.mjs +109 -0
- package/dist/es/build/copy-static.mjs +29 -0
- package/dist/es/common.mjs +37 -0
- package/dist/es/constants/example-code.mjs +202 -0
- package/dist/es/constants/index.mjs +74 -0
- package/dist/es/env/basic.mjs +6 -0
- package/dist/es/env/constants.mjs +97 -0
- package/dist/es/env/decide-model-config.mjs +172 -0
- package/dist/es/env/global-config-manager.mjs +82 -0
- package/dist/es/env/helper.mjs +45 -0
- package/dist/es/env/index.mjs +5 -0
- package/dist/es/env/init-debug.mjs +18 -0
- package/dist/es/env/model-config-manager.mjs +99 -0
- package/dist/es/env/parse.mjs +69 -0
- package/dist/es/env/types.mjs +265 -0
- package/dist/es/env/utils.mjs +18 -0
- package/dist/es/extractor/constants.mjs +2 -0
- package/dist/es/extractor/cs_postmessage.mjs +61 -0
- package/dist/es/extractor/customLocator.mjs +646 -0
- package/dist/es/extractor/debug.mjs +6 -0
- package/dist/es/extractor/dom-util.mjs +92 -0
- package/dist/es/extractor/index.mjs +7 -0
- package/dist/es/extractor/locator.mjs +95 -0
- package/dist/es/extractor/tree.mjs +81 -0
- package/dist/es/extractor/util.mjs +244 -0
- package/dist/es/extractor/web-extractor.mjs +361 -0
- package/dist/es/img/box-select.mjs +184 -0
- package/dist/es/img/draw-box.mjs +42 -0
- package/dist/es/img/get-jimp.mjs +10 -0
- package/dist/es/img/get-photon.mjs +19 -0
- package/dist/es/img/get-sharp.mjs +11 -0
- package/dist/es/img/index.mjs +5 -0
- package/dist/es/img/info.mjs +32 -0
- package/dist/es/img/transform.mjs +192 -0
- package/dist/es/index.mjs +3 -0
- package/dist/es/logger.mjs +61 -0
- package/dist/es/node/fs.mjs +44 -0
- package/dist/es/node/index.mjs +1 -0
- package/dist/es/polyfills/async-hooks.mjs +2 -0
- package/dist/es/polyfills/index.mjs +1 -0
- package/dist/es/types/index.mjs +3 -0
- package/dist/es/us-keyboard-layout.mjs +1414 -0
- package/dist/es/us-keyboard-layout.mjs.LICENSE.txt +5 -0
- package/dist/es/utils.mjs +66 -0
- package/dist/lib/baseDB.js +149 -0
- package/dist/lib/build/copy-static.js +77 -0
- package/dist/lib/common.js +93 -0
- package/dist/lib/constants/example-code.js +239 -0
- package/dist/lib/constants/index.js +153 -0
- package/dist/lib/env/basic.js +40 -0
- package/dist/lib/env/constants.js +143 -0
- package/dist/lib/env/decide-model-config.js +212 -0
- package/dist/lib/env/global-config-manager.js +116 -0
- package/dist/lib/env/helper.js +85 -0
- package/dist/lib/env/index.js +94 -0
- package/dist/lib/env/init-debug.js +52 -0
- package/dist/lib/env/model-config-manager.js +133 -0
- package/dist/lib/env/parse.js +106 -0
- package/dist/lib/env/types.js +650 -0
- package/dist/lib/env/utils.js +61 -0
- package/dist/lib/extractor/constants.js +42 -0
- package/dist/lib/extractor/cs_postmessage.js +98 -0
- package/dist/lib/extractor/customLocator.js +698 -0
- package/dist/lib/extractor/debug.js +12 -0
- package/dist/lib/extractor/dom-util.js +150 -0
- package/dist/lib/extractor/index.js +153 -0
- package/dist/lib/extractor/locator.js +141 -0
- package/dist/lib/extractor/tree.js +127 -0
- package/dist/lib/extractor/util.js +335 -0
- package/dist/lib/extractor/web-extractor.js +407 -0
- package/dist/lib/img/box-select.js +232 -0
- package/dist/lib/img/draw-box.js +89 -0
- package/dist/lib/img/get-jimp.js +72 -0
- package/dist/lib/img/get-photon.js +76 -0
- package/dist/lib/img/get-sharp.js +63 -0
- package/dist/lib/img/index.js +102 -0
- package/dist/lib/img/info.js +86 -0
- package/dist/lib/img/transform.js +279 -0
- package/dist/lib/index.js +43 -0
- package/dist/lib/logger.js +114 -0
- package/dist/lib/node/fs.js +97 -0
- package/dist/lib/node/index.js +60 -0
- package/dist/lib/polyfills/async-hooks.js +36 -0
- package/dist/lib/polyfills/index.js +60 -0
- package/dist/lib/types/index.js +37 -0
- package/dist/lib/us-keyboard-layout.js +1457 -0
- package/dist/lib/us-keyboard-layout.js.LICENSE.txt +5 -0
- package/dist/lib/utils.js +136 -0
- package/dist/types/baseDB.d.ts +25 -0
- package/dist/types/build/copy-static.d.ts +31 -0
- package/dist/types/common.d.ts +12 -0
- package/dist/types/constants/example-code.d.ts +2 -0
- package/dist/types/constants/index.d.ts +23 -0
- package/dist/types/env/basic.d.ts +6 -0
- package/dist/types/env/constants.d.ts +40 -0
- package/dist/types/env/decide-model-config.d.ts +14 -0
- package/dist/types/env/global-config-manager.d.ts +32 -0
- package/dist/types/env/helper.d.ts +6 -0
- package/dist/types/env/index.d.ts +4 -0
- package/dist/types/env/init-debug.d.ts +1 -0
- package/dist/types/env/model-config-manager.d.ts +24 -0
- package/dist/types/env/parse.d.ts +12 -0
- package/dist/types/env/types.d.ts +295 -0
- package/dist/types/env/utils.d.ts +7 -0
- package/dist/types/extractor/constants.d.ts +1 -0
- package/dist/types/extractor/cs_postmessage.d.ts +2 -0
- package/dist/types/extractor/customLocator.d.ts +69 -0
- package/dist/types/extractor/debug.d.ts +1 -0
- package/dist/types/extractor/dom-util.d.ts +26 -0
- package/dist/types/extractor/index.d.ts +36 -0
- package/dist/types/extractor/locator.d.ts +7 -0
- package/dist/types/extractor/tree.d.ts +9 -0
- package/dist/types/extractor/util.d.ts +43 -0
- package/dist/types/extractor/web-extractor.d.ts +19 -0
- package/dist/types/img/box-select.d.ts +25 -0
- package/dist/types/img/draw-box.d.ts +15 -0
- package/dist/types/img/get-jimp.d.ts +2 -0
- package/dist/types/img/get-photon.d.ts +8 -0
- package/dist/types/img/get-sharp.d.ts +3 -0
- package/dist/types/img/index.d.ts +4 -0
- package/dist/types/img/info.d.ts +29 -0
- package/dist/types/img/transform.d.ts +88 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/logger.d.ts +4 -0
- package/dist/types/node/fs.d.ts +15 -0
- package/dist/types/node/index.d.ts +1 -0
- package/dist/types/polyfills/async-hooks.d.ts +6 -0
- package/dist/types/polyfills/index.d.ts +4 -0
- package/dist/types/types/index.d.ts +37 -0
- package/dist/types/us-keyboard-layout.d.ts +32 -0
- package/dist/types/utils.d.ts +22 -0
- package/package.json +102 -0
- package/src/baseDB.ts +158 -0
- package/src/build/copy-static.ts +62 -0
- package/src/common.ts +67 -0
- package/src/constants/example-code.ts +202 -0
- package/src/constants/index.ts +81 -0
- package/src/env/basic.ts +12 -0
- package/src/env/constants.ts +291 -0
- package/src/env/decide-model-config.ts +319 -0
- package/src/env/global-config-manager.ts +174 -0
- package/src/env/helper.ts +80 -0
- package/src/env/index.ts +4 -0
- package/src/env/init-debug.ts +29 -0
- package/src/env/model-config-manager.ts +145 -0
- package/src/env/parse.ts +131 -0
- package/src/env/types.ts +573 -0
- package/src/env/utils.ts +39 -0
- package/src/extractor/constants.ts +5 -0
- package/src/extractor/cs_postmessage.ts +101 -0
- package/src/extractor/customLocator.ts +1138 -0
- package/src/extractor/debug.ts +10 -0
- package/src/extractor/dom-util.ts +141 -0
- package/src/extractor/index.ts +54 -0
- package/src/extractor/locator.ts +179 -0
- package/src/extractor/tree.ts +179 -0
- package/src/extractor/util.ts +468 -0
- package/src/extractor/web-extractor.ts +559 -0
- package/src/img/box-select.ts +346 -0
- package/src/img/draw-box.ts +60 -0
- package/src/img/get-jimp.ts +12 -0
- package/src/img/get-photon.ts +48 -0
- package/src/img/get-sharp.ts +18 -0
- package/src/img/index.ts +24 -0
- package/src/img/info.ts +79 -0
- package/src/img/jimp.d.ts +4 -0
- package/src/img/transform.ts +396 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +93 -0
- package/src/node/fs.ts +84 -0
- package/src/node/index.ts +1 -0
- package/src/polyfills/async-hooks.ts +6 -0
- package/src/polyfills/index.ts +4 -0
- package/src/types/index.ts +53 -0
- package/src/us-keyboard-layout.ts +723 -0
- package/src/utils.ts +127 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type Jimp from 'jimp';
|
|
6
|
+
import type { Rect } from 'src/types';
|
|
7
|
+
import { getDebug } from '../logger';
|
|
8
|
+
import { ifInNode } from '../utils';
|
|
9
|
+
import getJimp from './get-jimp';
|
|
10
|
+
import getPhoton from './get-photon';
|
|
11
|
+
import getSharp from './get-sharp';
|
|
12
|
+
|
|
13
|
+
const imgDebug = getDebug('img');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
/**
|
|
17
|
+
* Saves a Base64-encoded image to a file
|
|
18
|
+
*
|
|
19
|
+
* @param options - An object containing the Base64-encoded image data and the output file path
|
|
20
|
+
* @param options.base64Data - The Base64-encoded image data
|
|
21
|
+
* @param options.outputPath - The path where the image will be saved
|
|
22
|
+
* @throws Error if there is an error during the saving process
|
|
23
|
+
*/
|
|
24
|
+
export async function saveBase64Image(options: {
|
|
25
|
+
base64Data: string;
|
|
26
|
+
outputPath: string;
|
|
27
|
+
}): Promise<void> {
|
|
28
|
+
const { base64Data, outputPath } = options;
|
|
29
|
+
const { body } = parseBase64(base64Data);
|
|
30
|
+
|
|
31
|
+
// Converts base64 data to buffer
|
|
32
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
33
|
+
|
|
34
|
+
// Use Jimp to process the image and save it to the specified location
|
|
35
|
+
const Jimp = await getJimp();
|
|
36
|
+
const image = await Jimp.read(imageBuffer);
|
|
37
|
+
await image.writeAsync(outputPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resizes an image from Buffer, maybe return a new format
|
|
42
|
+
* - If the image is Resized, the returned format will be jpg.
|
|
43
|
+
* - If the image is not Resized, it will return to its original format.
|
|
44
|
+
* @returns { buffer: resized buffer, format: the new format}
|
|
45
|
+
*/
|
|
46
|
+
export async function resizeAndConvertImgBuffer(
|
|
47
|
+
inputFormat: string,
|
|
48
|
+
inputData: Buffer,
|
|
49
|
+
newSize: {
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
},
|
|
53
|
+
): Promise<{
|
|
54
|
+
buffer: Buffer;
|
|
55
|
+
// jpg, png, etc.
|
|
56
|
+
format: string;
|
|
57
|
+
}> {
|
|
58
|
+
if (typeof inputData === 'string')
|
|
59
|
+
throw Error('inputData is base64, use resizeImgBase64 instead');
|
|
60
|
+
|
|
61
|
+
assert(
|
|
62
|
+
newSize && newSize.width > 0 && newSize.height > 0,
|
|
63
|
+
'newSize must be positive',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const resizeStartTime = Date.now();
|
|
67
|
+
imgDebug(`resizeImg start, target size: ${newSize.width}x${newSize.height}`);
|
|
68
|
+
|
|
69
|
+
if (ifInNode) {
|
|
70
|
+
// Node.js environment: use Sharp
|
|
71
|
+
try {
|
|
72
|
+
const Sharp = await getSharp();
|
|
73
|
+
const metadata = await Sharp(inputData).metadata();
|
|
74
|
+
const { width: originalWidth, height: originalHeight } = metadata;
|
|
75
|
+
|
|
76
|
+
if (!originalWidth || !originalHeight) {
|
|
77
|
+
throw Error('Undefined width or height from the input image.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
newSize.width === originalWidth &&
|
|
82
|
+
newSize.height === originalHeight
|
|
83
|
+
) {
|
|
84
|
+
return {
|
|
85
|
+
buffer: inputData,
|
|
86
|
+
format: inputFormat,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resizedBuffer = await Sharp(inputData)
|
|
91
|
+
.resize(newSize.width, newSize.height)
|
|
92
|
+
.jpeg({ quality: 90 })
|
|
93
|
+
.toBuffer();
|
|
94
|
+
|
|
95
|
+
const resizeEndTime = Date.now();
|
|
96
|
+
imgDebug(
|
|
97
|
+
`resizeImg done (Sharp), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
buffer: resizedBuffer,
|
|
102
|
+
// by Sharp.jpeg()
|
|
103
|
+
format: 'jpeg',
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// browser environment: use Photon
|
|
111
|
+
const { PhotonImage, SamplingFilter, resize } = await getPhoton();
|
|
112
|
+
const inputBytes = new Uint8Array(inputData);
|
|
113
|
+
const inputImage = PhotonImage.new_from_byteslice(inputBytes);
|
|
114
|
+
const originalWidth = inputImage.get_width();
|
|
115
|
+
const originalHeight = inputImage.get_height();
|
|
116
|
+
|
|
117
|
+
if (!originalWidth || !originalHeight) {
|
|
118
|
+
inputImage.free();
|
|
119
|
+
throw Error('Undefined width or height from the input image.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (newSize.width === originalWidth && newSize.height === originalHeight) {
|
|
123
|
+
inputImage.free();
|
|
124
|
+
return {
|
|
125
|
+
buffer: inputData,
|
|
126
|
+
format: inputFormat,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Resize image using photon with bicubic-like sampling
|
|
131
|
+
const outputImage = resize(
|
|
132
|
+
inputImage,
|
|
133
|
+
newSize.width,
|
|
134
|
+
newSize.height,
|
|
135
|
+
SamplingFilter.CatmullRom,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const outputBytes = outputImage.get_bytes_jpeg(90);
|
|
139
|
+
const resizedBuffer = Buffer.from(outputBytes);
|
|
140
|
+
|
|
141
|
+
// Free memory
|
|
142
|
+
inputImage.free();
|
|
143
|
+
outputImage.free();
|
|
144
|
+
|
|
145
|
+
const resizeEndTime = Date.now();
|
|
146
|
+
|
|
147
|
+
imgDebug(
|
|
148
|
+
`resizeImg done (Photon), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
buffer: resizedBuffer,
|
|
153
|
+
// by Photon.get_bytes_jpeg()
|
|
154
|
+
format: 'jpeg',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const createImgBase64ByFormat = (format: string, body: string) => {
|
|
159
|
+
return `data:image/${format};base64,${body}`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export async function resizeImgBase64(
|
|
163
|
+
inputBase64: string,
|
|
164
|
+
newSize: {
|
|
165
|
+
width: number;
|
|
166
|
+
height: number;
|
|
167
|
+
},
|
|
168
|
+
): Promise<string> {
|
|
169
|
+
const { body, mimeType } = parseBase64(inputBase64);
|
|
170
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
171
|
+
const { buffer, format } = await resizeAndConvertImgBuffer(
|
|
172
|
+
mimeType.split('/')[1],
|
|
173
|
+
imageBuffer,
|
|
174
|
+
newSize,
|
|
175
|
+
);
|
|
176
|
+
return createImgBase64ByFormat(format, buffer.toString('base64'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Calculates new dimensions for an image while maintaining its aspect ratio.
|
|
181
|
+
*
|
|
182
|
+
* This function is designed to resize an image to fit within a specified maximum width and height
|
|
183
|
+
* while maintaining the original aspect ratio. If the original width or height exceeds the maximum
|
|
184
|
+
* dimensions, the image will be scaled down to fit.
|
|
185
|
+
*
|
|
186
|
+
* @param {number} originalWidth - The original width of the image.
|
|
187
|
+
* @param {number} originalHeight - The original height of the image.
|
|
188
|
+
* @returns {Object} An object containing the new width and height.
|
|
189
|
+
* @throws {Error} Throws an error if the width or height is not a positive number.
|
|
190
|
+
*/
|
|
191
|
+
export function zoomForGPT4o(originalWidth: number, originalHeight: number) {
|
|
192
|
+
// In low mode, the image is scaled to 512x512 pixels and 85 tokens are used to represent the image.
|
|
193
|
+
// In high mode, the model looks at low-resolution images and then creates detailed crop images, using 170 tokens for each 512x512 pixel tile. In practical applications, it is recommended to control the image size within 2048x768 pixels
|
|
194
|
+
const maxWidth = 2048; // Maximum width
|
|
195
|
+
const maxHeight = 768; // Maximum height
|
|
196
|
+
let newWidth = originalWidth;
|
|
197
|
+
let newHeight = originalHeight;
|
|
198
|
+
|
|
199
|
+
// Calculate the aspect ratio
|
|
200
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
201
|
+
|
|
202
|
+
// Width adjustment
|
|
203
|
+
if (originalWidth > maxWidth) {
|
|
204
|
+
newWidth = maxWidth;
|
|
205
|
+
newHeight = newWidth / aspectRatio;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Adjust height
|
|
209
|
+
if (newHeight > maxHeight) {
|
|
210
|
+
newHeight = maxHeight;
|
|
211
|
+
newWidth = newHeight * aspectRatio;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
width: Math.round(newWidth),
|
|
216
|
+
height: Math.round(newHeight),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function jimpFromBase64(base64: string): Promise<Jimp> {
|
|
221
|
+
const Jimp = await getJimp();
|
|
222
|
+
const { body } = parseBase64(base64);
|
|
223
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
224
|
+
return Jimp.read(imageBuffer);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// https://help.aliyun.com/zh/model-studio/user-guide/vision/
|
|
228
|
+
export async function paddingToMatchBlock(
|
|
229
|
+
image: Jimp,
|
|
230
|
+
blockSize = 28,
|
|
231
|
+
): Promise<{
|
|
232
|
+
width: number;
|
|
233
|
+
height: number;
|
|
234
|
+
image: Jimp;
|
|
235
|
+
}> {
|
|
236
|
+
const { width, height } = image.bitmap;
|
|
237
|
+
|
|
238
|
+
const targetWidth = Math.ceil(width / blockSize) * blockSize;
|
|
239
|
+
const targetHeight = Math.ceil(height / blockSize) * blockSize;
|
|
240
|
+
|
|
241
|
+
if (targetWidth === width && targetHeight === height) {
|
|
242
|
+
return { width, height, image };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const Jimp = await getJimp();
|
|
246
|
+
const paddedImage = new Jimp(targetWidth, targetHeight, 0xffffffff);
|
|
247
|
+
|
|
248
|
+
// Composite the original image onto the new canvas
|
|
249
|
+
paddedImage.composite(image, 0, 0);
|
|
250
|
+
return { width: targetWidth, height: targetHeight, image: paddedImage };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function paddingToMatchBlockByBase64(
|
|
254
|
+
imageBase64: string,
|
|
255
|
+
blockSize = 28,
|
|
256
|
+
): Promise<{
|
|
257
|
+
width: number;
|
|
258
|
+
height: number;
|
|
259
|
+
imageBase64: string;
|
|
260
|
+
}> {
|
|
261
|
+
const jimpImage = await jimpFromBase64(imageBase64);
|
|
262
|
+
const paddedResult = await paddingToMatchBlock(jimpImage, blockSize);
|
|
263
|
+
return {
|
|
264
|
+
width: paddedResult.width,
|
|
265
|
+
height: paddedResult.height,
|
|
266
|
+
imageBase64: await jimpToBase64(paddedResult.image),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function cropByRect(
|
|
271
|
+
imageBase64: string,
|
|
272
|
+
rect: Rect,
|
|
273
|
+
paddingImage: boolean,
|
|
274
|
+
): Promise<{
|
|
275
|
+
width: number;
|
|
276
|
+
height: number;
|
|
277
|
+
imageBase64: string;
|
|
278
|
+
}> {
|
|
279
|
+
const jimpImage = await jimpFromBase64(imageBase64);
|
|
280
|
+
const { left, top, width, height } = rect;
|
|
281
|
+
jimpImage.crop(left, top, width, height);
|
|
282
|
+
|
|
283
|
+
if (paddingImage) {
|
|
284
|
+
const paddedResult = await paddingToMatchBlock(jimpImage);
|
|
285
|
+
return {
|
|
286
|
+
width: paddedResult.width,
|
|
287
|
+
height: paddedResult.height,
|
|
288
|
+
imageBase64: await jimpToBase64(paddedResult.image),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
width: jimpImage.bitmap.width,
|
|
293
|
+
height: jimpImage.bitmap.height,
|
|
294
|
+
imageBase64: await jimpToBase64(jimpImage),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function jimpToBase64(image: Jimp): Promise<string> {
|
|
299
|
+
const Jimp = await getJimp();
|
|
300
|
+
return image.getBase64Async(Jimp.MIME_JPEG);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const httpImg2Base64 = async (url: string): Promise<string> => {
|
|
304
|
+
const response = await fetch(url);
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(`Failed to fetch image: ${url}`);
|
|
307
|
+
}
|
|
308
|
+
const contentType = response.headers.get('content-type');
|
|
309
|
+
if (!contentType) {
|
|
310
|
+
throw new Error(`Failed to fetch image: ${url}`);
|
|
311
|
+
}
|
|
312
|
+
assert(
|
|
313
|
+
contentType.startsWith('image/'),
|
|
314
|
+
`The url ${url} is not a image, because of content-type in header is ${contentType}.`,
|
|
315
|
+
);
|
|
316
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
317
|
+
return `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Convert image file to base64 string
|
|
322
|
+
* Because this method is synchronous, the npm package `sharp` cannot be used to detect the file type.
|
|
323
|
+
* TODO: convert to webp to reduce base64 size.
|
|
324
|
+
*/
|
|
325
|
+
export const localImg2Base64 = (
|
|
326
|
+
imgPath: string,
|
|
327
|
+
withoutHeader = false,
|
|
328
|
+
): string => {
|
|
329
|
+
const body = readFileSync(imgPath).toString('base64');
|
|
330
|
+
if (withoutHeader) {
|
|
331
|
+
return body;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Detect image type by extname.
|
|
335
|
+
const type = path.extname(imgPath).slice(1);
|
|
336
|
+
const finalType = type === 'svg' ? 'svg+xml' : type || 'jpg';
|
|
337
|
+
|
|
338
|
+
return `data:image/${finalType};base64,${body}`;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* PreProcess image url to ensure image is accessible to LLM.
|
|
343
|
+
* @param url - The url of the image, it can be a http url or a base64 string or a file path
|
|
344
|
+
* @param convertHttpImage2Base64 - Whether to convert http image to base64, if true, the http image will be converted to base64, otherwise, the http image will be returned as is
|
|
345
|
+
* @returns The base64 string of the image (when convertHttpImage2Base64 is true or url is a file path) or the http image url
|
|
346
|
+
*/
|
|
347
|
+
export const preProcessImageUrl = async (
|
|
348
|
+
url: string,
|
|
349
|
+
convertHttpImage2Base64: boolean,
|
|
350
|
+
) => {
|
|
351
|
+
if (typeof url !== 'string') {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`url must be a string, but got ${url} with type ${typeof url}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (url.startsWith('data:')) {
|
|
357
|
+
return url;
|
|
358
|
+
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
359
|
+
if (!convertHttpImage2Base64) {
|
|
360
|
+
return url;
|
|
361
|
+
}
|
|
362
|
+
return await httpImg2Base64(url);
|
|
363
|
+
} else {
|
|
364
|
+
return await localImg2Base64(url);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* parse base64 string to get mimeType and body
|
|
370
|
+
*/
|
|
371
|
+
export const parseBase64 = (
|
|
372
|
+
fullBase64String: string,
|
|
373
|
+
): {
|
|
374
|
+
mimeType: string;
|
|
375
|
+
body: string;
|
|
376
|
+
} => {
|
|
377
|
+
try {
|
|
378
|
+
const separator = ';base64,';
|
|
379
|
+
const index = fullBase64String.indexOf(separator);
|
|
380
|
+
if (index === -1) {
|
|
381
|
+
throw new Error('Invalid base64 string');
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
// 5 means 'data:'
|
|
385
|
+
mimeType: fullBase64String.slice(5, index),
|
|
386
|
+
body: fullBase64String.slice(index + separator.length),
|
|
387
|
+
};
|
|
388
|
+
} catch (e) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`parseBase64 fail because intput is not a valid base64 string: ${fullBase64String}`,
|
|
391
|
+
{
|
|
392
|
+
cause: e,
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
};
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import util from 'node:util';
|
|
4
|
+
import debug from 'debug';
|
|
5
|
+
import { getRpasceneRunSubDir } from './common';
|
|
6
|
+
import { ifInNode } from './utils';
|
|
7
|
+
|
|
8
|
+
const topicPrefix = 'rpascene';
|
|
9
|
+
// Map to store file streams
|
|
10
|
+
const logStreams = new Map<string, fs.WriteStream>();
|
|
11
|
+
// Map to store debug instances
|
|
12
|
+
const debugInstances = new Map<string, DebugFunction>();
|
|
13
|
+
|
|
14
|
+
// Function to get or create a log stream
|
|
15
|
+
function getLogStream(topic: string): fs.WriteStream {
|
|
16
|
+
const topicFileName = topic.replace(/:/g, '-');
|
|
17
|
+
if (!logStreams.has(topicFileName)) {
|
|
18
|
+
const logFile = path.join(
|
|
19
|
+
getRpasceneRunSubDir('log'),
|
|
20
|
+
`${topicFileName}.log`,
|
|
21
|
+
);
|
|
22
|
+
const stream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
23
|
+
logStreams.set(topicFileName, stream);
|
|
24
|
+
}
|
|
25
|
+
return logStreams.get(topicFileName)!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Function to write log to file
|
|
29
|
+
function writeLogToFile(topic: string, message: string): void {
|
|
30
|
+
if (!ifInNode) return;
|
|
31
|
+
|
|
32
|
+
const stream = getLogStream(topic);
|
|
33
|
+
// Generate ISO format timestamp with local timezone
|
|
34
|
+
const now = new Date();
|
|
35
|
+
// Use sv-SE locale to get ISO-like format (YYYY-MM-DD HH:mm:ss)
|
|
36
|
+
const isoDate = now.toLocaleDateString('sv-SE'); // YYYY-MM-DD
|
|
37
|
+
const isoTime = now.toLocaleTimeString('sv-SE'); // HH:mm:ss
|
|
38
|
+
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
|
|
39
|
+
// Calculate timezone offset manually for correct format (+HH:mm)
|
|
40
|
+
const timezoneOffsetMinutes = now.getTimezoneOffset();
|
|
41
|
+
const sign = timezoneOffsetMinutes <= 0 ? '+' : '-';
|
|
42
|
+
const hours = Math.floor(Math.abs(timezoneOffsetMinutes) / 60)
|
|
43
|
+
.toString()
|
|
44
|
+
.padStart(2, '0');
|
|
45
|
+
const minutes = (Math.abs(timezoneOffsetMinutes) % 60)
|
|
46
|
+
.toString()
|
|
47
|
+
.padStart(2, '0');
|
|
48
|
+
const timezoneString = `${sign}${hours}:${minutes}`;
|
|
49
|
+
const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
|
|
50
|
+
stream.write(`[${localISOTime}] ${message}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type DebugFunction = (...args: unknown[]) => void;
|
|
54
|
+
|
|
55
|
+
export function getDebug(topic: string): DebugFunction {
|
|
56
|
+
const fullTopic = `${topicPrefix}:${topic}`;
|
|
57
|
+
|
|
58
|
+
if (!debugInstances.has(fullTopic)) {
|
|
59
|
+
const debugFn = debug(fullTopic) as DebugFunction;
|
|
60
|
+
|
|
61
|
+
// Create wrapper that handles both file logging and debug output
|
|
62
|
+
const wrapper = (...args: unknown[]): void => {
|
|
63
|
+
if (ifInNode) {
|
|
64
|
+
const message = util.format(...args);
|
|
65
|
+
writeLogToFile(topic, message);
|
|
66
|
+
}
|
|
67
|
+
debugFn(...args);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
debugInstances.set(fullTopic, wrapper);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return debugInstances.get(fullTopic)!;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function enableDebug(topic: string): void {
|
|
77
|
+
if (ifInNode) {
|
|
78
|
+
// In Node.js, we don't need to enable debug as we're using file logging
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
debug.enable(`${topicPrefix}:${topic}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cleanup function to close all log streams
|
|
85
|
+
export function cleanupLogStreams(): void {
|
|
86
|
+
if (!ifInNode) return;
|
|
87
|
+
|
|
88
|
+
for (const stream of logStreams.values()) {
|
|
89
|
+
stream.end();
|
|
90
|
+
}
|
|
91
|
+
logStreams.clear();
|
|
92
|
+
debugInstances.clear();
|
|
93
|
+
}
|
package/src/node/fs.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ifInBrowser, ifInWorker } from '../utils';
|
|
4
|
+
|
|
5
|
+
declare const __HTML_ELEMENT_SCRIPT__: string;
|
|
6
|
+
|
|
7
|
+
interface PkgInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
dir: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pkgCacheMap: Record<string, PkgInfo> = {};
|
|
14
|
+
|
|
15
|
+
export function getRunningPkgInfo(dir?: string): PkgInfo | null {
|
|
16
|
+
if (ifInBrowser || ifInWorker) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const dirToCheck = dir || process.cwd();
|
|
20
|
+
if (pkgCacheMap[dirToCheck]) {
|
|
21
|
+
return pkgCacheMap[dirToCheck];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pkgDir = findNearestPackageJson(dirToCheck);
|
|
25
|
+
const pkgJsonFile = pkgDir ? join(pkgDir, 'package.json') : null;
|
|
26
|
+
|
|
27
|
+
if (pkgDir && pkgJsonFile) {
|
|
28
|
+
const { name, version } = JSON.parse(readFileSync(pkgJsonFile, 'utf-8'));
|
|
29
|
+
pkgCacheMap[dirToCheck] = {
|
|
30
|
+
name: name || 'rpascene-unknown-package-name',
|
|
31
|
+
version: version || '0.0.0',
|
|
32
|
+
dir: pkgDir,
|
|
33
|
+
};
|
|
34
|
+
return pkgCacheMap[dirToCheck];
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
name: 'rpascene-unknown-package-name',
|
|
38
|
+
version: '0.0.0',
|
|
39
|
+
dir: dirToCheck,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Find the nearest package.json file recursively
|
|
45
|
+
* @param {string} dir - Home directory
|
|
46
|
+
* @returns {string|null} - The most recent package.json file path or null
|
|
47
|
+
*/
|
|
48
|
+
export function findNearestPackageJson(dir: string): string | null {
|
|
49
|
+
const packageJsonPath = join(dir, 'package.json');
|
|
50
|
+
if (existsSync(packageJsonPath)) {
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parentDir = dirname(dir);
|
|
55
|
+
|
|
56
|
+
// Return null if the root directory has been reached
|
|
57
|
+
if (parentDir === dir) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return findNearestPackageJson(parentDir);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getElementInfosScriptContent() {
|
|
65
|
+
const htmlElementScript = __HTML_ELEMENT_SCRIPT__;
|
|
66
|
+
|
|
67
|
+
if (!htmlElementScript) {
|
|
68
|
+
throw new Error('HTML_ELEMENT_SCRIPT inject failed.');
|
|
69
|
+
}
|
|
70
|
+
return htmlElementScript;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getExtraReturnLogic(tree = false) {
|
|
74
|
+
if (ifInBrowser || ifInWorker) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const elementInfosScriptContent = `;rpascene_element_inspector.setNodeHashCacheListOnWindow();`;
|
|
79
|
+
|
|
80
|
+
if (tree) {
|
|
81
|
+
return `${elementInfosScriptContent};rpascene_element_inspector.webExtractNodeTree()`;
|
|
82
|
+
}
|
|
83
|
+
return `${elementInfosScriptContent};rpascene_element_inspector.webExtractTextWithPosition()`;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './fs';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { NodeType } from '../constants';
|
|
2
|
+
import type { ElementInfo } from '../extractor';
|
|
3
|
+
|
|
4
|
+
export interface Point {
|
|
5
|
+
left: number;
|
|
6
|
+
top: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Size {
|
|
10
|
+
width: number; // The image sent to AI model will be resized to this width, also the coordinates in the action space will be scaled to the range [0, width]. Usually you should set it to the logical pixel size
|
|
11
|
+
height: number; // The image sent to AI model will be resized to this height, also the coordinates in the action space will be scaled to the range [0, height]. Usually you should set it to the logical pixel size
|
|
12
|
+
dpr?: number; // this is deprecated, do NOT use it
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Rect = Point & Size & { zoom?: number };
|
|
16
|
+
|
|
17
|
+
export abstract class BaseElement {
|
|
18
|
+
abstract id: string;
|
|
19
|
+
|
|
20
|
+
abstract indexId?: number; // markerId for web
|
|
21
|
+
|
|
22
|
+
abstract attributes: {
|
|
23
|
+
nodeType: NodeType;
|
|
24
|
+
[key: string]: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
abstract content: string;
|
|
28
|
+
|
|
29
|
+
abstract rect: Rect;
|
|
30
|
+
|
|
31
|
+
abstract center: [number, number];
|
|
32
|
+
|
|
33
|
+
abstract xpaths?: string[];
|
|
34
|
+
|
|
35
|
+
abstract isVisible: boolean;
|
|
36
|
+
|
|
37
|
+
abstract allPaths?: any[]
|
|
38
|
+
|
|
39
|
+
abstract containerPaths?: any[]
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ElementTreeNode<
|
|
44
|
+
ElementType extends BaseElement = BaseElement,
|
|
45
|
+
> {
|
|
46
|
+
node: ElementType | null;
|
|
47
|
+
children: ElementTreeNode<ElementType>[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WebElementInfo extends ElementInfo {
|
|
51
|
+
zoom: number;
|
|
52
|
+
locator?: string
|
|
53
|
+
}
|