psforge 0.2.0__py3-none-any.whl
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.
- psforge/__init__.py +5 -0
- psforge/app.py +5 -0
- psforge/decorators.py +91 -0
- psforge/ps_adapter/__init__.py +12 -0
- psforge/ps_adapter/action_manager.py +5 -0
- psforge/ps_adapter/application.py +149 -0
- psforge/ps_adapter/context.py +219 -0
- psforge/ps_adapter/process_guard.py +109 -0
- psforge/ps_adapter/utils.py +80 -0
- psforge/registry.py +121 -0
- psforge/resources/__init__.py +3 -0
- psforge/resources/registry.py +3 -0
- psforge/server.py +79 -0
- psforge/tools/__init__.py +3 -0
- psforge/tools/action_tools.py +149 -0
- psforge/tools/adjustment_tools.py +316 -0
- psforge/tools/batch_tools.py +124 -0
- psforge/tools/document_tools.py +341 -0
- psforge/tools/filter_tools.py +252 -0
- psforge/tools/history_tools.py +241 -0
- psforge/tools/image_tools.py +201 -0
- psforge/tools/layer_ordering_tools.py +306 -0
- psforge/tools/layer_properties_tools.py +364 -0
- psforge/tools/layer_tools.py +316 -0
- psforge/tools/layer_transform_tools.py +331 -0
- psforge/tools/mask_tools.py +286 -0
- psforge/tools/registry.py +6 -0
- psforge/tools/selection_tools.py +248 -0
- psforge/tools/session_tools.py +244 -0
- psforge/tools/text_tools.py +390 -0
- psforge-0.2.0.dist-info/METADATA +594 -0
- psforge-0.2.0.dist-info/RECORD +35 -0
- psforge-0.2.0.dist-info/WHEEL +4 -0
- psforge-0.2.0.dist-info/entry_points.txt +3 -0
- psforge-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Layer ordering tools - move layers up/down/top/bottom, position relative to others."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from psforge.decorators import debug_tool, log_tool_call
|
|
8
|
+
from psforge.ps_adapter.application import PhotoshopApp
|
|
9
|
+
from psforge.ps_adapter.utils import js_escape_string
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all layer ordering tools with MCP server.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp: MCP server instance.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of registered tool names.
|
|
21
|
+
"""
|
|
22
|
+
registered_tools = []
|
|
23
|
+
|
|
24
|
+
@debug_tool
|
|
25
|
+
@log_tool_call
|
|
26
|
+
def move_layer_up() -> dict[str, Any]:
|
|
27
|
+
"""Move the currently active layer up one position in the layer stack.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
dict: Operation result and context.
|
|
31
|
+
"""
|
|
32
|
+
ps_app = PhotoshopApp()
|
|
33
|
+
doc = ps_app.get_active_document()
|
|
34
|
+
|
|
35
|
+
if not doc:
|
|
36
|
+
return {
|
|
37
|
+
"success": False,
|
|
38
|
+
"error": "No active document",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
move_script = """
|
|
43
|
+
var layer = app.activeDocument.activeLayer;
|
|
44
|
+
var layerName = layer.name;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
layer.move(layer.parent.layers[layer.itemIndex - 2], ElementPlacement.PLACEBEFORE);
|
|
48
|
+
} catch(e) {
|
|
49
|
+
throw new Error("Cannot move layer up - already at top or invalid position");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
layerName;
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"success": True,
|
|
59
|
+
"message": f"Moved layer '{layer_name}' up one position",
|
|
60
|
+
"layer_name": layer_name,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
error_msg = str(e)
|
|
65
|
+
if "already at top" in error_msg.lower() or "invalid position" in error_msg.lower():
|
|
66
|
+
return {
|
|
67
|
+
"success": False,
|
|
68
|
+
"error": "Layer is already at the top or cannot be moved",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.error(f"Failed to move layer up: {e}")
|
|
72
|
+
return {
|
|
73
|
+
"success": False,
|
|
74
|
+
"error": str(e),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@debug_tool
|
|
78
|
+
@log_tool_call
|
|
79
|
+
def move_layer_down() -> dict[str, Any]:
|
|
80
|
+
"""Move the currently active layer down one position in the layer stack.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
dict: Operation result and context.
|
|
84
|
+
"""
|
|
85
|
+
ps_app = PhotoshopApp()
|
|
86
|
+
doc = ps_app.get_active_document()
|
|
87
|
+
|
|
88
|
+
if not doc:
|
|
89
|
+
return {
|
|
90
|
+
"success": False,
|
|
91
|
+
"error": "No active document",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
move_script = """
|
|
96
|
+
var layer = app.activeDocument.activeLayer;
|
|
97
|
+
var layerName = layer.name;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
layer.move(layer.parent.layers[layer.itemIndex], ElementPlacement.PLACEAFTER);
|
|
101
|
+
} catch(e) {
|
|
102
|
+
throw new Error("Cannot move layer down - already at bottom or invalid position");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
layerName;
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"success": True,
|
|
112
|
+
"message": f"Moved layer '{layer_name}' down one position",
|
|
113
|
+
"layer_name": layer_name,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
error_msg = str(e)
|
|
118
|
+
if "already at bottom" in error_msg.lower() or "invalid position" in error_msg.lower():
|
|
119
|
+
return {
|
|
120
|
+
"success": False,
|
|
121
|
+
"error": "Layer is already at the bottom or cannot be moved",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.error(f"Failed to move layer down: {e}")
|
|
125
|
+
return {
|
|
126
|
+
"success": False,
|
|
127
|
+
"error": str(e),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@debug_tool
|
|
131
|
+
@log_tool_call
|
|
132
|
+
def move_layer_to_top() -> dict[str, Any]:
|
|
133
|
+
"""Move the currently active layer to the top of the layer stack.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict: Operation result and context.
|
|
137
|
+
"""
|
|
138
|
+
ps_app = PhotoshopApp()
|
|
139
|
+
doc = ps_app.get_active_document()
|
|
140
|
+
|
|
141
|
+
if not doc:
|
|
142
|
+
return {
|
|
143
|
+
"success": False,
|
|
144
|
+
"error": "No active document",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
move_script = """
|
|
149
|
+
var doc = app.activeDocument;
|
|
150
|
+
var layer = doc.activeLayer;
|
|
151
|
+
var layerName = layer.name;
|
|
152
|
+
|
|
153
|
+
// Move to top (before first layer)
|
|
154
|
+
layer.move(doc.layers[0], ElementPlacement.PLACEBEFORE);
|
|
155
|
+
|
|
156
|
+
layerName;
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"success": True,
|
|
163
|
+
"message": f"Moved layer '{layer_name}' to top",
|
|
164
|
+
"layer_name": layer_name,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Failed to move layer to top: {e}")
|
|
169
|
+
return {
|
|
170
|
+
"success": False,
|
|
171
|
+
"error": str(e),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@debug_tool
|
|
175
|
+
@log_tool_call
|
|
176
|
+
def move_layer_to_bottom() -> dict[str, Any]:
|
|
177
|
+
"""Move the currently active layer to the bottom of the layer stack (above background if exists).
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
dict: Operation result and context.
|
|
181
|
+
"""
|
|
182
|
+
ps_app = PhotoshopApp()
|
|
183
|
+
doc = ps_app.get_active_document()
|
|
184
|
+
|
|
185
|
+
if not doc:
|
|
186
|
+
return {
|
|
187
|
+
"success": False,
|
|
188
|
+
"error": "No active document",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
move_script = """
|
|
193
|
+
var doc = app.activeDocument;
|
|
194
|
+
var layer = doc.activeLayer;
|
|
195
|
+
var layerName = layer.name;
|
|
196
|
+
|
|
197
|
+
// Move to bottom (after last layer)
|
|
198
|
+
var targetLayer = doc.layers[doc.layers.length - 1];
|
|
199
|
+
layer.move(targetLayer, ElementPlacement.PLACEAFTER);
|
|
200
|
+
|
|
201
|
+
layerName;
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"success": True,
|
|
208
|
+
"message": f"Moved layer '{layer_name}' to bottom",
|
|
209
|
+
"layer_name": layer_name,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Failed to move layer to bottom: {e}")
|
|
214
|
+
return {
|
|
215
|
+
"success": False,
|
|
216
|
+
"error": str(e),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@debug_tool
|
|
220
|
+
@log_tool_call
|
|
221
|
+
def move_layer_to_position(target_layer_name: str, position: str = "ABOVE") -> dict[str, Any]:
|
|
222
|
+
"""Move the currently active layer relative to a target layer.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
target_layer_name: Name of the layer to position relative to.
|
|
226
|
+
position: Position relative to target - "ABOVE" or "BELOW" (default: "ABOVE").
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
dict: Operation result and context.
|
|
230
|
+
"""
|
|
231
|
+
position = position.upper()
|
|
232
|
+
if position not in ["ABOVE", "BELOW"]:
|
|
233
|
+
return {
|
|
234
|
+
"success": False,
|
|
235
|
+
"error": "position must be 'ABOVE' or 'BELOW'",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
ps_app = PhotoshopApp()
|
|
239
|
+
doc = ps_app.get_active_document()
|
|
240
|
+
|
|
241
|
+
if not doc:
|
|
242
|
+
return {
|
|
243
|
+
"success": False,
|
|
244
|
+
"error": "No active document",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
target_escaped = js_escape_string(target_layer_name)
|
|
249
|
+
placement = "PLACEBEFORE" if position == "ABOVE" else "PLACEAFTER"
|
|
250
|
+
|
|
251
|
+
move_script = f"""
|
|
252
|
+
var doc = app.activeDocument;
|
|
253
|
+
var layer = doc.activeLayer;
|
|
254
|
+
var layerName = layer.name;
|
|
255
|
+
|
|
256
|
+
// Find target layer
|
|
257
|
+
var targetLayer = null;
|
|
258
|
+
for (var i = 0; i < doc.layers.length; i++) {{
|
|
259
|
+
if (doc.layers[i].name === "{target_escaped}") {{
|
|
260
|
+
targetLayer = doc.layers[i];
|
|
261
|
+
break;
|
|
262
|
+
}}
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
if (!targetLayer) {{
|
|
266
|
+
throw new Error("Target layer not found: {target_escaped}");
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
// Move layer
|
|
270
|
+
layer.move(targetLayer, ElementPlacement.{placement});
|
|
271
|
+
|
|
272
|
+
layerName;
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"success": True,
|
|
279
|
+
"message": f"Moved layer '{layer_name}' {position.lower()} '{target_layer_name}'",
|
|
280
|
+
"layer_name": layer_name,
|
|
281
|
+
"target_layer": target_layer_name,
|
|
282
|
+
"position": position,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
error_msg = str(e)
|
|
287
|
+
if "not found" in error_msg.lower():
|
|
288
|
+
return {
|
|
289
|
+
"success": False,
|
|
290
|
+
"error": f"Target layer '{target_layer_name}' not found",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
logger.error(f"Failed to move layer to position: {e}")
|
|
294
|
+
return {
|
|
295
|
+
"success": False,
|
|
296
|
+
"error": str(e),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Register all tools
|
|
300
|
+
registered_tools.append(register_tool(mcp, move_layer_up, "move_layer_up"))
|
|
301
|
+
registered_tools.append(register_tool(mcp, move_layer_down, "move_layer_down"))
|
|
302
|
+
registered_tools.append(register_tool(mcp, move_layer_to_top, "move_layer_to_top"))
|
|
303
|
+
registered_tools.append(register_tool(mcp, move_layer_to_bottom, "move_layer_to_bottom"))
|
|
304
|
+
registered_tools.append(register_tool(mcp, move_layer_to_position, "move_layer_to_position"))
|
|
305
|
+
|
|
306
|
+
return registered_tools
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Layer properties tools - opacity, blend mode, visibility, locked, rename, fill."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from psforge.decorators import debug_tool, log_tool_call
|
|
8
|
+
from psforge.ps_adapter.application import PhotoshopApp
|
|
9
|
+
from psforge.ps_adapter.utils import js_escape_string, validate_color_channel, validate_numeric_range
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all layer property tools with MCP server.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp: MCP server instance.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of registered tool names.
|
|
21
|
+
"""
|
|
22
|
+
registered_tools = []
|
|
23
|
+
|
|
24
|
+
@debug_tool
|
|
25
|
+
@log_tool_call
|
|
26
|
+
def set_layer_opacity(opacity: float) -> dict[str, Any]:
|
|
27
|
+
"""Set the opacity of the currently active layer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
opacity: Opacity value from 0 (transparent) to 100 (opaque).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict: Operation result and context.
|
|
34
|
+
"""
|
|
35
|
+
validate_numeric_range(opacity, 0, 100, "opacity")
|
|
36
|
+
|
|
37
|
+
ps_app = PhotoshopApp()
|
|
38
|
+
doc = ps_app.get_active_document()
|
|
39
|
+
|
|
40
|
+
if not doc:
|
|
41
|
+
return {
|
|
42
|
+
"success": False,
|
|
43
|
+
"error": "No active document",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
set_opacity_script = f"""
|
|
48
|
+
var layer = app.activeDocument.activeLayer;
|
|
49
|
+
layer.opacity = {opacity};
|
|
50
|
+
layer.name;
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
layer_name = ps_app.execute_javascript(set_opacity_script)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"success": True,
|
|
57
|
+
"message": f"Set opacity of layer '{layer_name}' to {opacity}%",
|
|
58
|
+
"layer_name": layer_name,
|
|
59
|
+
"opacity": opacity,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to set layer opacity: {e}")
|
|
64
|
+
return {
|
|
65
|
+
"success": False,
|
|
66
|
+
"error": str(e),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@debug_tool
|
|
70
|
+
@log_tool_call
|
|
71
|
+
def set_layer_blend_mode(blend_mode: str) -> dict[str, Any]:
|
|
72
|
+
"""Set the blend mode of the currently active layer.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
blend_mode: Blend mode name (NORMAL, MULTIPLY, SCREEN, OVERLAY, SOFTLIGHT, HARDLIGHT,
|
|
76
|
+
COLORDODGE, COLORBURN, DARKEN, LIGHTEN, DIFFERENCE, EXCLUSION, HUE,
|
|
77
|
+
SATURATION, COLOR, LUMINOSITY, etc.).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
dict: Operation result and context.
|
|
81
|
+
"""
|
|
82
|
+
blend_mode = blend_mode.upper()
|
|
83
|
+
|
|
84
|
+
# Valid blend modes in Photoshop
|
|
85
|
+
valid_modes = [
|
|
86
|
+
"NORMAL",
|
|
87
|
+
"DISSOLVE",
|
|
88
|
+
"DARKEN",
|
|
89
|
+
"MULTIPLY",
|
|
90
|
+
"COLORBURN",
|
|
91
|
+
"LINEARBURN",
|
|
92
|
+
"DARKERCOLOR",
|
|
93
|
+
"LIGHTEN",
|
|
94
|
+
"SCREEN",
|
|
95
|
+
"COLORDODGE",
|
|
96
|
+
"LINEARDODGE",
|
|
97
|
+
"LIGHTERCOLOR",
|
|
98
|
+
"OVERLAY",
|
|
99
|
+
"SOFTLIGHT",
|
|
100
|
+
"HARDLIGHT",
|
|
101
|
+
"VIVIDLIGHT",
|
|
102
|
+
"LINEARLIGHT",
|
|
103
|
+
"PINLIGHT",
|
|
104
|
+
"HARDMIX",
|
|
105
|
+
"DIFFERENCE",
|
|
106
|
+
"EXCLUSION",
|
|
107
|
+
"SUBTRACT",
|
|
108
|
+
"DIVIDE",
|
|
109
|
+
"HUE",
|
|
110
|
+
"SATURATION",
|
|
111
|
+
"COLOR",
|
|
112
|
+
"LUMINOSITY",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
if blend_mode not in valid_modes:
|
|
116
|
+
return {
|
|
117
|
+
"success": False,
|
|
118
|
+
"error": f"Invalid blend_mode '{blend_mode}'. Must be one of: {', '.join(valid_modes[:10])}...",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ps_app = PhotoshopApp()
|
|
122
|
+
doc = ps_app.get_active_document()
|
|
123
|
+
|
|
124
|
+
if not doc:
|
|
125
|
+
return {
|
|
126
|
+
"success": False,
|
|
127
|
+
"error": "No active document",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
set_blend_script = f"""
|
|
132
|
+
var layer = app.activeDocument.activeLayer;
|
|
133
|
+
layer.blendMode = BlendMode.{blend_mode};
|
|
134
|
+
layer.name;
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
layer_name = ps_app.execute_javascript(set_blend_script)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"success": True,
|
|
141
|
+
"message": f"Set blend mode of layer '{layer_name}' to {blend_mode}",
|
|
142
|
+
"layer_name": layer_name,
|
|
143
|
+
"blend_mode": blend_mode,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Failed to set blend mode: {e}")
|
|
148
|
+
return {
|
|
149
|
+
"success": False,
|
|
150
|
+
"error": str(e),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@debug_tool
|
|
154
|
+
@log_tool_call
|
|
155
|
+
def set_layer_visibility(visible: bool) -> dict[str, Any]:
|
|
156
|
+
"""Set the visibility of the currently active layer.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
visible: True to show the layer, False to hide it.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
dict: Operation result and context.
|
|
163
|
+
"""
|
|
164
|
+
ps_app = PhotoshopApp()
|
|
165
|
+
doc = ps_app.get_active_document()
|
|
166
|
+
|
|
167
|
+
if not doc:
|
|
168
|
+
return {
|
|
169
|
+
"success": False,
|
|
170
|
+
"error": "No active document",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
visible_js = "true" if visible else "false"
|
|
175
|
+
|
|
176
|
+
set_visibility_script = f"""
|
|
177
|
+
var layer = app.activeDocument.activeLayer;
|
|
178
|
+
layer.visible = {visible_js};
|
|
179
|
+
layer.name;
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
layer_name = ps_app.execute_javascript(set_visibility_script)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"success": True,
|
|
186
|
+
"message": f"Layer '{layer_name}' is now {'visible' if visible else 'hidden'}",
|
|
187
|
+
"layer_name": layer_name,
|
|
188
|
+
"visible": visible,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"Failed to set layer visibility: {e}")
|
|
193
|
+
return {
|
|
194
|
+
"success": False,
|
|
195
|
+
"error": str(e),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@debug_tool
|
|
199
|
+
@log_tool_call
|
|
200
|
+
def set_layer_locked(locked: bool) -> dict[str, Any]:
|
|
201
|
+
"""Lock or unlock the currently active layer.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
locked: True to lock the layer, False to unlock it.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
dict: Operation result and context.
|
|
208
|
+
"""
|
|
209
|
+
ps_app = PhotoshopApp()
|
|
210
|
+
doc = ps_app.get_active_document()
|
|
211
|
+
|
|
212
|
+
if not doc:
|
|
213
|
+
return {
|
|
214
|
+
"success": False,
|
|
215
|
+
"error": "No active document",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
locked_js = "true" if locked else "false"
|
|
220
|
+
|
|
221
|
+
set_locked_script = f"""
|
|
222
|
+
var layer = app.activeDocument.activeLayer;
|
|
223
|
+
layer.allLocked = {locked_js};
|
|
224
|
+
layer.name;
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
layer_name = ps_app.execute_javascript(set_locked_script)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"success": True,
|
|
231
|
+
"message": f"Layer '{layer_name}' is now {'locked' if locked else 'unlocked'}",
|
|
232
|
+
"layer_name": layer_name,
|
|
233
|
+
"locked": locked,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"Failed to set layer lock: {e}")
|
|
238
|
+
return {
|
|
239
|
+
"success": False,
|
|
240
|
+
"error": str(e),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@debug_tool
|
|
244
|
+
@log_tool_call
|
|
245
|
+
def rename_layer(new_name: str) -> dict[str, Any]:
|
|
246
|
+
"""Rename the currently active layer.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
new_name: New name for the layer.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
dict: Operation result and context.
|
|
253
|
+
"""
|
|
254
|
+
if not new_name or not new_name.strip():
|
|
255
|
+
return {
|
|
256
|
+
"success": False,
|
|
257
|
+
"error": "new_name cannot be empty",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
ps_app = PhotoshopApp()
|
|
261
|
+
doc = ps_app.get_active_document()
|
|
262
|
+
|
|
263
|
+
if not doc:
|
|
264
|
+
return {
|
|
265
|
+
"success": False,
|
|
266
|
+
"error": "No active document",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
new_name_escaped = js_escape_string(new_name)
|
|
271
|
+
|
|
272
|
+
rename_script = f"""
|
|
273
|
+
var layer = app.activeDocument.activeLayer;
|
|
274
|
+
var oldName = layer.name;
|
|
275
|
+
layer.name = "{new_name_escaped}";
|
|
276
|
+
oldName;
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
old_name = ps_app.execute_javascript(rename_script)
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"success": True,
|
|
283
|
+
"message": f"Renamed layer from '{old_name}' to '{new_name}'",
|
|
284
|
+
"old_name": old_name,
|
|
285
|
+
"new_name": new_name,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Failed to rename layer: {e}")
|
|
290
|
+
return {
|
|
291
|
+
"success": False,
|
|
292
|
+
"error": str(e),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@debug_tool
|
|
296
|
+
@log_tool_call
|
|
297
|
+
def fill_layer(red: int, green: int, blue: int) -> dict[str, Any]:
|
|
298
|
+
"""Fill the currently active layer with a solid color.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
red: Red channel value (0-255).
|
|
302
|
+
green: Green channel value (0-255).
|
|
303
|
+
blue: Blue channel value (0-255).
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
dict: Operation result and context.
|
|
307
|
+
"""
|
|
308
|
+
validate_color_channel(red, "red")
|
|
309
|
+
validate_color_channel(green, "green")
|
|
310
|
+
validate_color_channel(blue, "blue")
|
|
311
|
+
|
|
312
|
+
ps_app = PhotoshopApp()
|
|
313
|
+
doc = ps_app.get_active_document()
|
|
314
|
+
|
|
315
|
+
if not doc:
|
|
316
|
+
return {
|
|
317
|
+
"success": False,
|
|
318
|
+
"error": "No active document",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
fill_script = f"""
|
|
323
|
+
var doc = app.activeDocument;
|
|
324
|
+
var layer = doc.activeLayer;
|
|
325
|
+
|
|
326
|
+
// Create solid color object
|
|
327
|
+
var color = new SolidColor();
|
|
328
|
+
color.rgb.red = {red};
|
|
329
|
+
color.rgb.green = {green};
|
|
330
|
+
color.rgb.blue = {blue};
|
|
331
|
+
|
|
332
|
+
// Fill layer
|
|
333
|
+
doc.selection.selectAll();
|
|
334
|
+
doc.selection.fill(color);
|
|
335
|
+
doc.selection.deselect();
|
|
336
|
+
|
|
337
|
+
layer.name;
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
layer_name = ps_app.execute_javascript(fill_script)
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
"success": True,
|
|
344
|
+
"message": f"Filled layer '{layer_name}' with RGB({red}, {green}, {blue})",
|
|
345
|
+
"layer_name": layer_name,
|
|
346
|
+
"color": {"red": red, "green": green, "blue": blue},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to fill layer: {e}")
|
|
351
|
+
return {
|
|
352
|
+
"success": False,
|
|
353
|
+
"error": str(e),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Register all tools
|
|
357
|
+
registered_tools.append(register_tool(mcp, set_layer_opacity, "set_layer_opacity"))
|
|
358
|
+
registered_tools.append(register_tool(mcp, set_layer_blend_mode, "set_layer_blend_mode"))
|
|
359
|
+
registered_tools.append(register_tool(mcp, set_layer_visibility, "set_layer_visibility"))
|
|
360
|
+
registered_tools.append(register_tool(mcp, set_layer_locked, "set_layer_locked"))
|
|
361
|
+
registered_tools.append(register_tool(mcp, rename_layer, "rename_layer"))
|
|
362
|
+
registered_tools.append(register_tool(mcp, fill_layer, "fill_layer"))
|
|
363
|
+
|
|
364
|
+
return registered_tools
|