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.
@@ -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