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,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