pi-image-tools 1.0.4 → 1.0.6

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/src/clipboard.ts CHANGED
@@ -1,385 +1,385 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createRequire } from "node:module";
3
-
4
- import type { ClipboardImage, ClipboardModule } from "./types.js";
5
-
6
- const require = createRequire(import.meta.url);
7
-
8
- const LIST_TYPES_TIMEOUT_MS = 1000;
9
- const READ_TIMEOUT_MS = 5000;
10
- const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
11
- const SUPPORTED_IMAGE_MIME_TYPES = [
12
- "image/png",
13
- "image/jpeg",
14
- "image/webp",
15
- "image/gif",
16
- "image/bmp",
17
- ] as const;
18
-
19
- let cachedClipboardModule: ClipboardModule | null | undefined;
20
-
21
- interface CommandResult {
22
- ok: boolean;
23
- stdout: Buffer;
24
- missingCommand: boolean;
25
- }
26
-
27
- interface ClipboardReadResult {
28
- available: boolean;
29
- image: ClipboardImage | null;
30
- }
31
-
32
- function isErrnoException(error: Error): error is NodeJS.ErrnoException {
33
- return "code" in error;
34
- }
35
-
36
- function hasGraphicalSession(platform: NodeJS.Platform, environment: NodeJS.ProcessEnv): boolean {
37
- return platform !== "linux" || Boolean(environment.DISPLAY || environment.WAYLAND_DISPLAY);
38
- }
39
-
40
- function isWaylandSession(environment: NodeJS.ProcessEnv): boolean {
41
- return Boolean(environment.WAYLAND_DISPLAY) || environment.XDG_SESSION_TYPE === "wayland";
42
- }
43
-
44
- function normalizeMimeType(mimeType: string): string {
45
- return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
46
- }
47
-
48
- function selectPreferredImageMimeType(mimeTypes: readonly string[]): string | null {
49
- const normalized = mimeTypes
50
- .map((mimeType) => mimeType.trim())
51
- .filter((mimeType) => mimeType.length > 0)
52
- .map((mimeType) => ({ raw: mimeType, normalized: normalizeMimeType(mimeType) }));
53
-
54
- for (const preferredMimeType of SUPPORTED_IMAGE_MIME_TYPES) {
55
- const match = normalized.find((mimeType) => mimeType.normalized === preferredMimeType);
56
- if (match) {
57
- return match.raw;
58
- }
59
- }
60
-
61
- const firstImage = normalized.find((mimeType) => mimeType.normalized.startsWith("image/"));
62
- return firstImage?.raw ?? null;
63
- }
64
-
65
- function loadClipboardModule(
66
- platform: NodeJS.Platform = process.platform,
67
- environment: NodeJS.ProcessEnv = process.env,
68
- ): ClipboardModule | null {
69
- if (cachedClipboardModule !== undefined) {
70
- return cachedClipboardModule;
71
- }
72
-
73
- if (environment.TERMUX_VERSION || !hasGraphicalSession(platform, environment)) {
74
- cachedClipboardModule = null;
75
- return cachedClipboardModule;
76
- }
77
-
78
- try {
79
- cachedClipboardModule = require("@mariozechner/clipboard") as ClipboardModule;
80
- } catch {
81
- cachedClipboardModule = null;
82
- }
83
-
84
- return cachedClipboardModule;
85
- }
86
-
87
- async function readClipboardImageViaNativeModule(
88
- platform: NodeJS.Platform,
89
- environment: NodeJS.ProcessEnv,
90
- ): Promise<ClipboardReadResult> {
91
- const clipboard = loadClipboardModule(platform, environment);
92
- if (!clipboard) {
93
- return { available: false, image: null };
94
- }
95
-
96
- if (!clipboard.hasImage()) {
97
- return { available: true, image: null };
98
- }
99
-
100
- const imageData = await clipboard.getImageBinary();
101
- if (!imageData || imageData.length === 0) {
102
- return { available: true, image: null };
103
- }
104
-
105
- const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
106
- return {
107
- available: true,
108
- image: {
109
- bytes,
110
- mimeType: "image/png",
111
- },
112
- };
113
- }
114
-
115
- function runCommand(
116
- command: string,
117
- args: string[],
118
- timeout: number,
119
- ): CommandResult {
120
- const result = spawnSync(command, args, {
121
- timeout,
122
- maxBuffer: MAX_BUFFER_BYTES,
123
- });
124
-
125
- if (result.error) {
126
- return {
127
- ok: false,
128
- stdout: Buffer.alloc(0),
129
- missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
130
- };
131
- }
132
-
133
- const stdout = Buffer.isBuffer(result.stdout)
134
- ? result.stdout
135
- : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf8" : undefined);
136
-
137
- return {
138
- ok: result.status === 0,
139
- stdout,
140
- missingCommand: false,
141
- };
142
- }
143
-
144
- function encodePowerShell(script: string): string {
145
- return Buffer.from(script, "utf16le").toString("base64");
146
- }
147
-
148
- function readClipboardImageViaPowerShell(): ClipboardReadResult {
149
- const script = `
150
- $ErrorActionPreference = 'Stop'
151
- Add-Type -AssemblyName System.Windows.Forms
152
- Add-Type -AssemblyName System.Drawing
153
-
154
- if (-not [System.Windows.Forms.Clipboard]::ContainsImage()) {
155
- return
156
- }
157
-
158
- $image = [System.Windows.Forms.Clipboard]::GetImage()
159
- if ($null -eq $image) {
160
- return
161
- }
162
-
163
- $stream = New-Object System.IO.MemoryStream
164
- try {
165
- $image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
166
- [System.Convert]::ToBase64String($stream.ToArray())
167
- } finally {
168
- $stream.Dispose()
169
- $image.Dispose()
170
- }
171
- `;
172
-
173
- const result = spawnSync(
174
- "powershell.exe",
175
- [
176
- "-NoProfile",
177
- "-NonInteractive",
178
- "-ExecutionPolicy",
179
- "Bypass",
180
- "-STA",
181
- "-EncodedCommand",
182
- encodePowerShell(script),
183
- ],
184
- {
185
- encoding: "utf8",
186
- timeout: READ_TIMEOUT_MS,
187
- maxBuffer: MAX_BUFFER_BYTES,
188
- windowsHide: true,
189
- },
190
- );
191
-
192
- if (result.error) {
193
- return {
194
- available: !isErrnoException(result.error) || result.error.code !== "ENOENT",
195
- image: null,
196
- };
197
- }
198
-
199
- if (result.status !== 0) {
200
- return { available: true, image: null };
201
- }
202
-
203
- const base64 = result.stdout.trim();
204
- if (!base64) {
205
- return { available: true, image: null };
206
- }
207
-
208
- try {
209
- const bytes = Buffer.from(base64, "base64");
210
- if (bytes.length === 0) {
211
- return { available: true, image: null };
212
- }
213
-
214
- return {
215
- available: true,
216
- image: {
217
- bytes: new Uint8Array(bytes),
218
- mimeType: "image/png",
219
- },
220
- };
221
- } catch {
222
- return { available: true, image: null };
223
- }
224
- }
225
-
226
- function readClipboardImageViaWlPaste(): ClipboardReadResult {
227
- const listTypes = runCommand("wl-paste", ["--list-types"], LIST_TYPES_TIMEOUT_MS);
228
- if (listTypes.missingCommand) {
229
- return { available: false, image: null };
230
- }
231
-
232
- if (!listTypes.ok) {
233
- return { available: true, image: null };
234
- }
235
-
236
- const mimeTypes = listTypes.stdout
237
- .toString("utf8")
238
- .split(/\r?\n/)
239
- .map((mimeType) => mimeType.trim())
240
- .filter((mimeType) => mimeType.length > 0);
241
-
242
- const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
243
- if (!selectedMimeType) {
244
- return { available: true, image: null };
245
- }
246
-
247
- const imageData = runCommand(
248
- "wl-paste",
249
- ["--type", selectedMimeType, "--no-newline"],
250
- READ_TIMEOUT_MS,
251
- );
252
-
253
- if (!imageData.ok || imageData.stdout.length === 0) {
254
- return { available: true, image: null };
255
- }
256
-
257
- return {
258
- available: true,
259
- image: {
260
- bytes: new Uint8Array(imageData.stdout),
261
- mimeType: normalizeMimeType(selectedMimeType),
262
- },
263
- };
264
- }
265
-
266
- function readClipboardImageViaXclip(): ClipboardReadResult {
267
- const targets = runCommand(
268
- "xclip",
269
- ["-selection", "clipboard", "-t", "TARGETS", "-o"],
270
- LIST_TYPES_TIMEOUT_MS,
271
- );
272
-
273
- if (targets.missingCommand) {
274
- return { available: false, image: null };
275
- }
276
-
277
- const advertisedMimeTypes = targets.ok
278
- ? targets.stdout
279
- .toString("utf8")
280
- .split(/\r?\n/)
281
- .map((mimeType) => mimeType.trim())
282
- .filter((mimeType) => mimeType.length > 0)
283
- : [];
284
-
285
- const preferredMimeType =
286
- advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
287
- const mimeTypesToTry = preferredMimeType
288
- ? [preferredMimeType, ...SUPPORTED_IMAGE_MIME_TYPES]
289
- : [...SUPPORTED_IMAGE_MIME_TYPES];
290
-
291
- for (const mimeType of mimeTypesToTry) {
292
- const imageData = runCommand(
293
- "xclip",
294
- ["-selection", "clipboard", "-t", mimeType, "-o"],
295
- READ_TIMEOUT_MS,
296
- );
297
-
298
- if (imageData.ok && imageData.stdout.length > 0) {
299
- return {
300
- available: true,
301
- image: {
302
- bytes: new Uint8Array(imageData.stdout),
303
- mimeType: normalizeMimeType(mimeType),
304
- },
305
- };
306
- }
307
- }
308
-
309
- return { available: true, image: null };
310
- }
311
-
312
- function getUnavailableReaderMessage(platform: NodeJS.Platform): string {
313
- switch (platform) {
314
- case "linux":
315
- return "No Linux clipboard image reader is available. Install wl-clipboard or xclip, or ensure @mariozechner/clipboard is installed.";
316
- case "darwin":
317
- return "No macOS clipboard image reader is available. Ensure @mariozechner/clipboard is installed.";
318
- case "win32":
319
- return "No Windows clipboard image reader is available. Ensure PowerShell is available or @mariozechner/clipboard is installed.";
320
- default:
321
- return `Clipboard image paste is not supported on platform: ${platform}`;
322
- }
323
- }
324
-
325
- export async function readClipboardImage(options?: {
326
- environment?: NodeJS.ProcessEnv;
327
- platform?: NodeJS.Platform;
328
- }): Promise<ClipboardImage | null> {
329
- const environment = options?.environment ?? process.env;
330
- const platform = options?.platform ?? process.platform;
331
-
332
- if (environment.TERMUX_VERSION) {
333
- return null;
334
- }
335
-
336
- if (!hasGraphicalSession(platform, environment)) {
337
- throw new Error("Clipboard image paste requires a graphical desktop session with DISPLAY or WAYLAND_DISPLAY.");
338
- }
339
-
340
- const readerResults: ClipboardReadResult[] = [];
341
-
342
- const recordResult = (result: ClipboardReadResult): ClipboardImage | null => {
343
- readerResults.push(result);
344
- return result.image;
345
- };
346
-
347
- if (platform === "win32") {
348
- const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
349
- if (nativeImage) {
350
- return nativeImage;
351
- }
352
-
353
- const powerShellImage = recordResult(readClipboardImageViaPowerShell());
354
- if (powerShellImage) {
355
- return powerShellImage;
356
- }
357
- } else if (platform === "linux") {
358
- const sessionReaders = isWaylandSession(environment)
359
- ? [readClipboardImageViaWlPaste, readClipboardImageViaXclip]
360
- : [readClipboardImageViaXclip, readClipboardImageViaWlPaste];
361
-
362
- for (const reader of sessionReaders) {
363
- const image = recordResult(reader());
364
- if (image) {
365
- return image;
366
- }
367
- }
368
-
369
- const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
370
- if (nativeImage) {
371
- return nativeImage;
372
- }
373
- } else {
374
- const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
375
- if (nativeImage) {
376
- return nativeImage;
377
- }
378
- }
379
-
380
- if (readerResults.some((result) => result.available)) {
381
- return null;
382
- }
383
-
384
- throw new Error(getUnavailableReaderMessage(platform));
385
- }
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+
4
+ import type { ClipboardImage, ClipboardModule } from "./types.js";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ const LIST_TYPES_TIMEOUT_MS = 1000;
9
+ const READ_TIMEOUT_MS = 5000;
10
+ const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
11
+ const SUPPORTED_IMAGE_MIME_TYPES = [
12
+ "image/png",
13
+ "image/jpeg",
14
+ "image/webp",
15
+ "image/gif",
16
+ "image/bmp",
17
+ ] as const;
18
+
19
+ let cachedClipboardModule: ClipboardModule | null | undefined;
20
+
21
+ interface CommandResult {
22
+ ok: boolean;
23
+ stdout: Buffer;
24
+ missingCommand: boolean;
25
+ }
26
+
27
+ interface ClipboardReadResult {
28
+ available: boolean;
29
+ image: ClipboardImage | null;
30
+ }
31
+
32
+ function isErrnoException(error: Error): error is NodeJS.ErrnoException {
33
+ return "code" in error;
34
+ }
35
+
36
+ function hasGraphicalSession(platform: NodeJS.Platform, environment: NodeJS.ProcessEnv): boolean {
37
+ return platform !== "linux" || Boolean(environment.DISPLAY || environment.WAYLAND_DISPLAY);
38
+ }
39
+
40
+ function isWaylandSession(environment: NodeJS.ProcessEnv): boolean {
41
+ return Boolean(environment.WAYLAND_DISPLAY) || environment.XDG_SESSION_TYPE === "wayland";
42
+ }
43
+
44
+ function normalizeMimeType(mimeType: string): string {
45
+ return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
46
+ }
47
+
48
+ function selectPreferredImageMimeType(mimeTypes: readonly string[]): string | null {
49
+ const normalized = mimeTypes
50
+ .map((mimeType) => mimeType.trim())
51
+ .filter((mimeType) => mimeType.length > 0)
52
+ .map((mimeType) => ({ raw: mimeType, normalized: normalizeMimeType(mimeType) }));
53
+
54
+ for (const preferredMimeType of SUPPORTED_IMAGE_MIME_TYPES) {
55
+ const match = normalized.find((mimeType) => mimeType.normalized === preferredMimeType);
56
+ if (match) {
57
+ return match.raw;
58
+ }
59
+ }
60
+
61
+ const firstImage = normalized.find((mimeType) => mimeType.normalized.startsWith("image/"));
62
+ return firstImage?.raw ?? null;
63
+ }
64
+
65
+ function loadClipboardModule(
66
+ platform: NodeJS.Platform = process.platform,
67
+ environment: NodeJS.ProcessEnv = process.env,
68
+ ): ClipboardModule | null {
69
+ if (cachedClipboardModule !== undefined) {
70
+ return cachedClipboardModule;
71
+ }
72
+
73
+ if (environment.TERMUX_VERSION || !hasGraphicalSession(platform, environment)) {
74
+ cachedClipboardModule = null;
75
+ return cachedClipboardModule;
76
+ }
77
+
78
+ try {
79
+ cachedClipboardModule = require("@mariozechner/clipboard") as ClipboardModule;
80
+ } catch {
81
+ cachedClipboardModule = null;
82
+ }
83
+
84
+ return cachedClipboardModule;
85
+ }
86
+
87
+ async function readClipboardImageViaNativeModule(
88
+ platform: NodeJS.Platform,
89
+ environment: NodeJS.ProcessEnv,
90
+ ): Promise<ClipboardReadResult> {
91
+ const clipboard = loadClipboardModule(platform, environment);
92
+ if (!clipboard) {
93
+ return { available: false, image: null };
94
+ }
95
+
96
+ if (!clipboard.hasImage()) {
97
+ return { available: true, image: null };
98
+ }
99
+
100
+ const imageData = await clipboard.getImageBinary();
101
+ if (!imageData || imageData.length === 0) {
102
+ return { available: true, image: null };
103
+ }
104
+
105
+ const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
106
+ return {
107
+ available: true,
108
+ image: {
109
+ bytes,
110
+ mimeType: "image/png",
111
+ },
112
+ };
113
+ }
114
+
115
+ function runCommand(
116
+ command: string,
117
+ args: string[],
118
+ timeout: number,
119
+ ): CommandResult {
120
+ const result = spawnSync(command, args, {
121
+ timeout,
122
+ maxBuffer: MAX_BUFFER_BYTES,
123
+ });
124
+
125
+ if (result.error) {
126
+ return {
127
+ ok: false,
128
+ stdout: Buffer.alloc(0),
129
+ missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
130
+ };
131
+ }
132
+
133
+ const stdout = Buffer.isBuffer(result.stdout)
134
+ ? result.stdout
135
+ : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf8" : undefined);
136
+
137
+ return {
138
+ ok: result.status === 0,
139
+ stdout,
140
+ missingCommand: false,
141
+ };
142
+ }
143
+
144
+ function encodePowerShell(script: string): string {
145
+ return Buffer.from(script, "utf16le").toString("base64");
146
+ }
147
+
148
+ function readClipboardImageViaPowerShell(): ClipboardReadResult {
149
+ const script = `
150
+ $ErrorActionPreference = 'Stop'
151
+ Add-Type -AssemblyName System.Windows.Forms
152
+ Add-Type -AssemblyName System.Drawing
153
+
154
+ if (-not [System.Windows.Forms.Clipboard]::ContainsImage()) {
155
+ return
156
+ }
157
+
158
+ $image = [System.Windows.Forms.Clipboard]::GetImage()
159
+ if ($null -eq $image) {
160
+ return
161
+ }
162
+
163
+ $stream = New-Object System.IO.MemoryStream
164
+ try {
165
+ $image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
166
+ [System.Convert]::ToBase64String($stream.ToArray())
167
+ } finally {
168
+ $stream.Dispose()
169
+ $image.Dispose()
170
+ }
171
+ `;
172
+
173
+ const result = spawnSync(
174
+ "powershell.exe",
175
+ [
176
+ "-NoProfile",
177
+ "-NonInteractive",
178
+ "-ExecutionPolicy",
179
+ "Bypass",
180
+ "-STA",
181
+ "-EncodedCommand",
182
+ encodePowerShell(script),
183
+ ],
184
+ {
185
+ encoding: "utf8",
186
+ timeout: READ_TIMEOUT_MS,
187
+ maxBuffer: MAX_BUFFER_BYTES,
188
+ windowsHide: true,
189
+ },
190
+ );
191
+
192
+ if (result.error) {
193
+ return {
194
+ available: !isErrnoException(result.error) || result.error.code !== "ENOENT",
195
+ image: null,
196
+ };
197
+ }
198
+
199
+ if (result.status !== 0) {
200
+ return { available: true, image: null };
201
+ }
202
+
203
+ const base64 = result.stdout.trim();
204
+ if (!base64) {
205
+ return { available: true, image: null };
206
+ }
207
+
208
+ try {
209
+ const bytes = Buffer.from(base64, "base64");
210
+ if (bytes.length === 0) {
211
+ return { available: true, image: null };
212
+ }
213
+
214
+ return {
215
+ available: true,
216
+ image: {
217
+ bytes: new Uint8Array(bytes),
218
+ mimeType: "image/png",
219
+ },
220
+ };
221
+ } catch {
222
+ return { available: true, image: null };
223
+ }
224
+ }
225
+
226
+ function readClipboardImageViaWlPaste(): ClipboardReadResult {
227
+ const listTypes = runCommand("wl-paste", ["--list-types"], LIST_TYPES_TIMEOUT_MS);
228
+ if (listTypes.missingCommand) {
229
+ return { available: false, image: null };
230
+ }
231
+
232
+ if (!listTypes.ok) {
233
+ return { available: true, image: null };
234
+ }
235
+
236
+ const mimeTypes = listTypes.stdout
237
+ .toString("utf8")
238
+ .split(/\r?\n/)
239
+ .map((mimeType) => mimeType.trim())
240
+ .filter((mimeType) => mimeType.length > 0);
241
+
242
+ const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
243
+ if (!selectedMimeType) {
244
+ return { available: true, image: null };
245
+ }
246
+
247
+ const imageData = runCommand(
248
+ "wl-paste",
249
+ ["--type", selectedMimeType, "--no-newline"],
250
+ READ_TIMEOUT_MS,
251
+ );
252
+
253
+ if (!imageData.ok || imageData.stdout.length === 0) {
254
+ return { available: true, image: null };
255
+ }
256
+
257
+ return {
258
+ available: true,
259
+ image: {
260
+ bytes: new Uint8Array(imageData.stdout),
261
+ mimeType: normalizeMimeType(selectedMimeType),
262
+ },
263
+ };
264
+ }
265
+
266
+ function readClipboardImageViaXclip(): ClipboardReadResult {
267
+ const targets = runCommand(
268
+ "xclip",
269
+ ["-selection", "clipboard", "-t", "TARGETS", "-o"],
270
+ LIST_TYPES_TIMEOUT_MS,
271
+ );
272
+
273
+ if (targets.missingCommand) {
274
+ return { available: false, image: null };
275
+ }
276
+
277
+ const advertisedMimeTypes = targets.ok
278
+ ? targets.stdout
279
+ .toString("utf8")
280
+ .split(/\r?\n/)
281
+ .map((mimeType) => mimeType.trim())
282
+ .filter((mimeType) => mimeType.length > 0)
283
+ : [];
284
+
285
+ const preferredMimeType =
286
+ advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
287
+ const mimeTypesToTry = preferredMimeType
288
+ ? [preferredMimeType, ...SUPPORTED_IMAGE_MIME_TYPES]
289
+ : [...SUPPORTED_IMAGE_MIME_TYPES];
290
+
291
+ for (const mimeType of mimeTypesToTry) {
292
+ const imageData = runCommand(
293
+ "xclip",
294
+ ["-selection", "clipboard", "-t", mimeType, "-o"],
295
+ READ_TIMEOUT_MS,
296
+ );
297
+
298
+ if (imageData.ok && imageData.stdout.length > 0) {
299
+ return {
300
+ available: true,
301
+ image: {
302
+ bytes: new Uint8Array(imageData.stdout),
303
+ mimeType: normalizeMimeType(mimeType),
304
+ },
305
+ };
306
+ }
307
+ }
308
+
309
+ return { available: true, image: null };
310
+ }
311
+
312
+ function getUnavailableReaderMessage(platform: NodeJS.Platform): string {
313
+ switch (platform) {
314
+ case "linux":
315
+ return "No Linux clipboard image reader is available. Install wl-clipboard or xclip, or ensure @mariozechner/clipboard is installed.";
316
+ case "darwin":
317
+ return "No macOS clipboard image reader is available. Ensure @mariozechner/clipboard is installed.";
318
+ case "win32":
319
+ return "No Windows clipboard image reader is available. Ensure PowerShell is available or @mariozechner/clipboard is installed.";
320
+ default:
321
+ return `Clipboard image paste is not supported on platform: ${platform}`;
322
+ }
323
+ }
324
+
325
+ export async function readClipboardImage(options?: {
326
+ environment?: NodeJS.ProcessEnv;
327
+ platform?: NodeJS.Platform;
328
+ }): Promise<ClipboardImage | null> {
329
+ const environment = options?.environment ?? process.env;
330
+ const platform = options?.platform ?? process.platform;
331
+
332
+ if (environment.TERMUX_VERSION) {
333
+ return null;
334
+ }
335
+
336
+ if (!hasGraphicalSession(platform, environment)) {
337
+ throw new Error("Clipboard image paste requires a graphical desktop session with DISPLAY or WAYLAND_DISPLAY.");
338
+ }
339
+
340
+ const readerResults: ClipboardReadResult[] = [];
341
+
342
+ const recordResult = (result: ClipboardReadResult): ClipboardImage | null => {
343
+ readerResults.push(result);
344
+ return result.image;
345
+ };
346
+
347
+ if (platform === "win32") {
348
+ const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
349
+ if (nativeImage) {
350
+ return nativeImage;
351
+ }
352
+
353
+ const powerShellImage = recordResult(readClipboardImageViaPowerShell());
354
+ if (powerShellImage) {
355
+ return powerShellImage;
356
+ }
357
+ } else if (platform === "linux") {
358
+ const sessionReaders = isWaylandSession(environment)
359
+ ? [readClipboardImageViaWlPaste, readClipboardImageViaXclip]
360
+ : [readClipboardImageViaXclip, readClipboardImageViaWlPaste];
361
+
362
+ for (const reader of sessionReaders) {
363
+ const image = recordResult(reader());
364
+ if (image) {
365
+ return image;
366
+ }
367
+ }
368
+
369
+ const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
370
+ if (nativeImage) {
371
+ return nativeImage;
372
+ }
373
+ } else {
374
+ const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
375
+ if (nativeImage) {
376
+ return nativeImage;
377
+ }
378
+ }
379
+
380
+ if (readerResults.some((result) => result.available)) {
381
+ return null;
382
+ }
383
+
384
+ throw new Error(getUnavailableReaderMessage(platform));
385
+ }