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,286 @@
1
+ """Mask tools - create, apply, delete layer masks."""
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.registry import register_tool
10
+
11
+
12
+ def register(mcp) -> list[str]:
13
+ """Register all mask tools with MCP server.
14
+
15
+ Args:
16
+ mcp: MCP server instance.
17
+
18
+ Returns:
19
+ List of registered tool names.
20
+ """
21
+ registered_tools = []
22
+
23
+ @debug_tool
24
+ @log_tool_call
25
+ def create_layer_mask(reveal_all: bool = True) -> dict[str, Any]:
26
+ """Create a layer mask for the currently active layer.
27
+
28
+ Args:
29
+ reveal_all: If True, create a reveal-all mask (white, shows everything).
30
+ If False, create a hide-all mask (black, hides everything).
31
+
32
+ Returns:
33
+ dict: Operation result and context.
34
+ """
35
+ ps_app = PhotoshopApp()
36
+ doc = ps_app.get_active_document()
37
+
38
+ if not doc:
39
+ return {
40
+ "success": False,
41
+ "error": "No active document",
42
+ }
43
+
44
+ try:
45
+ # Check if layer is background
46
+ check_bg_script = """
47
+ (function() {
48
+ try {
49
+ return app.activeDocument.activeLayer.isBackgroundLayer;
50
+ } catch(e) {
51
+ return false;
52
+ }
53
+ })();
54
+ """
55
+ is_background = ps_app.execute_javascript(check_bg_script)
56
+
57
+ if is_background:
58
+ return {
59
+ "success": False,
60
+ "error": "Cannot add mask to background layer. Convert it to a regular layer first.",
61
+ }
62
+
63
+ # Create mask using Action Descriptor (more reliable)
64
+ if reveal_all:
65
+ create_mask_script = """
66
+ var layer = app.activeDocument.activeLayer;
67
+ var layerName = layer.name;
68
+
69
+ // Add reveal-all mask
70
+ var idMk = charIDToTypeID("Mk ");
71
+ var desc = new ActionDescriptor();
72
+ var idNw = charIDToTypeID("Nw ");
73
+ var idChnl = charIDToTypeID("Chnl");
74
+ desc.putClass(idNw, idChnl);
75
+ var idAt = charIDToTypeID("At ");
76
+ var ref = new ActionReference();
77
+ var idChnl = charIDToTypeID("Chnl");
78
+ var idMsk = charIDToTypeID("Msk ");
79
+ ref.putEnumerated(idChnl, idChnl, idMsk);
80
+ desc.putReference(idAt, ref);
81
+ var idUsng = charIDToTypeID("Usng");
82
+ var idUsrM = charIDToTypeID("UsrM");
83
+ var idRvlA = charIDToTypeID("RvlA");
84
+ desc.putEnumerated(idUsng, idUsrM, idRvlA);
85
+ executeAction(idMk, desc, DialogModes.NO);
86
+
87
+ layerName;
88
+ """
89
+ else:
90
+ create_mask_script = """
91
+ var layer = app.activeDocument.activeLayer;
92
+ var layerName = layer.name;
93
+
94
+ // Add hide-all mask
95
+ var idMk = charIDToTypeID("Mk ");
96
+ var desc = new ActionDescriptor();
97
+ var idNw = charIDToTypeID("Nw ");
98
+ var idChnl = charIDToTypeID("Chnl");
99
+ desc.putClass(idNw, idChnl);
100
+ var idAt = charIDToTypeID("At ");
101
+ var ref = new ActionReference();
102
+ var idChnl = charIDToTypeID("Chnl");
103
+ var idMsk = charIDToTypeID("Msk ");
104
+ ref.putEnumerated(idChnl, idChnl, idMsk);
105
+ desc.putReference(idAt, ref);
106
+ var idUsng = charIDToTypeID("Usng");
107
+ var idUsrM = charIDToTypeID("UsrM");
108
+ var idHdAl = charIDToTypeID("HdAl");
109
+ desc.putEnumerated(idUsng, idUsrM, idHdAl);
110
+ executeAction(idMk, desc, DialogModes.NO);
111
+
112
+ layerName;
113
+ """
114
+
115
+ layer_name = ps_app.execute_javascript(create_mask_script)
116
+
117
+ mask_type = "reveal-all (white)" if reveal_all else "hide-all (black)"
118
+
119
+ return {
120
+ "success": True,
121
+ "message": f"Created {mask_type} mask for layer '{layer_name}'",
122
+ "layer_name": layer_name,
123
+ "mask_type": mask_type,
124
+ "reveal_all": reveal_all,
125
+ }
126
+
127
+ except Exception as e:
128
+ logger.error(f"Failed to create layer mask: {e}")
129
+ return {
130
+ "success": False,
131
+ "error": str(e),
132
+ }
133
+
134
+ @debug_tool
135
+ @log_tool_call
136
+ def apply_layer_mask() -> dict[str, Any]:
137
+ """Apply (flatten) the layer mask of the currently active layer.
138
+
139
+ This permanently applies the mask and removes it.
140
+
141
+ Returns:
142
+ dict: Operation result and context.
143
+ """
144
+ ps_app = PhotoshopApp()
145
+ doc = ps_app.get_active_document()
146
+
147
+ if not doc:
148
+ return {
149
+ "success": False,
150
+ "error": "No active document",
151
+ }
152
+
153
+ try:
154
+ # Check if layer has a mask
155
+ check_mask_script = """
156
+ (function() {
157
+ var layer = app.activeDocument.activeLayer;
158
+ try {
159
+ // Try to access the mask
160
+ var hasMask = layer.layerMaskDensity !== undefined;
161
+ return hasMask;
162
+ } catch(e) {
163
+ return false;
164
+ }
165
+ })();
166
+ """
167
+
168
+ has_mask = ps_app.execute_javascript(check_mask_script)
169
+
170
+ if not has_mask:
171
+ return {
172
+ "success": False,
173
+ "error": "Active layer does not have a mask",
174
+ }
175
+
176
+ apply_mask_script = """
177
+ var layer = app.activeDocument.activeLayer;
178
+ var layerName = layer.name;
179
+
180
+ // Apply layer mask using Action Descriptor
181
+ var idAppr = charIDToTypeID("Appr");
182
+ var desc = new ActionDescriptor();
183
+ var idNull = charIDToTypeID("null");
184
+ var ref = new ActionReference();
185
+ var idChnl = charIDToTypeID("Chnl");
186
+ var idMsk = charIDToTypeID("Msk ");
187
+ ref.putEnumerated(idChnl, idChnl, idMsk);
188
+ desc.putReference(idNull, ref);
189
+ executeAction(idAppr, desc, DialogModes.NO);
190
+
191
+ layerName;
192
+ """
193
+
194
+ layer_name = ps_app.execute_javascript(apply_mask_script)
195
+
196
+ return {
197
+ "success": True,
198
+ "message": f"Applied layer mask on layer '{layer_name}'",
199
+ "layer_name": layer_name,
200
+ }
201
+
202
+ except Exception as e:
203
+ logger.error(f"Failed to apply layer mask: {e}")
204
+ return {
205
+ "success": False,
206
+ "error": str(e),
207
+ }
208
+
209
+ @debug_tool
210
+ @log_tool_call
211
+ def delete_layer_mask() -> dict[str, Any]:
212
+ """Delete the layer mask of the currently active layer.
213
+
214
+ Returns:
215
+ dict: Operation result and context.
216
+ """
217
+ ps_app = PhotoshopApp()
218
+ doc = ps_app.get_active_document()
219
+
220
+ if not doc:
221
+ return {
222
+ "success": False,
223
+ "error": "No active document",
224
+ }
225
+
226
+ try:
227
+ # Check if layer has a mask
228
+ check_mask_script = """
229
+ (function() {
230
+ var layer = app.activeDocument.activeLayer;
231
+ try {
232
+ var hasMask = layer.layerMaskDensity !== undefined;
233
+ return hasMask;
234
+ } catch(e) {
235
+ return false;
236
+ }
237
+ })();
238
+ """
239
+
240
+ has_mask = ps_app.execute_javascript(check_mask_script)
241
+
242
+ if not has_mask:
243
+ return {
244
+ "success": False,
245
+ "error": "Active layer does not have a mask",
246
+ }
247
+
248
+ delete_mask_script = """
249
+ var layer = app.activeDocument.activeLayer;
250
+ var layerName = layer.name;
251
+
252
+ // Delete layer mask using Action Descriptor
253
+ var idDlt = charIDToTypeID("Dlt ");
254
+ var desc = new ActionDescriptor();
255
+ var idNull = charIDToTypeID("null");
256
+ var ref = new ActionReference();
257
+ var idChnl = charIDToTypeID("Chnl");
258
+ var idMsk = charIDToTypeID("Msk ");
259
+ ref.putEnumerated(idChnl, idChnl, idMsk);
260
+ desc.putReference(idNull, ref);
261
+ executeAction(idDlt, desc, DialogModes.NO);
262
+
263
+ layerName;
264
+ """
265
+
266
+ layer_name = ps_app.execute_javascript(delete_mask_script)
267
+
268
+ return {
269
+ "success": True,
270
+ "message": f"Deleted layer mask from layer '{layer_name}'",
271
+ "layer_name": layer_name,
272
+ }
273
+
274
+ except Exception as e:
275
+ logger.error(f"Failed to delete layer mask: {e}")
276
+ return {
277
+ "success": False,
278
+ "error": str(e),
279
+ }
280
+
281
+ # Register all tools
282
+ registered_tools.append(register_tool(mcp, create_layer_mask, "create_layer_mask"))
283
+ registered_tools.append(register_tool(mcp, apply_layer_mask, "apply_layer_mask"))
284
+ registered_tools.append(register_tool(mcp, delete_layer_mask, "delete_layer_mask"))
285
+
286
+ return registered_tools
@@ -0,0 +1,6 @@
1
+ """Tool registration utilities (imported by individual tool modules)."""
2
+
3
+ # This file can be empty - individual tool modules import register_tool from psforge.registry
4
+ # We keep it for consistency with the architecture
5
+
6
+ __all__ = []
@@ -0,0 +1,248 @@
1
+ """Selection tools - select all, rectangle selection, deselect, invert selection."""
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.registry import register_tool
10
+
11
+
12
+ def register(mcp) -> list[str]:
13
+ """Register all selection tools with MCP server.
14
+
15
+ Args:
16
+ mcp: MCP server instance.
17
+
18
+ Returns:
19
+ List of registered tool names.
20
+ """
21
+ registered_tools = []
22
+
23
+ @debug_tool
24
+ @log_tool_call
25
+ def select_all() -> dict[str, Any]:
26
+ """Select the entire document (all pixels).
27
+
28
+ Returns:
29
+ dict: Operation result and context.
30
+ """
31
+ ps_app = PhotoshopApp()
32
+ doc = ps_app.get_active_document()
33
+
34
+ if not doc:
35
+ return {
36
+ "success": False,
37
+ "error": "No active document",
38
+ }
39
+
40
+ try:
41
+ select_all_script = """
42
+ var doc = app.activeDocument;
43
+ doc.selection.selectAll();
44
+
45
+ var bounds = doc.selection.bounds;
46
+ JSON.stringify({
47
+ width: parseInt(bounds[2]) - parseInt(bounds[0]),
48
+ height: parseInt(bounds[3]) - parseInt(bounds[1])
49
+ });
50
+ """
51
+
52
+ import json
53
+
54
+ result_str = ps_app.execute_javascript(select_all_script)
55
+ result = json.loads(result_str) if isinstance(result_str, str) else result_str
56
+
57
+ return {
58
+ "success": True,
59
+ "message": "Selected entire document",
60
+ "selection_width": result["width"],
61
+ "selection_height": result["height"],
62
+ }
63
+
64
+ except Exception as e:
65
+ logger.error(f"Failed to select all: {e}")
66
+ return {
67
+ "success": False,
68
+ "error": str(e),
69
+ }
70
+
71
+ @debug_tool
72
+ @log_tool_call
73
+ def select_rectangle(top: int, left: int, bottom: int, right: int) -> dict[str, Any]:
74
+ """Create a rectangular selection.
75
+
76
+ Args:
77
+ top: Top edge position in pixels.
78
+ left: Left edge position in pixels.
79
+ bottom: Bottom edge position in pixels.
80
+ right: Right edge position in pixels.
81
+
82
+ Returns:
83
+ dict: Operation result and context.
84
+ """
85
+ # Validate bounds
86
+ if left >= right:
87
+ return {
88
+ "success": False,
89
+ "error": "left must be < right",
90
+ }
91
+ if top >= bottom:
92
+ return {
93
+ "success": False,
94
+ "error": "top must be < bottom",
95
+ }
96
+
97
+ ps_app = PhotoshopApp()
98
+ doc = ps_app.get_active_document()
99
+
100
+ if not doc:
101
+ return {
102
+ "success": False,
103
+ "error": "No active document",
104
+ }
105
+
106
+ try:
107
+ select_rect_script = f"""
108
+ var doc = app.activeDocument;
109
+
110
+ // Create selection region
111
+ var selRegion = [
112
+ [{left}, {top}],
113
+ [{right}, {top}],
114
+ [{right}, {bottom}],
115
+ [{left}, {bottom}]
116
+ ];
117
+
118
+ doc.selection.select(selRegion);
119
+
120
+ "Rectangle selected";
121
+ """
122
+
123
+ ps_app.execute_javascript(select_rect_script)
124
+
125
+ width = right - left
126
+ height = bottom - top
127
+
128
+ return {
129
+ "success": True,
130
+ "message": f"Created rectangular selection ({width}x{height}px)",
131
+ "bounds": {"top": top, "left": left, "bottom": bottom, "right": right},
132
+ "width": width,
133
+ "height": height,
134
+ }
135
+
136
+ except Exception as e:
137
+ logger.error(f"Failed to create rectangle selection: {e}")
138
+ return {
139
+ "success": False,
140
+ "error": str(e),
141
+ }
142
+
143
+ @debug_tool
144
+ @log_tool_call
145
+ def deselect() -> dict[str, Any]:
146
+ """Deselect (remove current selection).
147
+
148
+ Returns:
149
+ dict: Operation result and context.
150
+ """
151
+ ps_app = PhotoshopApp()
152
+ doc = ps_app.get_active_document()
153
+
154
+ if not doc:
155
+ return {
156
+ "success": False,
157
+ "error": "No active document",
158
+ }
159
+
160
+ try:
161
+ deselect_script = """
162
+ app.activeDocument.selection.deselect();
163
+ "Selection removed";
164
+ """
165
+
166
+ ps_app.execute_javascript(deselect_script)
167
+
168
+ return {
169
+ "success": True,
170
+ "message": "Selection removed",
171
+ }
172
+
173
+ except Exception as e:
174
+ logger.error(f"Failed to deselect: {e}")
175
+ return {
176
+ "success": False,
177
+ "error": str(e),
178
+ }
179
+
180
+ @debug_tool
181
+ @log_tool_call
182
+ def invert_selection() -> dict[str, Any]:
183
+ """Invert the current selection (select what was not selected, deselect what was selected).
184
+
185
+ Returns:
186
+ dict: Operation result and context.
187
+ """
188
+ ps_app = PhotoshopApp()
189
+ doc = ps_app.get_active_document()
190
+
191
+ if not doc:
192
+ return {
193
+ "success": False,
194
+ "error": "No active document",
195
+ }
196
+
197
+ try:
198
+ invert_script = """
199
+ var doc = app.activeDocument;
200
+
201
+ // Check if there's a selection
202
+ try {
203
+ var bounds = doc.selection.bounds;
204
+ if (!bounds) {
205
+ throw new Error("No selection to invert");
206
+ }
207
+ } catch(e) {
208
+ throw new Error("No selection to invert");
209
+ }
210
+
211
+ doc.selection.invert();
212
+ "Selection inverted";
213
+ """
214
+
215
+ result = ps_app.execute_javascript(invert_script)
216
+
217
+ if "No selection" in str(result):
218
+ return {
219
+ "success": False,
220
+ "error": "No selection to invert",
221
+ }
222
+
223
+ return {
224
+ "success": True,
225
+ "message": "Selection inverted",
226
+ }
227
+
228
+ except Exception as e:
229
+ error_msg = str(e)
230
+ if "no selection" in error_msg.lower():
231
+ return {
232
+ "success": False,
233
+ "error": "No selection to invert",
234
+ }
235
+
236
+ logger.error(f"Failed to invert selection: {e}")
237
+ return {
238
+ "success": False,
239
+ "error": str(e),
240
+ }
241
+
242
+ # Register all tools
243
+ registered_tools.append(register_tool(mcp, select_all, "select_all"))
244
+ registered_tools.append(register_tool(mcp, select_rectangle, "select_rectangle"))
245
+ registered_tools.append(register_tool(mcp, deselect, "deselect"))
246
+ registered_tools.append(register_tool(mcp, invert_selection, "invert_selection"))
247
+
248
+ return registered_tools