krita-cli 1.0.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.
- krita_cli/__init__.py +5 -0
- krita_cli/_shared.py +94 -0
- krita_cli/app.py +149 -0
- krita_cli/cli.py +59 -0
- krita_cli/commands/__init__.py +1 -0
- krita_cli/commands/batch.py +86 -0
- krita_cli/commands/brush.py +58 -0
- krita_cli/commands/call.py +49 -0
- krita_cli/commands/canvas.py +77 -0
- krita_cli/commands/color.py +42 -0
- krita_cli/commands/config.py +48 -0
- krita_cli/commands/file_ops.py +27 -0
- krita_cli/commands/health.py +32 -0
- krita_cli/commands/history_cmd.py +64 -0
- krita_cli/commands/introspect.py +46 -0
- krita_cli/commands/layers.py +112 -0
- krita_cli/commands/navigation.py +33 -0
- krita_cli/commands/replay.py +126 -0
- krita_cli/commands/rollback.py +39 -0
- krita_cli/commands/selection.py +364 -0
- krita_cli/commands/stroke.py +101 -0
- krita_cli/config_cmd.py +68 -0
- krita_cli/history.py +198 -0
- krita_cli-1.0.0.dist-info/METADATA +103 -0
- krita_cli-1.0.0.dist-info/RECORD +35 -0
- krita_cli-1.0.0.dist-info/WHEEL +4 -0
- krita_cli-1.0.0.dist-info/entry_points.txt +2 -0
- krita_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- krita_client/__init__.py +68 -0
- krita_client/client.py +690 -0
- krita_client/config.py +53 -0
- krita_client/models.py +507 -0
- krita_client/schema.py +109 -0
- krita_mcp/__init__.py +1 -0
- krita_mcp/server.py +1022 -0
krita_client/client.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""Typed HTTP client for communicating with the Krita plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from krita_client.config import ClientConfig
|
|
10
|
+
from krita_client.models import (
|
|
11
|
+
COMMAND_MODELS,
|
|
12
|
+
BatchParams,
|
|
13
|
+
ClearParams,
|
|
14
|
+
CreateLayerParams,
|
|
15
|
+
DeleteLayerParams,
|
|
16
|
+
DrawShapeParams,
|
|
17
|
+
ErrorCode,
|
|
18
|
+
FillParams,
|
|
19
|
+
GetCanvasParams,
|
|
20
|
+
GetColorAtParams,
|
|
21
|
+
ListBrushesParams,
|
|
22
|
+
ListLayersParams,
|
|
23
|
+
NewCanvasParams,
|
|
24
|
+
OpenFileParams,
|
|
25
|
+
RenameLayerParams,
|
|
26
|
+
RollbackParams,
|
|
27
|
+
SaveParams,
|
|
28
|
+
SelectLayerParams,
|
|
29
|
+
SetBrushParams,
|
|
30
|
+
SetColorParams,
|
|
31
|
+
SetLayerOpacityParams,
|
|
32
|
+
SetLayerVisibilityParams,
|
|
33
|
+
StrokeParams,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from typing import Self
|
|
38
|
+
|
|
39
|
+
from pydantic import BaseModel
|
|
40
|
+
|
|
41
|
+
MIN_PROTOCOL_VERSION = "1.0.0"
|
|
42
|
+
MAX_PROTOCOL_VERSION = "1.0.0"
|
|
43
|
+
_SUPPORTED_PROTOCOL_VERSION = 1
|
|
44
|
+
_CANNOT_CONNECT_MSG = "Cannot connect to Krita. Is Krita running with the MCP plugin enabled?"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class KritaError(Exception):
|
|
48
|
+
"""Base exception for Krita client errors."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, message: str, code: ErrorCode | None = None, recoverable: bool = False) -> None:
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
self.message = message
|
|
53
|
+
self.code = code
|
|
54
|
+
self.recoverable = recoverable
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class KritaConnectionError(KritaError):
|
|
58
|
+
"""Raised when the Krita plugin is unreachable."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class KritaCommandError(KritaError):
|
|
62
|
+
"""Raised when a command execution fails."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class KritaValidationError(KritaError):
|
|
66
|
+
"""Raised when command parameters fail validation."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class KritaClient:
|
|
70
|
+
"""Typed HTTP client for the Krita MCP plugin.
|
|
71
|
+
|
|
72
|
+
All public methods correspond to a Krita plugin action.
|
|
73
|
+
Parameters are validated via pydantic models before being sent.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, config: ClientConfig | None = None) -> None:
|
|
77
|
+
self._config = config or ClientConfig()
|
|
78
|
+
self._client = httpx.Client(
|
|
79
|
+
base_url=self._config.url,
|
|
80
|
+
timeout=httpx.Timeout(
|
|
81
|
+
connect=5.0,
|
|
82
|
+
read=self._config.default_timeout,
|
|
83
|
+
write=self._config.default_timeout,
|
|
84
|
+
pool=5.0,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def config(self) -> ClientConfig:
|
|
90
|
+
return self._config
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
self._client.close()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> Self:
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, *args: object) -> None:
|
|
99
|
+
self.close()
|
|
100
|
+
|
|
101
|
+
# -- Internal helpers -----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _send(
|
|
104
|
+
self,
|
|
105
|
+
action: str,
|
|
106
|
+
params: dict[str, object] | None = None,
|
|
107
|
+
timeout: float | None = None,
|
|
108
|
+
) -> dict[str, object]:
|
|
109
|
+
"""Send a command to the Krita plugin and return the JSON response."""
|
|
110
|
+
body: dict[str, object] = {"action": action, "params": params or {}}
|
|
111
|
+
request_timeout = timeout or self._config.default_timeout
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
response = self._client.post(
|
|
115
|
+
"/",
|
|
116
|
+
json=body,
|
|
117
|
+
timeout=httpx.Timeout(
|
|
118
|
+
connect=5.0,
|
|
119
|
+
read=request_timeout,
|
|
120
|
+
write=request_timeout,
|
|
121
|
+
pool=5.0,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
data = response.json()
|
|
125
|
+
if "error" in data:
|
|
126
|
+
err = data["error"]
|
|
127
|
+
if isinstance(err, dict):
|
|
128
|
+
msg = err.get("message", "Unknown error")
|
|
129
|
+
code = err.get("code")
|
|
130
|
+
recov = err.get("recoverable", False)
|
|
131
|
+
raise KritaCommandError(msg, code=code, recoverable=recov)
|
|
132
|
+
raise KritaCommandError(str(err))
|
|
133
|
+
return data # type: ignore[no-any-return]
|
|
134
|
+
except httpx.ConnectError as exc:
|
|
135
|
+
raise KritaConnectionError(
|
|
136
|
+
_CANNOT_CONNECT_MSG, code=ErrorCode.PLUGIN_UNREACHABLE, recoverable=True
|
|
137
|
+
) from exc
|
|
138
|
+
except httpx.HTTPStatusError as exc:
|
|
139
|
+
msg = f"HTTP {exc.response.status_code}: {exc.response.text}"
|
|
140
|
+
raise KritaCommandError(msg, code=ErrorCode.INTERNAL_ERROR) from exc
|
|
141
|
+
except httpx.HTTPError as exc:
|
|
142
|
+
raise KritaCommandError(str(exc), code=ErrorCode.COMMAND_TIMEOUT, recoverable=True) from exc
|
|
143
|
+
|
|
144
|
+
def _is_compatible(self, protocol_version: str) -> bool:
|
|
145
|
+
"""Check if a protocol version string is within the supported range."""
|
|
146
|
+
try:
|
|
147
|
+
parts = protocol_version.split(".")
|
|
148
|
+
if len(parts) != 3:
|
|
149
|
+
return False
|
|
150
|
+
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
|
151
|
+
min_parts = MIN_PROTOCOL_VERSION.split(".")
|
|
152
|
+
max_parts = MAX_PROTOCOL_VERSION.split(".")
|
|
153
|
+
min_tuple = (int(min_parts[0]), int(min_parts[1]), int(min_parts[2]))
|
|
154
|
+
max_tuple = (int(max_parts[0]), int(max_parts[1]), int(max_parts[2]))
|
|
155
|
+
return min_tuple <= (major, minor, patch) <= max_tuple
|
|
156
|
+
except (ValueError, IndexError):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def _health_get(self) -> dict[str, object]:
|
|
160
|
+
"""Send a GET request to the health endpoint."""
|
|
161
|
+
try:
|
|
162
|
+
response = self._client.get(
|
|
163
|
+
"/health",
|
|
164
|
+
timeout=httpx.Timeout(
|
|
165
|
+
connect=5.0,
|
|
166
|
+
read=self._config.health_timeout,
|
|
167
|
+
write=5.0,
|
|
168
|
+
pool=5.0,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
data = response.json() # type: ignore[no-any-return]
|
|
172
|
+
protocol_version = data.get("protocol_version")
|
|
173
|
+
if protocol_version is not None:
|
|
174
|
+
if isinstance(protocol_version, int) and protocol_version > _SUPPORTED_PROTOCOL_VERSION:
|
|
175
|
+
raise KritaConnectionError(
|
|
176
|
+
f"Plugin protocol mismatch. Client expects v{_SUPPORTED_PROTOCOL_VERSION} "
|
|
177
|
+
f"but plugin is running v{protocol_version}. Please upgrade krita-cli.",
|
|
178
|
+
code=ErrorCode.INCOMPATIBLE_PROTOCOL,
|
|
179
|
+
recoverable=True,
|
|
180
|
+
)
|
|
181
|
+
if isinstance(protocol_version, str) and not self._is_compatible(protocol_version):
|
|
182
|
+
raise KritaConnectionError(
|
|
183
|
+
f"Incompatible protocol version: {protocol_version}. "
|
|
184
|
+
f"Expected {MIN_PROTOCOL_VERSION}-{MAX_PROTOCOL_VERSION}",
|
|
185
|
+
code=ErrorCode.INCOMPATIBLE_PROTOCOL,
|
|
186
|
+
recoverable=True,
|
|
187
|
+
)
|
|
188
|
+
return data
|
|
189
|
+
except httpx.ConnectError as exc:
|
|
190
|
+
raise KritaConnectionError(
|
|
191
|
+
_CANNOT_CONNECT_MSG, code=ErrorCode.PLUGIN_UNREACHABLE, recoverable=True
|
|
192
|
+
) from exc
|
|
193
|
+
|
|
194
|
+
def _validate(
|
|
195
|
+
self,
|
|
196
|
+
model_cls: type[BaseModel],
|
|
197
|
+
params: dict[str, object],
|
|
198
|
+
) -> dict[str, object]:
|
|
199
|
+
"""Validate params against a pydantic model and return the validated dict."""
|
|
200
|
+
try:
|
|
201
|
+
instance = model_cls.model_validate(params)
|
|
202
|
+
return instance.model_dump(exclude_none=True)
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
raise KritaValidationError(str(exc)) from exc
|
|
205
|
+
|
|
206
|
+
# -- Public API -----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def health(self) -> dict[str, object]:
|
|
209
|
+
"""Check if Krita is running and the plugin is active."""
|
|
210
|
+
return self._health_get()
|
|
211
|
+
|
|
212
|
+
def new_canvas(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
width: int = 800,
|
|
216
|
+
height: int = 600,
|
|
217
|
+
name: str = "New Canvas",
|
|
218
|
+
background: str = "#1a1a2e",
|
|
219
|
+
) -> dict[str, object]:
|
|
220
|
+
"""Create a new canvas in Krita."""
|
|
221
|
+
validated = self._validate(
|
|
222
|
+
NewCanvasParams,
|
|
223
|
+
{"width": width, "height": height, "name": name, "background": background},
|
|
224
|
+
)
|
|
225
|
+
return self._send("new_canvas", validated)
|
|
226
|
+
|
|
227
|
+
def set_color(self, *, color: str) -> dict[str, object]:
|
|
228
|
+
"""Set the foreground paint color."""
|
|
229
|
+
validated = self._validate(SetColorParams, {"color": color})
|
|
230
|
+
return self._send("set_color", validated)
|
|
231
|
+
|
|
232
|
+
def set_brush(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
preset: str | None = None,
|
|
236
|
+
size: int | None = None,
|
|
237
|
+
opacity: float | None = None,
|
|
238
|
+
) -> dict[str, object]:
|
|
239
|
+
"""Set brush preset and properties."""
|
|
240
|
+
validated = self._validate(
|
|
241
|
+
SetBrushParams,
|
|
242
|
+
{"preset": preset, "size": size, "opacity": opacity},
|
|
243
|
+
)
|
|
244
|
+
return self._send("set_brush", validated)
|
|
245
|
+
|
|
246
|
+
def stroke(
|
|
247
|
+
self,
|
|
248
|
+
points: list[list[int]],
|
|
249
|
+
*,
|
|
250
|
+
pressure: float = 1.0,
|
|
251
|
+
size: int | None = None,
|
|
252
|
+
hardness: float = 0.5,
|
|
253
|
+
opacity: float = 1.0,
|
|
254
|
+
) -> dict[str, object]:
|
|
255
|
+
"""Paint a stroke through a series of points."""
|
|
256
|
+
validated = self._validate(
|
|
257
|
+
StrokeParams,
|
|
258
|
+
{
|
|
259
|
+
"points": points,
|
|
260
|
+
"pressure": pressure,
|
|
261
|
+
"size": size,
|
|
262
|
+
"hardness": hardness,
|
|
263
|
+
"opacity": opacity,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
return self._send("stroke", validated)
|
|
267
|
+
|
|
268
|
+
def fill(self, x: int, y: int, *, radius: int = 50) -> dict[str, object]:
|
|
269
|
+
"""Fill a circular area at a point."""
|
|
270
|
+
validated = self._validate(FillParams, {"x": x, "y": y, "radius": radius})
|
|
271
|
+
return self._send("fill", validated)
|
|
272
|
+
|
|
273
|
+
def draw_shape(
|
|
274
|
+
self,
|
|
275
|
+
shape: str,
|
|
276
|
+
x: int,
|
|
277
|
+
y: int,
|
|
278
|
+
*,
|
|
279
|
+
width: int = 100,
|
|
280
|
+
height: int = 100,
|
|
281
|
+
fill: bool = True,
|
|
282
|
+
stroke: bool = False,
|
|
283
|
+
x2: int | None = None,
|
|
284
|
+
y2: int | None = None,
|
|
285
|
+
line_width: int = 2,
|
|
286
|
+
) -> dict[str, object]:
|
|
287
|
+
"""Draw a shape on the canvas."""
|
|
288
|
+
validated = self._validate(
|
|
289
|
+
DrawShapeParams,
|
|
290
|
+
{
|
|
291
|
+
"shape": shape,
|
|
292
|
+
"x": x,
|
|
293
|
+
"y": y,
|
|
294
|
+
"width": width,
|
|
295
|
+
"height": height,
|
|
296
|
+
"fill": fill,
|
|
297
|
+
"stroke": stroke,
|
|
298
|
+
"x2": x2,
|
|
299
|
+
"y2": y2,
|
|
300
|
+
"line_width": line_width,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
return self._send("draw_shape", validated)
|
|
304
|
+
|
|
305
|
+
def get_canvas(self, *, filename: str = "canvas.png") -> dict[str, object]:
|
|
306
|
+
"""Export current canvas to a PNG file."""
|
|
307
|
+
validated = self._validate(GetCanvasParams, {"filename": filename})
|
|
308
|
+
return self._send("get_canvas", validated, timeout=self._config.export_timeout)
|
|
309
|
+
|
|
310
|
+
def undo(self) -> dict[str, object]:
|
|
311
|
+
"""Undo the last action."""
|
|
312
|
+
return self._send("undo", {})
|
|
313
|
+
|
|
314
|
+
def redo(self) -> dict[str, object]:
|
|
315
|
+
"""Redo the last undone action."""
|
|
316
|
+
return self._send("redo", {})
|
|
317
|
+
|
|
318
|
+
def clear(self, *, color: str = "#1a1a2e") -> dict[str, object]:
|
|
319
|
+
"""Clear the canvas to a solid color."""
|
|
320
|
+
validated = self._validate(ClearParams, {"color": color})
|
|
321
|
+
return self._send("clear", validated)
|
|
322
|
+
|
|
323
|
+
def save(self, path: str) -> dict[str, object]:
|
|
324
|
+
"""Save the current canvas to a specific file path."""
|
|
325
|
+
validated = self._validate(SaveParams, {"path": path})
|
|
326
|
+
return self._send("save", validated, timeout=self._config.export_timeout)
|
|
327
|
+
|
|
328
|
+
def get_color_at(self, x: int, y: int) -> dict[str, object]:
|
|
329
|
+
"""Sample the color at a specific pixel."""
|
|
330
|
+
validated = self._validate(GetColorAtParams, {"x": x, "y": y})
|
|
331
|
+
return self._send("get_color_at", validated)
|
|
332
|
+
|
|
333
|
+
def list_brushes(self, *, filter: str = "", limit: int = 20) -> dict[str, object]: # noqa: A002
|
|
334
|
+
"""List available brush presets."""
|
|
335
|
+
validated = self._validate(ListBrushesParams, {"filter": filter, "limit": limit})
|
|
336
|
+
return self._send("list_brushes", validated)
|
|
337
|
+
|
|
338
|
+
def open_file(self, path: str) -> dict[str, object]:
|
|
339
|
+
"""Open an existing file in Krita."""
|
|
340
|
+
validated = self._validate(OpenFileParams, {"path": path})
|
|
341
|
+
return self._send("open_file", validated)
|
|
342
|
+
|
|
343
|
+
def batch_execute(self, commands: list[dict[str, object]], *, stop_on_error: bool = False) -> dict[str, object]:
|
|
344
|
+
"""Execute multiple commands in a single batch.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
commands: List of command dicts, each with "action" and "params" keys.
|
|
348
|
+
stop_on_error: Stop executing remaining commands on first error.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dict with keys:
|
|
352
|
+
- "status": "ok" (all succeeded), "partial" (some failed), or "error" (all failed)
|
|
353
|
+
- "results": List of per-command results with "action", "status", and "result"/"error"
|
|
354
|
+
- "count": Number of commands executed
|
|
355
|
+
"""
|
|
356
|
+
validated = self._validate(BatchParams, {"commands": commands, "stop_on_error": stop_on_error})
|
|
357
|
+
return self._send("batch", validated)
|
|
358
|
+
|
|
359
|
+
def batch(self, commands: list[dict[str, object]], *, stop_on_error: bool = False) -> dict[str, object]:
|
|
360
|
+
"""Execute multiple commands in a single batch (alias for batch_execute)."""
|
|
361
|
+
return self.batch_execute(commands, stop_on_error=stop_on_error)
|
|
362
|
+
|
|
363
|
+
def get_command_history(self, *, limit: int = 20) -> dict[str, object]:
|
|
364
|
+
"""Retrieve recent command execution history."""
|
|
365
|
+
return self._send("get_command_history", {"limit": limit})
|
|
366
|
+
|
|
367
|
+
def rollback(self, batch_id: str) -> dict[str, object]:
|
|
368
|
+
"""Roll back a batch execution.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
batch_id: The unique ID of the batch to roll back.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dict with keys:
|
|
375
|
+
- "status": "ok" or "error"
|
|
376
|
+
- "message": Optional error message
|
|
377
|
+
"""
|
|
378
|
+
validated = self._validate(RollbackParams, {"batch_id": batch_id})
|
|
379
|
+
return self._send("rollback", validated)
|
|
380
|
+
|
|
381
|
+
def get_canvas_info(self) -> dict[str, object]:
|
|
382
|
+
"""Get information about the current canvas."""
|
|
383
|
+
return self._send("get_canvas_info", {})
|
|
384
|
+
|
|
385
|
+
def get_current_color(self) -> dict[str, object]:
|
|
386
|
+
"""Get the current foreground and background colors."""
|
|
387
|
+
return self._send("get_current_color", {})
|
|
388
|
+
|
|
389
|
+
def get_current_brush(self) -> dict[str, object]:
|
|
390
|
+
"""Get the current brush preset and properties."""
|
|
391
|
+
return self._send("get_current_brush", {})
|
|
392
|
+
|
|
393
|
+
def select_rect(
|
|
394
|
+
self,
|
|
395
|
+
x: int,
|
|
396
|
+
y: int,
|
|
397
|
+
*,
|
|
398
|
+
width: int,
|
|
399
|
+
height: int,
|
|
400
|
+
) -> dict[str, object]:
|
|
401
|
+
"""Select a rectangular area on the canvas."""
|
|
402
|
+
from krita_client.models import SelectRectParams
|
|
403
|
+
|
|
404
|
+
validated = self._validate(
|
|
405
|
+
SelectRectParams,
|
|
406
|
+
{"x": x, "y": y, "width": width, "height": height},
|
|
407
|
+
)
|
|
408
|
+
return self._send("select_rect", validated)
|
|
409
|
+
|
|
410
|
+
def select_ellipse(
|
|
411
|
+
self,
|
|
412
|
+
cx: int,
|
|
413
|
+
cy: int,
|
|
414
|
+
*,
|
|
415
|
+
rx: int,
|
|
416
|
+
ry: int,
|
|
417
|
+
) -> dict[str, object]:
|
|
418
|
+
"""Select an elliptical area on the canvas."""
|
|
419
|
+
from krita_client.models import SelectEllipseParams
|
|
420
|
+
|
|
421
|
+
validated = self._validate(
|
|
422
|
+
SelectEllipseParams,
|
|
423
|
+
{"cx": cx, "cy": cy, "rx": rx, "ry": ry},
|
|
424
|
+
)
|
|
425
|
+
return self._send("select_ellipse", validated)
|
|
426
|
+
|
|
427
|
+
def select_polygon(
|
|
428
|
+
self,
|
|
429
|
+
points: list[list[int]],
|
|
430
|
+
) -> dict[str, object]:
|
|
431
|
+
"""Select a polygonal area on the canvas."""
|
|
432
|
+
from krita_client.models import SelectPolygonParams
|
|
433
|
+
|
|
434
|
+
validated = self._validate(SelectPolygonParams, {"points": points})
|
|
435
|
+
return self._send("select_polygon", validated)
|
|
436
|
+
|
|
437
|
+
def selection_info(self) -> dict[str, object]:
|
|
438
|
+
"""Get information about the current selection."""
|
|
439
|
+
return self._send("selection_info", {})
|
|
440
|
+
|
|
441
|
+
def get_capabilities(self) -> dict[str, object]:
|
|
442
|
+
"""Get detected API capabilities from the plugin."""
|
|
443
|
+
return self._send("get_capabilities", {})
|
|
444
|
+
|
|
445
|
+
def get_security_status(self) -> dict[str, object]:
|
|
446
|
+
"""Get current security limits and usage from the plugin."""
|
|
447
|
+
return self._send("get_security_status", {})
|
|
448
|
+
|
|
449
|
+
def transform_selection(
|
|
450
|
+
self,
|
|
451
|
+
*,
|
|
452
|
+
dx: int = 0,
|
|
453
|
+
dy: int = 0,
|
|
454
|
+
angle: float = 0.0,
|
|
455
|
+
scale_x: float = 1.0,
|
|
456
|
+
scale_y: float = 1.0,
|
|
457
|
+
) -> dict[str, object]:
|
|
458
|
+
"""Transform the current selection (move, rotate, scale)."""
|
|
459
|
+
from krita_client.models import TransformSelectionParams
|
|
460
|
+
|
|
461
|
+
validated = self._validate(
|
|
462
|
+
TransformSelectionParams,
|
|
463
|
+
{"dx": dx, "dy": dy, "angle": angle, "scale_x": scale_x, "scale_y": scale_y},
|
|
464
|
+
)
|
|
465
|
+
return self._send("transform_selection", validated)
|
|
466
|
+
|
|
467
|
+
def grow_selection(self, pixels: int) -> dict[str, object]:
|
|
468
|
+
"""Grow the current selection outward by N pixels."""
|
|
469
|
+
from krita_client.models import ModifySelectionParams
|
|
470
|
+
|
|
471
|
+
validated = self._validate(ModifySelectionParams, {"pixels": pixels})
|
|
472
|
+
return self._send("grow_selection", validated)
|
|
473
|
+
|
|
474
|
+
def shrink_selection(self, pixels: int) -> dict[str, object]:
|
|
475
|
+
"""Shrink the current selection inward by N pixels."""
|
|
476
|
+
from krita_client.models import ModifySelectionParams
|
|
477
|
+
|
|
478
|
+
validated = self._validate(ModifySelectionParams, {"pixels": pixels})
|
|
479
|
+
return self._send("shrink_selection", validated)
|
|
480
|
+
|
|
481
|
+
def border_selection(self, pixels: int) -> dict[str, object]:
|
|
482
|
+
"""Create a border selection of N pixels around the current selection."""
|
|
483
|
+
from krita_client.models import ModifySelectionParams
|
|
484
|
+
|
|
485
|
+
validated = self._validate(ModifySelectionParams, {"pixels": pixels})
|
|
486
|
+
return self._send("border_selection", validated)
|
|
487
|
+
|
|
488
|
+
def combine_selections(
|
|
489
|
+
self,
|
|
490
|
+
operation: str,
|
|
491
|
+
) -> dict[str, object]:
|
|
492
|
+
"""Combine selections using union, intersect, or subtract."""
|
|
493
|
+
from krita_client.models import CombineSelectionParams
|
|
494
|
+
|
|
495
|
+
validated = self._validate(CombineSelectionParams, {"operation": operation})
|
|
496
|
+
return self._send("combine_selections", validated)
|
|
497
|
+
|
|
498
|
+
def clear_selection(self) -> dict[str, object]:
|
|
499
|
+
"""Clear the current selection."""
|
|
500
|
+
return self._send("clear_selection", {})
|
|
501
|
+
|
|
502
|
+
def invert_selection(self) -> dict[str, object]:
|
|
503
|
+
"""Invert the current selection."""
|
|
504
|
+
return self._send("invert_selection", {})
|
|
505
|
+
|
|
506
|
+
def fill_selection(self) -> dict[str, object]:
|
|
507
|
+
"""Fill the current selection with the foreground color."""
|
|
508
|
+
return self._send("fill_selection", {})
|
|
509
|
+
|
|
510
|
+
def deselect(self) -> dict[str, object]:
|
|
511
|
+
"""Remove the current selection."""
|
|
512
|
+
return self._send("deselect", {})
|
|
513
|
+
|
|
514
|
+
def select_by_color(
|
|
515
|
+
self,
|
|
516
|
+
*,
|
|
517
|
+
x: int | None = None,
|
|
518
|
+
y: int | None = None,
|
|
519
|
+
tolerance: float = 0.1,
|
|
520
|
+
contiguous: bool = True,
|
|
521
|
+
) -> dict[str, object]:
|
|
522
|
+
"""Select pixels by color similarity.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
x: X coordinate for point-based selection (magic wand). None for global.
|
|
526
|
+
y: Y coordinate for point-based selection (magic wand). None for global.
|
|
527
|
+
tolerance: Color tolerance (0.0-1.0).
|
|
528
|
+
contiguous: If True, only select contiguous region (magic wand).
|
|
529
|
+
If False, select all matching pixels globally.
|
|
530
|
+
"""
|
|
531
|
+
from krita_client.models import SelectByColorParams
|
|
532
|
+
|
|
533
|
+
params: dict[str, object] = {"tolerance": tolerance, "contiguous": contiguous}
|
|
534
|
+
if x is not None and y is not None:
|
|
535
|
+
params["x"] = x
|
|
536
|
+
params["y"] = y
|
|
537
|
+
validated = self._validate(SelectByColorParams, params)
|
|
538
|
+
return self._send("select_by_color", validated)
|
|
539
|
+
|
|
540
|
+
def select_by_alpha(
|
|
541
|
+
self,
|
|
542
|
+
*,
|
|
543
|
+
min_alpha: int = 1,
|
|
544
|
+
max_alpha: int = 255,
|
|
545
|
+
) -> dict[str, object]:
|
|
546
|
+
"""Select pixels by alpha value range.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
min_alpha: Minimum alpha value (0-255).
|
|
550
|
+
max_alpha: Maximum alpha value (0-255).
|
|
551
|
+
"""
|
|
552
|
+
from krita_client.models import SelectByAlphaParams
|
|
553
|
+
|
|
554
|
+
validated = self._validate(SelectByAlphaParams, {"min_alpha": min_alpha, "max_alpha": max_alpha})
|
|
555
|
+
return self._send("select_by_alpha", validated)
|
|
556
|
+
|
|
557
|
+
def save_selection(
|
|
558
|
+
self,
|
|
559
|
+
path: str,
|
|
560
|
+
*,
|
|
561
|
+
format: str = "png", # noqa: A002
|
|
562
|
+
) -> dict[str, object]:
|
|
563
|
+
"""Save current selection to a file as a mask.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
path: File path to save selection mask.
|
|
567
|
+
format: Image format (default: png).
|
|
568
|
+
"""
|
|
569
|
+
from krita_client.models import SaveSelectionParams
|
|
570
|
+
|
|
571
|
+
validated = self._validate(SaveSelectionParams, {"path": path, "format": format})
|
|
572
|
+
return self._send("save_selection", validated)
|
|
573
|
+
|
|
574
|
+
def load_selection(
|
|
575
|
+
self,
|
|
576
|
+
path: str,
|
|
577
|
+
) -> dict[str, object]:
|
|
578
|
+
"""Load selection from a mask file.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
path: File path to load selection mask from.
|
|
582
|
+
"""
|
|
583
|
+
from krita_client.models import LoadSelectionParams
|
|
584
|
+
|
|
585
|
+
validated = self._validate(LoadSelectionParams, {"path": path})
|
|
586
|
+
return self._send("load_selection", validated)
|
|
587
|
+
|
|
588
|
+
def selection_stats(self) -> dict[str, object]:
|
|
589
|
+
"""Get statistics about the current selection.
|
|
590
|
+
|
|
591
|
+
Returns pixel count, centroid, area percentage, etc.
|
|
592
|
+
"""
|
|
593
|
+
return self._send("selection_stats", {})
|
|
594
|
+
|
|
595
|
+
def save_selection_channel(self, *, name: str) -> dict[str, object]:
|
|
596
|
+
"""Save current selection as a named channel.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
name: Name for the selection channel.
|
|
600
|
+
"""
|
|
601
|
+
from krita_client.models import SelectionChannelParams
|
|
602
|
+
|
|
603
|
+
validated = self._validate(SelectionChannelParams, {"name": name})
|
|
604
|
+
return self._send("save_selection_channel", validated)
|
|
605
|
+
|
|
606
|
+
def load_selection_channel(self, *, name: str) -> dict[str, object]:
|
|
607
|
+
"""Load a named selection channel.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
name: Name of the selection channel to load.
|
|
611
|
+
"""
|
|
612
|
+
from krita_client.models import SelectionChannelParams
|
|
613
|
+
|
|
614
|
+
validated = self._validate(SelectionChannelParams, {"name": name})
|
|
615
|
+
return self._send("load_selection_channel", validated)
|
|
616
|
+
|
|
617
|
+
def list_selection_channels(self) -> dict[str, object]:
|
|
618
|
+
"""List all saved selection channels."""
|
|
619
|
+
return self._send("list_selection_channels", {})
|
|
620
|
+
|
|
621
|
+
def delete_selection_channel(self, *, name: str) -> dict[str, object]:
|
|
622
|
+
"""Delete a saved selection channel.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
name: Name of the selection channel to delete.
|
|
626
|
+
"""
|
|
627
|
+
from krita_client.models import SelectionChannelParams
|
|
628
|
+
|
|
629
|
+
validated = self._validate(SelectionChannelParams, {"name": name})
|
|
630
|
+
return self._send("delete_selection_channel", validated)
|
|
631
|
+
|
|
632
|
+
# -- Layer management -----------------------------------------------------
|
|
633
|
+
|
|
634
|
+
def list_layers(self) -> dict[str, object]:
|
|
635
|
+
"""List all layers in the current document."""
|
|
636
|
+
validated = self._validate(ListLayersParams, {})
|
|
637
|
+
return self._send("list_layers", validated)
|
|
638
|
+
|
|
639
|
+
def create_layer(
|
|
640
|
+
self,
|
|
641
|
+
*,
|
|
642
|
+
name: str = "New Layer",
|
|
643
|
+
layer_type: str = "paintlayer",
|
|
644
|
+
) -> dict[str, object]:
|
|
645
|
+
"""Create a new layer in the current document."""
|
|
646
|
+
validated = self._validate(CreateLayerParams, {"name": name, "layer_type": layer_type})
|
|
647
|
+
return self._send("create_layer", validated)
|
|
648
|
+
|
|
649
|
+
def select_layer(self, *, name: str) -> dict[str, object]:
|
|
650
|
+
"""Select a layer by name."""
|
|
651
|
+
validated = self._validate(SelectLayerParams, {"name": name})
|
|
652
|
+
return self._send("select_layer", validated)
|
|
653
|
+
|
|
654
|
+
def delete_layer(self, *, name: str) -> dict[str, object]:
|
|
655
|
+
"""Delete a layer by name."""
|
|
656
|
+
validated = self._validate(DeleteLayerParams, {"name": name})
|
|
657
|
+
return self._send("delete_layer", validated)
|
|
658
|
+
|
|
659
|
+
def rename_layer(self, *, old_name: str, new_name: str) -> dict[str, object]:
|
|
660
|
+
"""Rename a layer."""
|
|
661
|
+
validated = self._validate(RenameLayerParams, {"old_name": old_name, "new_name": new_name})
|
|
662
|
+
return self._send("rename_layer", validated)
|
|
663
|
+
|
|
664
|
+
def set_layer_opacity(self, *, name: str, opacity: float) -> dict[str, object]:
|
|
665
|
+
"""Set the opacity of a layer."""
|
|
666
|
+
validated = self._validate(SetLayerOpacityParams, {"name": name, "opacity": opacity})
|
|
667
|
+
return self._send("set_layer_opacity", validated)
|
|
668
|
+
|
|
669
|
+
def set_layer_visibility(self, *, name: str, visible: bool) -> dict[str, object]:
|
|
670
|
+
"""Toggle the visibility of a layer."""
|
|
671
|
+
validated = self._validate(SetLayerVisibilityParams, {"name": name, "visible": visible})
|
|
672
|
+
return self._send("set_layer_visibility", validated)
|
|
673
|
+
|
|
674
|
+
# -- Generic command dispatch ---------------------------------------------
|
|
675
|
+
|
|
676
|
+
def send_command(
|
|
677
|
+
self,
|
|
678
|
+
action: str,
|
|
679
|
+
params: dict[str, object] | None = None,
|
|
680
|
+
timeout: float | None = None,
|
|
681
|
+
) -> dict[str, object]:
|
|
682
|
+
"""Send an arbitrary command to the Krita plugin.
|
|
683
|
+
|
|
684
|
+
If the action is known, params are validated against the corresponding
|
|
685
|
+
pydantic model. Unknown actions are sent as-is.
|
|
686
|
+
"""
|
|
687
|
+
model_cls = COMMAND_MODELS.get(action)
|
|
688
|
+
if model_cls is not None and params is not None:
|
|
689
|
+
params = self._validate(model_cls, params)
|
|
690
|
+
return self._send(action, params, timeout=timeout)
|