@ps-generator-bridge/generator 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/LICENSE +21 -0
- package/dist/contract-C4vydf6-.d.ts +1270 -0
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js +19 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2482 -0
- package/dist/index.js.map +1 -0
- package/jsx/Action/autoCutout.jsx +12 -0
- package/jsx/Action/redo.jsx +5 -0
- package/jsx/Action/removeBackground.jsx +10 -0
- package/jsx/Common/alert.jsx +4 -0
- package/jsx/Common/debug.jsx +43 -0
- package/jsx/Common/event-dispatch.jsx +4 -0
- package/jsx/Document/exportDocument.jsx +128 -0
- package/jsx/Document/getDocumentInfo.jsx +222 -0
- package/jsx/Document/openPsd.jsx +6 -0
- package/jsx/Document/openPsdFile.jsx +7 -0
- package/jsx/Document/saveDocument.jsx +37 -0
- package/jsx/Layer/addImageLayer.jsx +182 -0
- package/jsx/Layer/createSelectionStroke.jsx +44 -0
- package/jsx/Layer/getActiveLayerID.jsx +1 -0
- package/jsx/Layer/getLayerBounds.jsx +114 -0
- package/jsx/Layer/getLayerInfo.jsx +323 -0
- package/jsx/Layer/getLayerPixmap.jsx +337 -0
- package/jsx/Layer/getSelection.jsx +6 -0
- package/jsx/Layer/layer.jsx +284 -0
- package/jsx/Layer/saveEngineDataToLayer.jsx +26 -0
- package/jsx/Layer/setLayerWorkpathMask.jsx +126 -0
- package/jsx/Layer/transformLayer.jsx +121 -0
- package/jsx/Selection/getSelectionPath.jsx +389 -0
- package/jsx/Selection/pathtosvg.jsx +257 -0
- package/jsx/Selection/registerEvent.jsx +10 -0
- package/jsx/Selection/selectiontopath.jsx +9 -0
- package/jsx/polyfills/Array.js +159 -0
- package/jsx/polyfills/Function.js +29 -0
- package/jsx/polyfills/JSON.js +182 -0
- package/jsx/polyfills/Number.js +24 -0
- package/jsx/polyfills/Object.js +80 -0
- package/jsx/polyfills/String.js +87 -0
- package/jsx/types/extendscript/README.md +34 -0
- package/jsx/types/extendscript/actions.d.ts +148 -0
- package/jsx/types/extendscript/application.d.ts +74 -0
- package/jsx/types/extendscript/channel.d.ts +70 -0
- package/jsx/types/extendscript/color.d.ts +92 -0
- package/jsx/types/extendscript/document.d.ts +110 -0
- package/jsx/types/extendscript/enums.d.ts +796 -0
- package/jsx/types/extendscript/index.d.ts +504 -0
- package/jsx/types/extendscript/layer.d.ts +530 -0
- package/jsx/types/extendscript/misc.d.ts +274 -0
- package/jsx/types/extendscript/open-options.d.ts +86 -0
- package/jsx/types/extendscript/path.d.ts +196 -0
- package/jsx/types/extendscript/save-options.d.ts +144 -0
- package/jsx/types/extendscript/selection.d.ts +191 -0
- package/jsx/types/extendscript/text.d.ts +169 -0
- package/main.js +16 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
9
|
+
var __typeError = (msg) => {
|
|
10
|
+
throw TypeError(msg);
|
|
11
|
+
};
|
|
12
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
13
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
27
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
28
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
29
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
30
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
31
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
32
|
+
mod
|
|
33
|
+
));
|
|
34
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
35
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
36
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
37
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
38
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
39
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
40
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
41
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
45
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
46
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
47
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
48
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
|
|
49
|
+
return __privateGet(this, extra);
|
|
50
|
+
}, set [name](x) {
|
|
51
|
+
return __privateSet(this, extra, x);
|
|
52
|
+
} }, name));
|
|
53
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
54
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
55
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
56
|
+
if (k) {
|
|
57
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
|
|
58
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
59
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
60
|
+
}
|
|
61
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
62
|
+
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
63
|
+
else if (typeof it !== "object" || it === null) __typeError("Object expected");
|
|
64
|
+
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
65
|
+
}
|
|
66
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
67
|
+
};
|
|
68
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
69
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
70
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
71
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
72
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
73
|
+
|
|
74
|
+
// src/index.ts
|
|
75
|
+
var src_exports = {};
|
|
76
|
+
__export(src_exports, {
|
|
77
|
+
JsxRunner: () => JsxRunner,
|
|
78
|
+
PsBridgeHost: () => PsBridgeHost,
|
|
79
|
+
init: () => init
|
|
80
|
+
});
|
|
81
|
+
module.exports = __toCommonJS(src_exports);
|
|
82
|
+
|
|
83
|
+
// src/plugin.ts
|
|
84
|
+
var import_node_path4 = require("path");
|
|
85
|
+
|
|
86
|
+
// src/server/index.ts
|
|
87
|
+
var import_fastify = __toESM(require("fastify"));
|
|
88
|
+
var import_websocket = __toESM(require("@fastify/websocket"));
|
|
89
|
+
var import_node_crypto = require("crypto");
|
|
90
|
+
|
|
91
|
+
// ../sdk/src/protocol.ts
|
|
92
|
+
var ProtocolMethod = {
|
|
93
|
+
GetServerInfo: "getServerInfo",
|
|
94
|
+
JsxRun: "jsx:run",
|
|
95
|
+
JsxExecute: "jsx:execute",
|
|
96
|
+
EventSubscribe: "event:subscribe",
|
|
97
|
+
EventUnsubscribe: "event:unsubscribe",
|
|
98
|
+
ActionAutoCutout: "action:autoCutout",
|
|
99
|
+
ActionRemoveBackground: "action:removeBackground",
|
|
100
|
+
LayerGetInfo: "layer:getInfo",
|
|
101
|
+
LayerGetInfoById: "layer:getInfoById",
|
|
102
|
+
LayerGetInfoByIndex: "layer:getInfoByIndex",
|
|
103
|
+
DocumentCurrent: "document:current",
|
|
104
|
+
DocumentExport: "document:export",
|
|
105
|
+
DocumentSave: "document:save",
|
|
106
|
+
ImageExportLayer: "image:exportLayer",
|
|
107
|
+
ImageGetPreview: "image:getPreview",
|
|
108
|
+
ImageExportDocument: "image:exportDocument"
|
|
109
|
+
};
|
|
110
|
+
var ErrorCode = {
|
|
111
|
+
UnknownMethod: "UNKNOWN_METHOD",
|
|
112
|
+
BadRequest: "BAD_REQUEST",
|
|
113
|
+
Internal: "INTERNAL"
|
|
114
|
+
};
|
|
115
|
+
function isRequest(value) {
|
|
116
|
+
if (typeof value !== "object" || value === null) return false;
|
|
117
|
+
const v = value;
|
|
118
|
+
return typeof v.id === "string" && typeof v.method === "string";
|
|
119
|
+
}
|
|
120
|
+
function parseFrame(data) {
|
|
121
|
+
return JSON.parse(data);
|
|
122
|
+
}
|
|
123
|
+
function serializeFrame(value) {
|
|
124
|
+
return JSON.stringify(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ../sdk/src/photoshop/jsx-runner.ts
|
|
128
|
+
function evalJson(jsx, expr) {
|
|
129
|
+
return jsx.run(`JSON.stringify(${expr})`).then((s) => JSON.parse(s));
|
|
130
|
+
}
|
|
131
|
+
function evalNumber(jsx, expr) {
|
|
132
|
+
return evalJson(jsx, `Number(${expr})`);
|
|
133
|
+
}
|
|
134
|
+
function evalString(jsx, expr) {
|
|
135
|
+
return evalJson(jsx, `String(${expr})`);
|
|
136
|
+
}
|
|
137
|
+
function evalBool(jsx, expr) {
|
|
138
|
+
return evalJson(jsx, `Boolean(${expr})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ../sdk/src/photoshop/JsxBuilder.ts
|
|
142
|
+
var JsxBuilder = class {
|
|
143
|
+
/**
|
|
144
|
+
* Escape a string into a JSX string literal. `JSON.stringify` handles quotes,
|
|
145
|
+
* newlines and Unicode.
|
|
146
|
+
*
|
|
147
|
+
* @example JsxBuilder.string("O'Brien") // -> "\"O'Brien\""
|
|
148
|
+
*/
|
|
149
|
+
static string(value) {
|
|
150
|
+
return JSON.stringify(value);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Serialize a path to an ExtendScript `File` constructor.
|
|
154
|
+
*
|
|
155
|
+
* @example JsxBuilder.file("/path/to/file.psd") // -> 'new File("/path/to/file.psd")'
|
|
156
|
+
*/
|
|
157
|
+
static file(path) {
|
|
158
|
+
return `new File(${JSON.stringify(path)})`;
|
|
159
|
+
}
|
|
160
|
+
/** Serialize a number, rejecting NaN/Infinity. */
|
|
161
|
+
static number(value) {
|
|
162
|
+
if (!isFinite(value)) {
|
|
163
|
+
throw new Error(`JsxBuilder.number: invalid value ${value}`);
|
|
164
|
+
}
|
|
165
|
+
return String(value);
|
|
166
|
+
}
|
|
167
|
+
/** Serialize a boolean. */
|
|
168
|
+
static boolean(value) {
|
|
169
|
+
return value ? "true" : "false";
|
|
170
|
+
}
|
|
171
|
+
/** Pass an enum string through (already in `EnumName.MEMBER` form). */
|
|
172
|
+
static enum_(value) {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Serialize a numeric array to a JSX array literal (bounds, crop, etc.).
|
|
177
|
+
*
|
|
178
|
+
* @example JsxBuilder.numberArray([0, 0, 100, 100]) // -> '[0,0,100,100]'
|
|
179
|
+
*/
|
|
180
|
+
static numberArray(arr) {
|
|
181
|
+
return JSON.stringify(arr);
|
|
182
|
+
}
|
|
183
|
+
/** Serialize a 2-D numeric array (selection boundary points). */
|
|
184
|
+
static regionArray(region) {
|
|
185
|
+
return JSON.stringify(region);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build a method-call expression from pre-serialized args.
|
|
189
|
+
*
|
|
190
|
+
* @example JsxBuilder.call("app.open", [JsxBuilder.file(path)])
|
|
191
|
+
* // -> 'app.open(new File("/path/to/file.psd"))'
|
|
192
|
+
*/
|
|
193
|
+
static call(path, args) {
|
|
194
|
+
return `${path}(${args.join(", ")})`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Build a property assignment statement.
|
|
198
|
+
*
|
|
199
|
+
* @example JsxBuilder.assign("app.activeDocument.activeLayer.name", JsxBuilder.string("New Name"))
|
|
200
|
+
* // -> 'app.activeDocument.activeLayer.name = "New Name"'
|
|
201
|
+
*/
|
|
202
|
+
static assign(path, value) {
|
|
203
|
+
return `${path} = ${value}`;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// ../sdk/src/photoshop/PhotoshopLayer.ts
|
|
208
|
+
var PhotoshopLayer = class _PhotoshopLayer {
|
|
209
|
+
constructor(_jsx, _path) {
|
|
210
|
+
this._jsx = _jsx;
|
|
211
|
+
this._path = _path;
|
|
212
|
+
}
|
|
213
|
+
// --- Layer read-only properties -----------------------------------------
|
|
214
|
+
/** Unique layer id. */
|
|
215
|
+
get id() {
|
|
216
|
+
return evalNumber(this._jsx, `${this._path}.id`);
|
|
217
|
+
}
|
|
218
|
+
/** Layer name. */
|
|
219
|
+
get name() {
|
|
220
|
+
return evalString(this._jsx, `${this._path}.name`);
|
|
221
|
+
}
|
|
222
|
+
/** Layer visibility. */
|
|
223
|
+
get visible() {
|
|
224
|
+
return evalBool(this._jsx, `${this._path}.visible`);
|
|
225
|
+
}
|
|
226
|
+
/** Layer opacity (0-100). */
|
|
227
|
+
get opacity() {
|
|
228
|
+
return evalNumber(this._jsx, `${this._path}.opacity`);
|
|
229
|
+
}
|
|
230
|
+
static {
|
|
231
|
+
/** BlendMode code -> enum-name map (all 27 members, plus newer ones). */
|
|
232
|
+
this._BLEND_MODE_MAP = {
|
|
233
|
+
1: "PASSTHROUGH",
|
|
234
|
+
2: "NORMAL",
|
|
235
|
+
3: "DISSOLVE",
|
|
236
|
+
4: "DARKEN",
|
|
237
|
+
5: "MULTIPLY",
|
|
238
|
+
6: "COLORBURN",
|
|
239
|
+
7: "LINEARBURN",
|
|
240
|
+
8: "LIGHTEN",
|
|
241
|
+
9: "SCREEN",
|
|
242
|
+
10: "COLORDODGE",
|
|
243
|
+
11: "LINEARDODGE",
|
|
244
|
+
12: "OVERLAY",
|
|
245
|
+
13: "SOFTLIGHT",
|
|
246
|
+
14: "HARDLIGHT",
|
|
247
|
+
15: "VIVIDLIGHT",
|
|
248
|
+
16: "LINEARLIGHT",
|
|
249
|
+
17: "PINLIGHT",
|
|
250
|
+
18: "DIFFERENCE",
|
|
251
|
+
19: "EXCLUSION",
|
|
252
|
+
20: "HUE",
|
|
253
|
+
21: "SATURATION",
|
|
254
|
+
22: "COLORBLEND",
|
|
255
|
+
23: "LUMINOSITY",
|
|
256
|
+
26: "HARDMIX",
|
|
257
|
+
27: "SUBTRACT",
|
|
258
|
+
28: "DARKERCOLOR",
|
|
259
|
+
29: "LIGHTERCOLOR",
|
|
260
|
+
30: "DIVIDE"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Blend mode as an enum-name string (e.g. "BlendMode.NORMAL"). ExtendScript
|
|
265
|
+
* yields the numeric code; the static map turns it into a readable name.
|
|
266
|
+
*/
|
|
267
|
+
get blendMode() {
|
|
268
|
+
return evalNumber(this._jsx, `${this._path}.blendMode`).then(
|
|
269
|
+
(code) => _PhotoshopLayer._BLEND_MODE_MAP[code] ?? `BlendMode.UNKNOWN_${code}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
/** Whether the layer is fully locked. */
|
|
273
|
+
get allLocked() {
|
|
274
|
+
return evalBool(this._jsx, `${this._path}.allLocked`);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Layer bounds `[left, top, right, bottom]`.
|
|
278
|
+
*
|
|
279
|
+
* @remarks Units follow `rulerUnits`; values are not pixels unless it is
|
|
280
|
+
* `Units.PIXELS`.
|
|
281
|
+
*/
|
|
282
|
+
get bounds() {
|
|
283
|
+
const expr = `(function(){ var b = ${this._path}.bounds; return [b[0], b[1], b[2], b[3]]; })()`;
|
|
284
|
+
return evalJson(this._jsx, expr);
|
|
285
|
+
}
|
|
286
|
+
static {
|
|
287
|
+
// --- ArtLayer-only properties -------------------------------------------
|
|
288
|
+
/** LayerKind code -> enum-name map. */
|
|
289
|
+
this._LAYER_KIND_MAP = {
|
|
290
|
+
1: "NORMAL",
|
|
291
|
+
2: "TEXT",
|
|
292
|
+
3: "SOLIDFILL",
|
|
293
|
+
4: "GRADIENTFILL",
|
|
294
|
+
5: "LEVELS",
|
|
295
|
+
6: "CURVES",
|
|
296
|
+
7: "COLORBALANCE",
|
|
297
|
+
8: "HUESATURATION",
|
|
298
|
+
9: "BRIGHTNESSCONTRAST",
|
|
299
|
+
10: "THRESHOLD",
|
|
300
|
+
11: "POSTERIZE",
|
|
301
|
+
12: "CHANNELMIXER",
|
|
302
|
+
13: "GRADIENTMAP",
|
|
303
|
+
14: "INVERSION",
|
|
304
|
+
15: "EXPOSURE",
|
|
305
|
+
16: "PHOTOFILTER",
|
|
306
|
+
17: "SELECTIVECOLOR",
|
|
307
|
+
18: "SMARTOBJECT",
|
|
308
|
+
20: "VIBRANCE",
|
|
309
|
+
21: "VIDEO",
|
|
310
|
+
22: "BLACKANDWHITE",
|
|
311
|
+
23: "LAYER3D",
|
|
312
|
+
26: "COLORLOOKUP"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Layer kind as an enum-name string (e.g. "LayerKind.NORMAL"). ArtLayer only;
|
|
317
|
+
* reading it on a LayerSet throws. Check `typename` first.
|
|
318
|
+
*
|
|
319
|
+
* @remarks GRADIENTFILL=4 and PATTERNFILL=4 collide in Adobe's enums, so a
|
|
320
|
+
* kind of 4 always maps to GRADIENTFILL.
|
|
321
|
+
*/
|
|
322
|
+
get kind() {
|
|
323
|
+
return evalNumber(this._jsx, `${this._path}.kind`).then(
|
|
324
|
+
(code) => _PhotoshopLayer._LAYER_KIND_MAP[code] ?? `LayerKind.UNKNOWN_${code}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
/** Object type name ("ArtLayer" or "LayerSet"). */
|
|
328
|
+
get typename() {
|
|
329
|
+
return evalString(this._jsx, `${this._path}.typename`);
|
|
330
|
+
}
|
|
331
|
+
// --- Property writes -----------------------------------------------------
|
|
332
|
+
/** Set the layer name. */
|
|
333
|
+
async setName(value) {
|
|
334
|
+
await this._jsx.run(JsxBuilder.assign(`${this._path}.name`, JsxBuilder.string(value)));
|
|
335
|
+
}
|
|
336
|
+
/** Set layer visibility. */
|
|
337
|
+
async setVisible(value) {
|
|
338
|
+
await this._jsx.run(JsxBuilder.assign(`${this._path}.visible`, JsxBuilder.boolean(value)));
|
|
339
|
+
}
|
|
340
|
+
/** Set layer opacity (0-100). */
|
|
341
|
+
async setOpacity(value) {
|
|
342
|
+
await this._jsx.run(JsxBuilder.assign(`${this._path}.opacity`, JsxBuilder.number(value)));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Set the blend mode.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* import { BlendMode } from "@ps-generator-bridge/sdk/plugin";
|
|
349
|
+
* await layer.setBlendMode(BlendMode.MULTIPLY);
|
|
350
|
+
*/
|
|
351
|
+
async setBlendMode(value) {
|
|
352
|
+
await this._jsx.run(JsxBuilder.assign(`${this._path}.blendMode`, JsxBuilder.enum_(value)));
|
|
353
|
+
}
|
|
354
|
+
/** Set whether the layer is fully locked. */
|
|
355
|
+
async setAllLocked(value) {
|
|
356
|
+
await this._jsx.run(JsxBuilder.assign(`${this._path}.allLocked`, JsxBuilder.boolean(value)));
|
|
357
|
+
}
|
|
358
|
+
// --- Methods -------------------------------------------------------------
|
|
359
|
+
/** Delete this layer. */
|
|
360
|
+
async remove() {
|
|
361
|
+
await this._jsx.run(`${this._path}.remove()`);
|
|
362
|
+
}
|
|
363
|
+
/** Duplicate this layer (the copy becomes `activeLayer`). */
|
|
364
|
+
async duplicate() {
|
|
365
|
+
await this._jsx.run(`${this._path}.duplicate()`);
|
|
366
|
+
return new _PhotoshopLayer(this._jsx, "app.activeDocument.activeLayer");
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Move this layer relative to another.
|
|
370
|
+
*
|
|
371
|
+
* @param relativeObjectJsxPath JSX path of the reference layer (e.g.
|
|
372
|
+
* "app.activeDocument.layers[0]").
|
|
373
|
+
* @param insertionLocation placement enum.
|
|
374
|
+
*
|
|
375
|
+
* @remarks Pass a bare JSX path expression. A `PhotoshopLayers.getByName()`
|
|
376
|
+
* path contains quotes and cannot be used as a reference expression here.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* import { ElementPlacement } from "@ps-generator-bridge/sdk/plugin";
|
|
380
|
+
* await layer.move("app.activeDocument.layers[0]", ElementPlacement.PLACEBEFORE);
|
|
381
|
+
*/
|
|
382
|
+
async move(relativeObjectJsxPath, insertionLocation) {
|
|
383
|
+
await this._jsx.run(
|
|
384
|
+
`${this._path}.move(${relativeObjectJsxPath}, ${JsxBuilder.enum_(insertionLocation)})`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
/** Translate the layer by a pixel delta. */
|
|
388
|
+
async translate(deltaX, deltaY) {
|
|
389
|
+
await this._jsx.run(
|
|
390
|
+
JsxBuilder.call(`${this._path}.translate`, [
|
|
391
|
+
JsxBuilder.number(deltaX),
|
|
392
|
+
JsxBuilder.number(deltaY)
|
|
393
|
+
])
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Scale the layer.
|
|
398
|
+
*
|
|
399
|
+
* @param horizontal horizontal scale percent (150 = 150%).
|
|
400
|
+
* @param vertical vertical scale percent.
|
|
401
|
+
* @param anchor scaling anchor (optional).
|
|
402
|
+
*/
|
|
403
|
+
async resize(horizontal, vertical, anchor) {
|
|
404
|
+
const args = [JsxBuilder.number(horizontal), JsxBuilder.number(vertical)];
|
|
405
|
+
if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
|
|
406
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.resize`, args));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Rotate the layer.
|
|
410
|
+
*
|
|
411
|
+
* @param angle degrees, clockwise positive.
|
|
412
|
+
* @param anchor rotation anchor (optional).
|
|
413
|
+
*/
|
|
414
|
+
async rotate(angle, anchor) {
|
|
415
|
+
const args = [JsxBuilder.number(angle)];
|
|
416
|
+
if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
|
|
417
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.rotate`, args));
|
|
418
|
+
}
|
|
419
|
+
/** Move the layer to the end of its stack. */
|
|
420
|
+
async moveToEnd() {
|
|
421
|
+
await this._jsx.run(`${this._path}.moveToEnd()`);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// ../sdk/src/photoshop/PhotoshopLayers.ts
|
|
426
|
+
var PhotoshopLayers = class {
|
|
427
|
+
constructor(_jsx, _path) {
|
|
428
|
+
this._jsx = _jsx;
|
|
429
|
+
this._path = _path;
|
|
430
|
+
}
|
|
431
|
+
/** Number of layers in the collection. */
|
|
432
|
+
get length() {
|
|
433
|
+
return evalNumber(this._jsx, `${this._path}.length`);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Access a layer by index. The collection is 0-based, matching JavaScript;
|
|
437
|
+
* `layers[0]` is the top-most layer.
|
|
438
|
+
*/
|
|
439
|
+
at(index) {
|
|
440
|
+
return new PhotoshopLayer(this._jsx, `${this._path}[${index}]`);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Look up a layer by name (case-sensitive). The returned wrapper works for
|
|
444
|
+
* property reads/writes but its path contains a `getByName(...)` call, so it
|
|
445
|
+
* must not be passed as `PhotoshopLayer.move()`'s reference path.
|
|
446
|
+
*/
|
|
447
|
+
getByName(name) {
|
|
448
|
+
const escapedName = JsxBuilder.string(name);
|
|
449
|
+
return new PhotoshopLayer(this._jsx, `${this._path}.getByName(${escapedName})`);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ../sdk/src/photoshop/PhotoshopSelection.ts
|
|
454
|
+
var PhotoshopSelection = class {
|
|
455
|
+
constructor(_jsx, _path) {
|
|
456
|
+
this._jsx = _jsx;
|
|
457
|
+
this._path = _path;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Selection bounds `[left, top, right, bottom]`. Throws when there is no
|
|
461
|
+
* selection.
|
|
462
|
+
*
|
|
463
|
+
* @remarks Units follow `rulerUnits`.
|
|
464
|
+
*/
|
|
465
|
+
get bounds() {
|
|
466
|
+
const expr = `(function(){ var b = ${this._path}.bounds; return [b[0], b[1], b[2], b[3]]; })()`;
|
|
467
|
+
return evalJson(this._jsx, expr);
|
|
468
|
+
}
|
|
469
|
+
/** Whether the selection is a solid (un-feathered) rectangle. */
|
|
470
|
+
get solid() {
|
|
471
|
+
return evalBool(this._jsx, `${this._path}.solid`);
|
|
472
|
+
}
|
|
473
|
+
// --- Methods -------------------------------------------------------------
|
|
474
|
+
/** Select the whole canvas. */
|
|
475
|
+
async selectAll() {
|
|
476
|
+
await this._jsx.run(`${this._path}.selectAll()`);
|
|
477
|
+
}
|
|
478
|
+
/** Deselect. */
|
|
479
|
+
async deselect() {
|
|
480
|
+
await this._jsx.run(`${this._path}.deselect()`);
|
|
481
|
+
}
|
|
482
|
+
/** Invert the selection. */
|
|
483
|
+
async invert() {
|
|
484
|
+
await this._jsx.run(`${this._path}.invert()`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Create a selection from a region of points.
|
|
488
|
+
*
|
|
489
|
+
* @param region polygon points, e.g. `[[0,0],[100,0],[100,100],[0,100]]`.
|
|
490
|
+
* @param type selection operation (optional, defaults to replace).
|
|
491
|
+
* @param feather feather radius in pixels (optional).
|
|
492
|
+
* @param antiAlias anti-alias the edges (optional).
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* import { SelectionType } from "@ps-generator-bridge/sdk/plugin";
|
|
496
|
+
* await sel.select([[0,0],[100,0],[100,100],[0,100]], SelectionType.REPLACE, 0, true);
|
|
497
|
+
*/
|
|
498
|
+
async select(region, type, feather, antiAlias) {
|
|
499
|
+
const args = [JsxBuilder.regionArray(region)];
|
|
500
|
+
if (type !== void 0) args.push(JsxBuilder.enum_(type));
|
|
501
|
+
if (feather !== void 0) args.push(JsxBuilder.number(feather));
|
|
502
|
+
if (antiAlias !== void 0) args.push(JsxBuilder.boolean(antiAlias));
|
|
503
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.select`, args));
|
|
504
|
+
}
|
|
505
|
+
/** Grow the selection by `by` pixels. */
|
|
506
|
+
async expand(by) {
|
|
507
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.expand`, [JsxBuilder.number(by)]));
|
|
508
|
+
}
|
|
509
|
+
/** Shrink the selection by `by` pixels. */
|
|
510
|
+
async contract(by) {
|
|
511
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.contract`, [JsxBuilder.number(by)]));
|
|
512
|
+
}
|
|
513
|
+
/** Feather the selection edge by `by` pixels. */
|
|
514
|
+
async feather(by) {
|
|
515
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.feather`, [JsxBuilder.number(by)]));
|
|
516
|
+
}
|
|
517
|
+
/** Translate the selection boundary (content stays put). */
|
|
518
|
+
async translateBoundary(deltaX, deltaY) {
|
|
519
|
+
await this._jsx.run(
|
|
520
|
+
JsxBuilder.call(`${this._path}.translateBoundary`, [
|
|
521
|
+
JsxBuilder.number(deltaX),
|
|
522
|
+
JsxBuilder.number(deltaY)
|
|
523
|
+
])
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// ../sdk/src/photoshop/PhotoshopDocument.ts
|
|
529
|
+
var PhotoshopDocument = class _PhotoshopDocument {
|
|
530
|
+
constructor(_jsx, _path) {
|
|
531
|
+
this._jsx = _jsx;
|
|
532
|
+
this._path = _path;
|
|
533
|
+
}
|
|
534
|
+
// --- Read-only properties -----------------------------------------------
|
|
535
|
+
/** Document name (file name, without directory). */
|
|
536
|
+
get name() {
|
|
537
|
+
return evalString(this._jsx, `${this._path}.name`);
|
|
538
|
+
}
|
|
539
|
+
/** Unique document id. */
|
|
540
|
+
get id() {
|
|
541
|
+
return evalNumber(this._jsx, `${this._path}.id`);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Document width.
|
|
545
|
+
*
|
|
546
|
+
* @remarks The unit follows Photoshop's current `app.preferences.rulerUnits`.
|
|
547
|
+
* For guaranteed pixels, set `rulerUnits` to `Units.PIXELS` first (e.g. via
|
|
548
|
+
* `this.jsx.run(...)`).
|
|
549
|
+
*/
|
|
550
|
+
get width() {
|
|
551
|
+
return evalNumber(this._jsx, `${this._path}.width`);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Document height.
|
|
555
|
+
*
|
|
556
|
+
* @remarks The unit follows `rulerUnits` (see {@link width}).
|
|
557
|
+
*/
|
|
558
|
+
get height() {
|
|
559
|
+
return evalNumber(this._jsx, `${this._path}.height`);
|
|
560
|
+
}
|
|
561
|
+
/** Document resolution (PPI). */
|
|
562
|
+
get resolution() {
|
|
563
|
+
return evalNumber(this._jsx, `${this._path}.resolution`);
|
|
564
|
+
}
|
|
565
|
+
/** Document color mode as an enum-name string (e.g. "DocumentMode.RGB"). */
|
|
566
|
+
get mode() {
|
|
567
|
+
const expr = `(function(){
|
|
568
|
+
var m = ${this._path}.mode;
|
|
569
|
+
if (m === DocumentMode.RGB) return "DocumentMode.RGB";
|
|
570
|
+
if (m === DocumentMode.CMYK) return "DocumentMode.CMYK";
|
|
571
|
+
if (m === DocumentMode.GRAYSCALE) return "DocumentMode.GRAYSCALE";
|
|
572
|
+
if (m === DocumentMode.LAB) return "DocumentMode.LAB";
|
|
573
|
+
if (m === DocumentMode.BITMAP) return "DocumentMode.BITMAP";
|
|
574
|
+
if (m === DocumentMode.INDEXEDCOLOR) return "DocumentMode.INDEXEDCOLOR";
|
|
575
|
+
if (m === DocumentMode.MULTICHANNEL) return "DocumentMode.MULTICHANNEL";
|
|
576
|
+
if (m === DocumentMode.DUOTONE) return "DocumentMode.DUOTONE";
|
|
577
|
+
return String(m);
|
|
578
|
+
})()`;
|
|
579
|
+
return evalJson(this._jsx, expr);
|
|
580
|
+
}
|
|
581
|
+
/** Whether the document is saved since its last change. */
|
|
582
|
+
get saved() {
|
|
583
|
+
return evalBool(this._jsx, `${this._path}.saved`);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Full document path (native `fsName`). For an unsaved document this may
|
|
587
|
+
* return a temporary path or throw.
|
|
588
|
+
*/
|
|
589
|
+
get fullName() {
|
|
590
|
+
return evalString(this._jsx, `${this._path}.fullName.fsName`);
|
|
591
|
+
}
|
|
592
|
+
/** Directory containing the document (native `fsName`). */
|
|
593
|
+
get path() {
|
|
594
|
+
return evalString(this._jsx, `${this._path}.path.fsName`);
|
|
595
|
+
}
|
|
596
|
+
// --- Child navigation (synchronous; no request) -------------------------
|
|
597
|
+
/** The active layer. */
|
|
598
|
+
get activeLayer() {
|
|
599
|
+
return new PhotoshopLayer(this._jsx, `${this._path}.activeLayer`);
|
|
600
|
+
}
|
|
601
|
+
/** The Layers collection (ArtLayers + LayerSets). */
|
|
602
|
+
get layers() {
|
|
603
|
+
return new PhotoshopLayers(this._jsx, `${this._path}.layers`);
|
|
604
|
+
}
|
|
605
|
+
/** The selection. */
|
|
606
|
+
get selection() {
|
|
607
|
+
return new PhotoshopSelection(this._jsx, `${this._path}.selection`);
|
|
608
|
+
}
|
|
609
|
+
// --- Methods -------------------------------------------------------------
|
|
610
|
+
/** Save the document in its current format. */
|
|
611
|
+
async save() {
|
|
612
|
+
await this._jsx.run(`${this._path}.save()`);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Close the document.
|
|
616
|
+
*
|
|
617
|
+
* @param saving save behavior before closing (defaults to not saving).
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* import { SaveOptions } from "@ps-generator-bridge/sdk/plugin";
|
|
621
|
+
* await doc.close(SaveOptions.DONOTSAVECHANGES);
|
|
622
|
+
*/
|
|
623
|
+
async close(saving = "SaveOptions.DONOTSAVECHANGES") {
|
|
624
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.close`, [JsxBuilder.enum_(saving)]));
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Save to a path. Mirrors ExtendScript
|
|
628
|
+
* `saveAs(saveIn, options?, asCopy?, extensionType?)`; only `saveIn` and
|
|
629
|
+
* `asCopy` are exposed, `options` is undefined and `extensionType` is left to
|
|
630
|
+
* the Photoshop default.
|
|
631
|
+
*
|
|
632
|
+
* @param saveIn destination path.
|
|
633
|
+
* @param asCopy save as a copy (does not change the document's saved state).
|
|
634
|
+
*/
|
|
635
|
+
async saveAs(saveIn, asCopy) {
|
|
636
|
+
const script = `${this._path}.saveAs(${JsxBuilder.file(saveIn)}${asCopy !== void 0 ? `, undefined, ${JsxBuilder.boolean(asCopy)}` : ""})`;
|
|
637
|
+
await this._jsx.run(script);
|
|
638
|
+
}
|
|
639
|
+
/** Flatten all layers into a single background layer. */
|
|
640
|
+
async flatten() {
|
|
641
|
+
await this._jsx.run(`${this._path}.flatten()`);
|
|
642
|
+
}
|
|
643
|
+
/** Merge all visible layers. */
|
|
644
|
+
async mergeVisibleLayers() {
|
|
645
|
+
await this._jsx.run(`${this._path}.mergeVisibleLayers()`);
|
|
646
|
+
}
|
|
647
|
+
/** Rasterize all layers. */
|
|
648
|
+
async rasterizeAllLayers() {
|
|
649
|
+
await this._jsx.run(`${this._path}.rasterizeAllLayers()`);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Duplicate the document.
|
|
653
|
+
*
|
|
654
|
+
* @param name optional name for the copy.
|
|
655
|
+
* @returns the duplicated document.
|
|
656
|
+
*
|
|
657
|
+
* @remarks Assumes `duplicate()` makes the copy the active document, so the
|
|
658
|
+
* result points at `app.activeDocument`.
|
|
659
|
+
*/
|
|
660
|
+
async duplicate(name) {
|
|
661
|
+
const args = name !== void 0 ? [JsxBuilder.string(name)] : [];
|
|
662
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.duplicate`, args));
|
|
663
|
+
return new _PhotoshopDocument(this._jsx, "app.activeDocument");
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Resize the canvas.
|
|
667
|
+
*
|
|
668
|
+
* @param width new width in pixels.
|
|
669
|
+
* @param height new height in pixels.
|
|
670
|
+
* @param anchor anchor position (optional, defaults to center).
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* import { AnchorPosition } from "@ps-generator-bridge/sdk/plugin";
|
|
674
|
+
* await doc.resizeCanvas(1920, 1080, AnchorPosition.MIDDLECENTER);
|
|
675
|
+
*/
|
|
676
|
+
async resizeCanvas(width, height, anchor) {
|
|
677
|
+
const args = [JsxBuilder.number(width), JsxBuilder.number(height)];
|
|
678
|
+
if (anchor !== void 0) args.push(JsxBuilder.enum_(anchor));
|
|
679
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.resizeCanvas`, args));
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Resize the image.
|
|
683
|
+
*
|
|
684
|
+
* @param width new width in pixels (optional).
|
|
685
|
+
* @param height new height in pixels (optional).
|
|
686
|
+
* @param resolution new resolution in PPI (optional).
|
|
687
|
+
*/
|
|
688
|
+
async resizeImage(width, height, resolution) {
|
|
689
|
+
const args = [
|
|
690
|
+
width !== void 0 ? JsxBuilder.number(width) : "undefined",
|
|
691
|
+
height !== void 0 ? JsxBuilder.number(height) : "undefined",
|
|
692
|
+
resolution !== void 0 ? JsxBuilder.number(resolution) : "undefined"
|
|
693
|
+
];
|
|
694
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.resizeImage`, args));
|
|
695
|
+
}
|
|
696
|
+
/** Rotate the canvas by `angle` degrees. */
|
|
697
|
+
async rotateCanvas(angle) {
|
|
698
|
+
await this._jsx.run(JsxBuilder.call(`${this._path}.rotateCanvas`, [JsxBuilder.number(angle)]));
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Crop the document to `bounds` `[left, top, right, bottom]` (pixels).
|
|
702
|
+
*
|
|
703
|
+
* @remarks Only `bounds` is exposed; ExtendScript `crop()` also takes angle,
|
|
704
|
+
* width and height — reach those via `this.jsx.run(...)` if needed.
|
|
705
|
+
*/
|
|
706
|
+
async crop(bounds) {
|
|
707
|
+
const script = `${this._path}.crop(${JsxBuilder.numberArray(Array.from(bounds))})`;
|
|
708
|
+
await this._jsx.run(script);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// ../sdk/src/photoshop/PhotoshopApp.ts
|
|
713
|
+
var PhotoshopApp = class {
|
|
714
|
+
constructor(_jsx) {
|
|
715
|
+
this._jsx = _jsx;
|
|
716
|
+
this._path = "app";
|
|
717
|
+
}
|
|
718
|
+
// --- Read-only properties -----------------------------------------------
|
|
719
|
+
/** Photoshop version (e.g. "25.0"). */
|
|
720
|
+
get version() {
|
|
721
|
+
return evalString(this._jsx, `${this._path}.version`);
|
|
722
|
+
}
|
|
723
|
+
/** Application locale (e.g. "zh_CN"). */
|
|
724
|
+
get locale() {
|
|
725
|
+
return evalString(this._jsx, `${this._path}.locale`);
|
|
726
|
+
}
|
|
727
|
+
/** Application name (e.g. "Adobe Photoshop"). */
|
|
728
|
+
get name() {
|
|
729
|
+
return evalString(this._jsx, `${this._path}.name`);
|
|
730
|
+
}
|
|
731
|
+
/** Internal build number. */
|
|
732
|
+
get build() {
|
|
733
|
+
return evalString(this._jsx, `${this._path}.build`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Install path (native `fsName`). `app.path` is a File in ExtendScript, so the
|
|
737
|
+
* string comes from `.fsName`.
|
|
738
|
+
*/
|
|
739
|
+
get path() {
|
|
740
|
+
return evalString(this._jsx, `${this._path}.path.fsName`);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Current foreground color (RGB).
|
|
744
|
+
*
|
|
745
|
+
* @remarks The first version returns only the RGB approximation; in CMYK/Lab
|
|
746
|
+
* documents this is Photoshop's converted RGB and may lose precision. The
|
|
747
|
+
* `cmyk` field is reserved and currently always undefined.
|
|
748
|
+
*/
|
|
749
|
+
get foregroundColor() {
|
|
750
|
+
const expr = `(function(){
|
|
751
|
+
var c = ${this._path}.foregroundColor;
|
|
752
|
+
return {
|
|
753
|
+
model: "rgb",
|
|
754
|
+
rgb: { red: c.rgb.red, green: c.rgb.green, blue: c.rgb.blue, hexValue: c.rgb.hexValue }
|
|
755
|
+
};
|
|
756
|
+
})()`;
|
|
757
|
+
return evalJson(this._jsx, expr);
|
|
758
|
+
}
|
|
759
|
+
/** Shortcut for `activeDocument` reached through the `app` path. */
|
|
760
|
+
get activeDocument() {
|
|
761
|
+
return new PhotoshopDocument(this._jsx, `${this._path}.activeDocument`);
|
|
762
|
+
}
|
|
763
|
+
// --- Methods -------------------------------------------------------------
|
|
764
|
+
/**
|
|
765
|
+
* Open a file and return its Document wrapper. The opened document becomes the
|
|
766
|
+
* active document.
|
|
767
|
+
*
|
|
768
|
+
* @param filePath native or POSIX path.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* const doc = await this.photoshop.app.open("/Users/me/design.psd");
|
|
772
|
+
*/
|
|
773
|
+
async open(filePath) {
|
|
774
|
+
const script = JsxBuilder.call(`${this._path}.open`, [JsxBuilder.file(filePath)]);
|
|
775
|
+
await this._jsx.run(script);
|
|
776
|
+
return new PhotoshopDocument(this._jsx, `${this._path}.activeDocument`);
|
|
777
|
+
}
|
|
778
|
+
/** Emit a beep. */
|
|
779
|
+
async beep() {
|
|
780
|
+
await this._jsx.run(`${this._path}.beep()`);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// ../sdk/src/photoshop/PsPhotoshopProxy.ts
|
|
785
|
+
var PsPhotoshopProxy = class {
|
|
786
|
+
constructor(jsx) {
|
|
787
|
+
this._jsx = jsx;
|
|
788
|
+
this.app = new PhotoshopApp(this._jsx);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* The active document (shortcut for `app.activeDocument`). A fresh wrapper is
|
|
792
|
+
* created on each access; wrappers are lightweight and stateless.
|
|
793
|
+
*/
|
|
794
|
+
get activeDocument() {
|
|
795
|
+
return new PhotoshopDocument(this._jsx, "app.activeDocument");
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/server/dispatch.ts
|
|
800
|
+
function unknownMethodResponse(id, method) {
|
|
801
|
+
return {
|
|
802
|
+
id,
|
|
803
|
+
ok: false,
|
|
804
|
+
error: { code: ErrorCode.UnknownMethod, message: `Unknown method: ${method}` }
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async function runMethod(handler, message, ctx) {
|
|
808
|
+
try {
|
|
809
|
+
const result = await handler(message.params, ctx);
|
|
810
|
+
return { id: message.id, ok: true, result };
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
813
|
+
const code = thrown.code;
|
|
814
|
+
const resolvedCode = typeof code === "string" ? code : ErrorCode.Internal;
|
|
815
|
+
return {
|
|
816
|
+
id: message.id,
|
|
817
|
+
ok: false,
|
|
818
|
+
error: {
|
|
819
|
+
code: resolvedCode,
|
|
820
|
+
message: thrown.message
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/server/methodTable.ts
|
|
827
|
+
var MethodTable = class {
|
|
828
|
+
constructor() {
|
|
829
|
+
this.methods = /* @__PURE__ */ new Map();
|
|
830
|
+
}
|
|
831
|
+
register(name, handler) {
|
|
832
|
+
this.methods.set(name, handler);
|
|
833
|
+
}
|
|
834
|
+
async tryDispatch(message, ctx) {
|
|
835
|
+
if (!isRequest(message)) return void 0;
|
|
836
|
+
const handler = this.methods.get(message.method);
|
|
837
|
+
if (!handler) return void 0;
|
|
838
|
+
return runMethod(handler, message, ctx);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// src/server/registry.ts
|
|
843
|
+
var Registry = class {
|
|
844
|
+
constructor(app) {
|
|
845
|
+
this.app = app;
|
|
846
|
+
this.methods = new MethodTable();
|
|
847
|
+
/**
|
|
848
|
+
* Reserved first path segments (plugin ids). Set by the plugin after plugins
|
|
849
|
+
* are registered and before modules bootstrap, so a module `@api` whose first
|
|
850
|
+
* segment collides with a plugin id fails loud at init (RFC 0004).
|
|
851
|
+
*/
|
|
852
|
+
this.reservedSegments = /* @__PURE__ */ new Set();
|
|
853
|
+
}
|
|
854
|
+
/** Register (or replace) a WS Request handler. Runtime-capable. */
|
|
855
|
+
registerMethod(method, handler) {
|
|
856
|
+
this.methods.register(method, handler);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Register an HTTP route. Must run before `listen()` (fastify). The handler is
|
|
860
|
+
* the devkit's opaque `ApiHandler`; cast to fastify's `RouteHandlerMethod` at
|
|
861
|
+
* this boundary. Throws if the route's first segment is a reserved plugin id.
|
|
862
|
+
*/
|
|
863
|
+
registerApi(route) {
|
|
864
|
+
const first = firstSegment(route.url);
|
|
865
|
+
if (first !== void 0 && this.reservedSegments.has(first)) {
|
|
866
|
+
throw new Error(`module @api '${route.url}' collides with reserved plugin id '${first}'`);
|
|
867
|
+
}
|
|
868
|
+
this.app.route({
|
|
869
|
+
method: route.method,
|
|
870
|
+
url: route.url,
|
|
871
|
+
handler: route.handler
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Route one parsed frame to its method handler, producing a response envelope.
|
|
876
|
+
* Returns `undefined` for non-request frames (nothing to respond to). This is
|
|
877
|
+
* the global fallback; per-plugin connections try the scoped table first.
|
|
878
|
+
*/
|
|
879
|
+
async dispatch(message, ctx) {
|
|
880
|
+
if (!isRequest(message)) {
|
|
881
|
+
return void 0;
|
|
882
|
+
}
|
|
883
|
+
const response = await this.methods.tryDispatch(message, ctx);
|
|
884
|
+
if (!response) {
|
|
885
|
+
return unknownMethodResponse(message.id, message.method);
|
|
886
|
+
}
|
|
887
|
+
return response;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
function firstSegment(url) {
|
|
891
|
+
const match = url.match(/^\/([^/?#]+)/);
|
|
892
|
+
return match ? match[1] : void 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/meta.ts
|
|
896
|
+
var PLUGIN_NAME = "PS Generator Bridge Service";
|
|
897
|
+
var PLUGIN_VERSION = "0.1.0";
|
|
898
|
+
|
|
899
|
+
// src/server/builtins.ts
|
|
900
|
+
function registerBuiltins(registry, plugins) {
|
|
901
|
+
registry.registerMethod(ProtocolMethod.GetServerInfo, async (_params, ctx) => {
|
|
902
|
+
const { generator } = ctx;
|
|
903
|
+
let psVersion;
|
|
904
|
+
try {
|
|
905
|
+
psVersion = await generator.getPhotoshopVersion();
|
|
906
|
+
} catch {
|
|
907
|
+
psVersion = void 0;
|
|
908
|
+
}
|
|
909
|
+
return { name: PLUGIN_NAME, version: PLUGIN_VERSION, psVersion, plugins: plugins() };
|
|
910
|
+
});
|
|
911
|
+
registry.registerMethod(ProtocolMethod.JsxRun, async (params, ctx) => {
|
|
912
|
+
const context = ctx;
|
|
913
|
+
const { script } = params;
|
|
914
|
+
if (typeof script !== "string") throw badRequest("script is required");
|
|
915
|
+
if (!context.jsx) throw badRequest("jsx is not available");
|
|
916
|
+
return context.jsx.run(script);
|
|
917
|
+
});
|
|
918
|
+
registry.registerMethod(ProtocolMethod.JsxExecute, async (params, ctx) => {
|
|
919
|
+
const context = ctx;
|
|
920
|
+
const { name, params: jsxParams } = params;
|
|
921
|
+
if (typeof name !== "string") throw badRequest("name is required");
|
|
922
|
+
if (!context.jsx) throw badRequest("jsx is not available");
|
|
923
|
+
return context.jsx.execute(name, jsxParams);
|
|
924
|
+
});
|
|
925
|
+
registry.registerMethod(ProtocolMethod.EventSubscribe, async (params, ctx) => {
|
|
926
|
+
const context = ctx;
|
|
927
|
+
if (!context.session) throw badRequest("event subscription is only available on /ws");
|
|
928
|
+
const { type } = params;
|
|
929
|
+
if (typeof type !== "string") throw badRequest("type is required");
|
|
930
|
+
context.session.subscribe(type);
|
|
931
|
+
return { ok: true };
|
|
932
|
+
});
|
|
933
|
+
registry.registerMethod(ProtocolMethod.EventUnsubscribe, async (params, ctx) => {
|
|
934
|
+
const context = ctx;
|
|
935
|
+
if (!context.session) throw badRequest("event subscription is only available on /ws");
|
|
936
|
+
const { type } = params;
|
|
937
|
+
if (typeof type !== "string") throw badRequest("type is required");
|
|
938
|
+
context.session.unsubscribe(type);
|
|
939
|
+
return { ok: true };
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function badRequest(message) {
|
|
943
|
+
const error = new Error(message);
|
|
944
|
+
error.code = ErrorCode.BadRequest;
|
|
945
|
+
return error;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ../sdk/src/plugin/base.ts
|
|
949
|
+
var BASE_PLUGIN_BRAND = /* @__PURE__ */ Symbol.for("ps-generator-bridge.BasePlugin");
|
|
950
|
+
var BasePlugin = class {
|
|
951
|
+
constructor(id, plugin) {
|
|
952
|
+
this.id = id;
|
|
953
|
+
this.plugin = plugin;
|
|
954
|
+
}
|
|
955
|
+
/** Feature modules, reached by short key (shortcut for `this.plugin.modules`). */
|
|
956
|
+
get modules() {
|
|
957
|
+
return this.plugin.modules;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* The jsx runner scoped to this plugin's own `jsx/` dir (shortcut for
|
|
961
|
+
* `this.plugin.jsx`, RFC 0005). `jsx.execute("x")` runs `<pluginRoot>/jsx/x.jsx`;
|
|
962
|
+
* `jsx.executeBuiltin("Document/getDocumentInfo")` reaches the host's built-in
|
|
963
|
+
* tree.
|
|
964
|
+
*/
|
|
965
|
+
get jsx() {
|
|
966
|
+
return this.plugin.jsx;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Typed, listen-only Photoshop event stream (shortcut for `this.plugin.events`).
|
|
970
|
+
* Subscriptions are lazy on the server side: the first `on`/`once` for an event
|
|
971
|
+
* subscribes upstream, and the last `off` unsubscribes.
|
|
972
|
+
*/
|
|
973
|
+
get events() {
|
|
974
|
+
return this.plugin.events;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Photoshop DOM proxy, a typed object wrapper over `this.jsx`. Read and write
|
|
978
|
+
* the live document through `this.photoshop.app` / `this.photoshop.activeDocument`
|
|
979
|
+
* (e.g. `await this.photoshop.activeDocument.name`) instead of hand-writing
|
|
980
|
+
* ExtendScript. Lazily built once and backed by this plugin's own jsx runner;
|
|
981
|
+
* drop to `this.jsx.run(...)` for anything the proxy does not cover.
|
|
982
|
+
*/
|
|
983
|
+
get photoshop() {
|
|
984
|
+
return this._photoshop ??= new PsPhotoshopProxy(this.jsx);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Attach the per-plugin client bus. Called by the server's assembler after
|
|
988
|
+
* construction and before `listen`, so `broadcast`/`send` are live by the time
|
|
989
|
+
* any handler can fire. Not part of the public Plugin authoring API.
|
|
990
|
+
*/
|
|
991
|
+
_attachBus(bus) {
|
|
992
|
+
this.bus = bus;
|
|
993
|
+
}
|
|
994
|
+
/** Push an Event to every online client of this plugin. */
|
|
995
|
+
broadcast(type, data) {
|
|
996
|
+
this.bus?.broadcast(type, data);
|
|
997
|
+
}
|
|
998
|
+
/** Push an Event to one client of this plugin (no-op if not connected). */
|
|
999
|
+
send(clientId, type, data) {
|
|
1000
|
+
this.bus?.send(clientId, type, data);
|
|
1001
|
+
}
|
|
1002
|
+
/** Called after a client handshake registers with this plugin. Default no-op. */
|
|
1003
|
+
onConnect(_clientId) {
|
|
1004
|
+
}
|
|
1005
|
+
/** Called after a client socket is removed from this plugin. Default no-op. */
|
|
1006
|
+
onDisconnect(_clientId) {
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
Object.defineProperty(BasePlugin.prototype, BASE_PLUGIN_BRAND, {
|
|
1010
|
+
value: true,
|
|
1011
|
+
enumerable: false,
|
|
1012
|
+
configurable: false,
|
|
1013
|
+
writable: false
|
|
1014
|
+
});
|
|
1015
|
+
function isBasePluginClass(S) {
|
|
1016
|
+
if (typeof S !== "function") return false;
|
|
1017
|
+
const proto = S.prototype;
|
|
1018
|
+
return proto != null && Boolean(proto[BASE_PLUGIN_BRAND]);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ../sdk/src/plugin/decorators.ts
|
|
1022
|
+
Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
|
|
1023
|
+
var METADATA = Symbol.metadata;
|
|
1024
|
+
var HANDLERS = /* @__PURE__ */ Symbol.for("ps-generator-bridge.handlers");
|
|
1025
|
+
function pushHandler(metadata, meta) {
|
|
1026
|
+
if (!Object.prototype.hasOwnProperty.call(metadata, HANDLERS)) {
|
|
1027
|
+
metadata[HANDLERS] = [];
|
|
1028
|
+
}
|
|
1029
|
+
metadata[HANDLERS].push(meta);
|
|
1030
|
+
}
|
|
1031
|
+
function ws(name) {
|
|
1032
|
+
return function(_value, context) {
|
|
1033
|
+
pushHandler(context.metadata, {
|
|
1034
|
+
kind: "ws",
|
|
1035
|
+
name,
|
|
1036
|
+
methodKey: String(context.name)
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function bootstrap(instance, target) {
|
|
1041
|
+
const ctor = instance.constructor;
|
|
1042
|
+
const collected = [];
|
|
1043
|
+
let metadata = ctor[METADATA];
|
|
1044
|
+
while (metadata) {
|
|
1045
|
+
if (Object.prototype.hasOwnProperty.call(metadata, HANDLERS)) {
|
|
1046
|
+
collected.push(...metadata[HANDLERS]);
|
|
1047
|
+
}
|
|
1048
|
+
metadata = Object.getPrototypeOf(metadata);
|
|
1049
|
+
}
|
|
1050
|
+
for (const meta of collected) {
|
|
1051
|
+
const fn = instance[meta.methodKey];
|
|
1052
|
+
if (typeof fn !== "function") continue;
|
|
1053
|
+
const bound = fn.bind(instance);
|
|
1054
|
+
if (meta.kind === "ws") {
|
|
1055
|
+
target.registerMethod(meta.name, bound);
|
|
1056
|
+
} else {
|
|
1057
|
+
target.registerApi({ method: meta.method, url: meta.url, handler: bound });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/server/clientStore.ts
|
|
1063
|
+
var ClientStore = class {
|
|
1064
|
+
constructor() {
|
|
1065
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Register `socket` under `clientId`. If an entry already exists (a reconnect
|
|
1069
|
+
* whose old socket is still half-open), the new connection takes over: the old
|
|
1070
|
+
* socket is closed and the entry replaced, but subscriptions are preserved.
|
|
1071
|
+
*/
|
|
1072
|
+
add(clientId, socket) {
|
|
1073
|
+
const existing = this.clients.get(clientId);
|
|
1074
|
+
const subscriptions = existing?.subscriptions ?? /* @__PURE__ */ new Set();
|
|
1075
|
+
const entry = { clientId, socket, connectedAt: Date.now(), subscriptions };
|
|
1076
|
+
this.clients.set(clientId, entry);
|
|
1077
|
+
if (existing && existing.socket !== socket) {
|
|
1078
|
+
try {
|
|
1079
|
+
existing.socket.close();
|
|
1080
|
+
} catch {
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return entry;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Drop the entry for `clientId` — but only if `socket` is still the current
|
|
1087
|
+
* one. A taken-over old socket firing `close` must not evict the live entry.
|
|
1088
|
+
*/
|
|
1089
|
+
remove(clientId, socket) {
|
|
1090
|
+
const entry = this.clients.get(clientId);
|
|
1091
|
+
if (entry && entry.socket === socket) {
|
|
1092
|
+
this.clients.delete(clientId);
|
|
1093
|
+
return entry;
|
|
1094
|
+
}
|
|
1095
|
+
return void 0;
|
|
1096
|
+
}
|
|
1097
|
+
subscribe(clientId, type) {
|
|
1098
|
+
const entry = this.clients.get(clientId);
|
|
1099
|
+
if (!entry || entry.subscriptions.has(type)) return false;
|
|
1100
|
+
entry.subscriptions.add(type);
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
unsubscribe(clientId, type) {
|
|
1104
|
+
const entry = this.clients.get(clientId);
|
|
1105
|
+
if (!entry || !entry.subscriptions.has(type)) return false;
|
|
1106
|
+
entry.subscriptions.delete(type);
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
/** Push an Event to one client (no-op if it is not connected). */
|
|
1110
|
+
emit(clientId, type, data) {
|
|
1111
|
+
const entry = this.clients.get(clientId);
|
|
1112
|
+
if (entry) {
|
|
1113
|
+
sendEvent(entry.socket, type, data);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/** Push an Event to every connected client. */
|
|
1117
|
+
broadcast(type, data) {
|
|
1118
|
+
for (const entry of this.clients.values()) {
|
|
1119
|
+
sendEvent(entry.socket, type, data);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/** Push an Event only to clients subscribed to that Event type. */
|
|
1123
|
+
broadcastSubscribed(type, data) {
|
|
1124
|
+
for (const entry of this.clients.values()) {
|
|
1125
|
+
if (entry.subscriptions.has(type)) {
|
|
1126
|
+
sendEvent(entry.socket, type, data);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
function sendEvent(socket, type, data) {
|
|
1132
|
+
socket.send(serializeFrame({ type, data }));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/server/scopedRegistry.ts
|
|
1136
|
+
var ScopedRegistry = class {
|
|
1137
|
+
constructor() {
|
|
1138
|
+
this.methods = new MethodTable();
|
|
1139
|
+
this.apiRoutes = [];
|
|
1140
|
+
}
|
|
1141
|
+
registerMethod(name, handler) {
|
|
1142
|
+
this.methods.register(name, handler);
|
|
1143
|
+
}
|
|
1144
|
+
registerApi(route) {
|
|
1145
|
+
this.apiRoutes.push(route);
|
|
1146
|
+
}
|
|
1147
|
+
/** The `@api` routes bootstrap collected, for the assembler to flush to fastify. */
|
|
1148
|
+
get routes() {
|
|
1149
|
+
return this.apiRoutes;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Dispatch a frame against the scoped table. Returns `undefined` when there is
|
|
1153
|
+
* no scoped handler (caller falls back to the global Registry) or the frame is
|
|
1154
|
+
* not a request (nothing to respond to). A found handler always yields an
|
|
1155
|
+
* envelope — including an error envelope — so only "not in this table" falls
|
|
1156
|
+
* through.
|
|
1157
|
+
*/
|
|
1158
|
+
async tryDispatch(message, ctx) {
|
|
1159
|
+
return this.methods.tryDispatch(message, ctx);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/server/pluginManager.ts
|
|
1164
|
+
var PluginManager = class {
|
|
1165
|
+
constructor(app) {
|
|
1166
|
+
this.app = app;
|
|
1167
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
1168
|
+
}
|
|
1169
|
+
/** All registered plugin ids, in registration order. */
|
|
1170
|
+
get ids() {
|
|
1171
|
+
return [...this.plugins.keys()];
|
|
1172
|
+
}
|
|
1173
|
+
/** The discovery list surfaced at `GET /plugins` and in `getServerInfo`. */
|
|
1174
|
+
list() {
|
|
1175
|
+
return [...this.plugins.values()].map((e) => ({ id: e.plugin.id }));
|
|
1176
|
+
}
|
|
1177
|
+
/** Look up a loaded plugin by id (used by the `/ws/:pluginId` route). */
|
|
1178
|
+
get(id) {
|
|
1179
|
+
return this.plugins.get(id);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Register a plugin: build its scoped table + ClientStore, attach the client
|
|
1183
|
+
* bus, bootstrap its handlers, and flush its `@api` routes to fastify. Throws
|
|
1184
|
+
* on a duplicate or illegal id. Must run before `listen()`.
|
|
1185
|
+
*/
|
|
1186
|
+
register(plugin) {
|
|
1187
|
+
const id = plugin.id;
|
|
1188
|
+
if (!isValidPluginId(id)) {
|
|
1189
|
+
throw new Error(`illegal plugin id: '${id}' (must match [A-Za-z0-9_-]+)`);
|
|
1190
|
+
}
|
|
1191
|
+
if (this.plugins.has(id)) {
|
|
1192
|
+
throw new Error(`duplicate plugin id: ${id}`);
|
|
1193
|
+
}
|
|
1194
|
+
const scoped = new ScopedRegistry();
|
|
1195
|
+
const clients = new ClientStore();
|
|
1196
|
+
const bus = {
|
|
1197
|
+
broadcast: (type, data) => clients.broadcast(type, data),
|
|
1198
|
+
send: (clientId, type, data) => clients.emit(clientId, type, data)
|
|
1199
|
+
};
|
|
1200
|
+
plugin._attachBus(bus);
|
|
1201
|
+
bootstrap(plugin, scoped);
|
|
1202
|
+
for (const route of scoped.routes) {
|
|
1203
|
+
this.app.route({
|
|
1204
|
+
method: route.method,
|
|
1205
|
+
url: `/${id}${route.url}`,
|
|
1206
|
+
handler: route.handler
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
const entry = { plugin, scoped, clients };
|
|
1210
|
+
this.plugins.set(id, entry);
|
|
1211
|
+
return entry;
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
function isValidPluginId(id) {
|
|
1215
|
+
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/utilis/eventManager.ts
|
|
1219
|
+
var import_node_events = require("events");
|
|
1220
|
+
var PHOTOSHOP_EVENTS = [
|
|
1221
|
+
"workspaceChanged",
|
|
1222
|
+
"toolChanged",
|
|
1223
|
+
"quickMaskStateChanged",
|
|
1224
|
+
"documentChanged",
|
|
1225
|
+
"closedDocument",
|
|
1226
|
+
"newDocumentViewCreated",
|
|
1227
|
+
"activeViewChanged",
|
|
1228
|
+
"currentDocumentChanged",
|
|
1229
|
+
"backgroundColorChanged",
|
|
1230
|
+
"foregroundColorChanged",
|
|
1231
|
+
"imageChanged"
|
|
1232
|
+
];
|
|
1233
|
+
var ALLOWED = new Set(PHOTOSHOP_EVENTS);
|
|
1234
|
+
var META = /* @__PURE__ */ new Set(["newListener", "removeListener", "error"]);
|
|
1235
|
+
var EventManager = class extends import_node_events.EventEmitter {
|
|
1236
|
+
constructor(generator) {
|
|
1237
|
+
super();
|
|
1238
|
+
this.generator = generator;
|
|
1239
|
+
// One bridge listener per active event, kept so we can detach the exact
|
|
1240
|
+
// function from the generator when the last consumer unsubscribes.
|
|
1241
|
+
this.bridges = /* @__PURE__ */ new Map();
|
|
1242
|
+
super.on("newListener", (event) => this.onAdd(event));
|
|
1243
|
+
super.on("removeListener", (event) => this.onRemove(event));
|
|
1244
|
+
}
|
|
1245
|
+
// --- typed surface (narrows EventEmitter's string|symbol signatures) ------
|
|
1246
|
+
on(event, listener) {
|
|
1247
|
+
return super.on(event, listener);
|
|
1248
|
+
}
|
|
1249
|
+
once(event, listener) {
|
|
1250
|
+
return super.once(event, listener);
|
|
1251
|
+
}
|
|
1252
|
+
addListener(event, listener) {
|
|
1253
|
+
return super.addListener(event, listener);
|
|
1254
|
+
}
|
|
1255
|
+
off(event, listener) {
|
|
1256
|
+
return super.off(event, listener);
|
|
1257
|
+
}
|
|
1258
|
+
removeListener(event, listener) {
|
|
1259
|
+
return super.removeListener(event, listener);
|
|
1260
|
+
}
|
|
1261
|
+
emit(event, ...args) {
|
|
1262
|
+
return super.emit(event, ...args);
|
|
1263
|
+
}
|
|
1264
|
+
// --- lazy subscribe / unsubscribe ----------------------------------------
|
|
1265
|
+
/** First listener for a PS event -> subscribe upstream once. */
|
|
1266
|
+
onAdd(event) {
|
|
1267
|
+
if (typeof event !== "string" || META.has(event)) return;
|
|
1268
|
+
if (!ALLOWED.has(event)) {
|
|
1269
|
+
throw new Error(`EventManager: unknown Photoshop event "${event}"`);
|
|
1270
|
+
}
|
|
1271
|
+
const key = event;
|
|
1272
|
+
if (this.listenerCount(key) !== 0) return;
|
|
1273
|
+
const bridge = (payload) => this.emit(key, payload);
|
|
1274
|
+
this.bridges.set(key, bridge);
|
|
1275
|
+
this.generator.onPhotoshopEvent(event, bridge);
|
|
1276
|
+
}
|
|
1277
|
+
/** Last listener for a PS event removed -> detach the upstream bridge. */
|
|
1278
|
+
onRemove(event) {
|
|
1279
|
+
if (typeof event !== "string" || META.has(event)) return;
|
|
1280
|
+
const key = event;
|
|
1281
|
+
if (this.listenerCount(key) !== 0) return;
|
|
1282
|
+
const bridge = this.bridges.get(key);
|
|
1283
|
+
if (!bridge) return;
|
|
1284
|
+
this.generator.removePhotoshopEventListener(event, bridge);
|
|
1285
|
+
this.bridges.delete(key);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/server/eventHub.ts
|
|
1290
|
+
var ALLOWED2 = new Set(PHOTOSHOP_EVENTS);
|
|
1291
|
+
var EventHub = class {
|
|
1292
|
+
constructor(events, clients) {
|
|
1293
|
+
this.events = events;
|
|
1294
|
+
this.clients = clients;
|
|
1295
|
+
this.refs = /* @__PURE__ */ new Map();
|
|
1296
|
+
this.bridges = /* @__PURE__ */ new Map();
|
|
1297
|
+
}
|
|
1298
|
+
subscribe(type) {
|
|
1299
|
+
const key = this.assertEvent(type);
|
|
1300
|
+
const next = (this.refs.get(key) ?? 0) + 1;
|
|
1301
|
+
this.refs.set(key, next);
|
|
1302
|
+
if (next !== 1) return;
|
|
1303
|
+
const bridge = (payload) => this.clients.broadcastSubscribed(type, payload);
|
|
1304
|
+
this.bridges.set(key, bridge);
|
|
1305
|
+
this.events.on(key, bridge);
|
|
1306
|
+
}
|
|
1307
|
+
unsubscribe(type) {
|
|
1308
|
+
const key = this.assertEvent(type);
|
|
1309
|
+
const current = this.refs.get(key) ?? 0;
|
|
1310
|
+
if (current <= 1) {
|
|
1311
|
+
this.refs.delete(key);
|
|
1312
|
+
const bridge = this.bridges.get(key);
|
|
1313
|
+
if (bridge) {
|
|
1314
|
+
this.events.off(key, bridge);
|
|
1315
|
+
this.bridges.delete(key);
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this.refs.set(key, current - 1);
|
|
1320
|
+
}
|
|
1321
|
+
assertEvent(type) {
|
|
1322
|
+
if (!ALLOWED2.has(type)) {
|
|
1323
|
+
throw new Error(`Unknown Photoshop event: ${type}`);
|
|
1324
|
+
}
|
|
1325
|
+
return type;
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// src/server/index.ts
|
|
1330
|
+
var DEFAULT_PORT = 7700;
|
|
1331
|
+
function createServer(options) {
|
|
1332
|
+
const { port, host = "127.0.0.1", generator, jsx, events, logger } = options;
|
|
1333
|
+
const app = (0, import_fastify.default)({ logger: false });
|
|
1334
|
+
let boundPort = 0;
|
|
1335
|
+
const pluginManager = new PluginManager(app);
|
|
1336
|
+
const registry = new Registry(app);
|
|
1337
|
+
registerBuiltins(registry, () => pluginManager.list());
|
|
1338
|
+
const rootClients = new ClientStore();
|
|
1339
|
+
const rootEvents = events ? new EventHub(events, rootClients) : void 0;
|
|
1340
|
+
app.get("/health", async () => ({ status: "ok" }));
|
|
1341
|
+
app.get("/plugins", async () => ({ plugins: pluginManager.list() }));
|
|
1342
|
+
app.register(import_websocket.default);
|
|
1343
|
+
app.register(async (instance) => {
|
|
1344
|
+
instance.get("/ws", { websocket: true }, (socket, req) => {
|
|
1345
|
+
const query = req.query;
|
|
1346
|
+
const requested = query?.id;
|
|
1347
|
+
const clientId = requested && requested.length > 0 ? requested : (0, import_node_crypto.randomUUID)();
|
|
1348
|
+
rootClients.add(clientId, socket);
|
|
1349
|
+
logger.info(`client connected: ${clientId} -> root`);
|
|
1350
|
+
socket.send(serializeFrame({ type: "connected", data: { clientId } }));
|
|
1351
|
+
const session = {
|
|
1352
|
+
clientId,
|
|
1353
|
+
subscribe: (type) => {
|
|
1354
|
+
const added = rootClients.subscribe(clientId, type);
|
|
1355
|
+
if (!added) return;
|
|
1356
|
+
try {
|
|
1357
|
+
rootEvents?.subscribe(type);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
rootClients.unsubscribe(clientId, type);
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
unsubscribe: (type) => {
|
|
1364
|
+
const removed = rootClients.unsubscribe(clientId, type);
|
|
1365
|
+
if (removed) rootEvents?.unsubscribe(type);
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const ctx = { generator, jsx, session };
|
|
1369
|
+
socket.on("message", (data) => {
|
|
1370
|
+
void handleRootFrame(socket, String(data), registry, ctx, logger);
|
|
1371
|
+
});
|
|
1372
|
+
socket.on("close", () => {
|
|
1373
|
+
const removed = rootClients.remove(clientId, socket);
|
|
1374
|
+
releaseSubscriptions(removed, rootEvents);
|
|
1375
|
+
logger.info(`client disconnected: ${clientId} -> root`);
|
|
1376
|
+
});
|
|
1377
|
+
socket.on("error", (error) => logger.error("socket error", error));
|
|
1378
|
+
});
|
|
1379
|
+
instance.get("/ws/:pluginId", { websocket: true }, (socket, req) => {
|
|
1380
|
+
const pluginId = req.params.pluginId;
|
|
1381
|
+
const entry = pluginManager.get(pluginId);
|
|
1382
|
+
if (!entry) {
|
|
1383
|
+
socket.send(
|
|
1384
|
+
serializeFrame({
|
|
1385
|
+
type: "error",
|
|
1386
|
+
data: { code: "UNKNOWN_PLUGIN", message: `unknown plugin: ${pluginId}`, pluginId }
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
socket.close();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
const query = req.query;
|
|
1393
|
+
const requested = query?.id;
|
|
1394
|
+
const clientId = requested && requested.length > 0 ? requested : (0, import_node_crypto.randomUUID)();
|
|
1395
|
+
entry.clients.add(clientId, socket);
|
|
1396
|
+
entry.plugin.onConnect(clientId);
|
|
1397
|
+
logger.info(`client connected: ${clientId} -> plugin ${pluginId}`);
|
|
1398
|
+
socket.send(serializeFrame({ type: "connected", data: { clientId } }));
|
|
1399
|
+
const ctx = { generator, jsx };
|
|
1400
|
+
socket.on("message", (data) => {
|
|
1401
|
+
void handlePluginFrame(socket, String(data), entry, registry, ctx, logger);
|
|
1402
|
+
});
|
|
1403
|
+
socket.on("close", () => {
|
|
1404
|
+
entry.clients.remove(clientId, socket);
|
|
1405
|
+
entry.plugin.onDisconnect(clientId);
|
|
1406
|
+
logger.info(`client disconnected: ${clientId} -> plugin ${pluginId}`);
|
|
1407
|
+
});
|
|
1408
|
+
socket.on("error", (error) => logger.error("socket error", error));
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
return {
|
|
1412
|
+
get port() {
|
|
1413
|
+
return boundPort;
|
|
1414
|
+
},
|
|
1415
|
+
registry,
|
|
1416
|
+
pluginManager,
|
|
1417
|
+
listen: async () => {
|
|
1418
|
+
await app.listen({ port, host });
|
|
1419
|
+
const address = app.server.address();
|
|
1420
|
+
boundPort = typeof address === "object" && address ? address.port : port;
|
|
1421
|
+
logger.info(
|
|
1422
|
+
`PS Generator Bridge server listening on http://${host}:${boundPort} (ws + /health + /plugins)`
|
|
1423
|
+
);
|
|
1424
|
+
},
|
|
1425
|
+
close: () => app.close()
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
async function handleRootFrame(socket, data, registry, ctx, logger) {
|
|
1429
|
+
let parsed;
|
|
1430
|
+
try {
|
|
1431
|
+
parsed = parseFrame(data);
|
|
1432
|
+
} catch {
|
|
1433
|
+
logger.warn("dropping non-JSON frame");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const response = await registry.dispatch(parsed, ctx);
|
|
1437
|
+
if (response) {
|
|
1438
|
+
socket.send(serializeFrame(response));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function releaseSubscriptions(entry, hub) {
|
|
1442
|
+
if (!entry || !hub) return;
|
|
1443
|
+
for (const type of entry.subscriptions) {
|
|
1444
|
+
hub.unsubscribe(type);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function handlePluginFrame(socket, data, entry, registry, ctx, logger) {
|
|
1448
|
+
let parsed;
|
|
1449
|
+
try {
|
|
1450
|
+
parsed = parseFrame(data);
|
|
1451
|
+
} catch {
|
|
1452
|
+
logger.warn("dropping non-JSON frame");
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const response = await entry.scoped.tryDispatch(parsed, ctx) ?? await registry.dispatch(parsed, ctx);
|
|
1456
|
+
if (response) {
|
|
1457
|
+
socket.send(serializeFrame(response));
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/server/pluginLoader.ts
|
|
1462
|
+
var import_node_fs = require("fs");
|
|
1463
|
+
var import_node_path = require("path");
|
|
1464
|
+
var import_node_url = require("url");
|
|
1465
|
+
async function loadPlugins(options) {
|
|
1466
|
+
const { pluginsDir, hostFor, knownIds, logger } = options;
|
|
1467
|
+
const loaded = [];
|
|
1468
|
+
const skipped = [];
|
|
1469
|
+
const taken = new Set(knownIds);
|
|
1470
|
+
let dirs;
|
|
1471
|
+
try {
|
|
1472
|
+
dirs = scanPluginDirs(pluginsDir);
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
logger.debug(`pluginsDir not loaded: ${pluginsDir} (${err.message})`);
|
|
1475
|
+
return { loaded, skipped };
|
|
1476
|
+
}
|
|
1477
|
+
for (const name of dirs) {
|
|
1478
|
+
const dir = (0, import_node_path.join)(pluginsDir, name);
|
|
1479
|
+
const outcome = await loadOne(dir, hostFor, taken);
|
|
1480
|
+
if (outcome.kind === "loaded") {
|
|
1481
|
+
loaded.push({ id: outcome.id, plugin: outcome.plugin, path: outcome.path });
|
|
1482
|
+
taken.add(outcome.id);
|
|
1483
|
+
logger.info(`plugin loaded: ${name} (${outcome.id})`);
|
|
1484
|
+
} else {
|
|
1485
|
+
skipped.push({ path: name, reason: outcome.reason });
|
|
1486
|
+
logger.warn(`plugin skipped: ${name} \u2014 ${outcome.reason}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return { loaded, skipped };
|
|
1490
|
+
}
|
|
1491
|
+
async function loadOne(dir, hostFor, taken) {
|
|
1492
|
+
let pkg;
|
|
1493
|
+
try {
|
|
1494
|
+
pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, "package.json"), "utf8"));
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
return {
|
|
1497
|
+
kind: "skipped",
|
|
1498
|
+
reason: `package.json missing or invalid: ${err.message}`
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
if (typeof pkg.main !== "string" || pkg.main.length === 0) {
|
|
1502
|
+
return { kind: "skipped", reason: 'package.json has no "main"' };
|
|
1503
|
+
}
|
|
1504
|
+
const root = (0, import_node_path.resolve)(dir);
|
|
1505
|
+
const entry = (0, import_node_path.resolve)(root, pkg.main);
|
|
1506
|
+
if (entry !== root && !entry.startsWith(root + import_node_path.sep)) {
|
|
1507
|
+
return { kind: "skipped", reason: `"main" escapes the plugin directory: ${pkg.main}` };
|
|
1508
|
+
}
|
|
1509
|
+
let mod;
|
|
1510
|
+
try {
|
|
1511
|
+
mod = await import((0, import_node_url.pathToFileURL)(entry).href);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
return { kind: "skipped", reason: `load failed: ${err.message}` };
|
|
1514
|
+
}
|
|
1515
|
+
let S = mod.default ?? mod;
|
|
1516
|
+
if (S !== null && typeof S === "object") {
|
|
1517
|
+
const inner = S.default;
|
|
1518
|
+
if (inner !== void 0) S = inner;
|
|
1519
|
+
}
|
|
1520
|
+
if (!isBasePluginClass(S)) {
|
|
1521
|
+
return { kind: "skipped", reason: "default export is not a BasePlugin subclass" };
|
|
1522
|
+
}
|
|
1523
|
+
const id = S.id;
|
|
1524
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1525
|
+
return { kind: "skipped", reason: "missing static id" };
|
|
1526
|
+
}
|
|
1527
|
+
if (!isValidPluginId(id)) {
|
|
1528
|
+
return { kind: "skipped", reason: `illegal id '${id}' (must match [A-Za-z0-9_-]+)` };
|
|
1529
|
+
}
|
|
1530
|
+
if (taken.has(id)) {
|
|
1531
|
+
return { kind: "skipped", reason: `duplicate id '${id}'` };
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
const host = hostFor(dir);
|
|
1535
|
+
const plugin = new S(id, host);
|
|
1536
|
+
return { kind: "loaded", id, plugin, path: entry };
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
return { kind: "skipped", reason: `construct failed: ${err.message}` };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function scanPluginDirs(dir) {
|
|
1542
|
+
return (0, import_node_fs.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/utilis/jsxRunner.ts
|
|
1546
|
+
var import_promises = require("fs/promises");
|
|
1547
|
+
var import_node_path2 = require("path");
|
|
1548
|
+
var ERROR_PREFIX = "Error:";
|
|
1549
|
+
var JsxRunner = class {
|
|
1550
|
+
constructor(generator, logger, polyfillsDir = (0, import_node_path2.join)(__dirname, "..", "jsx", "polyfills")) {
|
|
1551
|
+
this.generator = generator;
|
|
1552
|
+
this.logger = logger;
|
|
1553
|
+
this.polyfillsDir = polyfillsDir;
|
|
1554
|
+
this.jsxDir = (0, import_node_path2.join)(__dirname, "..", "jsx");
|
|
1555
|
+
this.polyfillsCache = "";
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* A jsx runner scoped to a plugin's own `jsx/` dir (RFC 0005). The returned
|
|
1559
|
+
* handle's `execute` resolves under `dir`, while `executeBuiltin` still targets
|
|
1560
|
+
* the built-in tree; `run` is unchanged. The scope is the *only* per-plugin
|
|
1561
|
+
* state — there is no shared mutable registry; the host builds one of these per
|
|
1562
|
+
* plugin in `hostFor` and injects it as `plugin.jsx`.
|
|
1563
|
+
*/
|
|
1564
|
+
forPlugin(dir) {
|
|
1565
|
+
return new ScopedJsx(this, dir);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Resolve a jsx name to an absolute `.jsx` path under `baseDir`. The name may
|
|
1569
|
+
* carry domain subdirs (e.g. `Document/getDocumentInfo`). No escape guard: jsx
|
|
1570
|
+
* names come from trusted in-process code (a module or a plugin's own source),
|
|
1571
|
+
* which already runs arbitrary JS, so a guard would add no boundary.
|
|
1572
|
+
*/
|
|
1573
|
+
resolvePath(baseDir, name) {
|
|
1574
|
+
return (0, import_node_path2.join)(baseDir, `${name}.jsx`);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Prime the default ExtendScript engine with ES polyfills. Reads every
|
|
1578
|
+
* `*.js` file under `polyfillsDir` (recursively, sorted by relative path for
|
|
1579
|
+
* deterministic concatenation order), concatenates them into `polyfillsCache`,
|
|
1580
|
+
* and evaluates the bundle once. Must be awaited before any `execute` call
|
|
1581
|
+
* that depends on the polyfills; `PsBridgeHost.onInit` does this before
|
|
1582
|
+
* `server.listen`.
|
|
1583
|
+
*
|
|
1584
|
+
* Missing dir -> throw (packaging bug). Empty dir -> no-op. Injection
|
|
1585
|
+
* returning `"Error:…"` or rejecting -> throw, so a broken polyfill surfaces
|
|
1586
|
+
* at startup rather than as a runtime `find is not a function`.
|
|
1587
|
+
*/
|
|
1588
|
+
async init() {
|
|
1589
|
+
const files = await this.collectPolyfillFiles();
|
|
1590
|
+
if (files.length === 0) {
|
|
1591
|
+
this.logger.debug("polyfills dir empty, skipping injection");
|
|
1592
|
+
this.polyfillsCache = "";
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const parts = await Promise.all(files.map((file) => (0, import_promises.readFile)(file, "utf8")));
|
|
1596
|
+
this.polyfillsCache = parts.join("\n");
|
|
1597
|
+
const value = await this.generator.evaluateJSXString(this.polyfillsCache);
|
|
1598
|
+
if (typeof value === "string" && value.startsWith(ERROR_PREFIX)) {
|
|
1599
|
+
throw new Error(value.slice(ERROR_PREFIX.length));
|
|
1600
|
+
}
|
|
1601
|
+
this.logger.debug(`polyfills injected: ${files.length} files`);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Run the jsx registered under `name` in the built-in `jsx/` tree (domain
|
|
1605
|
+
* subdirs included, e.g. `Document/getDocumentInfo`). `params` are inlined into
|
|
1606
|
+
* the script; `sharedEngineSafe` opts into Photoshop's shared script engine.
|
|
1607
|
+
* The root runner's own scope *is* the built-in tree, so `execute` and
|
|
1608
|
+
* `executeBuiltin` coincide here; a plugin's scoped view (see `forPlugin`)
|
|
1609
|
+
* splits them apart.
|
|
1610
|
+
*/
|
|
1611
|
+
async execute(name, params, sharedEngineSafe) {
|
|
1612
|
+
return this.executeIn(this.jsxDir, name, params, sharedEngineSafe);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Alias of `execute` on the root runner — always the built-in tree. Present so
|
|
1616
|
+
* the root satisfies `JsxRunnerApi` alongside the scoped view; the scoped view
|
|
1617
|
+
* overrides `execute` (plugin dir) while delegating `executeBuiltin` here.
|
|
1618
|
+
*/
|
|
1619
|
+
async executeBuiltin(name, params, sharedEngineSafe) {
|
|
1620
|
+
return this.execute(name, params, sharedEngineSafe);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Run the jsx under `name` resolved against `baseDir`. The seam through which
|
|
1624
|
+
* both the root runner (built-in tree) and the scoped view (a plugin's dir)
|
|
1625
|
+
* reach Photoshop; keeps path resolution + result normalization in one place.
|
|
1626
|
+
*
|
|
1627
|
+
* @internal Server-internal — not on `JsxRunnerApi`, not reachable by plugins.
|
|
1628
|
+
* Public only so the sibling `ScopedJsx` can delegate to it.
|
|
1629
|
+
*/
|
|
1630
|
+
async executeIn(baseDir, name, params, sharedEngineSafe) {
|
|
1631
|
+
const path = this.resolvePath(baseDir, name);
|
|
1632
|
+
const value = await this.generator.evaluateJSXFile(path, params, sharedEngineSafe);
|
|
1633
|
+
return this.normalizeJsxResult(value);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Evaluate an arbitrary jsx string in the default ExtendScript engine (the
|
|
1637
|
+
* same engine `init()` primed with polyfills). No `sharedEngineSafe` opt-out:
|
|
1638
|
+
* polyfills live only in the default engine, so a shared-engine variant would
|
|
1639
|
+
* be silently un-polyfilled. Return value follows the same convention as
|
|
1640
|
+
* `run` — verbatim, with `"Error:"`-prefixed strings turned into thrown
|
|
1641
|
+
* Errors.
|
|
1642
|
+
*/
|
|
1643
|
+
async run(script) {
|
|
1644
|
+
const value = await this.generator.evaluateJSXString(script);
|
|
1645
|
+
return this.normalizeJsxResult(value);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Open a built-in packaged jsx file via the low-level `_sendJSXFile` channel
|
|
1649
|
+
* and return a typed handle to its in-flight evaluation. A root-runner-only
|
|
1650
|
+
* seam (not on `JsxRunnerApi`): the only caller is `ImageModule.getPixmap`.
|
|
1651
|
+
* Plugins reach pixmaps through `plugin.modules.image`, not this channel.
|
|
1652
|
+
*
|
|
1653
|
+
* Unlike `run` (which
|
|
1654
|
+
* awaits a single resolved value), this exposes the raw progress stream so
|
|
1655
|
+
* the caller can collect the multi-message responses Photoshop emits for
|
|
1656
|
+
* pixmap-producing scripts (bounds + pixmap + optional ICC profile). The
|
|
1657
|
+
* caller owns completion: it must call `channel.resolve()` once it has
|
|
1658
|
+
* received every message it expected, or `channel.reject(err)` on failure.
|
|
1659
|
+
*
|
|
1660
|
+
* `sharedEngineSafe` defaults to `true` to match the pixmap protocol
|
|
1661
|
+
* (generator-core's `getPixmap` / `getDocumentPixmap` both use the shared
|
|
1662
|
+
* engine).
|
|
1663
|
+
*/
|
|
1664
|
+
openJSXFile(name, params, sharedEngineSafe = true) {
|
|
1665
|
+
const path = this.resolvePath(this.jsxDir, name);
|
|
1666
|
+
const deferred = this.generator._sendJSXFile(path, params, sharedEngineSafe);
|
|
1667
|
+
return {
|
|
1668
|
+
onProgress: (fn) => {
|
|
1669
|
+
deferred.promise.progress(fn);
|
|
1670
|
+
},
|
|
1671
|
+
onFail: (fn) => {
|
|
1672
|
+
deferred.promise.fail(fn);
|
|
1673
|
+
},
|
|
1674
|
+
resolve: () => {
|
|
1675
|
+
deferred.resolve();
|
|
1676
|
+
},
|
|
1677
|
+
reject: (err) => {
|
|
1678
|
+
deferred.reject(err);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Shared result normalization for `run` and `execute`: a string starting with
|
|
1684
|
+
* `"Error:"` becomes a thrown `Error` carrying the remainder; everything else
|
|
1685
|
+
* is returned verbatim (no `JSON.parse` — `T` is a labelling convenience).
|
|
1686
|
+
*/
|
|
1687
|
+
normalizeJsxResult(value) {
|
|
1688
|
+
if (typeof value === "string" && value.startsWith(ERROR_PREFIX)) {
|
|
1689
|
+
throw new Error(value.slice(ERROR_PREFIX.length));
|
|
1690
|
+
}
|
|
1691
|
+
return value;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Recursively list `*.js` files under `polyfillsDir`, sorted by relative path
|
|
1695
|
+
* (POSIX-normalized) so concatenation order is stable across platforms and
|
|
1696
|
+
* polyfills that depend on each other load in a fixed sequence. Throws if the
|
|
1697
|
+
* directory itself is missing.
|
|
1698
|
+
*/
|
|
1699
|
+
async collectPolyfillFiles() {
|
|
1700
|
+
const discovered = [];
|
|
1701
|
+
const walk = async (dir) => {
|
|
1702
|
+
let entries;
|
|
1703
|
+
try {
|
|
1704
|
+
entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
if (dir === this.polyfillsDir) {
|
|
1707
|
+
throw new Error(`polyfills dir not found: ${this.polyfillsDir}`);
|
|
1708
|
+
}
|
|
1709
|
+
throw error;
|
|
1710
|
+
}
|
|
1711
|
+
for (const entry of entries) {
|
|
1712
|
+
const full = (0, import_node_path2.join)(dir, entry.name);
|
|
1713
|
+
if (entry.isDirectory()) {
|
|
1714
|
+
await walk(full);
|
|
1715
|
+
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
1716
|
+
discovered.push(full);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
await walk(this.polyfillsDir);
|
|
1721
|
+
return discovered.sort((a, b) => {
|
|
1722
|
+
const ra = (0, import_node_path2.relative)(this.polyfillsDir, a).split(import_node_path2.sep).join("/");
|
|
1723
|
+
const rb = (0, import_node_path2.relative)(this.polyfillsDir, b).split(import_node_path2.sep).join("/");
|
|
1724
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
var ScopedJsx = class {
|
|
1729
|
+
constructor(root, baseDir) {
|
|
1730
|
+
this.root = root;
|
|
1731
|
+
this.baseDir = baseDir;
|
|
1732
|
+
}
|
|
1733
|
+
execute(name, params, sharedEngineSafe) {
|
|
1734
|
+
return this.root.executeIn(this.baseDir, name, params, sharedEngineSafe);
|
|
1735
|
+
}
|
|
1736
|
+
executeBuiltin(name, params, sharedEngineSafe) {
|
|
1737
|
+
return this.root.execute(name, params, sharedEngineSafe);
|
|
1738
|
+
}
|
|
1739
|
+
run(script) {
|
|
1740
|
+
return this.root.run(script);
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// src/modules/base.ts
|
|
1745
|
+
var BaseModule = class {
|
|
1746
|
+
constructor(name, plugin) {
|
|
1747
|
+
this.name = name;
|
|
1748
|
+
this.plugin = plugin;
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// src/modules/action/index.ts
|
|
1753
|
+
var _removeBackground_dec, _autoCutout_dec, _a, _init;
|
|
1754
|
+
var ActionModule = class extends (_a = BaseModule, _autoCutout_dec = [ws(ProtocolMethod.ActionAutoCutout)], _removeBackground_dec = [ws(ProtocolMethod.ActionRemoveBackground)], _a) {
|
|
1755
|
+
constructor(plugin) {
|
|
1756
|
+
super("action", plugin);
|
|
1757
|
+
__runInitializers(_init, 5, this);
|
|
1758
|
+
}
|
|
1759
|
+
async autoCutout() {
|
|
1760
|
+
await this.plugin.jsx.execute("Action/autoCutout");
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
async removeBackground() {
|
|
1764
|
+
const result = await this.plugin.jsx.execute("Action/removeBackground");
|
|
1765
|
+
return { success: result };
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
_init = __decoratorStart(_a);
|
|
1769
|
+
__decorateElement(_init, 1, "autoCutout", _autoCutout_dec, ActionModule);
|
|
1770
|
+
__decorateElement(_init, 1, "removeBackground", _removeBackground_dec, ActionModule);
|
|
1771
|
+
__decoratorMetadata(_init, ActionModule);
|
|
1772
|
+
|
|
1773
|
+
// src/modules/document/index.ts
|
|
1774
|
+
var _saveDocument_dec, _exportDocument_dec, _getCurrentDocument_dec, _a2, _init2;
|
|
1775
|
+
var DocumentModule = class extends (_a2 = BaseModule, _getCurrentDocument_dec = [ws(ProtocolMethod.DocumentCurrent)], _exportDocument_dec = [ws(ProtocolMethod.DocumentExport)], _saveDocument_dec = [ws(ProtocolMethod.DocumentSave)], _a2) {
|
|
1776
|
+
constructor(plugin) {
|
|
1777
|
+
super("document", plugin);
|
|
1778
|
+
__runInitializers(_init2, 5, this);
|
|
1779
|
+
this.currentDocument = null;
|
|
1780
|
+
}
|
|
1781
|
+
async getCurrentDocument() {
|
|
1782
|
+
const data = await this.plugin.jsx.execute("Document/getDocumentInfo");
|
|
1783
|
+
if (!data) throw new Error("No document is opened");
|
|
1784
|
+
return data;
|
|
1785
|
+
}
|
|
1786
|
+
async exportDocument(params) {
|
|
1787
|
+
if (!params.filePath) throw new Error("filePath is required");
|
|
1788
|
+
return await this.plugin.jsx.execute("Document/exportDocument", params);
|
|
1789
|
+
}
|
|
1790
|
+
async saveDocument(params) {
|
|
1791
|
+
return await this.plugin.jsx.execute("Document/saveDocument", params);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
_init2 = __decoratorStart(_a2);
|
|
1795
|
+
__decorateElement(_init2, 1, "getCurrentDocument", _getCurrentDocument_dec, DocumentModule);
|
|
1796
|
+
__decorateElement(_init2, 1, "exportDocument", _exportDocument_dec, DocumentModule);
|
|
1797
|
+
__decorateElement(_init2, 1, "saveDocument", _saveDocument_dec, DocumentModule);
|
|
1798
|
+
__decoratorMetadata(_init2, DocumentModule);
|
|
1799
|
+
|
|
1800
|
+
// src/modules/layer/index.ts
|
|
1801
|
+
var _getLayerInfoByIndex_dec, _getLayerInfoByID_dec, _getLayerInfo_dec, _a3, _init3;
|
|
1802
|
+
var LayerModule = class extends (_a3 = BaseModule, _getLayerInfo_dec = [ws(ProtocolMethod.LayerGetInfo)], _getLayerInfoByID_dec = [ws(ProtocolMethod.LayerGetInfoById)], _getLayerInfoByIndex_dec = [ws(ProtocolMethod.LayerGetInfoByIndex)], _a3) {
|
|
1803
|
+
constructor(plugin) {
|
|
1804
|
+
super("layer", plugin);
|
|
1805
|
+
__runInitializers(_init3, 5, this);
|
|
1806
|
+
}
|
|
1807
|
+
async getLayerInfo(options) {
|
|
1808
|
+
return await this.plugin.jsx.execute("Layer/getLayerInfo", {
|
|
1809
|
+
layerID: options?.id,
|
|
1810
|
+
layerIndex: options?.index,
|
|
1811
|
+
getChildren: options?.getChildren,
|
|
1812
|
+
getGeneratorSettings: options?.getGeneratorSettings
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
getLayerInfoByID(layerIDOrParams, options) {
|
|
1816
|
+
const layerID = typeof layerIDOrParams === "number" ? layerIDOrParams : layerIDOrParams.layerID;
|
|
1817
|
+
const resolvedOptions = typeof layerIDOrParams === "number" ? options : layerIDOrParams.options;
|
|
1818
|
+
if (layerID == null) throw new Error("Invalid layerID");
|
|
1819
|
+
const params = {
|
|
1820
|
+
layerID,
|
|
1821
|
+
getChildren: resolvedOptions?.getChildren
|
|
1822
|
+
};
|
|
1823
|
+
return this.plugin.jsx.execute("Layer/getLayerInfo", params);
|
|
1824
|
+
}
|
|
1825
|
+
getLayerInfoByIndex(layerIndexOrParams, options) {
|
|
1826
|
+
const layerIndex = typeof layerIndexOrParams === "number" ? layerIndexOrParams : layerIndexOrParams.layerIndex;
|
|
1827
|
+
const resolvedOptions = typeof layerIndexOrParams === "number" ? options : layerIndexOrParams.options;
|
|
1828
|
+
if (layerIndex == null) throw new Error("Invalid layerIndex");
|
|
1829
|
+
const params = {
|
|
1830
|
+
layerIndex,
|
|
1831
|
+
getChildren: resolvedOptions?.getChildren
|
|
1832
|
+
};
|
|
1833
|
+
return this.plugin.jsx.execute("Layer/getLayerInfo", params);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
_init3 = __decoratorStart(_a3);
|
|
1837
|
+
__decorateElement(_init3, 1, "getLayerInfo", _getLayerInfo_dec, LayerModule);
|
|
1838
|
+
__decorateElement(_init3, 1, "getLayerInfoByID", _getLayerInfoByID_dec, LayerModule);
|
|
1839
|
+
__decorateElement(_init3, 1, "getLayerInfoByIndex", _getLayerInfoByIndex_dec, LayerModule);
|
|
1840
|
+
__decoratorMetadata(_init3, LayerModule);
|
|
1841
|
+
|
|
1842
|
+
// src/modules/image/index.ts
|
|
1843
|
+
var import_sharp = __toESM(require("sharp"));
|
|
1844
|
+
|
|
1845
|
+
// src/utilis/pixmap.ts
|
|
1846
|
+
var Pixmap = class {
|
|
1847
|
+
constructor(buffer) {
|
|
1848
|
+
this.format = buffer.readUInt8(0);
|
|
1849
|
+
this.width = buffer.readUInt32BE(1);
|
|
1850
|
+
this.height = buffer.readUInt32BE(5);
|
|
1851
|
+
this.rowBytes = buffer.readUInt32BE(9);
|
|
1852
|
+
this.colorMode = buffer.readUInt8(13);
|
|
1853
|
+
this.channelCount = buffer.readUInt8(14);
|
|
1854
|
+
this.bitsPerChannel = buffer.readUInt8(15);
|
|
1855
|
+
this.pixels = buffer.slice(16, 16 + this.width * this.height * this.channelCount);
|
|
1856
|
+
this.bytesPerPixel = this.bitsPerChannel / 8 * this.channelCount;
|
|
1857
|
+
this.padding = this.rowBytes - this.width * this.channelCount;
|
|
1858
|
+
this.readChannel = this.getReadChannel(this.bitsPerChannel);
|
|
1859
|
+
this._initGetPixelMethod(this.channelCount);
|
|
1860
|
+
}
|
|
1861
|
+
getReadChannel(bitsPerChannel) {
|
|
1862
|
+
if (16 === bitsPerChannel) {
|
|
1863
|
+
return Buffer.prototype.readUInt16BE;
|
|
1864
|
+
}
|
|
1865
|
+
if (8 === bitsPerChannel) {
|
|
1866
|
+
return Buffer.prototype.readUInt8;
|
|
1867
|
+
}
|
|
1868
|
+
if (32 === bitsPerChannel) {
|
|
1869
|
+
return Buffer.prototype.readUInt32BE;
|
|
1870
|
+
}
|
|
1871
|
+
throw new Error(`Unsupported pixmap bit depth: ${bitsPerChannel}`);
|
|
1872
|
+
}
|
|
1873
|
+
getPixel1(n) {
|
|
1874
|
+
var pixel = this.getRawPixel(n);
|
|
1875
|
+
var grey = this.readChannel.call(pixel, 0);
|
|
1876
|
+
return {
|
|
1877
|
+
r: grey,
|
|
1878
|
+
g: grey,
|
|
1879
|
+
b: grey,
|
|
1880
|
+
a: 255
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
getPixel3(n) {
|
|
1884
|
+
var pixel = this.getRawPixel(n);
|
|
1885
|
+
return {
|
|
1886
|
+
r: this.readChannel.call(pixel, 2),
|
|
1887
|
+
g: this.readChannel.call(pixel, 1),
|
|
1888
|
+
b: this.readChannel.call(pixel),
|
|
1889
|
+
a: 255
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
getPixel4(n) {
|
|
1893
|
+
var pixel = this.getRawPixel(n);
|
|
1894
|
+
return {
|
|
1895
|
+
r: this.readChannel.call(pixel, 1),
|
|
1896
|
+
g: this.readChannel.call(pixel, 2),
|
|
1897
|
+
b: this.readChannel.call(pixel, 3),
|
|
1898
|
+
a: this.readChannel.call(pixel, 0)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
_initGetPixelMethod(channelCount) {
|
|
1902
|
+
if (channelCount === 4) {
|
|
1903
|
+
this.getPixel = this.getPixel4;
|
|
1904
|
+
}
|
|
1905
|
+
if (channelCount === 3) {
|
|
1906
|
+
this.getPixel = this.getPixel3;
|
|
1907
|
+
}
|
|
1908
|
+
if (channelCount === 1) {
|
|
1909
|
+
this.getPixel = this.getPixel1;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
getRawPixel(n) {
|
|
1913
|
+
var i = n * this.bytesPerPixel;
|
|
1914
|
+
return this.pixels.slice(i, i + this.bytesPerPixel);
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
|
|
1918
|
+
// src/modules/image/index.ts
|
|
1919
|
+
var _exportDocumentWs_dec, _getPreviewWs_dec, _exportLayerWs_dec, _a4, _init4;
|
|
1920
|
+
var ImageModule = class extends (_a4 = BaseModule, _exportLayerWs_dec = [ws(ProtocolMethod.ImageExportLayer)], _getPreviewWs_dec = [ws(ProtocolMethod.ImageGetPreview)], _exportDocumentWs_dec = [ws(ProtocolMethod.ImageExportDocument)], _a4) {
|
|
1921
|
+
constructor(plugin) {
|
|
1922
|
+
super("image", plugin);
|
|
1923
|
+
__runInitializers(_init4, 5, this);
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Export a single layer as a PNG buffer plus its bounds and pixel
|
|
1927
|
+
* dimensions. `settings` carries the `GetPixmapSettings` Photoshop accepts;
|
|
1928
|
+
* the four `include*` flags default to `true` when unspecified (the fix over
|
|
1929
|
+
* generator-core). `layerSpec` is required — the underlying jsx needs an
|
|
1930
|
+
* explicit layer id or index range.
|
|
1931
|
+
*/
|
|
1932
|
+
async exportImage(options) {
|
|
1933
|
+
const { documentId, layerSpec, settings = {} } = options;
|
|
1934
|
+
const resolvedDocId = this.resolveDocumentId(documentId);
|
|
1935
|
+
const pixmap = await this.getPixmap(resolvedDocId, layerSpec, settings);
|
|
1936
|
+
const buffer = await this.encodePng(pixmap);
|
|
1937
|
+
return {
|
|
1938
|
+
buffer,
|
|
1939
|
+
bounds: pixmap.bounds,
|
|
1940
|
+
width: pixmap.width,
|
|
1941
|
+
height: pixmap.height
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Export a downscaled preview of a single layer. The scale is computed so
|
|
1946
|
+
* the longer edge lands near 300px; scaling is done by Photoshop via
|
|
1947
|
+
* `scaleX/scaleY`, not by `sharp`. `includeClipped/ClipBase/Adjustors` are
|
|
1948
|
+
* forced to `false` to fetch only the body layer's pixels. `layerSpec` is
|
|
1949
|
+
* required (a layer id; the layer's `rect` drives the scale).
|
|
1950
|
+
*/
|
|
1951
|
+
async getPreview(options) {
|
|
1952
|
+
const { documentId, layerSpec } = options;
|
|
1953
|
+
const resolvedDocId = this.resolveDocumentId(documentId);
|
|
1954
|
+
const settings = {};
|
|
1955
|
+
const layer = await this.plugin.modules.layer.getLayerInfoByID(layerSpec);
|
|
1956
|
+
if (!layer?.rect) throw new Error("Invalid layer info for preview");
|
|
1957
|
+
settings.includeClipped = false;
|
|
1958
|
+
settings.includeClipBase = false;
|
|
1959
|
+
settings.includeAdjustors = false;
|
|
1960
|
+
const scale = getScale(layer.rect.width, layer.rect.height);
|
|
1961
|
+
settings.scaleX = scale;
|
|
1962
|
+
settings.scaleY = scale;
|
|
1963
|
+
const pixmap = await this.getPixmap(resolvedDocId, layerSpec, settings);
|
|
1964
|
+
const buffer = await this.encodePng(pixmap);
|
|
1965
|
+
return {
|
|
1966
|
+
buffer,
|
|
1967
|
+
bounds: pixmap.bounds,
|
|
1968
|
+
width: pixmap.width,
|
|
1969
|
+
height: pixmap.height
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Export the whole document (its current visibility state, flattened) as a PNG
|
|
1974
|
+
* buffer plus bounds and pixel dimensions. Unlike `exportImage` (a single layer
|
|
1975
|
+
* via the `Layer/getLayerPixmap` jsx protocol), this uses generator-core's
|
|
1976
|
+
* built-in `getDocumentPixmap`, which returns an already-parsed `PsPixmap`.
|
|
1977
|
+
* `documentId` defaults to the current document; `settings` carries the
|
|
1978
|
+
* `GetPixmapSettings` Photoshop accepts (e.g. `scaleX`/`scaleY`).
|
|
1979
|
+
*/
|
|
1980
|
+
async exportDocument(options) {
|
|
1981
|
+
const documentId = this.resolveDocumentId(options.documentId);
|
|
1982
|
+
const pixmap = await this.plugin.generator.getDocumentPixmap(
|
|
1983
|
+
documentId,
|
|
1984
|
+
options.settings ?? {}
|
|
1985
|
+
);
|
|
1986
|
+
const buffer = await this.encodePng(pixmap);
|
|
1987
|
+
return {
|
|
1988
|
+
buffer,
|
|
1989
|
+
bounds: pixmap.bounds,
|
|
1990
|
+
width: pixmap.width,
|
|
1991
|
+
height: pixmap.height
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
async exportLayerWs(options) {
|
|
1995
|
+
const result = await this.exportImage(options);
|
|
1996
|
+
const name = this.plugin.cos ? await this.resolveLayerName(options.layerSpec) : void 0;
|
|
1997
|
+
return this.toWsResult(result, { upload: true }, name);
|
|
1998
|
+
}
|
|
1999
|
+
async getPreviewWs(options) {
|
|
2000
|
+
const result = await this.getPreview(options);
|
|
2001
|
+
return this.toWsResult(result, { upload: false });
|
|
2002
|
+
}
|
|
2003
|
+
async exportDocumentWs(options) {
|
|
2004
|
+
const result = await this.exportDocument(options);
|
|
2005
|
+
const name = this.plugin.cos ? this.resolveDocumentName(options.documentId) : void 0;
|
|
2006
|
+
return this.toWsResult(result, { upload: true }, name);
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Turn a module-internal {@link ImageResult} (raw PNG `buffer`) into the
|
|
2010
|
+
* wire-friendly {@link WsImageResult} (`data` string). With `upload` set and
|
|
2011
|
+
* `plugin.cos` enabled, the buffer is uploaded and `data` is the signed URL;
|
|
2012
|
+
* otherwise `data` is a `data:image/png;base64,...` URI. Both forms drop
|
|
2013
|
+
* straight into an `<img src>`.
|
|
2014
|
+
*/
|
|
2015
|
+
async toWsResult(result, opts, name) {
|
|
2016
|
+
let data;
|
|
2017
|
+
if (opts.upload && this.plugin.cos) {
|
|
2018
|
+
data = await this.plugin.cos.uploadObject(result.buffer, name);
|
|
2019
|
+
} else {
|
|
2020
|
+
data = "data:image/png;base64," + Buffer.from(result.buffer).toString("base64");
|
|
2021
|
+
}
|
|
2022
|
+
return {
|
|
2023
|
+
data,
|
|
2024
|
+
bounds: result.bounds,
|
|
2025
|
+
width: result.width,
|
|
2026
|
+
height: result.height
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Resolve a layer's name for the COS object key. A numeric `layerSpec` is
|
|
2031
|
+
* looked up via the layer module; an index-range spec has no single name, so it
|
|
2032
|
+
* falls back to "layers". Lookup failures degrade to `layer-{id}` rather than
|
|
2033
|
+
* failing the export.
|
|
2034
|
+
*/
|
|
2035
|
+
async resolveLayerName(layerSpec) {
|
|
2036
|
+
if (typeof layerSpec !== "number") return "layers";
|
|
2037
|
+
try {
|
|
2038
|
+
const layer = await this.plugin.modules.layer.getLayerInfoByID(layerSpec);
|
|
2039
|
+
return layer?.name || `layer-${layerSpec}`;
|
|
2040
|
+
} catch {
|
|
2041
|
+
return `layer-${layerSpec}`;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Resolve a document's name for the COS object key. Uses the current document's
|
|
2046
|
+
* name when the target is the current document; otherwise falls back to
|
|
2047
|
+
* `doc-{id}` rather than spending a jsx round-trip to name an off-screen doc.
|
|
2048
|
+
*/
|
|
2049
|
+
resolveDocumentName(documentId) {
|
|
2050
|
+
const resolvedId = this.resolveDocumentId(documentId);
|
|
2051
|
+
const current = this.plugin.modules.document.currentDocument;
|
|
2052
|
+
if (current && current.id === resolvedId && current.name) return current.name;
|
|
2053
|
+
return `doc-${resolvedId}`;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Faithful port of LightAi `LayerManager.getPixmap` (index.ts:364-482).
|
|
2057
|
+
* Builds the params (dropping generator-core's stray `settings.thread` and
|
|
2058
|
+
* defaulting the four `include*` flags to `true`), opens the pixmap jsx over
|
|
2059
|
+
* the progress channel, and resolves three native promises as the
|
|
2060
|
+
* bounds/pixmap/iccProfile messages arrive. Once all expected messages are
|
|
2061
|
+
* in, signals the channel to settle and constructs a `Pixmap`.
|
|
2062
|
+
*/
|
|
2063
|
+
async getPixmap(documentId, layerSpec, settings) {
|
|
2064
|
+
const params = {
|
|
2065
|
+
documentId,
|
|
2066
|
+
layerSpec,
|
|
2067
|
+
compId: settings.compId,
|
|
2068
|
+
compIndex: settings.compIndex,
|
|
2069
|
+
inputRect: settings.inputRect,
|
|
2070
|
+
outputRect: settings.outputRect,
|
|
2071
|
+
scaleX: settings.scaleX || 1,
|
|
2072
|
+
scaleY: settings.scaleY || 1,
|
|
2073
|
+
bounds: true,
|
|
2074
|
+
boundsOnly: settings.boundsOnly,
|
|
2075
|
+
useJPGEncoding: settings.useJPGEncoding || "",
|
|
2076
|
+
useSmartScaling: settings.useSmartScaling || false,
|
|
2077
|
+
includeAncestorMasks: settings.includeAncestorMasks || false,
|
|
2078
|
+
convertToWorkingRGBProfile: settings.convertToWorkingRGBProfile || false,
|
|
2079
|
+
useICCProfile: settings.useICCProfile || "",
|
|
2080
|
+
getICCProfileData: settings.getICCProfileData || false,
|
|
2081
|
+
allowDither: settings.allowDither || false,
|
|
2082
|
+
useColorSettingsDither: settings.useColorSettingsDither || false,
|
|
2083
|
+
interpolationType: settings.interpolationType,
|
|
2084
|
+
forceSmartPSDPixelScaling: settings.forceSmartPSDPixelScaling || false,
|
|
2085
|
+
clipToDocumentBounds: settings.clipToDocumentBounds || false,
|
|
2086
|
+
maxDimension: settings.maxDimension || 1e4,
|
|
2087
|
+
clipBounds: settings.clipBounds,
|
|
2088
|
+
includeAdjustors: settings.includeAdjustors !== void 0 ? settings.includeAdjustors : true,
|
|
2089
|
+
includeChildren: settings.includeChildren !== void 0 ? settings.includeChildren : true,
|
|
2090
|
+
includeClipBase: settings.includeClipBase !== void 0 ? settings.includeClipBase : true,
|
|
2091
|
+
includeClipped: settings.includeClipped !== void 0 ? settings.includeClipped : true
|
|
2092
|
+
};
|
|
2093
|
+
const channel = this.plugin.jsx.openJSXFile("Layer/getLayerPixmap", params, true);
|
|
2094
|
+
let jsResolve;
|
|
2095
|
+
let jsReject;
|
|
2096
|
+
const jsPromise = new Promise((res, rej) => {
|
|
2097
|
+
jsResolve = res;
|
|
2098
|
+
jsReject = rej;
|
|
2099
|
+
});
|
|
2100
|
+
let pixmapResolve;
|
|
2101
|
+
let pixmapReject;
|
|
2102
|
+
const pixmapPromise = new Promise((res, rej) => {
|
|
2103
|
+
pixmapResolve = res;
|
|
2104
|
+
pixmapReject = rej;
|
|
2105
|
+
});
|
|
2106
|
+
let profileResolve;
|
|
2107
|
+
let profileReject;
|
|
2108
|
+
const profilePromise = new Promise((res, rej) => {
|
|
2109
|
+
profileResolve = res;
|
|
2110
|
+
profileReject = rej;
|
|
2111
|
+
});
|
|
2112
|
+
channel.onProgress((message) => {
|
|
2113
|
+
if (message.type === "javascript") {
|
|
2114
|
+
if (message.value instanceof Object && Object.prototype.hasOwnProperty.call(message.value, "bounds")) {
|
|
2115
|
+
jsResolve(message.value);
|
|
2116
|
+
}
|
|
2117
|
+
} else if (message.type === "pixmap") {
|
|
2118
|
+
pixmapResolve(message.value);
|
|
2119
|
+
} else if (message.type === "iccProfile") {
|
|
2120
|
+
profileResolve(message.value);
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
channel.onFail((err) => {
|
|
2124
|
+
jsReject(err);
|
|
2125
|
+
pixmapReject(err);
|
|
2126
|
+
profileReject(err);
|
|
2127
|
+
});
|
|
2128
|
+
if (params.boundsOnly) {
|
|
2129
|
+
pixmapResolve(void 0);
|
|
2130
|
+
profileResolve(void 0);
|
|
2131
|
+
}
|
|
2132
|
+
if (!params.getICCProfileData) {
|
|
2133
|
+
profileResolve(void 0);
|
|
2134
|
+
}
|
|
2135
|
+
const [js, iccProfileBuffer, pixmapBuffer] = await Promise.all([
|
|
2136
|
+
jsPromise,
|
|
2137
|
+
profilePromise,
|
|
2138
|
+
pixmapPromise
|
|
2139
|
+
]);
|
|
2140
|
+
channel.resolve();
|
|
2141
|
+
if (params.boundsOnly && js && js.bounds) {
|
|
2142
|
+
return js;
|
|
2143
|
+
}
|
|
2144
|
+
if (js && js.bounds && pixmapBuffer) {
|
|
2145
|
+
const pixmap = new Pixmap(pixmapBuffer);
|
|
2146
|
+
pixmap.bounds = js.bounds;
|
|
2147
|
+
if (iccProfileBuffer) {
|
|
2148
|
+
pixmap.iccProfile = iccProfileBuffer;
|
|
2149
|
+
}
|
|
2150
|
+
return pixmap;
|
|
2151
|
+
}
|
|
2152
|
+
throw new Error(
|
|
2153
|
+
`Unexpected response from PS in getLayerPixmap: js=${JSON.stringify(js)}, pixmap=${pixmapBuffer ? "truthy" : "falsy"}, iccExpected=${params.getICCProfileData}`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Resolve the document id: explicit override, else the document module's
|
|
2158
|
+
* current document, else fail loud (matches LightAi's "No document opened").
|
|
2159
|
+
*/
|
|
2160
|
+
resolveDocumentId(documentId) {
|
|
2161
|
+
if (documentId !== void 0) return documentId;
|
|
2162
|
+
const current = this.plugin.modules.document.currentDocument;
|
|
2163
|
+
if (current) return current.id;
|
|
2164
|
+
throw new Error("No document opened");
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Convert a pixmap's raw pixels into a tightly-packed RGBA buffer. Handles both
|
|
2168
|
+
* the single-layer protocol (`getLayerPixmap`: 4-channel, no row padding) and
|
|
2169
|
+
* generator-core's `getDocumentPixmap` (which may return 3-channel pixmaps and
|
|
2170
|
+
* rows padded to `rowBytes`). 4-channel is Photoshop's `[A,R,G,B]` layout;
|
|
2171
|
+
* 3-channel is `[R,G,B]` and gets an opaque alpha. Rows are walked by
|
|
2172
|
+
* `rowBytes` so any per-row padding is skipped rather than misaligning the image.
|
|
2173
|
+
*/
|
|
2174
|
+
parseRawPixels(pixmap) {
|
|
2175
|
+
const { width, height, channelCount, pixels } = pixmap;
|
|
2176
|
+
if (channelCount !== 3 && channelCount !== 4) {
|
|
2177
|
+
throw new Error(`Unsupported channelCount: ${channelCount}`);
|
|
2178
|
+
}
|
|
2179
|
+
const rowBytes = pixmap.rowBytes && pixmap.rowBytes >= width * channelCount ? pixmap.rowBytes : width * channelCount;
|
|
2180
|
+
const output = Buffer.allocUnsafe(width * height * 4);
|
|
2181
|
+
let o = 0;
|
|
2182
|
+
for (let y = 0; y < height; y++) {
|
|
2183
|
+
let p = y * rowBytes;
|
|
2184
|
+
for (let x = 0; x < width; x++) {
|
|
2185
|
+
if (channelCount === 4) {
|
|
2186
|
+
output[o++] = pixels.readUInt8(p + 1);
|
|
2187
|
+
output[o++] = pixels.readUInt8(p + 2);
|
|
2188
|
+
output[o++] = pixels.readUInt8(p + 3);
|
|
2189
|
+
output[o++] = pixels.readUInt8(p);
|
|
2190
|
+
} else {
|
|
2191
|
+
output[o++] = pixels.readUInt8(p);
|
|
2192
|
+
output[o++] = pixels.readUInt8(p + 1);
|
|
2193
|
+
output[o++] = pixels.readUInt8(p + 2);
|
|
2194
|
+
output[o++] = 255;
|
|
2195
|
+
}
|
|
2196
|
+
p += channelCount;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return output;
|
|
2200
|
+
}
|
|
2201
|
+
async encodePng(pixmap) {
|
|
2202
|
+
const rgba = this.parseRawPixels(pixmap);
|
|
2203
|
+
return (0, import_sharp.default)(rgba, {
|
|
2204
|
+
raw: {
|
|
2205
|
+
width: pixmap.width,
|
|
2206
|
+
height: pixmap.height,
|
|
2207
|
+
channels: 4
|
|
2208
|
+
}
|
|
2209
|
+
}).ensureAlpha().png().toBuffer();
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
_init4 = __decoratorStart(_a4);
|
|
2213
|
+
__decorateElement(_init4, 1, "exportLayerWs", _exportLayerWs_dec, ImageModule);
|
|
2214
|
+
__decorateElement(_init4, 1, "getPreviewWs", _getPreviewWs_dec, ImageModule);
|
|
2215
|
+
__decorateElement(_init4, 1, "exportDocumentWs", _exportDocumentWs_dec, ImageModule);
|
|
2216
|
+
__decoratorMetadata(_init4, ImageModule);
|
|
2217
|
+
var getScale = (width, height) => {
|
|
2218
|
+
const xScale = Math.floor(width / 300) || 1;
|
|
2219
|
+
const yScale = Math.floor(height / 300) || 1;
|
|
2220
|
+
return 1 / Math.min(xScale, yScale);
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
// src/modules/index.ts
|
|
2224
|
+
var MODULES = {
|
|
2225
|
+
layer: LayerModule,
|
|
2226
|
+
document: DocumentModule,
|
|
2227
|
+
action: ActionModule,
|
|
2228
|
+
image: ImageModule
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// src/services/cos.ts
|
|
2232
|
+
var import_cos_nodejs_sdk_v5 = __toESM(require("cos-nodejs-sdk-v5"));
|
|
2233
|
+
var import_node_path3 = require("path");
|
|
2234
|
+
var DEFAULT_KEY_PREFIX = "ps-bridge/exports";
|
|
2235
|
+
var DEFAULT_URL_EXPIRES_SECONDS = 31536e4;
|
|
2236
|
+
var MAX_NAME_LENGTH = 64;
|
|
2237
|
+
var CosService = class _CosService {
|
|
2238
|
+
constructor(config, logger) {
|
|
2239
|
+
this.config = config;
|
|
2240
|
+
this.logger = logger;
|
|
2241
|
+
this.cos = new import_cos_nodejs_sdk_v5.default({ SecretId: config.secretId, SecretKey: config.secretKey });
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Build a CosService from the environment, or return undefined when COS is not
|
|
2245
|
+
* configured. All four `PS_BRIDGE_COS_SECRET_ID/SECRET_KEY/BUCKET/REGION` must be
|
|
2246
|
+
* present and non-empty — a missing field means "not enabled", decided once at
|
|
2247
|
+
* startup rather than failing loudly on the first upload.
|
|
2248
|
+
*/
|
|
2249
|
+
static fromEnv(logger) {
|
|
2250
|
+
const secretId = process.env.PS_BRIDGE_COS_SECRET_ID?.trim();
|
|
2251
|
+
const secretKey = process.env.PS_BRIDGE_COS_SECRET_KEY?.trim();
|
|
2252
|
+
const bucket = process.env.PS_BRIDGE_COS_BUCKET?.trim();
|
|
2253
|
+
const region = process.env.PS_BRIDGE_COS_REGION?.trim();
|
|
2254
|
+
if (!secretId || !secretKey || !bucket || !region) return void 0;
|
|
2255
|
+
const keyPrefix = process.env.PS_BRIDGE_COS_KEY_PREFIX?.trim() || DEFAULT_KEY_PREFIX;
|
|
2256
|
+
const expiresRaw = Number(process.env.PS_BRIDGE_COS_URL_EXPIRES);
|
|
2257
|
+
const urlExpires = Number.isFinite(expiresRaw) && expiresRaw > 0 ? expiresRaw : DEFAULT_URL_EXPIRES_SECONDS;
|
|
2258
|
+
return new _CosService({ secretId, secretKey, bucket, region, keyPrefix, urlExpires }, logger);
|
|
2259
|
+
}
|
|
2260
|
+
async uploadObject(data, name) {
|
|
2261
|
+
const key = this.buildKey(name, ".png");
|
|
2262
|
+
await this.putObject(key, Buffer.from(data));
|
|
2263
|
+
return this.signedUrl(key);
|
|
2264
|
+
}
|
|
2265
|
+
async uploadFile(dir, name) {
|
|
2266
|
+
const key = this.buildKey(name, (0, import_node_path3.extname)(dir));
|
|
2267
|
+
await this.putFile(key, dir);
|
|
2268
|
+
return this.signedUrl(key);
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Compose the object key `{keyPrefix}/{name}-{ts}{ext}` (keyPrefix is env-
|
|
2272
|
+
* configurable, default `ps-bridge/exports`). `name` (a layer/document name) is
|
|
2273
|
+
* kept verbatim including non-ASCII (e.g. Chinese); only path separators and
|
|
2274
|
+
* whitespace are replaced — they would nest the key or break the URL — and the
|
|
2275
|
+
* label is length-capped. Uniqueness rides on the timestamp, not the label.
|
|
2276
|
+
*/
|
|
2277
|
+
buildKey(name, ext) {
|
|
2278
|
+
const label = this.sanitizeName(name ?? "image");
|
|
2279
|
+
return `${this.config.keyPrefix}/${label}-${Date.now()}${ext}`;
|
|
2280
|
+
}
|
|
2281
|
+
sanitizeName(name) {
|
|
2282
|
+
const cleaned = name.replace(/[/\\\s]+/g, "_").replace(/^_+/, "").slice(0, MAX_NAME_LENGTH);
|
|
2283
|
+
return cleaned || "image";
|
|
2284
|
+
}
|
|
2285
|
+
putObject(key, body) {
|
|
2286
|
+
return new Promise((resolve2, reject) => {
|
|
2287
|
+
this.cos.putObject(
|
|
2288
|
+
{ Bucket: this.config.bucket, Region: this.config.region, Key: key, Body: body },
|
|
2289
|
+
(err, data) => {
|
|
2290
|
+
if (err) return reject(new Error(err.message ?? String(err)));
|
|
2291
|
+
if (data.statusCode !== 200) {
|
|
2292
|
+
return reject(new Error(`COS upload failed: status ${data.statusCode}`));
|
|
2293
|
+
}
|
|
2294
|
+
this.logger.info(`CosService uploaded object ${key}`);
|
|
2295
|
+
resolve2();
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
putFile(key, filePath) {
|
|
2301
|
+
return new Promise((resolve2, reject) => {
|
|
2302
|
+
this.cos.uploadFile(
|
|
2303
|
+
{ Bucket: this.config.bucket, Region: this.config.region, Key: key, FilePath: filePath },
|
|
2304
|
+
(err) => {
|
|
2305
|
+
if (err) return reject(new Error(err.message ?? String(err)));
|
|
2306
|
+
this.logger.info(`CosService uploaded file ${key}`);
|
|
2307
|
+
resolve2();
|
|
2308
|
+
}
|
|
2309
|
+
);
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
signedUrl(key) {
|
|
2313
|
+
return new Promise((resolve2, reject) => {
|
|
2314
|
+
this.cos.getObjectUrl(
|
|
2315
|
+
{
|
|
2316
|
+
Bucket: this.config.bucket,
|
|
2317
|
+
Region: this.config.region,
|
|
2318
|
+
Key: key,
|
|
2319
|
+
Sign: true,
|
|
2320
|
+
Expires: this.config.urlExpires
|
|
2321
|
+
},
|
|
2322
|
+
(err, data) => {
|
|
2323
|
+
if (err) return reject(err instanceof Error ? err : new Error(String(err)));
|
|
2324
|
+
resolve2(data.Url);
|
|
2325
|
+
}
|
|
2326
|
+
);
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// src/plugin.ts
|
|
2332
|
+
var MENU_ID = "psGeneratorBridge";
|
|
2333
|
+
var MENU_LABEL = "PS Generator Bridge: Server";
|
|
2334
|
+
var MENU_EVENT = "generatorMenuChanged";
|
|
2335
|
+
var PsBridgeHost = class _PsBridgeHost {
|
|
2336
|
+
constructor(generator, config, logger, overrides) {
|
|
2337
|
+
this.generator = generator;
|
|
2338
|
+
this.config = config;
|
|
2339
|
+
this.logger = logger;
|
|
2340
|
+
this.plugins = [];
|
|
2341
|
+
this.modules = {
|
|
2342
|
+
layer: new MODULES.layer(this),
|
|
2343
|
+
document: new MODULES.document(this),
|
|
2344
|
+
action: new MODULES.action(this),
|
|
2345
|
+
image: new MODULES.image(this)
|
|
2346
|
+
};
|
|
2347
|
+
this._jsx = new JsxRunner(generator, logger, overrides?.polyfillsDir);
|
|
2348
|
+
this._events = new EventManager(generator);
|
|
2349
|
+
this.cos = CosService.fromEnv(logger);
|
|
2350
|
+
logger.info(this.cos ? "CosService enabled" : "CosService disabled (env incomplete)");
|
|
2351
|
+
}
|
|
2352
|
+
/** Run packaged jsx by name (ADR 0008). Used by modules and other server callers. */
|
|
2353
|
+
get jsx() {
|
|
2354
|
+
return this._jsx;
|
|
2355
|
+
}
|
|
2356
|
+
/** Photoshop event subscriptions owned by the host. */
|
|
2357
|
+
get events() {
|
|
2358
|
+
return this._events;
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Build the host contract for one plugin (RFC 0005). A shallow view that shares
|
|
2362
|
+
* the host's `modules` and `events` (both global-singleton semantics — they do
|
|
2363
|
+
* not split per plugin) but swaps in a `jsx` scoped to `<pluginDir>/jsx`, so the
|
|
2364
|
+
* plugin's `jsx.execute("x")` resolves to its own files while `executeBuiltin`
|
|
2365
|
+
* still reaches the built-in tree. Passed to `loadPlugins` as the `hostFor`
|
|
2366
|
+
* factory; the plugin never sees the concrete `PsBridgeHost`.
|
|
2367
|
+
*/
|
|
2368
|
+
hostFor(pluginDir) {
|
|
2369
|
+
return {
|
|
2370
|
+
modules: this.modules,
|
|
2371
|
+
events: this._events,
|
|
2372
|
+
jsx: this._jsx.forPlugin((0, import_node_path4.join)(pluginDir, "jsx")),
|
|
2373
|
+
cos: this.cos
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
/** Entry point: construct the host and run its async initialization. */
|
|
2377
|
+
static async init(generator, config, logger, overrides) {
|
|
2378
|
+
const host = new _PsBridgeHost(generator, config, logger, overrides);
|
|
2379
|
+
await host.onInit();
|
|
2380
|
+
return host;
|
|
2381
|
+
}
|
|
2382
|
+
async onInit() {
|
|
2383
|
+
this.logger.info(`${PLUGIN_NAME} v${PLUGIN_VERSION} initializing`);
|
|
2384
|
+
this.createMenuItem();
|
|
2385
|
+
const port = this.config.port ?? portFromEnv(this.logger) ?? DEFAULT_PORT;
|
|
2386
|
+
const server = createServer({
|
|
2387
|
+
port,
|
|
2388
|
+
generator: this.generator,
|
|
2389
|
+
jsx: this._jsx,
|
|
2390
|
+
events: this._events,
|
|
2391
|
+
logger: this.logger
|
|
2392
|
+
});
|
|
2393
|
+
this.server = server;
|
|
2394
|
+
const pluginsDir = this.config.pluginsDir ?? process.env.PS_BRIDGE_PLUGINS_DIR ?? (0, import_node_path4.join)(__dirname, "..", "plugins");
|
|
2395
|
+
const { loaded, skipped } = await loadPlugins({
|
|
2396
|
+
pluginsDir,
|
|
2397
|
+
hostFor: (pluginDir) => this.hostFor(pluginDir),
|
|
2398
|
+
knownIds: /* @__PURE__ */ new Set(),
|
|
2399
|
+
logger: this.logger
|
|
2400
|
+
});
|
|
2401
|
+
for (const s of skipped) this.logger.warn(`plugin skipped: ${s.path} \u2014 ${s.reason}`);
|
|
2402
|
+
this.plugins = loaded.map((l) => l.plugin);
|
|
2403
|
+
for (const plugin of this.plugins) {
|
|
2404
|
+
server.pluginManager.register(plugin);
|
|
2405
|
+
}
|
|
2406
|
+
server.registry.reservedSegments = new Set(server.pluginManager.ids);
|
|
2407
|
+
for (const module2 of Object.values(this.modules)) {
|
|
2408
|
+
bootstrap(module2, server.registry);
|
|
2409
|
+
}
|
|
2410
|
+
await this._jsx.init();
|
|
2411
|
+
await server.listen();
|
|
2412
|
+
this.logger.info(`${PLUGIN_NAME} initialized`);
|
|
2413
|
+
}
|
|
2414
|
+
createMenuItem() {
|
|
2415
|
+
this.generator.addMenuItem(MENU_ID, MENU_LABEL, true, false);
|
|
2416
|
+
this.generator.onPhotoshopEvent(
|
|
2417
|
+
MENU_EVENT,
|
|
2418
|
+
(event) => this.handleMenuClicked(event)
|
|
2419
|
+
);
|
|
2420
|
+
this.logger.debug(`menu item registered: ${MENU_ID}`);
|
|
2421
|
+
}
|
|
2422
|
+
// "generatorMenuChanged" fires for *every* plugin's menu, so we must filter to
|
|
2423
|
+
// our own id before acting.
|
|
2424
|
+
handleMenuClicked(event) {
|
|
2425
|
+
if (event?.generatorMenuChanged?.name !== MENU_ID) return;
|
|
2426
|
+
const port = this.server?.port ?? this.config.port ?? DEFAULT_PORT;
|
|
2427
|
+
this.generator.alert(
|
|
2428
|
+
`${PLUGIN_NAME} v${PLUGIN_VERSION} \u2014 listening on ws://127.0.0.1:${port}/ws`
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
/** Stop the WebSocket service (used by tests; PS teardown is process exit). */
|
|
2432
|
+
async close() {
|
|
2433
|
+
await this.server?.close();
|
|
2434
|
+
this.server = void 0;
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
function portFromEnv(logger) {
|
|
2438
|
+
const raw = process.env.PS_BRIDGE_PORT;
|
|
2439
|
+
if (!raw) return void 0;
|
|
2440
|
+
const port = Number(raw);
|
|
2441
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) return port;
|
|
2442
|
+
logger.warn(`ignoring invalid PS_BRIDGE_PORT: ${raw}`);
|
|
2443
|
+
return void 0;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/utilis/logger.ts
|
|
2447
|
+
function format(args) {
|
|
2448
|
+
return args.map((arg) => {
|
|
2449
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
2450
|
+
if (typeof arg === "object" && arg !== null) return JSON.stringify(arg);
|
|
2451
|
+
return String(arg);
|
|
2452
|
+
}).join(" ");
|
|
2453
|
+
}
|
|
2454
|
+
function createLogger(name = "ps-bridge") {
|
|
2455
|
+
const emit = (level, message, args) => {
|
|
2456
|
+
const line = `[${level}] ${name}: ${message}${args.length ? ` ${format(args)}` : ""}`;
|
|
2457
|
+
if (level === "error") console.error(line);
|
|
2458
|
+
else if (level === "warn") console.warn(line);
|
|
2459
|
+
else console.log(line);
|
|
2460
|
+
};
|
|
2461
|
+
return {
|
|
2462
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
2463
|
+
info: (message, ...args) => emit("info", message, args),
|
|
2464
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
2465
|
+
error: (message, ...args) => emit("error", message, args)
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// src/index.ts
|
|
2470
|
+
function init(generator, config) {
|
|
2471
|
+
const logger = createLogger();
|
|
2472
|
+
void PsBridgeHost.init(generator, config ?? {}, logger).catch(
|
|
2473
|
+
(error) => logger.error("plugin init failed", error)
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2477
|
+
0 && (module.exports = {
|
|
2478
|
+
JsxRunner,
|
|
2479
|
+
PsBridgeHost,
|
|
2480
|
+
init
|
|
2481
|
+
});
|
|
2482
|
+
//# sourceMappingURL=index.js.map
|