@midscene/shared 1.10.5-beta-20260716133905.0 → 1.10.5
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/env/parse-model-config.mjs +1 -1
- package/dist/es/img/box-select.mjs +1 -46
- package/dist/es/img/get-photon.mjs +6 -3
- package/dist/es/img/index.mjs +2 -2
- package/dist/es/img/info.mjs +1 -19
- package/dist/es/img/transform.mjs +24 -57
- package/dist/es/logger.mjs +2 -23
- package/dist/lib/env/parse-model-config.js +1 -1
- package/dist/lib/img/box-select.js +0 -46
- package/dist/lib/img/get-photon.js +5 -2
- package/dist/lib/img/index.js +4 -1
- package/dist/lib/img/info.js +1 -20
- package/dist/lib/img/transform.js +29 -59
- package/dist/lib/logger.js +2 -23
- package/dist/types/img/get-photon.d.ts +13 -13
- package/dist/types/img/index.d.ts +1 -1
- package/dist/types/img/transform.d.ts +3 -1
- package/dist/types/recorder.d.ts +0 -14
- package/package.json +2 -1
- package/src/img/box-select.ts +2 -92
- package/src/img/get-photon.ts +25 -17
- package/src/img/index.ts +1 -0
- package/src/img/info.ts +1 -21
- package/src/img/transform.ts +96 -113
- package/src/logger.ts +3 -38
- package/src/recorder.ts +0 -15
package/src/img/transform.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import type { PhotonImage as PhotonImageType } from '@silvia-odwyer/photon';
|
|
6
|
+
import type { PhotonImage as PhotonImageType } from '@silvia-odwyer/photon-node';
|
|
7
7
|
import { getDebug } from '../logger';
|
|
8
8
|
import type { Rect } from '../types';
|
|
9
9
|
import { ifInNode } from '../utils';
|
|
@@ -61,36 +61,44 @@ export async function resizeAndConvertImgBuffer(
|
|
|
61
61
|
imgDebug(`resizeImg start, target size: ${newSize.width}x${newSize.height}`);
|
|
62
62
|
|
|
63
63
|
if (ifInNode) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Node.js environment: use Sharp
|
|
65
|
+
try {
|
|
66
|
+
const Sharp = await getSharp();
|
|
67
|
+
const metadata = await Sharp(inputData).metadata();
|
|
68
|
+
const { width: originalWidth, height: originalHeight } = metadata;
|
|
69
|
+
|
|
70
|
+
if (!originalWidth || !originalHeight) {
|
|
71
|
+
throw Error('Undefined width or height from the input image.');
|
|
72
|
+
}
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
if (
|
|
75
|
+
newSize.width === originalWidth &&
|
|
76
|
+
newSize.height === originalHeight
|
|
77
|
+
) {
|
|
78
|
+
return {
|
|
79
|
+
buffer: inputData,
|
|
80
|
+
format: inputFormat,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resizedBuffer = await Sharp(inputData)
|
|
85
|
+
.resize(newSize.width, newSize.height)
|
|
86
|
+
.jpeg({ quality: 90 })
|
|
87
|
+
.toBuffer();
|
|
88
|
+
|
|
89
|
+
const resizeEndTime = Date.now();
|
|
90
|
+
imgDebug(
|
|
91
|
+
`resizeImg done (Sharp), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
92
|
+
);
|
|
71
93
|
|
|
72
|
-
if (newSize.width === originalWidth && newSize.height === originalHeight) {
|
|
73
94
|
return {
|
|
74
|
-
buffer:
|
|
75
|
-
|
|
95
|
+
buffer: resizedBuffer,
|
|
96
|
+
// by Sharp.jpeg()
|
|
97
|
+
format: 'jpeg',
|
|
76
98
|
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
77
101
|
}
|
|
78
|
-
|
|
79
|
-
const resizedBuffer = await Sharp(inputData)
|
|
80
|
-
.resize(newSize.width, newSize.height)
|
|
81
|
-
.jpeg({ quality: 90 })
|
|
82
|
-
.toBuffer();
|
|
83
|
-
|
|
84
|
-
const resizeEndTime = Date.now();
|
|
85
|
-
imgDebug(
|
|
86
|
-
`resizeImg done (Sharp), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
buffer: resizedBuffer,
|
|
91
|
-
// by Sharp.jpeg()
|
|
92
|
-
format: 'jpeg',
|
|
93
|
-
};
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
// browser environment: use Photon (or Canvas fallback)
|
|
@@ -148,6 +156,31 @@ export async function resizeAndConvertImgBuffer(
|
|
|
148
156
|
|
|
149
157
|
export const normalizeBase64Body = (body: string) => body.replace(/\s/g, '');
|
|
150
158
|
|
|
159
|
+
/** Convert an image buffer to JPEG without changing its dimensions. */
|
|
160
|
+
export async function convertImgBufferToJpeg(
|
|
161
|
+
inputData: Buffer,
|
|
162
|
+
quality = 90,
|
|
163
|
+
): Promise<Buffer> {
|
|
164
|
+
if (ifInNode) {
|
|
165
|
+
try {
|
|
166
|
+
const Sharp = await getSharp();
|
|
167
|
+
return await Sharp(inputData).jpeg({ quality }).toBuffer();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const mimeType = detectImageMimeTypeFromBuffer(inputData) ?? 'image/png';
|
|
174
|
+
const photonImage = await photonFromBase64(
|
|
175
|
+
`data:${mimeType};base64,${inputData.toString('base64')}`,
|
|
176
|
+
);
|
|
177
|
+
try {
|
|
178
|
+
return Buffer.from(photonImage.get_bytes_jpeg(quality));
|
|
179
|
+
} finally {
|
|
180
|
+
photonImage.free();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
const base64ImageDataUrlPattern = /^data:image\/[a-zA-Z0-9.+-]+;base64,/i;
|
|
152
185
|
const supportedScreenshotDataUriPattern =
|
|
153
186
|
/^data:image\/(png|jpe?g);base64,([\s\S]*)$/i;
|
|
@@ -377,38 +410,6 @@ export async function paddingToMatchBlockByBase64(
|
|
|
377
410
|
height: number;
|
|
378
411
|
imageBase64: string;
|
|
379
412
|
}> {
|
|
380
|
-
if (ifInNode) {
|
|
381
|
-
const { body } = parseBase64(imageBase64);
|
|
382
|
-
const inputBuffer = Buffer.from(body, 'base64');
|
|
383
|
-
const Sharp = await getSharp();
|
|
384
|
-
const metadata = await Sharp(inputBuffer).metadata();
|
|
385
|
-
const width = metadata.width;
|
|
386
|
-
const height = metadata.height;
|
|
387
|
-
if (!width || !height) {
|
|
388
|
-
throw new Error('Failed to get image dimensions');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const targetWidth = Math.ceil(width / blockSize) * blockSize;
|
|
392
|
-
const targetHeight = Math.ceil(height / blockSize) * blockSize;
|
|
393
|
-
if (targetWidth === width && targetHeight === height) {
|
|
394
|
-
return { width, height, imageBase64 };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const output = await Sharp(inputBuffer)
|
|
398
|
-
.extend({
|
|
399
|
-
right: targetWidth - width,
|
|
400
|
-
bottom: targetHeight - height,
|
|
401
|
-
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
|
402
|
-
})
|
|
403
|
-
.jpeg({ quality: 90 })
|
|
404
|
-
.toBuffer();
|
|
405
|
-
return {
|
|
406
|
-
width: targetWidth,
|
|
407
|
-
height: targetHeight,
|
|
408
|
-
imageBase64: createImgBase64ByFormat('jpeg', output.toString('base64')),
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
413
|
const photonImage = await photonFromBase64(imageBase64);
|
|
413
414
|
try {
|
|
414
415
|
const paddedResult = await paddingToMatchBlock(photonImage, blockSize);
|
|
@@ -434,29 +435,6 @@ export async function cropByRect(
|
|
|
434
435
|
height: number;
|
|
435
436
|
imageBase64: string;
|
|
436
437
|
}> {
|
|
437
|
-
if (ifInNode) {
|
|
438
|
-
const { body } = parseBase64(imageBase64);
|
|
439
|
-
const Sharp = await getSharp();
|
|
440
|
-
const left = Math.trunc(rect.left);
|
|
441
|
-
const top = Math.trunc(rect.top);
|
|
442
|
-
const width = Math.trunc(rect.left + rect.width) - left;
|
|
443
|
-
const height = Math.trunc(rect.top + rect.height) - top;
|
|
444
|
-
const output = await Sharp(Buffer.from(body, 'base64'))
|
|
445
|
-
.extract({
|
|
446
|
-
left,
|
|
447
|
-
top,
|
|
448
|
-
width,
|
|
449
|
-
height,
|
|
450
|
-
})
|
|
451
|
-
.jpeg({ quality: 90 })
|
|
452
|
-
.toBuffer();
|
|
453
|
-
return {
|
|
454
|
-
width,
|
|
455
|
-
height,
|
|
456
|
-
imageBase64: createImgBase64ByFormat('jpeg', output.toString('base64')),
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
438
|
const { crop } = await getPhoton();
|
|
461
439
|
const photonImage = await photonFromBase64(imageBase64);
|
|
462
440
|
const { left, top, width, height } = rect;
|
|
@@ -615,43 +593,48 @@ export async function scaleImage(
|
|
|
615
593
|
imgDebug(`scaleImage start, scale factor: ${scale}`);
|
|
616
594
|
|
|
617
595
|
if (ifInNode) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
596
|
+
// Node.js environment: use Sharp
|
|
597
|
+
try {
|
|
598
|
+
const Sharp = await getSharp();
|
|
599
|
+
const metadata = await Sharp(buffer).metadata();
|
|
600
|
+
const originalWidth = metadata.width || 0;
|
|
601
|
+
const originalHeight = metadata.height || 0;
|
|
602
|
+
|
|
603
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
604
|
+
throw new Error('Failed to get image dimensions');
|
|
605
|
+
}
|
|
626
606
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
607
|
+
const newWidth = Math.round(originalWidth * scale);
|
|
608
|
+
const newHeight = Math.round(originalHeight * scale);
|
|
609
|
+
|
|
610
|
+
const resizedBuffer = await Sharp(buffer)
|
|
611
|
+
.resize(newWidth, newHeight, {
|
|
612
|
+
kernel: 'lanczos3',
|
|
613
|
+
fit: 'fill',
|
|
614
|
+
})
|
|
615
|
+
.jpeg({
|
|
616
|
+
quality: 90,
|
|
617
|
+
})
|
|
618
|
+
.toBuffer();
|
|
619
|
+
|
|
620
|
+
const scaleEndTime = Date.now();
|
|
621
|
+
imgDebug(
|
|
622
|
+
`scaleImage done (Sharp): ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (scale=${scale}), cost: ${scaleEndTime - scaleStartTime}ms`,
|
|
623
|
+
);
|
|
644
624
|
|
|
645
|
-
|
|
625
|
+
const base64 = `data:image/jpeg;base64,${resizedBuffer.toString('base64')}`;
|
|
646
626
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
627
|
+
return {
|
|
628
|
+
width: newWidth,
|
|
629
|
+
height: newHeight,
|
|
630
|
+
imageBase64: base64,
|
|
631
|
+
};
|
|
632
|
+
} catch (error) {
|
|
633
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
634
|
+
}
|
|
652
635
|
}
|
|
653
636
|
|
|
654
|
-
// Browser environment: use Photon (or Canvas fallback)
|
|
637
|
+
// Browser environment or Sharp failed: use Photon (or Canvas fallback)
|
|
655
638
|
const { PhotonImage, SamplingFilter, resize } = await getPhoton();
|
|
656
639
|
const inputBytes = new Uint8Array(buffer);
|
|
657
640
|
// Support both sync (Photon) and async (Canvas fallback) versions
|
package/src/logger.ts
CHANGED
|
@@ -8,53 +8,28 @@ import { ifInNode } from './utils';
|
|
|
8
8
|
const topicPrefix = 'midscene';
|
|
9
9
|
// Map to store file streams
|
|
10
10
|
const logStreams = new Map<string, fs.WriteStream>();
|
|
11
|
-
// A WriteStream queues every write made after it reports backpressure. The
|
|
12
|
-
// main process can keep running while macOS has deprioritized a backgrounded
|
|
13
|
-
// app, so retaining diagnostic logs here can otherwise grow without bound.
|
|
14
|
-
// Drop best-effort logs until the stream drains instead.
|
|
15
|
-
const backpressuredLogStreams = new Set<string>();
|
|
16
|
-
const unavailableLogStreams = new Set<string>();
|
|
17
11
|
// Map to store debug instances
|
|
18
12
|
const debugInstances = new Map<string, DebugFunction>();
|
|
19
13
|
|
|
20
14
|
// Function to get or create a log stream
|
|
21
|
-
function getLogStream(topic: string): fs.WriteStream
|
|
15
|
+
function getLogStream(topic: string): fs.WriteStream {
|
|
22
16
|
const topicFileName = topic.replace(/:/g, '-');
|
|
23
|
-
if (unavailableLogStreams.has(topicFileName)) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
17
|
if (!logStreams.has(topicFileName)) {
|
|
27
18
|
const logFile = path.join(
|
|
28
19
|
getMidsceneRunSubDir('log'),
|
|
29
20
|
`${topicFileName}.log`,
|
|
30
21
|
);
|
|
31
22
|
const stream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
32
|
-
// A stream error without a listener terminates the Electron main process.
|
|
33
|
-
// Logging must remain best-effort, so disable this topic after a file error
|
|
34
|
-
// rather than repeatedly queuing writes to a broken stream.
|
|
35
|
-
stream.on('error', () => {
|
|
36
|
-
unavailableLogStreams.add(topicFileName);
|
|
37
|
-
backpressuredLogStreams.delete(topicFileName);
|
|
38
|
-
if (logStreams.get(topicFileName) === stream) {
|
|
39
|
-
logStreams.delete(topicFileName);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
23
|
logStreams.set(topicFileName, stream);
|
|
43
24
|
}
|
|
44
|
-
return logStreams.get(topicFileName)
|
|
25
|
+
return logStreams.get(topicFileName)!;
|
|
45
26
|
}
|
|
46
27
|
|
|
47
28
|
// Function to write log to file
|
|
48
29
|
function writeLogToFile(topic: string, message: string): void {
|
|
49
30
|
if (!ifInNode) return;
|
|
50
31
|
|
|
51
|
-
const topicFileName = topic.replace(/:/g, '-');
|
|
52
|
-
if (backpressuredLogStreams.has(topicFileName)) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
32
|
const stream = getLogStream(topic);
|
|
57
|
-
if (!stream) return;
|
|
58
33
|
// Generate ISO format timestamp with local timezone
|
|
59
34
|
const now = new Date();
|
|
60
35
|
// Use sv-SE locale to get ISO-like format (YYYY-MM-DD HH:mm:ss)
|
|
@@ -72,17 +47,7 @@ function writeLogToFile(topic: string, message: string): void {
|
|
|
72
47
|
.padStart(2, '0');
|
|
73
48
|
const timezoneString = `${sign}${hours}:${minutes}`;
|
|
74
49
|
const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
|
|
75
|
-
|
|
76
|
-
if (!stream.write(`[${localISOTime}] ${message}\n`)) {
|
|
77
|
-
backpressuredLogStreams.add(topicFileName);
|
|
78
|
-
stream.once('drain', () => {
|
|
79
|
-
backpressuredLogStreams.delete(topicFileName);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
unavailableLogStreams.add(topicFileName);
|
|
84
|
-
backpressuredLogStreams.delete(topicFileName);
|
|
85
|
-
}
|
|
50
|
+
stream.write(`[${localISOTime}] ${message}\n`);
|
|
86
51
|
}
|
|
87
52
|
|
|
88
53
|
export type DebugFunction = (...args: unknown[]) => void;
|
package/src/recorder.ts
CHANGED
|
@@ -34,19 +34,6 @@ export interface MidsceneRecorderPageInfo {
|
|
|
34
34
|
height: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* A screenshot stored outside the recording event payload.
|
|
39
|
-
*
|
|
40
|
-
* Recorder events are persisted in the Studio renderer. Keeping full data
|
|
41
|
-
* URLs there makes long recordings retain every screenshot in the renderer
|
|
42
|
-
* heap, so screenshot bytes live in the Playground run directory instead.
|
|
43
|
-
*/
|
|
44
|
-
export interface MidsceneRecorderScreenshotAssetRef {
|
|
45
|
-
id: string;
|
|
46
|
-
mimeType: string;
|
|
47
|
-
bytes: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
37
|
export interface MidsceneRecorderEvent {
|
|
51
38
|
type: MidsceneRecorderEventType;
|
|
52
39
|
source?: MidsceneRecorderSourceKind;
|
|
@@ -59,8 +46,6 @@ export interface MidsceneRecorderEvent {
|
|
|
59
46
|
pageInfo: MidsceneRecorderPageInfo;
|
|
60
47
|
screenshotBefore?: string;
|
|
61
48
|
screenshotAfter?: string;
|
|
62
|
-
/** The single screenshot retained for AI description and Markdown export. */
|
|
63
|
-
screenshotAsset?: MidsceneRecorderScreenshotAssetRef;
|
|
64
49
|
semantic?: MidsceneRecorderSemantic;
|
|
65
50
|
elementDescription?: string;
|
|
66
51
|
descriptionLoading?: boolean;
|