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,244 @@
|
|
|
1
|
+
"""Session information tools - PS version, document info, selection info."""
|
|
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.context import get_context_info
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all session 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 get_session_info() -> dict[str, Any]:
|
|
27
|
+
"""Get Photoshop version and session status information.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
dict: Session information including:
|
|
31
|
+
- success: bool
|
|
32
|
+
- ps_version: Photoshop version string
|
|
33
|
+
- ps_running: Whether PS is accessible
|
|
34
|
+
- has_document: Whether any document is open
|
|
35
|
+
- context: Current PS context
|
|
36
|
+
"""
|
|
37
|
+
ps_app = PhotoshopApp()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
version = ps_app.get_photoshop_version()
|
|
41
|
+
has_doc = ps_app.has_active_document()
|
|
42
|
+
context = get_context_info()
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"success": True,
|
|
46
|
+
"ps_version": version,
|
|
47
|
+
"ps_running": True,
|
|
48
|
+
"has_document": has_doc,
|
|
49
|
+
"context": context,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Failed to get session info: {e}")
|
|
54
|
+
return {
|
|
55
|
+
"success": False,
|
|
56
|
+
"error": str(e),
|
|
57
|
+
"ps_running": False,
|
|
58
|
+
"context": get_context_info(),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@debug_tool
|
|
62
|
+
@log_tool_call
|
|
63
|
+
def get_active_document_info() -> dict[str, Any]:
|
|
64
|
+
"""Get detailed information about the currently active document.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: Document information including:
|
|
68
|
+
- success: bool
|
|
69
|
+
- document: Document details (name, dimensions, resolution, etc.)
|
|
70
|
+
- layer_count: Number of layers
|
|
71
|
+
- has_selection: Whether a selection is active
|
|
72
|
+
- context: Current PS context
|
|
73
|
+
"""
|
|
74
|
+
ps_app = PhotoshopApp()
|
|
75
|
+
doc = ps_app.get_active_document()
|
|
76
|
+
|
|
77
|
+
if not doc:
|
|
78
|
+
return {
|
|
79
|
+
"success": False,
|
|
80
|
+
"error": "No active document",
|
|
81
|
+
"context": get_context_info(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Get comprehensive document info via JavaScript
|
|
86
|
+
doc_info_script = """
|
|
87
|
+
(function() {
|
|
88
|
+
if (!app.documents.length) return null;
|
|
89
|
+
|
|
90
|
+
var doc = app.activeDocument;
|
|
91
|
+
|
|
92
|
+
var colorModeMap = {
|
|
93
|
+
1: "BITMAP",
|
|
94
|
+
2: "GRAYSCALE",
|
|
95
|
+
3: "INDEXED",
|
|
96
|
+
4: "RGB",
|
|
97
|
+
5: "CMYK",
|
|
98
|
+
7: "MULTICHANNEL",
|
|
99
|
+
8: "DUOTONE",
|
|
100
|
+
9: "LAB"
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
var bitDepthMap = {
|
|
104
|
+
1: 1,
|
|
105
|
+
8: 8,
|
|
106
|
+
16: 16,
|
|
107
|
+
32: 32
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
var info = {
|
|
111
|
+
name: doc.name,
|
|
112
|
+
path: doc.fullName ? doc.fullName.toString() : null,
|
|
113
|
+
width: parseInt(doc.width),
|
|
114
|
+
height: parseInt(doc.height),
|
|
115
|
+
resolution: parseFloat(doc.resolution),
|
|
116
|
+
color_mode: colorModeMap[doc.mode] || "UNKNOWN",
|
|
117
|
+
bit_depth: bitDepthMap[doc.bitsPerChannel] || 8,
|
|
118
|
+
layer_count: doc.layers.length,
|
|
119
|
+
has_background_layer: false,
|
|
120
|
+
has_selection: false,
|
|
121
|
+
saved: !doc.saved
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Check for background layer
|
|
125
|
+
try {
|
|
126
|
+
var bg = doc.backgroundLayer;
|
|
127
|
+
if (bg) info.has_background_layer = true;
|
|
128
|
+
} catch(e) {}
|
|
129
|
+
|
|
130
|
+
// Check for selection
|
|
131
|
+
try {
|
|
132
|
+
var selBounds = doc.selection.bounds;
|
|
133
|
+
if (selBounds) info.has_selection = true;
|
|
134
|
+
} catch(e) {}
|
|
135
|
+
|
|
136
|
+
return JSON.stringify(info);
|
|
137
|
+
})();
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
result = ps_app.execute_javascript(doc_info_script)
|
|
141
|
+
|
|
142
|
+
import json
|
|
143
|
+
|
|
144
|
+
doc_info = json.loads(result) if isinstance(result, str) else result
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"success": True,
|
|
148
|
+
"document": doc_info,
|
|
149
|
+
"context": get_context_info(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to get document info: {e}")
|
|
154
|
+
return {
|
|
155
|
+
"success": False,
|
|
156
|
+
"error": str(e),
|
|
157
|
+
"context": get_context_info(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@debug_tool
|
|
161
|
+
@log_tool_call
|
|
162
|
+
def get_selection_info() -> dict[str, Any]:
|
|
163
|
+
"""Get information about the current selection (if any).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
dict: Selection information including:
|
|
167
|
+
- success: bool
|
|
168
|
+
- has_selection: Whether a selection exists
|
|
169
|
+
- bounds: Selection bounds {left, top, right, bottom} if exists
|
|
170
|
+
- width: Selection width (if exists)
|
|
171
|
+
- height: Selection height (if exists)
|
|
172
|
+
- context: Current PS context
|
|
173
|
+
"""
|
|
174
|
+
ps_app = PhotoshopApp()
|
|
175
|
+
doc = ps_app.get_active_document()
|
|
176
|
+
|
|
177
|
+
if not doc:
|
|
178
|
+
return {
|
|
179
|
+
"success": False,
|
|
180
|
+
"error": "No active document",
|
|
181
|
+
"context": get_context_info(),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
selection_script = """
|
|
186
|
+
(function() {
|
|
187
|
+
if (!app.documents.length) return null;
|
|
188
|
+
|
|
189
|
+
var doc = app.activeDocument;
|
|
190
|
+
var result = {
|
|
191
|
+
has_selection: false,
|
|
192
|
+
bounds: null,
|
|
193
|
+
width: 0,
|
|
194
|
+
height: 0
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
var bounds = doc.selection.bounds;
|
|
199
|
+
if (bounds && bounds.length === 4) {
|
|
200
|
+
result.has_selection = true;
|
|
201
|
+
result.bounds = {
|
|
202
|
+
left: parseInt(bounds[0]),
|
|
203
|
+
top: parseInt(bounds[1]),
|
|
204
|
+
right: parseInt(bounds[2]),
|
|
205
|
+
bottom: parseInt(bounds[3])
|
|
206
|
+
};
|
|
207
|
+
result.width = result.bounds.right - result.bounds.left;
|
|
208
|
+
result.height = result.bounds.bottom - result.bounds.top;
|
|
209
|
+
}
|
|
210
|
+
} catch(e) {
|
|
211
|
+
// No selection
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return JSON.stringify(result);
|
|
215
|
+
})();
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
result = ps_app.execute_javascript(selection_script)
|
|
219
|
+
|
|
220
|
+
import json
|
|
221
|
+
|
|
222
|
+
selection_info = json.loads(result) if isinstance(result, str) else result
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"success": True,
|
|
226
|
+
**selection_info,
|
|
227
|
+
"context": get_context_info(),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(f"Failed to get selection info: {e}")
|
|
232
|
+
return {
|
|
233
|
+
"success": False,
|
|
234
|
+
"error": str(e),
|
|
235
|
+
"has_selection": False,
|
|
236
|
+
"context": get_context_info(),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Register all tools
|
|
240
|
+
registered_tools.append(register_tool(mcp, get_session_info, "get_session_info"))
|
|
241
|
+
registered_tools.append(register_tool(mcp, get_active_document_info, "get_active_document_info"))
|
|
242
|
+
registered_tools.append(register_tool(mcp, get_selection_info, "get_selection_info"))
|
|
243
|
+
|
|
244
|
+
return registered_tools
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Text layer tools - create, update content, font, color, alignment."""
|
|
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
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all text 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_text_layer(
|
|
27
|
+
text: str,
|
|
28
|
+
x: float = 100,
|
|
29
|
+
y: float = 100,
|
|
30
|
+
font_size: float = 24,
|
|
31
|
+
color_r: int = 0,
|
|
32
|
+
color_g: int = 0,
|
|
33
|
+
color_b: int = 0,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Create a new text layer with specified content and styling.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
text: Text content to display.
|
|
39
|
+
x: Horizontal position in pixels (default: 100).
|
|
40
|
+
y: Vertical position in pixels (default: 100).
|
|
41
|
+
font_size: Font size in points (default: 24).
|
|
42
|
+
color_r: Red channel 0-255 (default: 0).
|
|
43
|
+
color_g: Green channel 0-255 (default: 0).
|
|
44
|
+
color_b: Blue channel 0-255 (default: 0).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
dict: Operation result with text layer info and context.
|
|
48
|
+
"""
|
|
49
|
+
validate_color_channel(color_r, "color_r")
|
|
50
|
+
validate_color_channel(color_g, "color_g")
|
|
51
|
+
validate_color_channel(color_b, "color_b")
|
|
52
|
+
|
|
53
|
+
ps_app = PhotoshopApp()
|
|
54
|
+
doc = ps_app.get_active_document()
|
|
55
|
+
|
|
56
|
+
if not doc:
|
|
57
|
+
return {
|
|
58
|
+
"success": False,
|
|
59
|
+
"error": "No active document",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
text_escaped = js_escape_string(text)
|
|
64
|
+
|
|
65
|
+
create_text_script = f"""
|
|
66
|
+
var doc = app.activeDocument;
|
|
67
|
+
|
|
68
|
+
// Create text layer
|
|
69
|
+
var textLayer = doc.artLayers.add();
|
|
70
|
+
textLayer.kind = LayerKind.TEXT;
|
|
71
|
+
|
|
72
|
+
var textItem = textLayer.textItem;
|
|
73
|
+
textItem.contents = "{text_escaped}";
|
|
74
|
+
textItem.position = [{x}, {y}];
|
|
75
|
+
textItem.size = {font_size};
|
|
76
|
+
|
|
77
|
+
// Set color
|
|
78
|
+
var textColor = new SolidColor();
|
|
79
|
+
textColor.rgb.red = {color_r};
|
|
80
|
+
textColor.rgb.green = {color_g};
|
|
81
|
+
textColor.rgb.blue = {color_b};
|
|
82
|
+
textItem.color = textColor;
|
|
83
|
+
|
|
84
|
+
textLayer.name;
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
layer_name = ps_app.execute_javascript(create_text_script)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"success": True,
|
|
91
|
+
"message": f"Created text layer '{layer_name}'",
|
|
92
|
+
"layer_name": layer_name,
|
|
93
|
+
"text": text,
|
|
94
|
+
"position": {"x": x, "y": y},
|
|
95
|
+
"font_size": font_size,
|
|
96
|
+
"color": {"r": color_r, "g": color_g, "b": color_b},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Failed to create text layer: {e}")
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": str(e),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@debug_tool
|
|
107
|
+
@log_tool_call
|
|
108
|
+
def update_text_content(new_text: str) -> dict[str, Any]:
|
|
109
|
+
"""Update the text content of the currently active text layer.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
new_text: New text content.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: Operation result and context.
|
|
116
|
+
"""
|
|
117
|
+
ps_app = PhotoshopApp()
|
|
118
|
+
doc = ps_app.get_active_document()
|
|
119
|
+
|
|
120
|
+
if not doc:
|
|
121
|
+
return {
|
|
122
|
+
"success": False,
|
|
123
|
+
"error": "No active document",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
text_escaped = js_escape_string(new_text)
|
|
128
|
+
|
|
129
|
+
update_script = f"""
|
|
130
|
+
var layer = app.activeDocument.activeLayer;
|
|
131
|
+
|
|
132
|
+
// Check if it's a text layer
|
|
133
|
+
if (layer.kind !== LayerKind.TEXT) {{
|
|
134
|
+
throw new Error("Active layer is not a text layer");
|
|
135
|
+
}}
|
|
136
|
+
|
|
137
|
+
var oldText = layer.textItem.contents;
|
|
138
|
+
layer.textItem.contents = "{text_escaped}";
|
|
139
|
+
|
|
140
|
+
JSON.stringify({{
|
|
141
|
+
layer_name: layer.name,
|
|
142
|
+
old_text: oldText,
|
|
143
|
+
new_text: "{text_escaped}"
|
|
144
|
+
}});
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
import json
|
|
148
|
+
|
|
149
|
+
result_str = ps_app.execute_javascript(update_script)
|
|
150
|
+
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"success": True,
|
|
154
|
+
"message": f"Updated text in layer '{result['layer_name']}'",
|
|
155
|
+
"layer_name": result["layer_name"],
|
|
156
|
+
"old_text": result["old_text"],
|
|
157
|
+
"new_text": new_text,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
error_msg = str(e)
|
|
162
|
+
if "not a text layer" in error_msg.lower():
|
|
163
|
+
return {
|
|
164
|
+
"success": False,
|
|
165
|
+
"error": "Active layer is not a text layer",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
logger.error(f"Failed to update text content: {e}")
|
|
169
|
+
return {
|
|
170
|
+
"success": False,
|
|
171
|
+
"error": str(e),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@debug_tool
|
|
175
|
+
@log_tool_call
|
|
176
|
+
def set_text_font(font_name: str, font_size: float = None) -> dict[str, Any]:
|
|
177
|
+
"""Set the font family and/or size of the currently active text layer.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
font_name: Font family name (e.g., "Arial", "Times New Roman").
|
|
181
|
+
font_size: Optional font size in points.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
dict: Operation result and context.
|
|
185
|
+
"""
|
|
186
|
+
ps_app = PhotoshopApp()
|
|
187
|
+
doc = ps_app.get_active_document()
|
|
188
|
+
|
|
189
|
+
if not doc:
|
|
190
|
+
return {
|
|
191
|
+
"success": False,
|
|
192
|
+
"error": "No active document",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
font_escaped = js_escape_string(font_name)
|
|
197
|
+
|
|
198
|
+
if font_size is not None:
|
|
199
|
+
set_font_script = f"""
|
|
200
|
+
var layer = app.activeDocument.activeLayer;
|
|
201
|
+
|
|
202
|
+
if (layer.kind !== LayerKind.TEXT) {{
|
|
203
|
+
throw new Error("Active layer is not a text layer");
|
|
204
|
+
}}
|
|
205
|
+
|
|
206
|
+
layer.textItem.font = "{font_escaped}";
|
|
207
|
+
layer.textItem.size = {font_size};
|
|
208
|
+
|
|
209
|
+
layer.name;
|
|
210
|
+
"""
|
|
211
|
+
else:
|
|
212
|
+
set_font_script = f"""
|
|
213
|
+
var layer = app.activeDocument.activeLayer;
|
|
214
|
+
|
|
215
|
+
if (layer.kind !== LayerKind.TEXT) {{
|
|
216
|
+
throw new Error("Active layer is not a text layer");
|
|
217
|
+
}}
|
|
218
|
+
|
|
219
|
+
layer.textItem.font = "{font_escaped}";
|
|
220
|
+
|
|
221
|
+
layer.name;
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
layer_name = ps_app.execute_javascript(set_font_script)
|
|
225
|
+
|
|
226
|
+
message = f"Set font of layer '{layer_name}' to '{font_name}'"
|
|
227
|
+
if font_size:
|
|
228
|
+
message += f" at {font_size}pt"
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"success": True,
|
|
232
|
+
"message": message,
|
|
233
|
+
"layer_name": layer_name,
|
|
234
|
+
"font_name": font_name,
|
|
235
|
+
"font_size": font_size,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
error_msg = str(e)
|
|
240
|
+
if "not a text layer" in error_msg.lower():
|
|
241
|
+
return {
|
|
242
|
+
"success": False,
|
|
243
|
+
"error": "Active layer is not a text layer",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logger.error(f"Failed to set text font: {e}")
|
|
247
|
+
return {
|
|
248
|
+
"success": False,
|
|
249
|
+
"error": str(e),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@debug_tool
|
|
253
|
+
@log_tool_call
|
|
254
|
+
def set_text_color(red: int, green: int, blue: int) -> dict[str, Any]:
|
|
255
|
+
"""Set the color of the currently active text layer.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
red: Red channel 0-255.
|
|
259
|
+
green: Green channel 0-255.
|
|
260
|
+
blue: Blue channel 0-255.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
dict: Operation result and context.
|
|
264
|
+
"""
|
|
265
|
+
validate_color_channel(red, "red")
|
|
266
|
+
validate_color_channel(green, "green")
|
|
267
|
+
validate_color_channel(blue, "blue")
|
|
268
|
+
|
|
269
|
+
ps_app = PhotoshopApp()
|
|
270
|
+
doc = ps_app.get_active_document()
|
|
271
|
+
|
|
272
|
+
if not doc:
|
|
273
|
+
return {
|
|
274
|
+
"success": False,
|
|
275
|
+
"error": "No active document",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
set_color_script = f"""
|
|
280
|
+
var layer = app.activeDocument.activeLayer;
|
|
281
|
+
|
|
282
|
+
if (layer.kind !== LayerKind.TEXT) {{
|
|
283
|
+
throw new Error("Active layer is not a text layer");
|
|
284
|
+
}}
|
|
285
|
+
|
|
286
|
+
var textColor = new SolidColor();
|
|
287
|
+
textColor.rgb.red = {red};
|
|
288
|
+
textColor.rgb.green = {green};
|
|
289
|
+
textColor.rgb.blue = {blue};
|
|
290
|
+
layer.textItem.color = textColor;
|
|
291
|
+
|
|
292
|
+
layer.name;
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
layer_name = ps_app.execute_javascript(set_color_script)
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"success": True,
|
|
299
|
+
"message": f"Set color of text layer '{layer_name}' to RGB({red}, {green}, {blue})",
|
|
300
|
+
"layer_name": layer_name,
|
|
301
|
+
"color": {"red": red, "green": green, "blue": blue},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
error_msg = str(e)
|
|
306
|
+
if "not a text layer" in error_msg.lower():
|
|
307
|
+
return {
|
|
308
|
+
"success": False,
|
|
309
|
+
"error": "Active layer is not a text layer",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
logger.error(f"Failed to set text color: {e}")
|
|
313
|
+
return {
|
|
314
|
+
"success": False,
|
|
315
|
+
"error": str(e),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@debug_tool
|
|
319
|
+
@log_tool_call
|
|
320
|
+
def set_text_alignment(alignment: str) -> dict[str, Any]:
|
|
321
|
+
"""Set the alignment of the currently active text layer.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
alignment: Text alignment - LEFT, CENTER, or RIGHT.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
dict: Operation result and context.
|
|
328
|
+
"""
|
|
329
|
+
alignment = alignment.upper()
|
|
330
|
+
valid_alignments = ["LEFT", "CENTER", "RIGHT"]
|
|
331
|
+
|
|
332
|
+
if alignment not in valid_alignments:
|
|
333
|
+
return {
|
|
334
|
+
"success": False,
|
|
335
|
+
"error": f"Invalid alignment '{alignment}'. Must be: {', '.join(valid_alignments)}",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
ps_app = PhotoshopApp()
|
|
339
|
+
doc = ps_app.get_active_document()
|
|
340
|
+
|
|
341
|
+
if not doc:
|
|
342
|
+
return {
|
|
343
|
+
"success": False,
|
|
344
|
+
"error": "No active document",
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
set_alignment_script = f"""
|
|
349
|
+
var layer = app.activeDocument.activeLayer;
|
|
350
|
+
|
|
351
|
+
if (layer.kind !== LayerKind.TEXT) {{
|
|
352
|
+
throw new Error("Active layer is not a text layer");
|
|
353
|
+
}}
|
|
354
|
+
|
|
355
|
+
layer.textItem.justification = Justification.{alignment};
|
|
356
|
+
|
|
357
|
+
layer.name;
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
layer_name = ps_app.execute_javascript(set_alignment_script)
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"message": f"Set alignment of text layer '{layer_name}' to {alignment}",
|
|
365
|
+
"layer_name": layer_name,
|
|
366
|
+
"alignment": alignment,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
error_msg = str(e)
|
|
371
|
+
if "not a text layer" in error_msg.lower():
|
|
372
|
+
return {
|
|
373
|
+
"success": False,
|
|
374
|
+
"error": "Active layer is not a text layer",
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
logger.error(f"Failed to set text alignment: {e}")
|
|
378
|
+
return {
|
|
379
|
+
"success": False,
|
|
380
|
+
"error": str(e),
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Register all tools
|
|
384
|
+
registered_tools.append(register_tool(mcp, create_text_layer, "create_text_layer"))
|
|
385
|
+
registered_tools.append(register_tool(mcp, update_text_content, "update_text_content"))
|
|
386
|
+
registered_tools.append(register_tool(mcp, set_text_font, "set_text_font"))
|
|
387
|
+
registered_tools.append(register_tool(mcp, set_text_color, "set_text_color"))
|
|
388
|
+
registered_tools.append(register_tool(mcp, set_text_alignment, "set_text_alignment"))
|
|
389
|
+
|
|
390
|
+
return registered_tools
|