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,252 @@
|
|
|
1
|
+
"""Filter tools - gaussian blur, motion blur, sharpen, noise."""
|
|
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 validate_numeric_range
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all filter 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 apply_gaussian_blur(radius: float) -> dict[str, Any]:
|
|
27
|
+
"""Apply Gaussian Blur filter to the currently active layer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
radius: Blur radius in pixels (0.1-250).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict: Operation result and context.
|
|
34
|
+
"""
|
|
35
|
+
validate_numeric_range(radius, 0.1, 250, "radius")
|
|
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
|
+
blur_script = f"""
|
|
48
|
+
var layer = app.activeDocument.activeLayer;
|
|
49
|
+
var layerName = layer.name;
|
|
50
|
+
|
|
51
|
+
// Apply Gaussian Blur
|
|
52
|
+
layer.applyGaussianBlur({radius});
|
|
53
|
+
|
|
54
|
+
layerName;
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
layer_name = ps_app.execute_javascript(blur_script)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"success": True,
|
|
61
|
+
"message": f"Applied Gaussian Blur (radius: {radius}px) to layer '{layer_name}'",
|
|
62
|
+
"layer_name": layer_name,
|
|
63
|
+
"filter": "Gaussian Blur",
|
|
64
|
+
"radius": radius,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to apply Gaussian Blur: {e}")
|
|
69
|
+
return {
|
|
70
|
+
"success": False,
|
|
71
|
+
"error": str(e),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@debug_tool
|
|
75
|
+
@log_tool_call
|
|
76
|
+
def apply_motion_blur(angle: float, radius: int) -> dict[str, Any]:
|
|
77
|
+
"""Apply Motion Blur filter to the currently active layer.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
angle: Blur angle in degrees (-360 to 360).
|
|
81
|
+
radius: Blur distance in pixels (1-999).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
dict: Operation result and context.
|
|
85
|
+
"""
|
|
86
|
+
validate_numeric_range(angle, -360, 360, "angle")
|
|
87
|
+
validate_numeric_range(radius, 1, 999, "radius")
|
|
88
|
+
|
|
89
|
+
ps_app = PhotoshopApp()
|
|
90
|
+
doc = ps_app.get_active_document()
|
|
91
|
+
|
|
92
|
+
if not doc:
|
|
93
|
+
return {
|
|
94
|
+
"success": False,
|
|
95
|
+
"error": "No active document",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
blur_script = f"""
|
|
100
|
+
var layer = app.activeDocument.activeLayer;
|
|
101
|
+
var layerName = layer.name;
|
|
102
|
+
|
|
103
|
+
// Apply Motion Blur
|
|
104
|
+
layer.applyMotionBlur({angle}, {radius});
|
|
105
|
+
|
|
106
|
+
layerName;
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
layer_name = ps_app.execute_javascript(blur_script)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"success": True,
|
|
113
|
+
"message": f"Applied Motion Blur (angle: {angle}°, distance: {radius}px) to layer '{layer_name}'",
|
|
114
|
+
"layer_name": layer_name,
|
|
115
|
+
"filter": "Motion Blur",
|
|
116
|
+
"angle": angle,
|
|
117
|
+
"radius": radius,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Failed to apply Motion Blur: {e}")
|
|
122
|
+
return {
|
|
123
|
+
"success": False,
|
|
124
|
+
"error": str(e),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@debug_tool
|
|
128
|
+
@log_tool_call
|
|
129
|
+
def apply_sharpen(amount: int, radius: float, threshold: int) -> dict[str, Any]:
|
|
130
|
+
"""Apply Unsharp Mask (USM) sharpening filter to the currently active layer.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
amount: Sharpening amount percentage (1-500).
|
|
134
|
+
radius: Radius in pixels (0.1-250).
|
|
135
|
+
threshold: Threshold levels (0-255).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: Operation result and context.
|
|
139
|
+
"""
|
|
140
|
+
validate_numeric_range(amount, 1, 500, "amount")
|
|
141
|
+
validate_numeric_range(radius, 0.1, 250, "radius")
|
|
142
|
+
validate_numeric_range(threshold, 0, 255, "threshold")
|
|
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
|
+
sharpen_script = f"""
|
|
155
|
+
var layer = app.activeDocument.activeLayer;
|
|
156
|
+
var layerName = layer.name;
|
|
157
|
+
|
|
158
|
+
// Apply Unsharp Mask
|
|
159
|
+
layer.applyUnSharpMask({amount}, {radius}, {threshold});
|
|
160
|
+
|
|
161
|
+
layerName;
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
layer_name = ps_app.execute_javascript(sharpen_script)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"success": True,
|
|
168
|
+
"message": f"Applied Unsharp Mask to layer '{layer_name}'",
|
|
169
|
+
"layer_name": layer_name,
|
|
170
|
+
"filter": "Unsharp Mask",
|
|
171
|
+
"amount": amount,
|
|
172
|
+
"radius": radius,
|
|
173
|
+
"threshold": threshold,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to apply Unsharp Mask: {e}")
|
|
178
|
+
return {
|
|
179
|
+
"success": False,
|
|
180
|
+
"error": str(e),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@debug_tool
|
|
184
|
+
@log_tool_call
|
|
185
|
+
def apply_noise(amount: float, distribution: str = "UNIFORM", monochromatic: bool = False) -> dict[str, Any]:
|
|
186
|
+
"""Apply Add Noise filter to the currently active layer.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
amount: Noise amount percentage (0.1-400).
|
|
190
|
+
distribution: Noise distribution - UNIFORM or GAUSSIAN (default: UNIFORM).
|
|
191
|
+
monochromatic: If True, apply noise to tones only without changing colors.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
dict: Operation result and context.
|
|
195
|
+
"""
|
|
196
|
+
validate_numeric_range(amount, 0.1, 400, "amount")
|
|
197
|
+
|
|
198
|
+
distribution = distribution.upper()
|
|
199
|
+
if distribution not in ["UNIFORM", "GAUSSIAN"]:
|
|
200
|
+
return {
|
|
201
|
+
"success": False,
|
|
202
|
+
"error": "distribution must be 'UNIFORM' or 'GAUSSIAN'",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ps_app = PhotoshopApp()
|
|
206
|
+
doc = ps_app.get_active_document()
|
|
207
|
+
|
|
208
|
+
if not doc:
|
|
209
|
+
return {
|
|
210
|
+
"success": False,
|
|
211
|
+
"error": "No active document",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
mono_js = "true" if monochromatic else "false"
|
|
216
|
+
|
|
217
|
+
noise_script = f"""
|
|
218
|
+
var layer = app.activeDocument.activeLayer;
|
|
219
|
+
var layerName = layer.name;
|
|
220
|
+
|
|
221
|
+
// Apply Add Noise
|
|
222
|
+
layer.applyAddNoise({amount}, NoiseDistribution.{distribution}, {mono_js});
|
|
223
|
+
|
|
224
|
+
layerName;
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
layer_name = ps_app.execute_javascript(noise_script)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"success": True,
|
|
231
|
+
"message": f"Applied Add Noise ({distribution}, {amount}%) to layer '{layer_name}'",
|
|
232
|
+
"layer_name": layer_name,
|
|
233
|
+
"filter": "Add Noise",
|
|
234
|
+
"amount": amount,
|
|
235
|
+
"distribution": distribution,
|
|
236
|
+
"monochromatic": monochromatic,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Failed to apply Add Noise: {e}")
|
|
241
|
+
return {
|
|
242
|
+
"success": False,
|
|
243
|
+
"error": str(e),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Register all tools
|
|
247
|
+
registered_tools.append(register_tool(mcp, apply_gaussian_blur, "apply_gaussian_blur"))
|
|
248
|
+
registered_tools.append(register_tool(mcp, apply_motion_blur, "apply_motion_blur"))
|
|
249
|
+
registered_tools.append(register_tool(mcp, apply_sharpen, "apply_sharpen"))
|
|
250
|
+
registered_tools.append(register_tool(mcp, apply_noise, "apply_noise"))
|
|
251
|
+
|
|
252
|
+
return registered_tools
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""History tools - undo, redo, get history."""
|
|
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 validate_numeric_range
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all history 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 undo(steps: int = 1) -> dict[str, Any]:
|
|
27
|
+
"""Undo one or more steps in the history.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
steps: Number of steps to undo (default: 1, max: 50).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict: Operation result and context.
|
|
34
|
+
"""
|
|
35
|
+
validate_numeric_range(steps, 1, 50, "steps")
|
|
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
|
+
# Undo multiple steps
|
|
48
|
+
undo_script = f"""
|
|
49
|
+
var doc = app.activeDocument;
|
|
50
|
+
var stepsToUndo = {steps};
|
|
51
|
+
var actualSteps = 0;
|
|
52
|
+
|
|
53
|
+
for (var i = 0; i < stepsToUndo; i++) {{
|
|
54
|
+
try {{
|
|
55
|
+
doc.activeHistoryState = doc.activeHistoryState.parent;
|
|
56
|
+
actualSteps++;
|
|
57
|
+
}} catch(e) {{
|
|
58
|
+
// No more history to undo
|
|
59
|
+
break;
|
|
60
|
+
}}
|
|
61
|
+
}}
|
|
62
|
+
|
|
63
|
+
JSON.stringify({{
|
|
64
|
+
steps_undone: actualSteps,
|
|
65
|
+
current_state: doc.activeHistoryState.name
|
|
66
|
+
}});
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
import json
|
|
70
|
+
|
|
71
|
+
result_str = ps_app.execute_javascript(undo_script)
|
|
72
|
+
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"message": f"Undone {result['steps_undone']} step(s)",
|
|
77
|
+
"steps_undone": result["steps_undone"],
|
|
78
|
+
"current_state": result["current_state"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Failed to undo: {e}")
|
|
83
|
+
return {
|
|
84
|
+
"success": False,
|
|
85
|
+
"error": str(e),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@debug_tool
|
|
89
|
+
@log_tool_call
|
|
90
|
+
def redo(steps: int = 1) -> dict[str, Any]:
|
|
91
|
+
"""Redo one or more steps in the history.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
steps: Number of steps to redo (default: 1, max: 50).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
dict: Operation result and context.
|
|
98
|
+
"""
|
|
99
|
+
validate_numeric_range(steps, 1, 50, "steps")
|
|
100
|
+
|
|
101
|
+
ps_app = PhotoshopApp()
|
|
102
|
+
doc = ps_app.get_active_document()
|
|
103
|
+
|
|
104
|
+
if not doc:
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": "No active document",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Note: ExtendScript doesn't have a direct "redo" - we need to navigate forward in history
|
|
112
|
+
redo_script = f"""
|
|
113
|
+
var doc = app.activeDocument;
|
|
114
|
+
var stepsToRedo = {steps};
|
|
115
|
+
var actualSteps = 0;
|
|
116
|
+
|
|
117
|
+
// Get all history states
|
|
118
|
+
var historyStates = doc.historyStates;
|
|
119
|
+
var currentIndex = -1;
|
|
120
|
+
|
|
121
|
+
// Find current history state index
|
|
122
|
+
for (var i = 0; i < historyStates.length; i++) {{
|
|
123
|
+
if (historyStates[i] === doc.activeHistoryState) {{
|
|
124
|
+
currentIndex = i;
|
|
125
|
+
break;
|
|
126
|
+
}}
|
|
127
|
+
}}
|
|
128
|
+
|
|
129
|
+
// Move forward in history
|
|
130
|
+
for (var i = 0; i < stepsToRedo; i++) {{
|
|
131
|
+
var nextIndex = currentIndex + i + 1;
|
|
132
|
+
if (nextIndex < historyStates.length) {{
|
|
133
|
+
doc.activeHistoryState = historyStates[nextIndex];
|
|
134
|
+
actualSteps++;
|
|
135
|
+
}} else {{
|
|
136
|
+
break;
|
|
137
|
+
}}
|
|
138
|
+
}}
|
|
139
|
+
|
|
140
|
+
JSON.stringify({{
|
|
141
|
+
steps_redone: actualSteps,
|
|
142
|
+
current_state: doc.activeHistoryState.name
|
|
143
|
+
}});
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
import json
|
|
147
|
+
|
|
148
|
+
result_str = ps_app.execute_javascript(redo_script)
|
|
149
|
+
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"success": True,
|
|
153
|
+
"message": f"Redone {result['steps_redone']} step(s)",
|
|
154
|
+
"steps_redone": result["steps_redone"],
|
|
155
|
+
"current_state": result["current_state"],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"Failed to redo: {e}")
|
|
160
|
+
return {
|
|
161
|
+
"success": False,
|
|
162
|
+
"error": str(e),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@debug_tool
|
|
166
|
+
@log_tool_call
|
|
167
|
+
def get_history() -> dict[str, Any]:
|
|
168
|
+
"""Get the list of history states for the active document.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dict: Operation result with history states array and context.
|
|
172
|
+
"""
|
|
173
|
+
ps_app = PhotoshopApp()
|
|
174
|
+
doc = ps_app.get_active_document()
|
|
175
|
+
|
|
176
|
+
if not doc:
|
|
177
|
+
return {
|
|
178
|
+
"success": False,
|
|
179
|
+
"error": "No active document",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
get_history_script = """
|
|
184
|
+
(function() {
|
|
185
|
+
var doc = app.activeDocument;
|
|
186
|
+
var historyStates = doc.historyStates;
|
|
187
|
+
var currentState = doc.activeHistoryState;
|
|
188
|
+
|
|
189
|
+
var states = [];
|
|
190
|
+
var currentIndex = -1;
|
|
191
|
+
|
|
192
|
+
for (var i = 0; i < historyStates.length; i++) {
|
|
193
|
+
var state = historyStates[i];
|
|
194
|
+
|
|
195
|
+
if (state === currentState) {
|
|
196
|
+
currentIndex = i;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
states.push({
|
|
200
|
+
index: i,
|
|
201
|
+
name: state.name,
|
|
202
|
+
snapshot: state.snapshot,
|
|
203
|
+
is_current: (state === currentState)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
total_states: states.length,
|
|
209
|
+
current_index: currentIndex,
|
|
210
|
+
current_state: currentState.name,
|
|
211
|
+
states: states
|
|
212
|
+
});
|
|
213
|
+
})();
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
result_str = ps_app.execute_javascript(get_history_script)
|
|
219
|
+
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"success": True,
|
|
223
|
+
"total_states": result["total_states"],
|
|
224
|
+
"current_index": result["current_index"],
|
|
225
|
+
"current_state": result["current_state"],
|
|
226
|
+
"states": result["states"],
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"Failed to get history: {e}")
|
|
231
|
+
return {
|
|
232
|
+
"success": False,
|
|
233
|
+
"error": str(e),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Register all tools
|
|
237
|
+
registered_tools.append(register_tool(mcp, undo, "undo"))
|
|
238
|
+
registered_tools.append(register_tool(mcp, redo, "redo"))
|
|
239
|
+
registered_tools.append(register_tool(mcp, get_history, "get_history"))
|
|
240
|
+
|
|
241
|
+
return registered_tools
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Image tools - place image, get layers info."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from psforge.decorators import debug_tool, log_tool_call
|
|
9
|
+
from psforge.ps_adapter.application import PhotoshopApp
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all image 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 place_image(file_path: str, x: float = 0, y: float = 0) -> dict[str, Any]:
|
|
27
|
+
"""Place an image file into the current document as a new layer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file_path: Full path to the image file to place.
|
|
31
|
+
x: Horizontal position offset in pixels (default: 0).
|
|
32
|
+
y: Vertical position offset in pixels (default: 0).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
dict: Operation result with placed layer info and context.
|
|
36
|
+
"""
|
|
37
|
+
if not os.path.exists(file_path):
|
|
38
|
+
return {
|
|
39
|
+
"success": False,
|
|
40
|
+
"error": f"File not found: {file_path}",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ps_app = PhotoshopApp()
|
|
44
|
+
doc = ps_app.get_active_document()
|
|
45
|
+
|
|
46
|
+
if not doc:
|
|
47
|
+
return {
|
|
48
|
+
"success": False,
|
|
49
|
+
"error": "No active document",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Convert to Windows path format and escape
|
|
54
|
+
file_path_escaped = file_path.replace("\\", "\\\\")
|
|
55
|
+
|
|
56
|
+
place_script = f"""
|
|
57
|
+
var doc = app.activeDocument;
|
|
58
|
+
var fileRef = new File("{file_path_escaped}");
|
|
59
|
+
|
|
60
|
+
// Place the file
|
|
61
|
+
var placedLayer = doc.artLayers.add();
|
|
62
|
+
doc.activeLayer = placedLayer;
|
|
63
|
+
|
|
64
|
+
// Use File.open to place the image
|
|
65
|
+
var idPlc = charIDToTypeID("Plc ");
|
|
66
|
+
var desc = new ActionDescriptor();
|
|
67
|
+
desc.putPath(charIDToTypeID("null"), fileRef);
|
|
68
|
+
desc.putEnumerated(charIDToTypeID("FTcs"), charIDToTypeID("QCSt"), charIDToTypeID("Qcsa"));
|
|
69
|
+
var idOfst = charIDToTypeID("Ofst");
|
|
70
|
+
var offsetDesc = new ActionDescriptor();
|
|
71
|
+
offsetDesc.putUnitDouble(charIDToTypeID("Hrzn"), charIDToTypeID("#Pxl"), {x});
|
|
72
|
+
offsetDesc.putUnitDouble(charIDToTypeID("Vrtc"), charIDToTypeID("#Pxl"), {y});
|
|
73
|
+
desc.putObject(idOfst, idOfst, offsetDesc);
|
|
74
|
+
executeAction(idPlc, desc, DialogModes.NO);
|
|
75
|
+
|
|
76
|
+
doc.activeLayer.name;
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
layer_name = ps_app.execute_javascript(place_script)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"success": True,
|
|
83
|
+
"message": f"Placed image '{os.path.basename(file_path)}' as layer '{layer_name}'",
|
|
84
|
+
"layer_name": layer_name,
|
|
85
|
+
"file_path": file_path,
|
|
86
|
+
"position": {"x": x, "y": y},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to place image: {e}")
|
|
91
|
+
return {
|
|
92
|
+
"success": False,
|
|
93
|
+
"error": str(e),
|
|
94
|
+
"file_path": file_path,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@debug_tool
|
|
98
|
+
@log_tool_call
|
|
99
|
+
def get_layers() -> dict[str, Any]:
|
|
100
|
+
"""Get information about all layers in the active document.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: Operation result with layers array and context.
|
|
104
|
+
"""
|
|
105
|
+
ps_app = PhotoshopApp()
|
|
106
|
+
doc = ps_app.get_active_document()
|
|
107
|
+
|
|
108
|
+
if not doc:
|
|
109
|
+
return {
|
|
110
|
+
"success": False,
|
|
111
|
+
"error": "No active document",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
get_layers_script = """
|
|
116
|
+
(function() {
|
|
117
|
+
var doc = app.activeDocument;
|
|
118
|
+
var layers = [];
|
|
119
|
+
|
|
120
|
+
var layerKindMap = {
|
|
121
|
+
1: "NORMAL",
|
|
122
|
+
2: "TEXT",
|
|
123
|
+
3: "SOLIDFILL",
|
|
124
|
+
4: "GRADIENTFILL",
|
|
125
|
+
5: "PATTERNFILL",
|
|
126
|
+
17: "SMARTOBJECT"
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function processLayer(layer, index) {
|
|
130
|
+
var isBackground = false;
|
|
131
|
+
try {
|
|
132
|
+
isBackground = layer.isBackgroundLayer;
|
|
133
|
+
} catch(e) {}
|
|
134
|
+
|
|
135
|
+
var bounds = {left: 0, top: 0, right: 0, bottom: 0};
|
|
136
|
+
try {
|
|
137
|
+
bounds = {
|
|
138
|
+
left: parseInt(layer.bounds[0]),
|
|
139
|
+
top: parseInt(layer.bounds[1]),
|
|
140
|
+
right: parseInt(layer.bounds[2]),
|
|
141
|
+
bottom: parseInt(layer.bounds[3])
|
|
142
|
+
};
|
|
143
|
+
} catch(e) {}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
index: index,
|
|
147
|
+
name: layer.name,
|
|
148
|
+
kind: layerKindMap[layer.kind] || "UNKNOWN",
|
|
149
|
+
visible: layer.visible,
|
|
150
|
+
opacity: parseFloat(layer.opacity),
|
|
151
|
+
blend_mode: layer.blendMode.toString(),
|
|
152
|
+
locked: layer.allLocked || layer.pixelsLocked,
|
|
153
|
+
is_background: isBackground,
|
|
154
|
+
bounds: bounds,
|
|
155
|
+
width: bounds.right - bounds.left,
|
|
156
|
+
height: bounds.bottom - bounds.top
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get all layers
|
|
161
|
+
for (var i = 0; i < doc.layers.length; i++) {
|
|
162
|
+
layers.push(processLayer(doc.layers[i], i));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add background layer if exists
|
|
166
|
+
try {
|
|
167
|
+
if (doc.backgroundLayer) {
|
|
168
|
+
layers.push(processLayer(doc.backgroundLayer, layers.length));
|
|
169
|
+
}
|
|
170
|
+
} catch(e) {}
|
|
171
|
+
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
total_layers: layers.length,
|
|
174
|
+
layers: layers
|
|
175
|
+
});
|
|
176
|
+
})();
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
import json
|
|
180
|
+
|
|
181
|
+
result_str = ps_app.execute_javascript(get_layers_script)
|
|
182
|
+
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"success": True,
|
|
186
|
+
"total_layers": result["total_layers"],
|
|
187
|
+
"layers": result["layers"],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Failed to get layers: {e}")
|
|
192
|
+
return {
|
|
193
|
+
"success": False,
|
|
194
|
+
"error": str(e),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Register all tools
|
|
198
|
+
registered_tools.append(register_tool(mcp, place_image, "place_image"))
|
|
199
|
+
registered_tools.append(register_tool(mcp, get_layers, "get_layers"))
|
|
200
|
+
|
|
201
|
+
return registered_tools
|