@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.
@@ -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
- const Sharp = await getSharp();
65
- const metadata = await Sharp(inputData).metadata();
66
- const { width: originalWidth, height: originalHeight } = metadata;
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
- if (!originalWidth || !originalHeight) {
69
- throw Error('Undefined width or height from the input image.');
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: inputData,
75
- format: inputFormat,
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
- const Sharp = await getSharp();
619
- const metadata = await Sharp(buffer).metadata();
620
- const originalWidth = metadata.width || 0;
621
- const originalHeight = metadata.height || 0;
622
-
623
- if (originalWidth === 0 || originalHeight === 0) {
624
- throw new Error('Failed to get image dimensions');
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
- const newWidth = Math.round(originalWidth * scale);
628
- const newHeight = Math.round(originalHeight * scale);
629
-
630
- const resizedBuffer = await Sharp(buffer)
631
- .resize(newWidth, newHeight, {
632
- kernel: 'lanczos3',
633
- fit: 'fill',
634
- })
635
- .jpeg({
636
- quality: 90,
637
- })
638
- .toBuffer();
639
-
640
- const scaleEndTime = Date.now();
641
- imgDebug(
642
- `scaleImage done (Sharp): ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (scale=${scale}), cost: ${scaleEndTime - scaleStartTime}ms`,
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
- const base64 = `data:image/jpeg;base64,${resizedBuffer.toString('base64')}`;
625
+ const base64 = `data:image/jpeg;base64,${resizedBuffer.toString('base64')}`;
646
626
 
647
- return {
648
- width: newWidth,
649
- height: newHeight,
650
- imageBase64: base64,
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 | null {
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) ?? null;
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
- try {
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;