@ps-generator-bridge/sdk 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,1131 @@
1
+ 'use strict';
2
+
3
+ // src/protocol.ts
4
+ var PROTOCOL_VERSION = 1;
5
+ var ProtocolMethod = {
6
+ GetServerInfo: "getServerInfo",
7
+ JsxRun: "jsx:run",
8
+ JsxExecute: "jsx:execute",
9
+ EventSubscribe: "event:subscribe",
10
+ EventUnsubscribe: "event:unsubscribe",
11
+ ActionAutoCutout: "action:autoCutout",
12
+ ActionRemoveBackground: "action:removeBackground",
13
+ LayerGetInfo: "layer:getInfo",
14
+ LayerGetInfoById: "layer:getInfoById",
15
+ LayerGetInfoByIndex: "layer:getInfoByIndex",
16
+ DocumentCurrent: "document:current",
17
+ DocumentExport: "document:export",
18
+ DocumentSave: "document:save",
19
+ ImageExportLayer: "image:exportLayer",
20
+ ImageGetPreview: "image:getPreview",
21
+ ImageExportDocument: "image:exportDocument"
22
+ };
23
+ var ErrorCode = {
24
+ UnknownMethod: "UNKNOWN_METHOD",
25
+ BadRequest: "BAD_REQUEST",
26
+ Internal: "INTERNAL"
27
+ };
28
+ function isRequest(value) {
29
+ if (typeof value !== "object" || value === null) return false;
30
+ const v = value;
31
+ return typeof v.id === "string" && typeof v.method === "string";
32
+ }
33
+ function isResponse(value) {
34
+ if (typeof value !== "object" || value === null) return false;
35
+ const v = value;
36
+ return typeof v.id === "string" && typeof v.ok === "boolean";
37
+ }
38
+ function isEvent(value) {
39
+ if (typeof value !== "object" || value === null) return false;
40
+ const v = value;
41
+ return typeof v.type === "string" && v.id === void 0;
42
+ }
43
+ function parseFrame(data) {
44
+ return JSON.parse(data);
45
+ }
46
+ function serializeFrame(value) {
47
+ return JSON.stringify(value);
48
+ }
49
+
50
+ // src/requestTracker.ts
51
+ var requestCounter = 0;
52
+ function nextId() {
53
+ requestCounter += 1;
54
+ return `req-${requestCounter}`;
55
+ }
56
+ var RequestTracker = class {
57
+ constructor(timeoutMs) {
58
+ this.timeoutMs = timeoutMs;
59
+ this.pending = /* @__PURE__ */ new Map();
60
+ }
61
+ send(transport, method, params) {
62
+ const id = nextId();
63
+ const envelope = { id, method, params };
64
+ return new Promise((resolve, reject) => {
65
+ const timer = setTimeout(() => {
66
+ this.pending.delete(id);
67
+ reject(new Error(`Request "${method}" timed out after ${this.timeoutMs}ms`));
68
+ }, this.timeoutMs);
69
+ this.pending.set(id, {
70
+ resolve,
71
+ reject,
72
+ timer
73
+ });
74
+ transport?.send(serializeFrame(envelope));
75
+ });
76
+ }
77
+ settleFrame(data) {
78
+ let message;
79
+ try {
80
+ message = parseFrame(data);
81
+ } catch {
82
+ return false;
83
+ }
84
+ return this.settle(message);
85
+ }
86
+ settle(message) {
87
+ if (!isResponse(message)) return false;
88
+ this.settleResponse(message);
89
+ return true;
90
+ }
91
+ failAll(error) {
92
+ for (const pending of this.pending.values()) {
93
+ clearTimeout(pending.timer);
94
+ pending.reject(error);
95
+ }
96
+ this.pending.clear();
97
+ }
98
+ settleResponse(message) {
99
+ const pending = this.pending.get(message.id);
100
+ if (!pending) return;
101
+ this.pending.delete(message.id);
102
+ clearTimeout(pending.timer);
103
+ if (message.ok) {
104
+ pending.resolve(message.result);
105
+ } else {
106
+ pending.reject(new Error(`${message.error.code}: ${message.error.message}`));
107
+ }
108
+ }
109
+ };
110
+
111
+ // src/transport.ts
112
+ function createWebSocketTransport(url, WebSocketImpl) {
113
+ const Ctor = WebSocketImpl ?? resolveGlobalWebSocket();
114
+ const ws = new Ctor(url);
115
+ let opened = false;
116
+ let resolveReady;
117
+ let rejectReady;
118
+ const readyPromise = new Promise((resolve, reject) => {
119
+ resolveReady = resolve;
120
+ rejectReady = reject;
121
+ });
122
+ let closeListener;
123
+ ws.addEventListener("open", () => {
124
+ opened = true;
125
+ resolveReady();
126
+ });
127
+ ws.addEventListener("error", () => {
128
+ if (!opened) rejectReady(new Error(`WebSocket connection to ${url} failed`));
129
+ });
130
+ ws.addEventListener("close", () => closeListener?.());
131
+ return {
132
+ ready: () => readyPromise,
133
+ send: (data) => ws.send(data),
134
+ onMessage: (listener) => ws.addEventListener("message", (event) => listener(String(event.data))),
135
+ onClose: (listener) => {
136
+ closeListener = listener;
137
+ },
138
+ close: () => ws.close()
139
+ };
140
+ }
141
+ function resolveGlobalWebSocket() {
142
+ const candidate = globalThis.WebSocket;
143
+ if (!candidate) {
144
+ throw new Error(
145
+ "No global WebSocket is available (Node 18-21 lack it). Upgrade to Node 22+, or inject a transport, e.g. `new PsBridgeClient({ url, WebSocket: (await import('ws')).WebSocket })`."
146
+ );
147
+ }
148
+ return candidate;
149
+ }
150
+
151
+ // src/connection.ts
152
+ var RawConnection = class {
153
+ constructor(options) {
154
+ this.options = options;
155
+ this.listeners = /* @__PURE__ */ new Map();
156
+ this.attempts = 0;
157
+ this.state = "connecting";
158
+ this.readyWaiters = [];
159
+ this.maxRetries = options.maxRetries ?? 5;
160
+ this.retryDelayMs = options.retryDelayMs ?? 5e3;
161
+ this.requests = new RequestTracker(options.timeoutMs ?? 1e4);
162
+ this.clientId = options.clientId;
163
+ this.connect();
164
+ }
165
+ /** The clientId assigned by the server (undefined until the first handshake). */
166
+ get id() {
167
+ return this.clientId;
168
+ }
169
+ /** Resolves once connected (handshake received); rejects if retries are exhausted. */
170
+ ready() {
171
+ if (this.state === "ready") return Promise.resolve();
172
+ if (this.state === "failed") return Promise.reject(this.failError);
173
+ return new Promise((resolve, reject) => {
174
+ this.readyWaiters.push({ resolve, reject });
175
+ });
176
+ }
177
+ /** Fetch the server's identity + (when connected to PS) its Photoshop version. */
178
+ getServerInfo() {
179
+ return this.invoke("getServerInfo", {});
180
+ }
181
+ async invoke(method, params) {
182
+ await this.ready();
183
+ return this.requests.send(this.transport, method, params);
184
+ }
185
+ on(type, listener) {
186
+ let set = this.listeners.get(type);
187
+ if (!set) {
188
+ set = /* @__PURE__ */ new Set();
189
+ this.listeners.set(type, set);
190
+ }
191
+ set.add(listener);
192
+ }
193
+ /** Unsubscribe a previously registered Event listener. */
194
+ off(type, listener) {
195
+ this.listeners.get(type)?.delete(listener);
196
+ }
197
+ /** Close the connection: stop reconnecting and reject all in-flight work. */
198
+ close() {
199
+ if (this.state === "failed") return;
200
+ if (this.retryTimer) clearTimeout(this.retryTimer);
201
+ this.fail(new Error("Connection closed"));
202
+ this.transport?.close();
203
+ }
204
+ connect() {
205
+ if (this.state === "failed") return;
206
+ const url = this.buildUrl();
207
+ const transport = this.options.transportFactory ? this.options.transportFactory(url) : createWebSocketTransport(url, this.options.WebSocket);
208
+ this.transport = transport;
209
+ let settled = false;
210
+ const onDead = () => {
211
+ if (settled) return;
212
+ settled = true;
213
+ if (this.transport === transport) this.handleDrop();
214
+ };
215
+ transport.onMessage((data) => this.handleMessage(data));
216
+ transport.onClose(onDead);
217
+ transport.ready().then(() => void 0, onDead);
218
+ }
219
+ buildUrl() {
220
+ if (!this.clientId) return this.options.url;
221
+ const sep = this.options.url.includes("?") ? "&" : "?";
222
+ return `${this.options.url}${sep}id=${encodeURIComponent(this.clientId)}`;
223
+ }
224
+ handleMessage(data) {
225
+ let message;
226
+ try {
227
+ message = parseFrame(data);
228
+ } catch {
229
+ return;
230
+ }
231
+ if (isEvent(message)) {
232
+ if (message.type === "connected") {
233
+ this.clientId = message.data.clientId;
234
+ this.attempts = 0;
235
+ this.markReady();
236
+ }
237
+ this.dispatchEvent(message.type, message.data);
238
+ return;
239
+ }
240
+ this.requests.settle(message);
241
+ }
242
+ handleDrop() {
243
+ if (this.state === "failed") return;
244
+ this.transport = void 0;
245
+ this.state = "connecting";
246
+ if (this.attempts >= this.maxRetries) {
247
+ this.fail(
248
+ new Error(`Connection to ${this.options.url} failed after ${this.maxRetries} retries`)
249
+ );
250
+ return;
251
+ }
252
+ this.attempts += 1;
253
+ this.retryTimer = setTimeout(() => this.connect(), this.retryDelayMs);
254
+ }
255
+ markReady() {
256
+ this.state = "ready";
257
+ const waiters = this.readyWaiters.splice(0);
258
+ for (const waiter of waiters) waiter.resolve();
259
+ }
260
+ fail(error) {
261
+ this.state = "failed";
262
+ this.failError = error;
263
+ const waiters = this.readyWaiters.splice(0);
264
+ for (const waiter of waiters) waiter.reject(error);
265
+ this.requests.failAll(error);
266
+ }
267
+ dispatchEvent(type, data) {
268
+ const set = this.listeners.get(type);
269
+ if (!set) return;
270
+ for (const listener of set) listener(data);
271
+ }
272
+ };
273
+
274
+ // src/photoshop/jsx-runner.ts
275
+ function evalJson(jsx, expr) {
276
+ return jsx.run(`JSON.stringify(${expr})`).then((s) => JSON.parse(s));
277
+ }
278
+ function evalNumber(jsx, expr) {
279
+ return evalJson(jsx, `Number(${expr})`);
280
+ }
281
+ function evalString(jsx, expr) {
282
+ return evalJson(jsx, `String(${expr})`);
283
+ }
284
+ function evalBool(jsx, expr) {
285
+ return evalJson(jsx, `Boolean(${expr})`);
286
+ }
287
+
288
+ // src/photoshop/JsxBuilder.ts
289
+ var JsxBuilder = class {
290
+ /**
291
+ * Escape a string into a JSX string literal. `JSON.stringify` handles quotes,
292
+ * newlines and Unicode.
293
+ *
294
+ * @example JsxBuilder.string("O'Brien") // -> "\"O'Brien\""
295
+ */
296
+ static string(value) {
297
+ return JSON.stringify(value);
298
+ }
299
+ /**
300
+ * Serialize a path to an ExtendScript `File` constructor.
301
+ *
302
+ * @example JsxBuilder.file("/path/to/file.psd") // -> 'new File("/path/to/file.psd")'
303
+ */
304
+ static file(path) {
305
+ return `new File(${JSON.stringify(path)})`;
306
+ }
307
+ /** Serialize a number, rejecting NaN/Infinity. */
308
+ static number(value) {
309
+ if (!isFinite(value)) {
310
+ throw new Error(`JsxBuilder.number: invalid value ${value}`);
311
+ }
312
+ return String(value);
313
+ }
314
+ /** Serialize a boolean. */
315
+ static boolean(value) {
316
+ return value ? "true" : "false";
317
+ }
318
+ /** Pass an enum string through (already in `EnumName.MEMBER` form). */
319
+ static enum_(value) {
320
+ return value;
321
+ }
322
+ /**
323
+ * Serialize a numeric array to a JSX array literal (bounds, crop, etc.).
324
+ *
325
+ * @example JsxBuilder.numberArray([0, 0, 100, 100]) // -> '[0,0,100,100]'
326
+ */
327
+ static numberArray(arr) {
328
+ return JSON.stringify(arr);
329
+ }
330
+ /** Serialize a 2-D numeric array (selection boundary points). */
331
+ static regionArray(region) {
332
+ return JSON.stringify(region);
333
+ }
334
+ /**
335
+ * Build a method-call expression from pre-serialized args.
336
+ *
337
+ * @example JsxBuilder.call("app.open", [JsxBuilder.file(path)])
338
+ * // -> 'app.open(new File("/path/to/file.psd"))'
339
+ */
340
+ static call(path, args) {
341
+ return `${path}(${args.join(", ")})`;
342
+ }
343
+ /**
344
+ * Build a property assignment statement.
345
+ *
346
+ * @example JsxBuilder.assign("app.activeDocument.activeLayer.name", JsxBuilder.string("New Name"))
347
+ * // -> 'app.activeDocument.activeLayer.name = "New Name"'
348
+ */
349
+ static assign(path, value) {
350
+ return `${path} = ${value}`;
351
+ }
352
+ };
353
+
354
+ // src/photoshop/PhotoshopLayer.ts
355
+ var _PhotoshopLayer = class _PhotoshopLayer {
356
+ constructor(_jsx, _path) {
357
+ this._jsx = _jsx;
358
+ this._path = _path;
359
+ }
360
+ // --- Layer read-only properties -----------------------------------------
361
+ /** Unique layer id. */
362
+ get id() {
363
+ return evalNumber(this._jsx, `${this._path}.id`);
364
+ }
365
+ /** Layer name. */
366
+ get name() {
367
+ return evalString(this._jsx, `${this._path}.name`);
368
+ }
369
+ /** Layer visibility. */
370
+ get visible() {
371
+ return evalBool(this._jsx, `${this._path}.visible`);
372
+ }
373
+ /** Layer opacity (0-100). */
374
+ get opacity() {
375
+ return evalNumber(this._jsx, `${this._path}.opacity`);
376
+ }
377
+ /**
378
+ * Blend mode as an enum-name string (e.g. "BlendMode.NORMAL"). ExtendScript
379
+ * yields the numeric code; the static map turns it into a readable name.
380
+ */
381
+ get blendMode() {
382
+ return evalNumber(this._jsx, `${this._path}.blendMode`).then(
383
+ (code) => _PhotoshopLayer._BLEND_MODE_MAP[code] ?? `BlendMode.UNKNOWN_${code}`
384
+ );
385
+ }
386
+ /** Whether the layer is fully locked. */
387
+ get allLocked() {
388
+ return evalBool(this._jsx, `${this._path}.allLocked`);
389
+ }
390
+ /**
391
+ * Layer bounds `[left, top, right, bottom]`.
392
+ *
393
+ * @remarks Units follow `rulerUnits`; values are not pixels unless it is
394
+ * `Units.PIXELS`.
395
+ */
396
+ get bounds() {
397
+ const expr = `(function(){ var b = ${this._path}.bounds; return [b[0], b[1], b[2], b[3]]; })()`;
398
+ return evalJson(this._jsx, expr);
399
+ }
400
+ /**
401
+ * Layer kind as an enum-name string (e.g. "LayerKind.NORMAL"). ArtLayer only;
402
+ * reading it on a LayerSet throws. Check `typename` first.
403
+ *
404
+ * @remarks GRADIENTFILL=4 and PATTERNFILL=4 collide in Adobe's enums, so a
405
+ * kind of 4 always maps to GRADIENTFILL.
406
+ */
407
+ get kind() {
408
+ return evalNumber(this._jsx, `${this._path}.kind`).then(
409
+ (code) => _PhotoshopLayer._LAYER_KIND_MAP[code] ?? `LayerKind.UNKNOWN_${code}`
410
+ );
411
+ }
412
+ /** Object type name ("ArtLayer" or "LayerSet"). */
413
+ get typename() {
414
+ return evalString(this._jsx, `${this._path}.typename`);
415
+ }
416
+ // --- Property writes -----------------------------------------------------
417
+ /** Set the layer name. */
418
+ async setName(value) {
419
+ await this._jsx.run(JsxBuilder.assign(`${this._path}.name`, JsxBuilder.string(value)));
420
+ }
421
+ /** Set layer visibility. */
422
+ async setVisible(value) {
423
+ await this._jsx.run(JsxBuilder.assign(`${this._path}.visible`, JsxBuilder.boolean(value)));
424
+ }
425
+ /** Set layer opacity (0-100). */
426
+ async setOpacity(value) {
427
+ await this._jsx.run(JsxBuilder.assign(`${this._path}.opacity`, JsxBuilder.number(value)));
428
+ }
429
+ /**
430
+ * Set the blend mode.
431
+ *
432
+ * @example
433
+ * import { BlendMode } from "@ps-generator-bridge/sdk/plugin";
434
+ * await layer.setBlendMode(BlendMode.MULTIPLY);
435
+ */
436
+ async setBlendMode(value) {
437
+ await this._jsx.run(JsxBuilder.assign(`${this._path}.blendMode`, JsxBuilder.enum_(value)));
438
+ }
439
+ /** Set whether the layer is fully locked. */
440
+ async setAllLocked(value) {
441
+ await this._jsx.run(JsxBuilder.assign(`${this._path}.allLocked`, JsxBuilder.boolean(value)));
442
+ }
443
+ // --- Methods -------------------------------------------------------------
444
+ /** Delete this layer. */
445
+ async remove() {
446
+ await this._jsx.run(`${this._path}.remove()`);
447
+ }
448
+ /** Duplicate this layer (the copy becomes `activeLayer`). */
449
+ async duplicate() {
450
+ await this._jsx.run(`${this._path}.duplicate()`);
451
+ return new _PhotoshopLayer(this._jsx, "app.activeDocument.activeLayer");
452
+ }
453
+ /**
454
+ * Move this layer relative to another.
455
+ *
456
+ * @param relativeObjectJsxPath JSX path of the reference layer (e.g.
457
+ * "app.activeDocument.layers[0]").
458
+ * @param insertionLocation placement enum.
459
+ *
460
+ * @remarks Pass a bare JSX path expression. A `PhotoshopLayers.getByName()`
461
+ * path contains quotes and cannot be used as a reference expression here.
462
+ *
463
+ * @example
464
+ * import { ElementPlacement } from "@ps-generator-bridge/sdk/plugin";
465
+ * await layer.move("app.activeDocument.layers[0]", ElementPlacement.PLACEBEFORE);
466
+ */
467
+ async move(relativeObjectJsxPath, insertionLocation) {
468
+ await this._jsx.run(
469
+ `${this._path}.move(${relativeObjectJsxPath}, ${JsxBuilder.enum_(insertionLocation)})`
470
+ );
471
+ }
472
+ /** Translate the layer by a pixel delta. */
473
+ async translate(deltaX, deltaY) {
474
+ await this._jsx.run(
475
+ JsxBuilder.call(`${this._path}.translate`, [
476
+ JsxBuilder.number(deltaX),
477
+ JsxBuilder.number(deltaY)
478
+ ])
479
+ );
480
+ }
481
+ /**
482
+ * Scale the layer.
483
+ *
484
+ * @param horizontal horizontal scale percent (150 = 150%).
485
+ * @param vertical vertical scale percent.
486
+ * @param anchor scaling anchor (optional).
487
+ */
488
+ async resize(horizontal, vertical, anchor) {
489
+ const args = [JsxBuilder.number(horizontal), JsxBuilder.number(vertical)];
490
+ if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
491
+ await this._jsx.run(JsxBuilder.call(`${this._path}.resize`, args));
492
+ }
493
+ /**
494
+ * Rotate the layer.
495
+ *
496
+ * @param angle degrees, clockwise positive.
497
+ * @param anchor rotation anchor (optional).
498
+ */
499
+ async rotate(angle, anchor) {
500
+ const args = [JsxBuilder.number(angle)];
501
+ if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
502
+ await this._jsx.run(JsxBuilder.call(`${this._path}.rotate`, args));
503
+ }
504
+ /** Move the layer to the end of its stack. */
505
+ async moveToEnd() {
506
+ await this._jsx.run(`${this._path}.moveToEnd()`);
507
+ }
508
+ };
509
+ /** BlendMode code -> enum-name map (all 27 members, plus newer ones). */
510
+ _PhotoshopLayer._BLEND_MODE_MAP = {
511
+ 1: "PASSTHROUGH",
512
+ 2: "NORMAL",
513
+ 3: "DISSOLVE",
514
+ 4: "DARKEN",
515
+ 5: "MULTIPLY",
516
+ 6: "COLORBURN",
517
+ 7: "LINEARBURN",
518
+ 8: "LIGHTEN",
519
+ 9: "SCREEN",
520
+ 10: "COLORDODGE",
521
+ 11: "LINEARDODGE",
522
+ 12: "OVERLAY",
523
+ 13: "SOFTLIGHT",
524
+ 14: "HARDLIGHT",
525
+ 15: "VIVIDLIGHT",
526
+ 16: "LINEARLIGHT",
527
+ 17: "PINLIGHT",
528
+ 18: "DIFFERENCE",
529
+ 19: "EXCLUSION",
530
+ 20: "HUE",
531
+ 21: "SATURATION",
532
+ 22: "COLORBLEND",
533
+ 23: "LUMINOSITY",
534
+ 26: "HARDMIX",
535
+ 27: "SUBTRACT",
536
+ 28: "DARKERCOLOR",
537
+ 29: "LIGHTERCOLOR",
538
+ 30: "DIVIDE"
539
+ };
540
+ // --- ArtLayer-only properties -------------------------------------------
541
+ /** LayerKind code -> enum-name map. */
542
+ _PhotoshopLayer._LAYER_KIND_MAP = {
543
+ 1: "NORMAL",
544
+ 2: "TEXT",
545
+ 3: "SOLIDFILL",
546
+ 4: "GRADIENTFILL",
547
+ 5: "LEVELS",
548
+ 6: "CURVES",
549
+ 7: "COLORBALANCE",
550
+ 8: "HUESATURATION",
551
+ 9: "BRIGHTNESSCONTRAST",
552
+ 10: "THRESHOLD",
553
+ 11: "POSTERIZE",
554
+ 12: "CHANNELMIXER",
555
+ 13: "GRADIENTMAP",
556
+ 14: "INVERSION",
557
+ 15: "EXPOSURE",
558
+ 16: "PHOTOFILTER",
559
+ 17: "SELECTIVECOLOR",
560
+ 18: "SMARTOBJECT",
561
+ 20: "VIBRANCE",
562
+ 21: "VIDEO",
563
+ 22: "BLACKANDWHITE",
564
+ 23: "LAYER3D",
565
+ 26: "COLORLOOKUP"
566
+ };
567
+ var PhotoshopLayer = _PhotoshopLayer;
568
+
569
+ // src/photoshop/PhotoshopLayers.ts
570
+ var PhotoshopLayers = class {
571
+ constructor(_jsx, _path) {
572
+ this._jsx = _jsx;
573
+ this._path = _path;
574
+ }
575
+ /** Number of layers in the collection. */
576
+ get length() {
577
+ return evalNumber(this._jsx, `${this._path}.length`);
578
+ }
579
+ /**
580
+ * Access a layer by index. The collection is 0-based, matching JavaScript;
581
+ * `layers[0]` is the top-most layer.
582
+ */
583
+ at(index) {
584
+ return new PhotoshopLayer(this._jsx, `${this._path}[${index}]`);
585
+ }
586
+ /**
587
+ * Look up a layer by name (case-sensitive). The returned wrapper works for
588
+ * property reads/writes but its path contains a `getByName(...)` call, so it
589
+ * must not be passed as `PhotoshopLayer.move()`'s reference path.
590
+ */
591
+ getByName(name) {
592
+ const escapedName = JsxBuilder.string(name);
593
+ return new PhotoshopLayer(this._jsx, `${this._path}.getByName(${escapedName})`);
594
+ }
595
+ };
596
+
597
+ // src/photoshop/PhotoshopSelection.ts
598
+ var PhotoshopSelection = class {
599
+ constructor(_jsx, _path) {
600
+ this._jsx = _jsx;
601
+ this._path = _path;
602
+ }
603
+ /**
604
+ * Selection bounds `[left, top, right, bottom]`. Throws when there is no
605
+ * selection.
606
+ *
607
+ * @remarks Units follow `rulerUnits`.
608
+ */
609
+ get bounds() {
610
+ const expr = `(function(){ var b = ${this._path}.bounds; return [b[0], b[1], b[2], b[3]]; })()`;
611
+ return evalJson(this._jsx, expr);
612
+ }
613
+ /** Whether the selection is a solid (un-feathered) rectangle. */
614
+ get solid() {
615
+ return evalBool(this._jsx, `${this._path}.solid`);
616
+ }
617
+ // --- Methods -------------------------------------------------------------
618
+ /** Select the whole canvas. */
619
+ async selectAll() {
620
+ await this._jsx.run(`${this._path}.selectAll()`);
621
+ }
622
+ /** Deselect. */
623
+ async deselect() {
624
+ await this._jsx.run(`${this._path}.deselect()`);
625
+ }
626
+ /** Invert the selection. */
627
+ async invert() {
628
+ await this._jsx.run(`${this._path}.invert()`);
629
+ }
630
+ /**
631
+ * Create a selection from a region of points.
632
+ *
633
+ * @param region polygon points, e.g. `[[0,0],[100,0],[100,100],[0,100]]`.
634
+ * @param type selection operation (optional, defaults to replace).
635
+ * @param feather feather radius in pixels (optional).
636
+ * @param antiAlias anti-alias the edges (optional).
637
+ *
638
+ * @example
639
+ * import { SelectionType } from "@ps-generator-bridge/sdk/plugin";
640
+ * await sel.select([[0,0],[100,0],[100,100],[0,100]], SelectionType.REPLACE, 0, true);
641
+ */
642
+ async select(region, type, feather, antiAlias) {
643
+ const args = [JsxBuilder.regionArray(region)];
644
+ if (type !== void 0) args.push(JsxBuilder.enum_(type));
645
+ if (feather !== void 0) args.push(JsxBuilder.number(feather));
646
+ if (antiAlias !== void 0) args.push(JsxBuilder.boolean(antiAlias));
647
+ await this._jsx.run(JsxBuilder.call(`${this._path}.select`, args));
648
+ }
649
+ /** Grow the selection by `by` pixels. */
650
+ async expand(by) {
651
+ await this._jsx.run(JsxBuilder.call(`${this._path}.expand`, [JsxBuilder.number(by)]));
652
+ }
653
+ /** Shrink the selection by `by` pixels. */
654
+ async contract(by) {
655
+ await this._jsx.run(JsxBuilder.call(`${this._path}.contract`, [JsxBuilder.number(by)]));
656
+ }
657
+ /** Feather the selection edge by `by` pixels. */
658
+ async feather(by) {
659
+ await this._jsx.run(JsxBuilder.call(`${this._path}.feather`, [JsxBuilder.number(by)]));
660
+ }
661
+ /** Translate the selection boundary (content stays put). */
662
+ async translateBoundary(deltaX, deltaY) {
663
+ await this._jsx.run(
664
+ JsxBuilder.call(`${this._path}.translateBoundary`, [
665
+ JsxBuilder.number(deltaX),
666
+ JsxBuilder.number(deltaY)
667
+ ])
668
+ );
669
+ }
670
+ };
671
+
672
+ // src/photoshop/PhotoshopDocument.ts
673
+ var PhotoshopDocument = class _PhotoshopDocument {
674
+ constructor(_jsx, _path) {
675
+ this._jsx = _jsx;
676
+ this._path = _path;
677
+ }
678
+ // --- Read-only properties -----------------------------------------------
679
+ /** Document name (file name, without directory). */
680
+ get name() {
681
+ return evalString(this._jsx, `${this._path}.name`);
682
+ }
683
+ /** Unique document id. */
684
+ get id() {
685
+ return evalNumber(this._jsx, `${this._path}.id`);
686
+ }
687
+ /**
688
+ * Document width.
689
+ *
690
+ * @remarks The unit follows Photoshop's current `app.preferences.rulerUnits`.
691
+ * For guaranteed pixels, set `rulerUnits` to `Units.PIXELS` first (e.g. via
692
+ * `this.jsx.run(...)`).
693
+ */
694
+ get width() {
695
+ return evalNumber(this._jsx, `${this._path}.width`);
696
+ }
697
+ /**
698
+ * Document height.
699
+ *
700
+ * @remarks The unit follows `rulerUnits` (see {@link width}).
701
+ */
702
+ get height() {
703
+ return evalNumber(this._jsx, `${this._path}.height`);
704
+ }
705
+ /** Document resolution (PPI). */
706
+ get resolution() {
707
+ return evalNumber(this._jsx, `${this._path}.resolution`);
708
+ }
709
+ /** Document color mode as an enum-name string (e.g. "DocumentMode.RGB"). */
710
+ get mode() {
711
+ const expr = `(function(){
712
+ var m = ${this._path}.mode;
713
+ if (m === DocumentMode.RGB) return "DocumentMode.RGB";
714
+ if (m === DocumentMode.CMYK) return "DocumentMode.CMYK";
715
+ if (m === DocumentMode.GRAYSCALE) return "DocumentMode.GRAYSCALE";
716
+ if (m === DocumentMode.LAB) return "DocumentMode.LAB";
717
+ if (m === DocumentMode.BITMAP) return "DocumentMode.BITMAP";
718
+ if (m === DocumentMode.INDEXEDCOLOR) return "DocumentMode.INDEXEDCOLOR";
719
+ if (m === DocumentMode.MULTICHANNEL) return "DocumentMode.MULTICHANNEL";
720
+ if (m === DocumentMode.DUOTONE) return "DocumentMode.DUOTONE";
721
+ return String(m);
722
+ })()`;
723
+ return evalJson(this._jsx, expr);
724
+ }
725
+ /** Whether the document is saved since its last change. */
726
+ get saved() {
727
+ return evalBool(this._jsx, `${this._path}.saved`);
728
+ }
729
+ /**
730
+ * Full document path (native `fsName`). For an unsaved document this may
731
+ * return a temporary path or throw.
732
+ */
733
+ get fullName() {
734
+ return evalString(this._jsx, `${this._path}.fullName.fsName`);
735
+ }
736
+ /** Directory containing the document (native `fsName`). */
737
+ get path() {
738
+ return evalString(this._jsx, `${this._path}.path.fsName`);
739
+ }
740
+ // --- Child navigation (synchronous; no request) -------------------------
741
+ /** The active layer. */
742
+ get activeLayer() {
743
+ return new PhotoshopLayer(this._jsx, `${this._path}.activeLayer`);
744
+ }
745
+ /** The Layers collection (ArtLayers + LayerSets). */
746
+ get layers() {
747
+ return new PhotoshopLayers(this._jsx, `${this._path}.layers`);
748
+ }
749
+ /** The selection. */
750
+ get selection() {
751
+ return new PhotoshopSelection(this._jsx, `${this._path}.selection`);
752
+ }
753
+ // --- Methods -------------------------------------------------------------
754
+ /** Save the document in its current format. */
755
+ async save() {
756
+ await this._jsx.run(`${this._path}.save()`);
757
+ }
758
+ /**
759
+ * Close the document.
760
+ *
761
+ * @param saving save behavior before closing (defaults to not saving).
762
+ *
763
+ * @example
764
+ * import { SaveOptions } from "@ps-generator-bridge/sdk/plugin";
765
+ * await doc.close(SaveOptions.DONOTSAVECHANGES);
766
+ */
767
+ async close(saving = "SaveOptions.DONOTSAVECHANGES") {
768
+ await this._jsx.run(JsxBuilder.call(`${this._path}.close`, [JsxBuilder.enum_(saving)]));
769
+ }
770
+ /**
771
+ * Save to a path. Mirrors ExtendScript
772
+ * `saveAs(saveIn, options?, asCopy?, extensionType?)`; only `saveIn` and
773
+ * `asCopy` are exposed, `options` is undefined and `extensionType` is left to
774
+ * the Photoshop default.
775
+ *
776
+ * @param saveIn destination path.
777
+ * @param asCopy save as a copy (does not change the document's saved state).
778
+ */
779
+ async saveAs(saveIn, asCopy) {
780
+ const script = `${this._path}.saveAs(${JsxBuilder.file(saveIn)}${asCopy !== void 0 ? `, undefined, ${JsxBuilder.boolean(asCopy)}` : ""})`;
781
+ await this._jsx.run(script);
782
+ }
783
+ /** Flatten all layers into a single background layer. */
784
+ async flatten() {
785
+ await this._jsx.run(`${this._path}.flatten()`);
786
+ }
787
+ /** Merge all visible layers. */
788
+ async mergeVisibleLayers() {
789
+ await this._jsx.run(`${this._path}.mergeVisibleLayers()`);
790
+ }
791
+ /** Rasterize all layers. */
792
+ async rasterizeAllLayers() {
793
+ await this._jsx.run(`${this._path}.rasterizeAllLayers()`);
794
+ }
795
+ /**
796
+ * Duplicate the document.
797
+ *
798
+ * @param name optional name for the copy.
799
+ * @returns the duplicated document.
800
+ *
801
+ * @remarks Assumes `duplicate()` makes the copy the active document, so the
802
+ * result points at `app.activeDocument`.
803
+ */
804
+ async duplicate(name) {
805
+ const args = name !== void 0 ? [JsxBuilder.string(name)] : [];
806
+ await this._jsx.run(JsxBuilder.call(`${this._path}.duplicate`, args));
807
+ return new _PhotoshopDocument(this._jsx, "app.activeDocument");
808
+ }
809
+ /**
810
+ * Resize the canvas.
811
+ *
812
+ * @param width new width in pixels.
813
+ * @param height new height in pixels.
814
+ * @param anchor anchor position (optional, defaults to center).
815
+ *
816
+ * @example
817
+ * import { AnchorPosition } from "@ps-generator-bridge/sdk/plugin";
818
+ * await doc.resizeCanvas(1920, 1080, AnchorPosition.MIDDLECENTER);
819
+ */
820
+ async resizeCanvas(width, height, anchor) {
821
+ const args = [JsxBuilder.number(width), JsxBuilder.number(height)];
822
+ if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
823
+ await this._jsx.run(JsxBuilder.call(`${this._path}.resizeCanvas`, args));
824
+ }
825
+ /**
826
+ * Resize the image.
827
+ *
828
+ * @param width new width in pixels (optional).
829
+ * @param height new height in pixels (optional).
830
+ * @param resolution new resolution in PPI (optional).
831
+ */
832
+ async resizeImage(width, height, resolution) {
833
+ const args = [
834
+ width !== void 0 ? JsxBuilder.number(width) : "undefined",
835
+ height !== void 0 ? JsxBuilder.number(height) : "undefined",
836
+ resolution !== void 0 ? JsxBuilder.number(resolution) : "undefined"
837
+ ];
838
+ await this._jsx.run(JsxBuilder.call(`${this._path}.resizeImage`, args));
839
+ }
840
+ /** Rotate the canvas by `angle` degrees. */
841
+ async rotateCanvas(angle) {
842
+ await this._jsx.run(JsxBuilder.call(`${this._path}.rotateCanvas`, [JsxBuilder.number(angle)]));
843
+ }
844
+ /**
845
+ * Crop the document to `bounds` `[left, top, right, bottom]` (pixels).
846
+ *
847
+ * @remarks Only `bounds` is exposed; ExtendScript `crop()` also takes angle,
848
+ * width and height — reach those via `this.jsx.run(...)` if needed.
849
+ */
850
+ async crop(bounds) {
851
+ const script = `${this._path}.crop(${JsxBuilder.numberArray(Array.from(bounds))})`;
852
+ await this._jsx.run(script);
853
+ }
854
+ };
855
+
856
+ // src/photoshop/PhotoshopApp.ts
857
+ var PhotoshopApp = class {
858
+ constructor(_jsx) {
859
+ this._jsx = _jsx;
860
+ this._path = "app";
861
+ }
862
+ // --- Read-only properties -----------------------------------------------
863
+ /** Photoshop version (e.g. "25.0"). */
864
+ get version() {
865
+ return evalString(this._jsx, `${this._path}.version`);
866
+ }
867
+ /** Application locale (e.g. "zh_CN"). */
868
+ get locale() {
869
+ return evalString(this._jsx, `${this._path}.locale`);
870
+ }
871
+ /** Application name (e.g. "Adobe Photoshop"). */
872
+ get name() {
873
+ return evalString(this._jsx, `${this._path}.name`);
874
+ }
875
+ /** Internal build number. */
876
+ get build() {
877
+ return evalString(this._jsx, `${this._path}.build`);
878
+ }
879
+ /**
880
+ * Install path (native `fsName`). `app.path` is a File in ExtendScript, so the
881
+ * string comes from `.fsName`.
882
+ */
883
+ get path() {
884
+ return evalString(this._jsx, `${this._path}.path.fsName`);
885
+ }
886
+ /**
887
+ * Current foreground color (RGB).
888
+ *
889
+ * @remarks The first version returns only the RGB approximation; in CMYK/Lab
890
+ * documents this is Photoshop's converted RGB and may lose precision. The
891
+ * `cmyk` field is reserved and currently always undefined.
892
+ */
893
+ get foregroundColor() {
894
+ const expr = `(function(){
895
+ var c = ${this._path}.foregroundColor;
896
+ return {
897
+ model: "rgb",
898
+ rgb: { red: c.rgb.red, green: c.rgb.green, blue: c.rgb.blue, hexValue: c.rgb.hexValue }
899
+ };
900
+ })()`;
901
+ return evalJson(this._jsx, expr);
902
+ }
903
+ /** Shortcut for `activeDocument` reached through the `app` path. */
904
+ get activeDocument() {
905
+ return new PhotoshopDocument(this._jsx, `${this._path}.activeDocument`);
906
+ }
907
+ // --- Methods -------------------------------------------------------------
908
+ /**
909
+ * Open a file and return its Document wrapper. The opened document becomes the
910
+ * active document.
911
+ *
912
+ * @param filePath native or POSIX path.
913
+ *
914
+ * @example
915
+ * const doc = await this.photoshop.app.open("/Users/me/design.psd");
916
+ */
917
+ async open(filePath) {
918
+ const script = JsxBuilder.call(`${this._path}.open`, [JsxBuilder.file(filePath)]);
919
+ await this._jsx.run(script);
920
+ return new PhotoshopDocument(this._jsx, `${this._path}.activeDocument`);
921
+ }
922
+ /** Emit a beep. */
923
+ async beep() {
924
+ await this._jsx.run(`${this._path}.beep()`);
925
+ }
926
+ };
927
+
928
+ // src/photoshop/PsPhotoshopProxy.ts
929
+ var PsPhotoshopProxy = class {
930
+ constructor(jsx) {
931
+ this._jsx = jsx;
932
+ this.app = new PhotoshopApp(this._jsx);
933
+ }
934
+ /**
935
+ * The active document (shortcut for `app.activeDocument`). A fresh wrapper is
936
+ * created on each access; wrappers are lightweight and stateless.
937
+ */
938
+ get activeDocument() {
939
+ return new PhotoshopDocument(this._jsx, "app.activeDocument");
940
+ }
941
+ };
942
+
943
+ // src/publicConnection.ts
944
+ var DEFAULT_CONNECTION_URL = "ws://127.0.0.1:7700/ws";
945
+ var PublicEventClient = class {
946
+ constructor(invoke) {
947
+ this.invoke = invoke;
948
+ this.listeners = /* @__PURE__ */ new Map();
949
+ this.wrappers = /* @__PURE__ */ new WeakMap();
950
+ this.activeSubscriptions = /* @__PURE__ */ new Set();
951
+ this.pendingSubscriptions = /* @__PURE__ */ new Set();
952
+ }
953
+ on(type, listener) {
954
+ const hadListeners = this.listenerCount(type) > 0;
955
+ this.add(type, listener);
956
+ if (!hadListeners) this.subscribe(type);
957
+ }
958
+ once(type, listener) {
959
+ const wrapped = ((data) => {
960
+ this.off(type, wrapped);
961
+ listener(data);
962
+ });
963
+ this.wrappers.set(listener, wrapped);
964
+ this.on(type, wrapped);
965
+ }
966
+ off(type, listener) {
967
+ const key = listener;
968
+ const wrapped = this.wrappers.get(key);
969
+ this.wrappers.delete(key);
970
+ const removed = this.remove(type, wrapped ?? key);
971
+ if (removed && this.listenerCount(type) === 0) this.unsubscribe(type);
972
+ }
973
+ dispatch(type, data) {
974
+ const set = this.listeners.get(type);
975
+ if (!set) return;
976
+ for (const listener of set) listener(data);
977
+ }
978
+ replay() {
979
+ this.activeSubscriptions.clear();
980
+ for (const type of this.listeners.keys()) this.subscribe(type);
981
+ }
982
+ add(type, listener) {
983
+ let set = this.listeners.get(type);
984
+ if (!set) {
985
+ set = /* @__PURE__ */ new Set();
986
+ this.listeners.set(type, set);
987
+ }
988
+ set.add(listener);
989
+ }
990
+ remove(type, listener) {
991
+ const set = this.listeners.get(type);
992
+ if (!set) return false;
993
+ const removed = set.delete(listener);
994
+ if (set.size === 0) this.listeners.delete(type);
995
+ return removed;
996
+ }
997
+ listenerCount(type) {
998
+ return this.listeners.get(type)?.size ?? 0;
999
+ }
1000
+ subscribe(type) {
1001
+ if (this.activeSubscriptions.has(type) || this.pendingSubscriptions.has(type)) return;
1002
+ this.pendingSubscriptions.add(type);
1003
+ void this.invoke(ProtocolMethod.EventSubscribe, { type }).then(() => {
1004
+ if (this.listenerCount(type) > 0) this.activeSubscriptions.add(type);
1005
+ }).catch(
1006
+ (error) => console.warn(`event subscribe failed for ${type}: ${error.message}`)
1007
+ ).finally(() => this.pendingSubscriptions.delete(type));
1008
+ }
1009
+ unsubscribe(type) {
1010
+ this.activeSubscriptions.delete(type);
1011
+ this.pendingSubscriptions.delete(type);
1012
+ void this.invoke(ProtocolMethod.EventUnsubscribe, { type }).catch(
1013
+ (error) => console.warn(`event unsubscribe failed for ${type}: ${error.message}`)
1014
+ );
1015
+ }
1016
+ };
1017
+ var PublicJsxRunner = class {
1018
+ constructor(invoke) {
1019
+ this.invoke = invoke;
1020
+ }
1021
+ run(script) {
1022
+ return this.invoke(ProtocolMethod.JsxRun, { script });
1023
+ }
1024
+ execute(name, params) {
1025
+ return this.invoke(ProtocolMethod.JsxExecute, { name, params });
1026
+ }
1027
+ };
1028
+ var PublicPluginClient = class {
1029
+ constructor(getServerInfo) {
1030
+ this.getServerInfo = getServerInfo;
1031
+ }
1032
+ async list() {
1033
+ return (await this.getServerInfo()).plugins ?? [];
1034
+ }
1035
+ async has(id) {
1036
+ return (await this.list()).some((plugin) => plugin.id === id);
1037
+ }
1038
+ };
1039
+ var PublicModules = class {
1040
+ constructor(invoke) {
1041
+ this.invoke = invoke;
1042
+ this.layer = {
1043
+ getLayerInfo: (params) => this.invoke(ProtocolMethod.LayerGetInfo, params),
1044
+ getLayerInfoByID: (layerID, options) => this.invoke(ProtocolMethod.LayerGetInfoById, { layerID, options }),
1045
+ getLayerInfoByIndex: (layerIndex, options) => this.invoke(ProtocolMethod.LayerGetInfoByIndex, { layerIndex, options })
1046
+ };
1047
+ this.document = {
1048
+ getCurrentDocument: () => this.invoke(ProtocolMethod.DocumentCurrent, {}),
1049
+ exportDocument: (params) => this.invoke(ProtocolMethod.DocumentExport, params),
1050
+ saveDocument: (params) => this.invoke(ProtocolMethod.DocumentSave, params)
1051
+ };
1052
+ this.action = {
1053
+ autoCutout: () => this.invoke(ProtocolMethod.ActionAutoCutout, {}),
1054
+ removeBackground: () => this.invoke(ProtocolMethod.ActionRemoveBackground, {})
1055
+ };
1056
+ }
1057
+ };
1058
+ var Connection = class {
1059
+ constructor(options = {}) {
1060
+ this.raw = new RawConnection({ ...options, url: options.url ?? DEFAULT_CONNECTION_URL });
1061
+ this.call = (method, params) => this.raw.invoke(method, params);
1062
+ this.eventClient = new PublicEventClient(this.call);
1063
+ this.jsxClient = new PublicJsxRunner(this.call);
1064
+ this.event = this.eventClient;
1065
+ this.jsx = this.jsxClient;
1066
+ this.photoshop = new PsPhotoshopProxy(this.jsxClient);
1067
+ this.plugin = new PublicPluginClient(() => this.getServerInfo());
1068
+ this.modules = new PublicModules(this.call);
1069
+ this.raw.on("connected", () => this.eventClient.replay());
1070
+ }
1071
+ get id() {
1072
+ return this.raw.id;
1073
+ }
1074
+ ready() {
1075
+ return this.raw.ready();
1076
+ }
1077
+ close() {
1078
+ this.raw.close();
1079
+ }
1080
+ getServerInfo() {
1081
+ return this.call(ProtocolMethod.GetServerInfo, {});
1082
+ }
1083
+ };
1084
+
1085
+ // src/client.ts
1086
+ var PsBridgeClient = class {
1087
+ constructor(options) {
1088
+ this.requests = new RequestTracker(options.timeoutMs ?? 1e4);
1089
+ if (options.transport) {
1090
+ this.transport = options.transport;
1091
+ } else if (options.url) {
1092
+ this.transport = createWebSocketTransport(options.url, options.WebSocket);
1093
+ } else {
1094
+ throw new Error("PsBridgeClient requires either `url` or `transport`.");
1095
+ }
1096
+ this.transport.onMessage((data) => this.handleMessage(data));
1097
+ }
1098
+ /** Fetch the server's identity + (when connected to PS) its Photoshop version. */
1099
+ getServerInfo() {
1100
+ return this.request("getServerInfo", {});
1101
+ }
1102
+ /** Send a typed request and await its correlated response. */
1103
+ async request(method, params) {
1104
+ await this.transport.ready();
1105
+ return this.requests.send(this.transport, method, params);
1106
+ }
1107
+ /** Reject all in-flight requests and close the transport. */
1108
+ close() {
1109
+ this.requests.failAll(new Error("Client closed"));
1110
+ this.transport.close();
1111
+ }
1112
+ handleMessage(data) {
1113
+ this.requests.settleFrame(data);
1114
+ }
1115
+ };
1116
+
1117
+ exports.Connection = Connection;
1118
+ exports.DEFAULT_CONNECTION_URL = DEFAULT_CONNECTION_URL;
1119
+ exports.ErrorCode = ErrorCode;
1120
+ exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1121
+ exports.ProtocolMethod = ProtocolMethod;
1122
+ exports.PsBridgeClient = PsBridgeClient;
1123
+ exports.RawConnection = RawConnection;
1124
+ exports.createWebSocketTransport = createWebSocketTransport;
1125
+ exports.isEvent = isEvent;
1126
+ exports.isRequest = isRequest;
1127
+ exports.isResponse = isResponse;
1128
+ exports.parseFrame = parseFrame;
1129
+ exports.serializeFrame = serializeFrame;
1130
+ //# sourceMappingURL=index.cjs.map
1131
+ //# sourceMappingURL=index.cjs.map