@sqaitech/shared 0.30.10
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 +24 -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 +91 -0
- package/dist/es/env/helper.mjs +48 -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 +253 -0
- package/dist/es/env/utils.mjs +18 -0
- package/dist/es/extractor/constants.mjs +2 -0
- package/dist/es/extractor/debug.mjs +6 -0
- package/dist/es/extractor/dom-util.mjs +92 -0
- package/dist/es/extractor/index.mjs +6 -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 +310 -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 +100 -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 +125 -0
- package/dist/lib/env/helper.js +89 -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 +635 -0
- package/dist/lib/env/utils.js +61 -0
- package/dist/lib/extractor/constants.js +42 -0
- package/dist/lib/extractor/debug.js +12 -0
- package/dist/lib/extractor/dom-util.js +150 -0
- package/dist/lib/extractor/index.js +88 -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 +356 -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 +22 -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 +294 -0
- package/dist/types/env/utils.d.ts +7 -0
- package/dist/types/extractor/constants.d.ts +1 -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 +33 -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 +34 -0
- package/dist/types/us-keyboard-layout.d.ts +32 -0
- package/dist/types/utils.d.ts +22 -0
- package/package.json +106 -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 +30 -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 +79 -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 +560 -0
- package/src/env/utils.ts +39 -0
- package/src/extractor/constants.ts +5 -0
- package/src/extractor/debug.ts +10 -0
- package/src/extractor/dom-util.ts +152 -0
- package/src/extractor/index.ts +50 -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 +481 -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 +47 -0
- package/src/us-keyboard-layout.ts +723 -0
- package/src/utils.ts +127 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import node_assert from "node:assert";
|
|
2
|
+
import get_jimp from "./get-jimp.mjs";
|
|
3
|
+
import { bufferFromBase64, imageInfoOfBase64 } from "./index.mjs";
|
|
4
|
+
let cachedFont = null;
|
|
5
|
+
const loadFonts = async ()=>{
|
|
6
|
+
const Jimp = await get_jimp();
|
|
7
|
+
try {
|
|
8
|
+
const fonts = await Jimp.loadFont(Jimp.FONT_SANS_16_WHITE);
|
|
9
|
+
return fonts;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.warn('Error loading font, will try to load online fonts', error);
|
|
12
|
+
const onlineFonts = 'https://cdn.jsdelivr.net/npm/jimp-compact@0.16.1-2/fonts/open-sans/open-sans-16-white/open-sans-16-white.fnt';
|
|
13
|
+
const fonts = await Jimp.loadFont(onlineFonts);
|
|
14
|
+
return fonts;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const createSvgOverlay = async (elements, imageWidth, imageHeight, boxPadding = 5, borderThickness = 2, prompt)=>{
|
|
18
|
+
const Jimp = await get_jimp();
|
|
19
|
+
const image = new Jimp(imageWidth, imageHeight, 0x00000000);
|
|
20
|
+
const colors = [
|
|
21
|
+
{
|
|
22
|
+
rect: 0xc62300ff,
|
|
23
|
+
text: 0xffffffff
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
rect: 0x0000ffff,
|
|
27
|
+
text: 0xffffffff
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
rect: 0x8b4513ff,
|
|
31
|
+
text: 0xffffffff
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
rect: 0x3e7b27ff,
|
|
35
|
+
text: 0xffffffff
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
rect: 0x500073ff,
|
|
39
|
+
text: 0xffffffff
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
if (prompt) try {
|
|
43
|
+
cachedFont = cachedFont || await loadFonts();
|
|
44
|
+
const promptPadding = 10;
|
|
45
|
+
const promptMargin = 20;
|
|
46
|
+
const promptHeight = 30;
|
|
47
|
+
const promptY = imageHeight - promptHeight - promptMargin;
|
|
48
|
+
image.scan(0, promptY, imageWidth, promptHeight, (x, y, idx)=>{
|
|
49
|
+
image.bitmap.data[idx + 0] = 0x00;
|
|
50
|
+
image.bitmap.data[idx + 1] = 0x00;
|
|
51
|
+
image.bitmap.data[idx + 2] = 0x00;
|
|
52
|
+
image.bitmap.data[idx + 3] = 0xcc;
|
|
53
|
+
});
|
|
54
|
+
image.print(cachedFont, promptPadding, promptY, {
|
|
55
|
+
text: prompt,
|
|
56
|
+
alignmentX: Jimp.HORIZONTAL_ALIGN_LEFT,
|
|
57
|
+
alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
|
|
58
|
+
}, imageWidth - 2 * promptPadding, promptHeight);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error drawing prompt text', error);
|
|
61
|
+
}
|
|
62
|
+
for(let index = 0; index < elements.length; index++){
|
|
63
|
+
const element = elements[index];
|
|
64
|
+
const color = colors[index % colors.length];
|
|
65
|
+
const paddedLeft = Math.max(0, element.rect.left - boxPadding);
|
|
66
|
+
const paddedTop = Math.max(0, element.rect.top - boxPadding);
|
|
67
|
+
const paddedWidth = Math.min(imageWidth - paddedLeft, element.rect.width + 2 * boxPadding);
|
|
68
|
+
const paddedHeight = Math.min(imageHeight - paddedTop, element.rect.height + 2 * boxPadding);
|
|
69
|
+
const paddedRect = {
|
|
70
|
+
left: paddedLeft,
|
|
71
|
+
top: paddedTop,
|
|
72
|
+
width: paddedWidth,
|
|
73
|
+
height: paddedHeight
|
|
74
|
+
};
|
|
75
|
+
image.scan(paddedRect.left, paddedRect.top, paddedRect.width, paddedRect.height, (x, y, idx)=>{
|
|
76
|
+
if (x >= paddedRect.left && x < paddedRect.left + borderThickness || x <= paddedRect.left + paddedRect.width - 1 && x > paddedRect.left + paddedRect.width - borderThickness || y >= paddedRect.top && y < paddedRect.top + borderThickness || y <= paddedRect.top + paddedRect.height - 1 && y > paddedRect.top + paddedRect.height - borderThickness) {
|
|
77
|
+
image.bitmap.data[idx + 0] = color.rect >> 24 & 0xff;
|
|
78
|
+
image.bitmap.data[idx + 1] = color.rect >> 16 & 0xff;
|
|
79
|
+
image.bitmap.data[idx + 2] = color.rect >> 8 & 0xff;
|
|
80
|
+
image.bitmap.data[idx + 3] = 0xff & color.rect;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const indexId = element.indexId;
|
|
84
|
+
if ('number' != typeof indexId) continue;
|
|
85
|
+
const textWidth = 8 * indexId.toString().length;
|
|
86
|
+
const textHeight = 12;
|
|
87
|
+
const rectWidth = textWidth + 5;
|
|
88
|
+
const rectHeight = textHeight + 4;
|
|
89
|
+
let rectX = paddedRect.left - rectWidth;
|
|
90
|
+
let rectY = paddedRect.top + paddedRect.height / 2 - textHeight / 2 - 2;
|
|
91
|
+
const checkOverlap = (x, y)=>elements.slice(0, index).some((otherElement)=>x < otherElement.rect.left + otherElement.rect.width && x + rectWidth > otherElement.rect.left && y < otherElement.rect.top + otherElement.rect.height && y + rectHeight > otherElement.rect.top);
|
|
92
|
+
const isWithinBounds = (x, y)=>x >= 0 && x + rectWidth <= imageWidth && y >= 0 && y + rectHeight <= imageHeight;
|
|
93
|
+
if (checkOverlap(rectX, rectY) || !isWithinBounds(rectX, rectY)) if (!checkOverlap(paddedRect.left, paddedRect.top - rectHeight - 2) && isWithinBounds(paddedRect.left, paddedRect.top - rectHeight - 2)) {
|
|
94
|
+
rectX = paddedRect.left;
|
|
95
|
+
rectY = paddedRect.top - rectHeight - 2;
|
|
96
|
+
} else if (!checkOverlap(paddedRect.left, paddedRect.top + paddedRect.height + 2) && isWithinBounds(paddedRect.left, paddedRect.top + paddedRect.height + 2)) {
|
|
97
|
+
rectX = paddedRect.left;
|
|
98
|
+
rectY = paddedRect.top + paddedRect.height + 2;
|
|
99
|
+
} else if (!checkOverlap(paddedRect.left + paddedRect.width + 2, paddedRect.top) && isWithinBounds(paddedRect.left + paddedRect.width + 2, paddedRect.top)) {
|
|
100
|
+
rectX = paddedRect.left + paddedRect.width + 2;
|
|
101
|
+
rectY = paddedRect.top;
|
|
102
|
+
} else {
|
|
103
|
+
rectX = paddedRect.left;
|
|
104
|
+
rectY = paddedRect.top + 2;
|
|
105
|
+
}
|
|
106
|
+
image.scan(rectX, rectY, rectWidth, rectHeight, (x, y, idx)=>{
|
|
107
|
+
image.bitmap.data[idx + 0] = color.rect >> 24 & 0xff;
|
|
108
|
+
image.bitmap.data[idx + 1] = color.rect >> 16 & 0xff;
|
|
109
|
+
image.bitmap.data[idx + 2] = color.rect >> 8 & 0xff;
|
|
110
|
+
image.bitmap.data[idx + 3] = 0xff & color.rect;
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
cachedFont = cachedFont || await loadFonts();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Error loading font', error);
|
|
116
|
+
}
|
|
117
|
+
image.print(cachedFont, rectX, rectY, {
|
|
118
|
+
text: indexId.toString(),
|
|
119
|
+
alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
|
|
120
|
+
alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
|
|
121
|
+
}, rectWidth, rectHeight);
|
|
122
|
+
}
|
|
123
|
+
return image;
|
|
124
|
+
};
|
|
125
|
+
const compositeElementInfoImg = async (options)=>{
|
|
126
|
+
node_assert(options.inputImgBase64, 'inputImgBase64 is required');
|
|
127
|
+
let width = 0;
|
|
128
|
+
let height = 0;
|
|
129
|
+
let jimpImage;
|
|
130
|
+
const Jimp = await get_jimp();
|
|
131
|
+
if (options.size) {
|
|
132
|
+
width = options.size.width;
|
|
133
|
+
height = options.size.height;
|
|
134
|
+
}
|
|
135
|
+
if (width && height) {
|
|
136
|
+
const imageBuffer = await bufferFromBase64(options.inputImgBase64);
|
|
137
|
+
jimpImage = await Jimp.read(imageBuffer);
|
|
138
|
+
const imageBitmap = jimpImage.bitmap;
|
|
139
|
+
if (imageBitmap.width !== width || imageBitmap.height !== height) jimpImage.resize(width, height, Jimp.RESIZE_NEAREST_NEIGHBOR);
|
|
140
|
+
} else {
|
|
141
|
+
const info = await imageInfoOfBase64(options.inputImgBase64);
|
|
142
|
+
width = info.width;
|
|
143
|
+
height = info.height;
|
|
144
|
+
jimpImage = info.jimpImage;
|
|
145
|
+
}
|
|
146
|
+
if (!width || !height) throw Error('Image processing failed because width or height is undefined');
|
|
147
|
+
const { elementsPositionInfo, prompt } = options;
|
|
148
|
+
const result = await Promise.resolve(jimpImage).then(async (image)=>{
|
|
149
|
+
const svgOverlay = await createSvgOverlay(elementsPositionInfo, width, height, options.annotationPadding, options.borderThickness, prompt);
|
|
150
|
+
const svgImage = await Jimp.read(svgOverlay);
|
|
151
|
+
const compositeImage = await image.composite(svgImage, 0, 0, {
|
|
152
|
+
mode: Jimp.BLEND_SOURCE_OVER,
|
|
153
|
+
opacitySource: 1,
|
|
154
|
+
opacityDest: 1
|
|
155
|
+
});
|
|
156
|
+
return compositeImage;
|
|
157
|
+
}).then(async (compositeImage)=>{
|
|
158
|
+
compositeImage.quality(90);
|
|
159
|
+
const base64 = await compositeImage.getBase64Async(Jimp.MIME_JPEG);
|
|
160
|
+
return base64;
|
|
161
|
+
}).catch((error)=>{
|
|
162
|
+
throw error;
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
const processImageElementInfo = async (options)=>{
|
|
167
|
+
const base64Image = options.inputImgBase64.split(';base64,').pop();
|
|
168
|
+
node_assert(base64Image, 'base64Image is undefined');
|
|
169
|
+
const [compositeElementInfoImgBase64, compositeElementInfoImgWithoutTextBase64] = await Promise.all([
|
|
170
|
+
compositeElementInfoImg({
|
|
171
|
+
inputImgBase64: options.inputImgBase64,
|
|
172
|
+
elementsPositionInfo: options.elementsPositionInfo
|
|
173
|
+
}),
|
|
174
|
+
compositeElementInfoImg({
|
|
175
|
+
inputImgBase64: options.inputImgBase64,
|
|
176
|
+
elementsPositionInfo: options.elementsPositionInfoWithoutText
|
|
177
|
+
})
|
|
178
|
+
]);
|
|
179
|
+
return {
|
|
180
|
+
compositeElementInfoImgBase64,
|
|
181
|
+
compositeElementInfoImgWithoutTextBase64
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
export { compositeElementInfoImg, processImageElementInfo };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import get_jimp from "./get-jimp.mjs";
|
|
2
|
+
import { bufferFromBase64 } from "./info.mjs";
|
|
3
|
+
import { saveBase64Image } from "./transform.mjs";
|
|
4
|
+
async function drawBoxOnImage(options) {
|
|
5
|
+
const { inputImgBase64, rect } = options;
|
|
6
|
+
const color = {
|
|
7
|
+
r: 255,
|
|
8
|
+
g: 0,
|
|
9
|
+
b: 0,
|
|
10
|
+
a: 255
|
|
11
|
+
};
|
|
12
|
+
const Jimp = await get_jimp();
|
|
13
|
+
const imageBuffer = await bufferFromBase64(inputImgBase64);
|
|
14
|
+
const image = await Jimp.read(imageBuffer);
|
|
15
|
+
const centerX = rect.x;
|
|
16
|
+
const centerY = rect.y;
|
|
17
|
+
const radius = 5;
|
|
18
|
+
image.scan(Math.floor(centerX - radius), Math.floor(centerY - radius), 2 * radius, 2 * radius, (x, y, idx)=>{
|
|
19
|
+
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
|
20
|
+
if (distance <= radius) {
|
|
21
|
+
image.bitmap.data[idx + 0] = color.r;
|
|
22
|
+
image.bitmap.data[idx + 1] = color.g;
|
|
23
|
+
image.bitmap.data[idx + 2] = color.b;
|
|
24
|
+
image.bitmap.data[idx + 3] = color.a;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
image.quality(90);
|
|
28
|
+
const resultBase64 = await image.getBase64Async(Jimp.MIME_JPEG);
|
|
29
|
+
return resultBase64;
|
|
30
|
+
}
|
|
31
|
+
async function savePositionImg(options) {
|
|
32
|
+
const { inputImgBase64, rect, outputPath } = options;
|
|
33
|
+
const imgBase64 = await drawBoxOnImage({
|
|
34
|
+
inputImgBase64,
|
|
35
|
+
rect
|
|
36
|
+
});
|
|
37
|
+
await saveBase64Image({
|
|
38
|
+
base64Data: imgBase64,
|
|
39
|
+
outputPath
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export { drawBoxOnImage, savePositionImg };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import jimp from "jimp";
|
|
2
|
+
import { ifInBrowser, ifInWorker } from "../utils.mjs";
|
|
3
|
+
async function getJimp() {
|
|
4
|
+
if (ifInBrowser || ifInWorker) {
|
|
5
|
+
await import("jimp/browser/lib/jimp.js");
|
|
6
|
+
return ('undefined' != typeof window ? window : self).Jimp;
|
|
7
|
+
}
|
|
8
|
+
return jimp;
|
|
9
|
+
}
|
|
10
|
+
export { getJimp as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ifInBrowser, ifInNode, ifInWorker } from "../utils.mjs";
|
|
2
|
+
let photonModule = null;
|
|
3
|
+
let isInitialized = false;
|
|
4
|
+
async function getPhoton() {
|
|
5
|
+
if (photonModule && isInitialized) return photonModule;
|
|
6
|
+
try {
|
|
7
|
+
if (ifInBrowser || ifInWorker) {
|
|
8
|
+
const photon = await import("@silvia-odwyer/photon");
|
|
9
|
+
if ('function' == typeof photon.default) await photon.default();
|
|
10
|
+
photonModule = photon;
|
|
11
|
+
} else if (ifInNode) photonModule = await import("@silvia-odwyer/photon-node");
|
|
12
|
+
if (!photonModule.PhotonImage || !photonModule.PhotonImage.new_from_byteslice) throw new Error('PhotonImage.new_from_byteslice is not available');
|
|
13
|
+
isInitialized = true;
|
|
14
|
+
return photonModule;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(`Failed to load photon module: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export { getPhoton as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ifInNode } from "../utils.mjs";
|
|
2
|
+
async function getSharp() {
|
|
3
|
+
if (!ifInNode) throw new Error('Sharp is only available in Node.js environment');
|
|
4
|
+
try {
|
|
5
|
+
const sharp = await import("sharp");
|
|
6
|
+
return sharp.default;
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(`Failed to load sharp module: ${error instanceof Error ? error.message : String(error)}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export { getSharp as default };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { bufferFromBase64, imageInfo, imageInfoOfBase64, isValidPNGImageBuffer } from "./info.mjs";
|
|
2
|
+
import { createImgBase64ByFormat, cropByRect, httpImg2Base64, jimpFromBase64, jimpToBase64, localImg2Base64, paddingToMatchBlock, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, zoomForGPT4o } from "./transform.mjs";
|
|
3
|
+
import { compositeElementInfoImg, processImageElementInfo } from "./box-select.mjs";
|
|
4
|
+
import { drawBoxOnImage, savePositionImg } from "./draw-box.mjs";
|
|
5
|
+
export { bufferFromBase64, compositeElementInfoImg, createImgBase64ByFormat, cropByRect, drawBoxOnImage, httpImg2Base64, imageInfo, imageInfoOfBase64, isValidPNGImageBuffer, jimpFromBase64, jimpToBase64, localImg2Base64, paddingToMatchBlock, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, processImageElementInfo, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, savePositionImg, zoomForGPT4o };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import node_assert from "node:assert";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import get_jimp from "./get-jimp.mjs";
|
|
4
|
+
async function imageInfo(image) {
|
|
5
|
+
const Jimp = await get_jimp();
|
|
6
|
+
let jimpImage;
|
|
7
|
+
if ('string' == typeof image) jimpImage = await Jimp.read(image);
|
|
8
|
+
else if (Buffer.isBuffer(image)) jimpImage = await Jimp.read(image);
|
|
9
|
+
else if (image instanceof Jimp) jimpImage = image;
|
|
10
|
+
else throw new Error('Invalid image input: must be a string path or a Buffer');
|
|
11
|
+
const { width, height } = jimpImage.bitmap;
|
|
12
|
+
node_assert(width && height, `Invalid image: ${'string' == typeof image ? image : 'Buffer'}`);
|
|
13
|
+
return {
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
jimpImage
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function imageInfoOfBase64(imageBase64) {
|
|
20
|
+
const buffer = await bufferFromBase64(imageBase64);
|
|
21
|
+
return imageInfo(buffer);
|
|
22
|
+
}
|
|
23
|
+
async function bufferFromBase64(imageBase64) {
|
|
24
|
+
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
|
|
25
|
+
return Buffer.from(base64Data, 'base64');
|
|
26
|
+
}
|
|
27
|
+
function isValidPNGImageBuffer(buffer) {
|
|
28
|
+
if (!buffer || buffer.length < 8) return false;
|
|
29
|
+
const isPNG = 0x89 === buffer[0] && 0x50 === buffer[1] && 0x4e === buffer[2] && 0x47 === buffer[3];
|
|
30
|
+
return isPNG;
|
|
31
|
+
}
|
|
32
|
+
export { bufferFromBase64, imageInfo, imageInfoOfBase64, isValidPNGImageBuffer };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import node_assert from "node:assert";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
import { getDebug } from "../logger.mjs";
|
|
6
|
+
import { ifInNode } from "../utils.mjs";
|
|
7
|
+
import get_jimp from "./get-jimp.mjs";
|
|
8
|
+
import get_photon from "./get-photon.mjs";
|
|
9
|
+
import get_sharp from "./get-sharp.mjs";
|
|
10
|
+
const imgDebug = getDebug('img');
|
|
11
|
+
async function saveBase64Image(options) {
|
|
12
|
+
const { base64Data, outputPath } = options;
|
|
13
|
+
const { body } = parseBase64(base64Data);
|
|
14
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
15
|
+
const Jimp = await get_jimp();
|
|
16
|
+
const image = await Jimp.read(imageBuffer);
|
|
17
|
+
await image.writeAsync(outputPath);
|
|
18
|
+
}
|
|
19
|
+
async function resizeAndConvertImgBuffer(inputFormat, inputData, newSize) {
|
|
20
|
+
if ('string' == typeof inputData) throw Error('inputData is base64, use resizeImgBase64 instead');
|
|
21
|
+
node_assert(newSize && newSize.width > 0 && newSize.height > 0, 'newSize must be positive');
|
|
22
|
+
const resizeStartTime = Date.now();
|
|
23
|
+
imgDebug(`resizeImg start, target size: ${newSize.width}x${newSize.height}`);
|
|
24
|
+
if (ifInNode) try {
|
|
25
|
+
const Sharp = await get_sharp();
|
|
26
|
+
const metadata = await Sharp(inputData).metadata();
|
|
27
|
+
const { width: originalWidth, height: originalHeight } = metadata;
|
|
28
|
+
if (!originalWidth || !originalHeight) throw Error('Undefined width or height from the input image.');
|
|
29
|
+
if (newSize.width === originalWidth && newSize.height === originalHeight) return {
|
|
30
|
+
buffer: inputData,
|
|
31
|
+
format: inputFormat
|
|
32
|
+
};
|
|
33
|
+
const resizedBuffer = await Sharp(inputData).resize(newSize.width, newSize.height).jpeg({
|
|
34
|
+
quality: 90
|
|
35
|
+
}).toBuffer();
|
|
36
|
+
const resizeEndTime = Date.now();
|
|
37
|
+
imgDebug(`resizeImg done (Sharp), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`);
|
|
38
|
+
return {
|
|
39
|
+
buffer: resizedBuffer,
|
|
40
|
+
format: 'jpeg'
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
44
|
+
}
|
|
45
|
+
const { PhotonImage, SamplingFilter, resize } = await get_photon();
|
|
46
|
+
const inputBytes = new Uint8Array(inputData);
|
|
47
|
+
const inputImage = PhotonImage.new_from_byteslice(inputBytes);
|
|
48
|
+
const originalWidth = inputImage.get_width();
|
|
49
|
+
const originalHeight = inputImage.get_height();
|
|
50
|
+
if (!originalWidth || !originalHeight) {
|
|
51
|
+
inputImage.free();
|
|
52
|
+
throw Error('Undefined width or height from the input image.');
|
|
53
|
+
}
|
|
54
|
+
if (newSize.width === originalWidth && newSize.height === originalHeight) {
|
|
55
|
+
inputImage.free();
|
|
56
|
+
return {
|
|
57
|
+
buffer: inputData,
|
|
58
|
+
format: inputFormat
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const outputImage = resize(inputImage, newSize.width, newSize.height, SamplingFilter.CatmullRom);
|
|
62
|
+
const outputBytes = outputImage.get_bytes_jpeg(90);
|
|
63
|
+
const resizedBuffer = Buffer.from(outputBytes);
|
|
64
|
+
inputImage.free();
|
|
65
|
+
outputImage.free();
|
|
66
|
+
const resizeEndTime = Date.now();
|
|
67
|
+
imgDebug(`resizeImg done (Photon), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`);
|
|
68
|
+
return {
|
|
69
|
+
buffer: resizedBuffer,
|
|
70
|
+
format: 'jpeg'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const createImgBase64ByFormat = (format, body)=>`data:image/${format};base64,${body}`;
|
|
74
|
+
async function resizeImgBase64(inputBase64, newSize) {
|
|
75
|
+
const { body, mimeType } = parseBase64(inputBase64);
|
|
76
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
77
|
+
const { buffer, format } = await resizeAndConvertImgBuffer(mimeType.split('/')[1], imageBuffer, newSize);
|
|
78
|
+
return createImgBase64ByFormat(format, buffer.toString('base64'));
|
|
79
|
+
}
|
|
80
|
+
function zoomForGPT4o(originalWidth, originalHeight) {
|
|
81
|
+
const maxWidth = 2048;
|
|
82
|
+
const maxHeight = 768;
|
|
83
|
+
let newWidth = originalWidth;
|
|
84
|
+
let newHeight = originalHeight;
|
|
85
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
86
|
+
if (originalWidth > maxWidth) {
|
|
87
|
+
newWidth = maxWidth;
|
|
88
|
+
newHeight = newWidth / aspectRatio;
|
|
89
|
+
}
|
|
90
|
+
if (newHeight > maxHeight) {
|
|
91
|
+
newHeight = maxHeight;
|
|
92
|
+
newWidth = newHeight * aspectRatio;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
width: Math.round(newWidth),
|
|
96
|
+
height: Math.round(newHeight)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function jimpFromBase64(base64) {
|
|
100
|
+
const Jimp = await get_jimp();
|
|
101
|
+
const { body } = parseBase64(base64);
|
|
102
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
103
|
+
return Jimp.read(imageBuffer);
|
|
104
|
+
}
|
|
105
|
+
async function paddingToMatchBlock(image, blockSize = 28) {
|
|
106
|
+
const { width, height } = image.bitmap;
|
|
107
|
+
const targetWidth = Math.ceil(width / blockSize) * blockSize;
|
|
108
|
+
const targetHeight = Math.ceil(height / blockSize) * blockSize;
|
|
109
|
+
if (targetWidth === width && targetHeight === height) return {
|
|
110
|
+
width,
|
|
111
|
+
height,
|
|
112
|
+
image
|
|
113
|
+
};
|
|
114
|
+
const Jimp = await get_jimp();
|
|
115
|
+
const paddedImage = new Jimp(targetWidth, targetHeight, 0xffffffff);
|
|
116
|
+
paddedImage.composite(image, 0, 0);
|
|
117
|
+
return {
|
|
118
|
+
width: targetWidth,
|
|
119
|
+
height: targetHeight,
|
|
120
|
+
image: paddedImage
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function paddingToMatchBlockByBase64(imageBase64, blockSize = 28) {
|
|
124
|
+
const jimpImage = await jimpFromBase64(imageBase64);
|
|
125
|
+
const paddedResult = await paddingToMatchBlock(jimpImage, blockSize);
|
|
126
|
+
return {
|
|
127
|
+
width: paddedResult.width,
|
|
128
|
+
height: paddedResult.height,
|
|
129
|
+
imageBase64: await jimpToBase64(paddedResult.image)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function cropByRect(imageBase64, rect, paddingImage) {
|
|
133
|
+
const jimpImage = await jimpFromBase64(imageBase64);
|
|
134
|
+
const { left, top, width, height } = rect;
|
|
135
|
+
jimpImage.crop(left, top, width, height);
|
|
136
|
+
if (paddingImage) {
|
|
137
|
+
const paddedResult = await paddingToMatchBlock(jimpImage);
|
|
138
|
+
return {
|
|
139
|
+
width: paddedResult.width,
|
|
140
|
+
height: paddedResult.height,
|
|
141
|
+
imageBase64: await jimpToBase64(paddedResult.image)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
width: jimpImage.bitmap.width,
|
|
146
|
+
height: jimpImage.bitmap.height,
|
|
147
|
+
imageBase64: await jimpToBase64(jimpImage)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function jimpToBase64(image) {
|
|
151
|
+
const Jimp = await get_jimp();
|
|
152
|
+
return image.getBase64Async(Jimp.MIME_JPEG);
|
|
153
|
+
}
|
|
154
|
+
const httpImg2Base64 = async (url)=>{
|
|
155
|
+
const response = await fetch(url);
|
|
156
|
+
if (!response.ok) throw new Error(`Failed to fetch image: ${url}`);
|
|
157
|
+
const contentType = response.headers.get('content-type');
|
|
158
|
+
if (!contentType) throw new Error(`Failed to fetch image: ${url}`);
|
|
159
|
+
node_assert(contentType.startsWith('image/'), `The url ${url} is not a image, because of content-type in header is ${contentType}.`);
|
|
160
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
161
|
+
return `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
162
|
+
};
|
|
163
|
+
const localImg2Base64 = (imgPath, withoutHeader = false)=>{
|
|
164
|
+
const body = readFileSync(imgPath).toString('base64');
|
|
165
|
+
if (withoutHeader) return body;
|
|
166
|
+
const type = node_path.extname(imgPath).slice(1);
|
|
167
|
+
const finalType = 'svg' === type ? 'svg+xml' : type || 'jpg';
|
|
168
|
+
return `data:image/${finalType};base64,${body}`;
|
|
169
|
+
};
|
|
170
|
+
const preProcessImageUrl = async (url, convertHttpImage2Base64)=>{
|
|
171
|
+
if ('string' != typeof url) throw new Error(`url must be a string, but got ${url} with type ${typeof url}`);
|
|
172
|
+
if (url.startsWith('data:')) return url;
|
|
173
|
+
if (!(url.startsWith('http://') || url.startsWith('https://'))) return await localImg2Base64(url);
|
|
174
|
+
if (!convertHttpImage2Base64) return url;
|
|
175
|
+
return await httpImg2Base64(url);
|
|
176
|
+
};
|
|
177
|
+
const parseBase64 = (fullBase64String)=>{
|
|
178
|
+
try {
|
|
179
|
+
const separator = ';base64,';
|
|
180
|
+
const index = fullBase64String.indexOf(separator);
|
|
181
|
+
if (-1 === index) throw new Error('Invalid base64 string');
|
|
182
|
+
return {
|
|
183
|
+
mimeType: fullBase64String.slice(5, index),
|
|
184
|
+
body: fullBase64String.slice(index + separator.length)
|
|
185
|
+
};
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw new Error(`parseBase64 fail because intput is not a valid base64 string: ${fullBase64String}`, {
|
|
188
|
+
cause: e
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
export { createImgBase64ByFormat, cropByRect, httpImg2Base64, jimpFromBase64, jimpToBase64, localImg2Base64, paddingToMatchBlock, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, zoomForGPT4o };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import node_fs from "node:fs";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import node_util from "node:util";
|
|
4
|
+
import debug from "debug";
|
|
5
|
+
import { getMidsceneRunSubDir } from "./common.mjs";
|
|
6
|
+
import { ifInNode } from "./utils.mjs";
|
|
7
|
+
const topicPrefix = 'midscene';
|
|
8
|
+
const logStreams = new Map();
|
|
9
|
+
const debugInstances = new Map();
|
|
10
|
+
function getLogStream(topic) {
|
|
11
|
+
const topicFileName = topic.replace(/:/g, '-');
|
|
12
|
+
if (!logStreams.has(topicFileName)) {
|
|
13
|
+
const logFile = node_path.join(getMidsceneRunSubDir('log'), `${topicFileName}.log`);
|
|
14
|
+
const stream = node_fs.createWriteStream(logFile, {
|
|
15
|
+
flags: 'a'
|
|
16
|
+
});
|
|
17
|
+
logStreams.set(topicFileName, stream);
|
|
18
|
+
}
|
|
19
|
+
return logStreams.get(topicFileName);
|
|
20
|
+
}
|
|
21
|
+
function writeLogToFile(topic, message) {
|
|
22
|
+
if (!ifInNode) return;
|
|
23
|
+
const stream = getLogStream(topic);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const isoDate = now.toLocaleDateString('sv-SE');
|
|
26
|
+
const isoTime = now.toLocaleTimeString('sv-SE');
|
|
27
|
+
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
|
|
28
|
+
const timezoneOffsetMinutes = now.getTimezoneOffset();
|
|
29
|
+
const sign = timezoneOffsetMinutes <= 0 ? '+' : '-';
|
|
30
|
+
const hours = Math.floor(Math.abs(timezoneOffsetMinutes) / 60).toString().padStart(2, '0');
|
|
31
|
+
const minutes = (Math.abs(timezoneOffsetMinutes) % 60).toString().padStart(2, '0');
|
|
32
|
+
const timezoneString = `${sign}${hours}:${minutes}`;
|
|
33
|
+
const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
|
|
34
|
+
stream.write(`[${localISOTime}] ${message}\n`);
|
|
35
|
+
}
|
|
36
|
+
function getDebug(topic) {
|
|
37
|
+
const fullTopic = `${topicPrefix}:${topic}`;
|
|
38
|
+
if (!debugInstances.has(fullTopic)) {
|
|
39
|
+
const debugFn = debug(fullTopic);
|
|
40
|
+
const wrapper = (...args)=>{
|
|
41
|
+
if (ifInNode) {
|
|
42
|
+
const message = node_util.format(...args);
|
|
43
|
+
writeLogToFile(topic, message);
|
|
44
|
+
}
|
|
45
|
+
debugFn(...args);
|
|
46
|
+
};
|
|
47
|
+
debugInstances.set(fullTopic, wrapper);
|
|
48
|
+
}
|
|
49
|
+
return debugInstances.get(fullTopic);
|
|
50
|
+
}
|
|
51
|
+
function enableDebug(topic) {
|
|
52
|
+
if (ifInNode) return;
|
|
53
|
+
debug.enable(`${topicPrefix}:${topic}`);
|
|
54
|
+
}
|
|
55
|
+
function cleanupLogStreams() {
|
|
56
|
+
if (!ifInNode) return;
|
|
57
|
+
for (const stream of logStreams.values())stream.end();
|
|
58
|
+
logStreams.clear();
|
|
59
|
+
debugInstances.clear();
|
|
60
|
+
}
|
|
61
|
+
export { cleanupLogStreams, enableDebug, getDebug };
|