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,316 @@
|
|
|
1
|
+
"""Layer management tools - create, delete, duplicate, merge, flatten, rasterize."""
|
|
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 management 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 create_layer(name: str = "New Layer") -> dict[str, Any]:
|
|
27
|
+
"""Create a new empty layer in the active document.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: Name for the new layer (default: "New Layer").
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
dict: Operation result with new layer info 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
|
+
name_escaped = js_escape_string(name)
|
|
46
|
+
|
|
47
|
+
create_layer_script = f"""
|
|
48
|
+
var doc = app.activeDocument;
|
|
49
|
+
var newLayer = doc.artLayers.add();
|
|
50
|
+
newLayer.name = "{name_escaped}";
|
|
51
|
+
newLayer.name;
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
layer_name = ps_app.execute_javascript(create_layer_script)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"success": True,
|
|
58
|
+
"message": f"Created layer '{layer_name}'",
|
|
59
|
+
"layer_name": layer_name,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to create layer: {e}")
|
|
64
|
+
return {
|
|
65
|
+
"success": False,
|
|
66
|
+
"error": str(e),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@debug_tool
|
|
70
|
+
@log_tool_call
|
|
71
|
+
def delete_layer() -> dict[str, Any]:
|
|
72
|
+
"""Delete the currently active layer.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
dict: Operation result and context.
|
|
76
|
+
"""
|
|
77
|
+
ps_app = PhotoshopApp()
|
|
78
|
+
doc = ps_app.get_active_document()
|
|
79
|
+
|
|
80
|
+
if not doc:
|
|
81
|
+
return {
|
|
82
|
+
"success": False,
|
|
83
|
+
"error": "No active document",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Get layer name before deleting
|
|
88
|
+
get_name_script = "app.activeDocument.activeLayer.name;"
|
|
89
|
+
layer_name = ps_app.execute_javascript(get_name_script)
|
|
90
|
+
|
|
91
|
+
# Check if it's a background layer
|
|
92
|
+
check_bg_script = """
|
|
93
|
+
(function() {
|
|
94
|
+
try {
|
|
95
|
+
return app.activeDocument.activeLayer.isBackgroundLayer;
|
|
96
|
+
} catch(e) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
"""
|
|
101
|
+
is_background = ps_app.execute_javascript(check_bg_script)
|
|
102
|
+
|
|
103
|
+
if is_background:
|
|
104
|
+
return {
|
|
105
|
+
"success": False,
|
|
106
|
+
"error": "Cannot delete background layer. Convert it to a regular layer first.",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Delete the layer
|
|
110
|
+
delete_script = "app.activeDocument.activeLayer.remove();"
|
|
111
|
+
ps_app.execute_javascript(delete_script)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"success": True,
|
|
115
|
+
"message": f"Deleted layer '{layer_name}'",
|
|
116
|
+
"deleted_layer": layer_name,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to delete layer: {e}")
|
|
121
|
+
return {
|
|
122
|
+
"success": False,
|
|
123
|
+
"error": str(e),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@debug_tool
|
|
127
|
+
@log_tool_call
|
|
128
|
+
def duplicate_layer(new_name: str = "") -> dict[str, Any]:
|
|
129
|
+
"""Duplicate the currently active layer.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
new_name: Optional name for the duplicated layer (default: auto-generated).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
dict: Operation result with duplicated layer info and context.
|
|
136
|
+
"""
|
|
137
|
+
ps_app = PhotoshopApp()
|
|
138
|
+
doc = ps_app.get_active_document()
|
|
139
|
+
|
|
140
|
+
if not doc:
|
|
141
|
+
return {
|
|
142
|
+
"success": False,
|
|
143
|
+
"error": "No active document",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
if new_name:
|
|
148
|
+
name_escaped = js_escape_string(new_name)
|
|
149
|
+
duplicate_script = f"""
|
|
150
|
+
var originalLayer = app.activeDocument.activeLayer;
|
|
151
|
+
var duplicatedLayer = originalLayer.duplicate();
|
|
152
|
+
duplicatedLayer.name = "{name_escaped}";
|
|
153
|
+
duplicatedLayer.name;
|
|
154
|
+
"""
|
|
155
|
+
else:
|
|
156
|
+
duplicate_script = """
|
|
157
|
+
var originalLayer = app.activeDocument.activeLayer;
|
|
158
|
+
var duplicatedLayer = originalLayer.duplicate();
|
|
159
|
+
duplicatedLayer.name;
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
duplicated_name = ps_app.execute_javascript(duplicate_script)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"success": True,
|
|
166
|
+
"message": f"Duplicated layer as '{duplicated_name}'",
|
|
167
|
+
"new_layer_name": duplicated_name,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Failed to duplicate layer: {e}")
|
|
172
|
+
return {
|
|
173
|
+
"success": False,
|
|
174
|
+
"error": str(e),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@debug_tool
|
|
178
|
+
@log_tool_call
|
|
179
|
+
def merge_visible_layers() -> dict[str, Any]:
|
|
180
|
+
"""Merge all visible layers in the active document.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
dict: Operation result and context.
|
|
184
|
+
"""
|
|
185
|
+
ps_app = PhotoshopApp()
|
|
186
|
+
doc = ps_app.get_active_document()
|
|
187
|
+
|
|
188
|
+
if not doc:
|
|
189
|
+
return {
|
|
190
|
+
"success": False,
|
|
191
|
+
"error": "No active document",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
merge_script = """
|
|
196
|
+
app.activeDocument.mergeVisibleLayers();
|
|
197
|
+
"Visible layers merged";
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
ps_app.execute_javascript(merge_script)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"success": True,
|
|
204
|
+
"message": "Merged all visible layers",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to merge visible layers: {e}")
|
|
209
|
+
return {
|
|
210
|
+
"success": False,
|
|
211
|
+
"error": str(e),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@debug_tool
|
|
215
|
+
@log_tool_call
|
|
216
|
+
def flatten_image() -> dict[str, Any]:
|
|
217
|
+
"""Flatten the image by merging all layers into a single background layer.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
dict: Operation result and context.
|
|
221
|
+
"""
|
|
222
|
+
ps_app = PhotoshopApp()
|
|
223
|
+
doc = ps_app.get_active_document()
|
|
224
|
+
|
|
225
|
+
if not doc:
|
|
226
|
+
return {
|
|
227
|
+
"success": False,
|
|
228
|
+
"error": "No active document",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
flatten_script = """
|
|
233
|
+
app.activeDocument.flatten();
|
|
234
|
+
"Image flattened";
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
ps_app.execute_javascript(flatten_script)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"success": True,
|
|
241
|
+
"message": "Image flattened - all layers merged into background",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Failed to flatten image: {e}")
|
|
246
|
+
return {
|
|
247
|
+
"success": False,
|
|
248
|
+
"error": str(e),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@debug_tool
|
|
252
|
+
@log_tool_call
|
|
253
|
+
def rasterize_layer() -> dict[str, Any]:
|
|
254
|
+
"""Rasterize the currently active layer (converts text/shape/smart object to pixels).
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
dict: Operation result and context.
|
|
258
|
+
"""
|
|
259
|
+
ps_app = PhotoshopApp()
|
|
260
|
+
doc = ps_app.get_active_document()
|
|
261
|
+
|
|
262
|
+
if not doc:
|
|
263
|
+
return {
|
|
264
|
+
"success": False,
|
|
265
|
+
"error": "No active document",
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Get layer info before rasterizing
|
|
270
|
+
layer_info_script = """
|
|
271
|
+
(function() {
|
|
272
|
+
var layer = app.activeDocument.activeLayer;
|
|
273
|
+
return {
|
|
274
|
+
name: layer.name,
|
|
275
|
+
kind: layer.kind.toString()
|
|
276
|
+
};
|
|
277
|
+
})();
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
import json
|
|
281
|
+
|
|
282
|
+
layer_info_str = ps_app.execute_javascript(layer_info_script)
|
|
283
|
+
layer_info = json.loads(layer_info_str) if isinstance(layer_info_str, str) else layer_info_str
|
|
284
|
+
|
|
285
|
+
# Rasterize the layer
|
|
286
|
+
rasterize_script = """
|
|
287
|
+
var layer = app.activeDocument.activeLayer;
|
|
288
|
+
layer.rasterize(RasterizeType.ENTIRELAYER);
|
|
289
|
+
"Layer rasterized";
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
ps_app.execute_javascript(rasterize_script)
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"success": True,
|
|
296
|
+
"message": f"Rasterized layer '{layer_info.get('name', 'unknown')}'",
|
|
297
|
+
"layer_name": layer_info.get("name"),
|
|
298
|
+
"previous_kind": layer_info.get("kind"),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error(f"Failed to rasterize layer: {e}")
|
|
303
|
+
return {
|
|
304
|
+
"success": False,
|
|
305
|
+
"error": str(e),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Register all tools
|
|
309
|
+
registered_tools.append(register_tool(mcp, create_layer, "create_layer"))
|
|
310
|
+
registered_tools.append(register_tool(mcp, delete_layer, "delete_layer"))
|
|
311
|
+
registered_tools.append(register_tool(mcp, duplicate_layer, "duplicate_layer"))
|
|
312
|
+
registered_tools.append(register_tool(mcp, merge_visible_layers, "merge_visible_layers"))
|
|
313
|
+
registered_tools.append(register_tool(mcp, flatten_image, "flatten_image"))
|
|
314
|
+
registered_tools.append(register_tool(mcp, rasterize_layer, "rasterize_layer"))
|
|
315
|
+
|
|
316
|
+
return registered_tools
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Layer transformation tools - move, scale, rotate, fit to canvas, resize."""
|
|
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 layer transformation 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(x: float, y: float) -> dict[str, Any]:
|
|
27
|
+
"""Move the currently active layer to a specific position.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
x: Horizontal position offset in pixels (can be negative).
|
|
31
|
+
y: Vertical position offset in pixels (can be negative).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: Operation result and context.
|
|
35
|
+
"""
|
|
36
|
+
ps_app = PhotoshopApp()
|
|
37
|
+
doc = ps_app.get_active_document()
|
|
38
|
+
|
|
39
|
+
if not doc:
|
|
40
|
+
return {
|
|
41
|
+
"success": False,
|
|
42
|
+
"error": "No active document",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
move_script = f"""
|
|
47
|
+
var layer = app.activeDocument.activeLayer;
|
|
48
|
+
var layerName = layer.name;
|
|
49
|
+
|
|
50
|
+
// Translate (move relative)
|
|
51
|
+
layer.translate({x}, {y});
|
|
52
|
+
|
|
53
|
+
layerName;
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
layer_name = ps_app.execute_javascript(move_script)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"success": True,
|
|
60
|
+
"message": f"Moved layer '{layer_name}' by ({x}, {y})px",
|
|
61
|
+
"layer_name": layer_name,
|
|
62
|
+
"offset_x": x,
|
|
63
|
+
"offset_y": y,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Failed to move layer: {e}")
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": str(e),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@debug_tool
|
|
74
|
+
@log_tool_call
|
|
75
|
+
def scale_layer(width_percent: float, height_percent: float = None) -> dict[str, Any]:
|
|
76
|
+
"""Scale the currently active layer by percentage.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
width_percent: Width scale percentage (e.g., 100 = original, 50 = half, 200 = double).
|
|
80
|
+
height_percent: Height scale percentage (if None, uses width_percent for proportional scaling).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
dict: Operation result and context.
|
|
84
|
+
"""
|
|
85
|
+
validate_numeric_range(width_percent, 0.1, 10000, "width_percent")
|
|
86
|
+
|
|
87
|
+
if height_percent is None:
|
|
88
|
+
height_percent = width_percent
|
|
89
|
+
else:
|
|
90
|
+
validate_numeric_range(height_percent, 0.1, 10000, "height_percent")
|
|
91
|
+
|
|
92
|
+
ps_app = PhotoshopApp()
|
|
93
|
+
doc = ps_app.get_active_document()
|
|
94
|
+
|
|
95
|
+
if not doc:
|
|
96
|
+
return {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": "No active document",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
scale_script = f"""
|
|
103
|
+
var layer = app.activeDocument.activeLayer;
|
|
104
|
+
var layerName = layer.name;
|
|
105
|
+
|
|
106
|
+
// Resize (scale from center)
|
|
107
|
+
layer.resize({width_percent}, {height_percent}, AnchorPosition.MIDDLECENTER);
|
|
108
|
+
|
|
109
|
+
layerName;
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
layer_name = ps_app.execute_javascript(scale_script)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"success": True,
|
|
116
|
+
"message": f"Scaled layer '{layer_name}' to {width_percent}% x {height_percent}%",
|
|
117
|
+
"layer_name": layer_name,
|
|
118
|
+
"width_percent": width_percent,
|
|
119
|
+
"height_percent": height_percent,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Failed to scale layer: {e}")
|
|
124
|
+
return {
|
|
125
|
+
"success": False,
|
|
126
|
+
"error": str(e),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@debug_tool
|
|
130
|
+
@log_tool_call
|
|
131
|
+
def rotate_layer(angle: float) -> dict[str, Any]:
|
|
132
|
+
"""Rotate the currently active layer by a specified angle.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
angle: Rotation angle in degrees (positive = clockwise, negative = counter-clockwise).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: Operation result and context.
|
|
139
|
+
"""
|
|
140
|
+
validate_numeric_range(angle, -360, 360, "angle")
|
|
141
|
+
|
|
142
|
+
ps_app = PhotoshopApp()
|
|
143
|
+
doc = ps_app.get_active_document()
|
|
144
|
+
|
|
145
|
+
if not doc:
|
|
146
|
+
return {
|
|
147
|
+
"success": False,
|
|
148
|
+
"error": "No active document",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
rotate_script = f"""
|
|
153
|
+
var layer = app.activeDocument.activeLayer;
|
|
154
|
+
var layerName = layer.name;
|
|
155
|
+
|
|
156
|
+
// Rotate around center
|
|
157
|
+
layer.rotate({angle}, AnchorPosition.MIDDLECENTER);
|
|
158
|
+
|
|
159
|
+
layerName;
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
layer_name = ps_app.execute_javascript(rotate_script)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"success": True,
|
|
166
|
+
"message": f"Rotated layer '{layer_name}' by {angle} degrees",
|
|
167
|
+
"layer_name": layer_name,
|
|
168
|
+
"angle": angle,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Failed to rotate layer: {e}")
|
|
173
|
+
return {
|
|
174
|
+
"success": False,
|
|
175
|
+
"error": str(e),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@debug_tool
|
|
179
|
+
@log_tool_call
|
|
180
|
+
def fit_layer_to_document(fill_document: bool = False) -> dict[str, Any]:
|
|
181
|
+
"""Resize the currently active layer to fit or fill the document canvas.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
fill_document: If True, fill entire canvas (may crop layer).
|
|
185
|
+
If False, fit within canvas (may have margins).
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
dict: Operation result and context.
|
|
189
|
+
"""
|
|
190
|
+
ps_app = PhotoshopApp()
|
|
191
|
+
doc = ps_app.get_active_document()
|
|
192
|
+
|
|
193
|
+
if not doc:
|
|
194
|
+
return {
|
|
195
|
+
"success": False,
|
|
196
|
+
"error": "No active document",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
fill_js = "true" if fill_document else "false"
|
|
201
|
+
|
|
202
|
+
fit_script = f"""
|
|
203
|
+
var doc = app.activeDocument;
|
|
204
|
+
var layer = doc.activeLayer;
|
|
205
|
+
var layerName = layer.name;
|
|
206
|
+
|
|
207
|
+
// Get document dimensions
|
|
208
|
+
var docWidth = doc.width.as('px');
|
|
209
|
+
var docHeight = doc.height.as('px');
|
|
210
|
+
|
|
211
|
+
// Get layer bounds
|
|
212
|
+
var bounds = layer.bounds;
|
|
213
|
+
var layerWidth = bounds[2].as('px') - bounds[0].as('px');
|
|
214
|
+
var layerHeight = bounds[3].as('px') - bounds[1].as('px');
|
|
215
|
+
|
|
216
|
+
// Calculate scale ratio
|
|
217
|
+
var widthRatio = docWidth / layerWidth * 100;
|
|
218
|
+
var heightRatio = docHeight / layerHeight * 100;
|
|
219
|
+
|
|
220
|
+
var scaleRatio;
|
|
221
|
+
if ({fill_js}) {{
|
|
222
|
+
// Fill: use larger ratio to cover entire canvas
|
|
223
|
+
scaleRatio = Math.max(widthRatio, heightRatio);
|
|
224
|
+
}} else {{
|
|
225
|
+
// Fit: use smaller ratio to fit within canvas
|
|
226
|
+
scaleRatio = Math.min(widthRatio, heightRatio);
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
// Apply scale
|
|
230
|
+
layer.resize(scaleRatio, scaleRatio, AnchorPosition.MIDDLECENTER);
|
|
231
|
+
|
|
232
|
+
// Center layer
|
|
233
|
+
var newBounds = layer.bounds;
|
|
234
|
+
var offsetX = (docWidth - (newBounds[2].as('px') - newBounds[0].as('px'))) / 2 - newBounds[0].as('px');
|
|
235
|
+
var offsetY = (docHeight - (newBounds[3].as('px') - newBounds[1].as('px'))) / 2 - newBounds[1].as('px');
|
|
236
|
+
layer.translate(offsetX, offsetY);
|
|
237
|
+
|
|
238
|
+
layerName;
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
layer_name = ps_app.execute_javascript(fit_script)
|
|
242
|
+
|
|
243
|
+
mode = "fill" if fill_document else "fit"
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"success": True,
|
|
247
|
+
"message": f"Layer '{layer_name}' resized to {mode} document canvas",
|
|
248
|
+
"layer_name": layer_name,
|
|
249
|
+
"mode": mode,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"Failed to fit layer to document: {e}")
|
|
254
|
+
return {
|
|
255
|
+
"success": False,
|
|
256
|
+
"error": str(e),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@debug_tool
|
|
260
|
+
@log_tool_call
|
|
261
|
+
def resize_image(width: int, height: int, resample_method: str = "BICUBIC") -> dict[str, Any]:
|
|
262
|
+
"""Resize the entire image (document and all layers).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
width: New width in pixels (1-300000).
|
|
266
|
+
height: New height in pixels (1-300000).
|
|
267
|
+
resample_method: Resampling method - BICUBIC, BILINEAR, NEARESTNEIGHBOR,
|
|
268
|
+
BICUBICSHARPER, BICUBICSMOOTHER (default: BICUBIC).
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
dict: Operation result and context.
|
|
272
|
+
"""
|
|
273
|
+
validate_numeric_range(width, 1, 300000, "width")
|
|
274
|
+
validate_numeric_range(height, 1, 300000, "height")
|
|
275
|
+
|
|
276
|
+
resample_method = resample_method.upper()
|
|
277
|
+
valid_methods = ["BICUBIC", "BILINEAR", "NEARESTNEIGHBOR", "BICUBICSHARPER", "BICUBICSMOOTHER"]
|
|
278
|
+
|
|
279
|
+
if resample_method not in valid_methods:
|
|
280
|
+
return {
|
|
281
|
+
"success": False,
|
|
282
|
+
"error": f"Invalid resample_method '{resample_method}'. Must be one of: {', '.join(valid_methods)}",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ps_app = PhotoshopApp()
|
|
286
|
+
doc = ps_app.get_active_document()
|
|
287
|
+
|
|
288
|
+
if not doc:
|
|
289
|
+
return {
|
|
290
|
+
"success": False,
|
|
291
|
+
"error": "No active document",
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
resize_script = f"""
|
|
296
|
+
var doc = app.activeDocument;
|
|
297
|
+
var oldWidth = doc.width.as('px');
|
|
298
|
+
var oldHeight = doc.height.as('px');
|
|
299
|
+
|
|
300
|
+
// Resize image
|
|
301
|
+
doc.resizeImage({width}, {height}, null, ResampleMethod.{resample_method});
|
|
302
|
+
|
|
303
|
+
"Resized from " + oldWidth + "x" + oldHeight + " to {width}x{height}";
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
result = ps_app.execute_javascript(resize_script)
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"success": True,
|
|
310
|
+
"message": f"Resized image to {width}x{height}px using {resample_method}",
|
|
311
|
+
"new_width": width,
|
|
312
|
+
"new_height": height,
|
|
313
|
+
"resample_method": resample_method,
|
|
314
|
+
"result": result,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(f"Failed to resize image: {e}")
|
|
319
|
+
return {
|
|
320
|
+
"success": False,
|
|
321
|
+
"error": str(e),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Register all tools
|
|
325
|
+
registered_tools.append(register_tool(mcp, move_layer, "move_layer"))
|
|
326
|
+
registered_tools.append(register_tool(mcp, scale_layer, "scale_layer"))
|
|
327
|
+
registered_tools.append(register_tool(mcp, rotate_layer, "rotate_layer"))
|
|
328
|
+
registered_tools.append(register_tool(mcp, fit_layer_to_document, "fit_layer_to_document"))
|
|
329
|
+
registered_tools.append(register_tool(mcp, resize_image, "resize_image"))
|
|
330
|
+
|
|
331
|
+
return registered_tools
|