editmamei 1.5.1 → 1.7.0

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.
Files changed (46) hide show
  1. package/README.md +22 -1
  2. package/dist/backends/detect-editors.js +79 -0
  3. package/dist/backends/gimp/backend.js +99 -0
  4. package/dist/backends/gimp/bridge/lib.py +1988 -0
  5. package/dist/backends/gimp/bridge/ops.py +3120 -0
  6. package/dist/backends/gimp/detect.js +86 -0
  7. package/dist/backends/gimp/errors.js +21 -0
  8. package/dist/backends/gimp/launcher.js +6 -0
  9. package/dist/backends/gimp/session.js +632 -0
  10. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  11. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  12. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  13. package/dist/cli/config.js +28 -0
  14. package/dist/core/preview-privacy.js +54 -0
  15. package/dist/core/server.js +98 -49
  16. package/dist/core/settings.js +25 -0
  17. package/dist/core/tool-activity.js +122 -0
  18. package/dist/core/tool-groups.js +24 -0
  19. package/dist/core/tool-tiers.js +24 -0
  20. package/dist/diagnostics/collect.js +4 -0
  21. package/dist/index.js +5 -1
  22. package/dist/kernel/kernel.js +3 -0
  23. package/dist/modules/gimp/index.js +43 -0
  24. package/dist/skills/editmamei-skill.zip +0 -0
  25. package/dist/telemetry/activity.js +1 -93
  26. package/dist/tools/adjustment-tools.js +2 -1
  27. package/dist/tools/gimp-adjustment-tools.js +385 -0
  28. package/dist/tools/gimp-checkpoint-tools.js +541 -0
  29. package/dist/tools/gimp-compose-tools.js +399 -0
  30. package/dist/tools/gimp-core-tools.js +302 -0
  31. package/dist/tools/gimp-document-tools.js +321 -0
  32. package/dist/tools/gimp-effect-tools.js +267 -0
  33. package/dist/tools/gimp-filter-tools.js +94 -0
  34. package/dist/tools/gimp-geometry-tools.js +223 -0
  35. package/dist/tools/gimp-inspect-tools.js +157 -0
  36. package/dist/tools/gimp-layer-tools.js +385 -0
  37. package/dist/tools/gimp-mask-tools.js +102 -0
  38. package/dist/tools/gimp-shared.js +58 -0
  39. package/dist/tools/gimp-verify-tools.js +409 -0
  40. package/dist/utils/gimp-path.js +27 -0
  41. package/dist/utils/operation-timeouts.js +24 -0
  42. package/dist/utils/session-log.js +19 -1
  43. package/dist/utils/tool-helpers.js +14 -0
  44. package/dist/utils/validate.js +11 -0
  45. package/dist/version.js +1 -1
  46. package/package.json +3 -3
Binary file
@@ -1,96 +1,4 @@
1
- export const READ_ONLY_TOOLS = new Set([
2
- 'ps_sequence',
3
- 'ps_batch',
4
- 'ps_ping',
5
- 'ps_overview',
6
- 'ps_list_capabilities',
7
- 'ps_get_preview',
8
- 'ps_get_selection_preview',
9
- 'ps_get_histogram',
10
- 'ps_inspect',
11
- 'ps_read_scene',
12
- 'ps_compare_regions',
13
- 'ps_get_layer_bounds_diff',
14
- 'ps_template_list',
15
- 'ps_template_recall',
16
- 'ps_list_actions',
17
- 'ps_report_problem',
18
- 'ps_detect',
19
- 'ps_detect_landmarks',
20
- 'ps_document',
21
- 'ps_template_verify',
22
- 'ps_resolve_placement',
23
- 'ps_template_save',
24
- 'ps_template_delete',
25
- 'ps_template_create_evidence',
26
- ]);
27
- export const KEPT_WORK_TOOLS = new Set(['ps_export', 'ps_save_psd']);
28
- export const MUTATING_TOOLS = new Set([
29
- 'ps_play_action',
30
- 'ps_execute_script',
31
- 'ps_add_adjustment_layer',
32
- 'ps_apply_adjustment',
33
- 'ps_create_document',
34
- 'ps_close_document',
35
- 'ps_open_document',
36
- 'ps_develop_raw',
37
- 'ps_filter',
38
- 'ps_group',
39
- 'ps_clipping_mask',
40
- 'ps_undo',
41
- 'ps_redo',
42
- 'ps_place_image',
43
- 'ps_resize_image',
44
- 'ps_crop_document',
45
- 'ps_convert_image_mode',
46
- 'ps_move_layer_to_position',
47
- 'ps_convert_to_smart_object',
48
- 'ps_rasterize_layer',
49
- 'ps_set_layer',
50
- 'ps_duplicate_layer',
51
- 'ps_copy_to_new_layer',
52
- 'ps_merge',
53
- 'ps_bake_layer',
54
- 'ps_add_layer_style',
55
- 'ps_create_layer',
56
- 'ps_delete_layer',
57
- 'ps_fill_layer',
58
- 'ps_add_fill_layer',
59
- 'ps_select_layer',
60
- 'ps_transform_layer',
61
- 'ps_warp_layer',
62
- 'ps_warp_layer_mesh',
63
- 'ps_warp_layer_along',
64
- 'ps_warp_layer_region',
65
- 'ps_warp_layer_to',
66
- 'ps_apply_camera_raw',
67
- 'ps_transform_canvas',
68
- 'ps_guides',
69
- 'ps_retouch',
70
- 'ps_apply_brush_stroke',
71
- 'ps_edit_object',
72
- 'ps_portrait_touchup',
73
- 'ps_add_text_to_object',
74
- 'ps_select_face_feature',
75
- 'ps_stroke_face_contour',
76
- 'ps_select_by_reference',
77
- 'ps_select',
78
- 'ps_select_subject',
79
- 'ps_select_sky',
80
- 'ps_select_subject_instance',
81
- 'ps_select_object',
82
- 'ps_replace_sky',
83
- 'ps_modify_selection',
84
- 'ps_selection_channel',
85
- 'ps_layer_mask',
86
- 'ps_path',
87
- 'ps_vector_mask',
88
- 'ps_apply_image',
89
- 'ps_calculations',
90
- 'ps_shape',
91
- 'ps_template_apply',
92
- 'ps_text',
93
- ]);
1
+ export { READ_ONLY_TOOLS, KEPT_WORK_TOOLS, MUTATING_TOOLS } from '../core/tool-activity.js';
94
2
  export function mapClientName(name) {
95
3
  if (name === undefined)
96
4
  return 'other';
@@ -2,6 +2,7 @@ import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
3
  import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
4
4
  import { getPendingRawDevelop } from '../core/raw-develop-state.js';
5
+ import { CAMERA_RAW_TOOL } from '../core/tool-activity.js';
5
6
  const addAdjustmentLayerSchema = {
6
7
  type: 'object',
7
8
  properties: {
@@ -844,7 +845,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
844
845
  if (pendingRaw) {
845
846
  const source = pendingRaw.documentName || pendingRaw.filePath || 'a raw source';
846
847
  const note = `This document was opened from raw source ${source} with no Camera Raw develop pass yet — ` +
847
- 'consider ps_apply_camera_raw first for global tone/color. Nothing failed; ignore if the ' +
848
+ `consider ${CAMERA_RAW_TOOL} first for global tone/color. Nothing failed; ignore if the ` +
848
849
  'user prescribed this exact layer or the file was already developed elsewhere.';
849
850
  if (typeof result === 'object' && result !== null) {
850
851
  structured.raw_develop_pending = note;
@@ -0,0 +1,385 @@
1
+ import { validateArgs } from '../utils/validate.js';
2
+ import { toolGimpErrorResult } from '../utils/tool-helpers.js';
3
+ import { GIMP_IMAGE_PROP, GIMP_LAYER_ID_PROP, GIMP_LAYER_PROP, pickSchemaDeclaredKeys, } from './gimp-shared.js';
4
+ const ADJUST_TYPES = [
5
+ 'curves',
6
+ 'levels',
7
+ 'exposure',
8
+ 'brightness_contrast',
9
+ 'hue_saturation',
10
+ 'color_balance',
11
+ 'color_temperature',
12
+ 'shadows_highlights',
13
+ 'saturation',
14
+ 'vibrance',
15
+ 'sharpen',
16
+ 'noise_reduction',
17
+ 'gaussian_blur',
18
+ ];
19
+ const curvesLevelsChannelProp = {
20
+ type: 'string',
21
+ enum: ['value', 'red', 'green', 'blue'],
22
+ description: "curves/levels only. 'value' adjusts all channels together (tone); red/green/blue adjusts " +
23
+ 'ONE channel (color) — a filter carries only one channel, so shifting color needs separate ' +
24
+ 'filters for red and blue. Default (when creating): value. Omitted on a re-edit keeps the ' +
25
+ "existing filter's own channel.",
26
+ };
27
+ const adjustSchema = {
28
+ type: 'object',
29
+ properties: {
30
+ image: GIMP_IMAGE_PROP,
31
+ layer: {
32
+ ...GIMP_LAYER_PROP,
33
+ description: GIMP_LAYER_PROP.description +
34
+ ' The filter applies to that ONE layer, not to the flattened image, so on a multi-layer ' +
35
+ 'document name the layer you mean. A layer inside a layer group can be named directly.',
36
+ },
37
+ layer_id: GIMP_LAYER_ID_PROP,
38
+ type: {
39
+ type: 'string',
40
+ enum: [...ADJUST_TYPES],
41
+ description: 'Which adjustment to apply/re-edit. Each type maps to exactly one GEGL/GIMP operation; ' +
42
+ 'see the per-field descriptions below for which fields apply to which type.',
43
+ },
44
+ filter_id: {
45
+ type: 'integer',
46
+ description: "Re-edit this existing filter IN PLACE instead of adding a new one (must match `type`'s " +
47
+ 'operation). Any per-type field you omit keeps the value it already had — this is a ' +
48
+ 'MERGE, not a reset. The values gimp_filter op=list reports for it are in these same ' +
49
+ 'field names and units, so they can be passed straight back. The mask is fixed at ' +
50
+ 'creation and cannot change on a re-edit. Only filters Editmamei created can be re-edited ' +
51
+ '(gimp_filter op=list shows source: "editmamei"); re-editing any other filter (for example ' +
52
+ 'one added in the GIMP GUI) is refused, because its current values cannot be read back ' +
53
+ 'exactly — delete it and re-create it instead. A re-edit keeps the filter on its own ' +
54
+ 'layer; `layer` and `layer_id` are ignored.',
55
+ },
56
+ mask: {
57
+ type: 'string',
58
+ description: 'Name of a mask channel from gimp_create_mask, confining a NEW filter to it. Only valid ' +
59
+ "when creating (no filter_id) — a filter's mask is fixed at creation. ORDER MATTERS: " +
60
+ 'straighten / flip / resize the canvas first (gimp_transform_canvas, gimp_resize_image), ' +
61
+ 'then crop, THEN add masked adjustments — rotate, flip, and resize all refuse outright ' +
62
+ 'once any masked adjustment filter exists on the image, or any filter not created by ' +
63
+ 'Editmamei (for example one added in the GIMP GUI), since they cannot keep a mask aligned ' +
64
+ 'through those transforms. Cropping afterward is always safe.',
65
+ },
66
+ name: {
67
+ type: 'string',
68
+ description: 'Label for a new filter, e.g. "Lift shadows". Made unique automatically.',
69
+ },
70
+ channel: curvesLevelsChannelProp,
71
+ points: {
72
+ type: 'array',
73
+ minItems: 2,
74
+ description: 'curves only. [input, output] pairs, 0-255 each, ascending input — include the endpoints, ' +
75
+ 'e.g. [[0,0],[64,90],[255,255]] lifts shadows. Required when creating (no filter_id); ' +
76
+ 'omitted on a re-edit keeps the existing curve.',
77
+ items: {
78
+ type: 'array',
79
+ items: { type: 'number', minimum: 0, maximum: 255 },
80
+ minItems: 2,
81
+ maxItems: 2,
82
+ },
83
+ },
84
+ in_low: {
85
+ type: 'number',
86
+ minimum: 0,
87
+ maximum: 255,
88
+ description: 'levels only. Input black point; must stay below in_high (an empty or inverted input ' +
89
+ 'range is refused). Default (when creating): 0.',
90
+ },
91
+ in_high: {
92
+ type: 'number',
93
+ minimum: 0,
94
+ maximum: 255,
95
+ description: 'levels only. Input white point; must stay above in_low. Default (when creating): 255.',
96
+ },
97
+ gamma: {
98
+ type: 'number',
99
+ minimum: 0.1,
100
+ maximum: 10,
101
+ description: 'levels only. Midtone gamma, 0.1..10 — >1 brightens midtones, <1 darkens. Default (when ' +
102
+ 'creating): 1.0.',
103
+ },
104
+ out_low: {
105
+ type: 'number',
106
+ minimum: 0,
107
+ maximum: 255,
108
+ description: 'levels only. Output black. Default (when creating): 0.',
109
+ },
110
+ out_high: {
111
+ type: 'number',
112
+ minimum: 0,
113
+ maximum: 255,
114
+ description: 'levels only. Output white. Default (when creating): 255.',
115
+ },
116
+ exposure: {
117
+ type: 'number',
118
+ minimum: -10,
119
+ maximum: 10,
120
+ description: 'exposure only. Stops. Default (when creating): 0.',
121
+ },
122
+ black_level: {
123
+ type: 'number',
124
+ minimum: -0.1,
125
+ maximum: 0.1,
126
+ description: 'exposure only. Shadow black-point shift. Default (when creating): 0.0.',
127
+ },
128
+ brightness: {
129
+ type: 'number',
130
+ minimum: -100,
131
+ maximum: 100,
132
+ description: 'brightness_contrast only. Default (when creating): 0.',
133
+ },
134
+ contrast: {
135
+ type: 'number',
136
+ minimum: -100,
137
+ maximum: 100,
138
+ description: 'brightness_contrast only. Default (when creating): 0.',
139
+ },
140
+ range: {
141
+ type: 'string',
142
+ enum: [
143
+ 'all',
144
+ 'red',
145
+ 'yellow',
146
+ 'green',
147
+ 'cyan',
148
+ 'blue',
149
+ 'magenta',
150
+ 'shadows',
151
+ 'midtones',
152
+ 'highlights',
153
+ ],
154
+ description: "hue_saturation: one of all/red/yellow/green/cyan/blue/magenta (default 'all'). " +
155
+ "color_balance: one of shadows/midtones/highlights (default 'midtones'). ONE range per " +
156
+ 'filter: adjusting reds and blues, or shadows and highlights, takes one filter per range ' +
157
+ "(the same rule as one curves filter per channel). Passing a value from the other type's " +
158
+ 'set is rejected by the bridge naming the real allowed set for the `type` you gave.',
159
+ },
160
+ hue: {
161
+ type: 'number',
162
+ minimum: -180,
163
+ maximum: 180,
164
+ description: 'hue_saturation only. Degrees. Default (when creating): 0.',
165
+ },
166
+ saturation: {
167
+ type: 'number',
168
+ minimum: -100,
169
+ maximum: 100,
170
+ description: 'hue_saturation: -100..100 percent (default 0 when creating), the field that actually ' +
171
+ 'adjusts saturation for that type. vibrance: a SEPARATE, plain saturation multiplier ' +
172
+ 'layered on top of the vibrance field below — 1.0 = unchanged (default when creating), ' +
173
+ '0..10 scale. It is NOT the vibrance knob; normally leave it alone and use `vibrance` ' +
174
+ 'instead. The bridge enforces the precise range for whichever `type` you gave.',
175
+ },
176
+ lightness: {
177
+ type: 'number',
178
+ minimum: -100,
179
+ maximum: 100,
180
+ description: 'hue_saturation only. Default (when creating): 0.',
181
+ },
182
+ cyan_red: {
183
+ type: 'number',
184
+ minimum: -100,
185
+ maximum: 100,
186
+ description: 'color_balance only. Default (when creating): 0.',
187
+ },
188
+ magenta_green: {
189
+ type: 'number',
190
+ minimum: -100,
191
+ maximum: 100,
192
+ description: 'color_balance only. Default (when creating): 0.',
193
+ },
194
+ yellow_blue: {
195
+ type: 'number',
196
+ minimum: -100,
197
+ maximum: 100,
198
+ description: 'color_balance only. Default (when creating): 0.',
199
+ },
200
+ preserve_luminosity: {
201
+ type: 'boolean',
202
+ description: 'color_balance only. Default (when creating): true.',
203
+ },
204
+ from_kelvin: {
205
+ type: 'number',
206
+ minimum: 1000,
207
+ maximum: 12000,
208
+ description: 'color_temperature only. The temperature the photo currently looks shot under. Default ' +
209
+ '(when creating): 6500.',
210
+ },
211
+ to_kelvin: {
212
+ type: 'number',
213
+ minimum: 1000,
214
+ maximum: 12000,
215
+ description: 'color_temperature only. The corrected target temperature. COUNTER-INTUITIVE DIRECTION: ' +
216
+ 'RAISING to_kelvin WARMS the image (it means "render as if shot under this light" — a ' +
217
+ 'higher assumed light temperature reads as a warmer correction). Example: from_kelvin 6500, ' +
218
+ 'to_kelvin 8000 renders WARMER, not cooler. Default (when creating): 6500.',
219
+ },
220
+ shadows: {
221
+ type: 'number',
222
+ minimum: -100,
223
+ maximum: 100,
224
+ description: 'shadows_highlights only. Default (when creating): 0.',
225
+ },
226
+ highlights: {
227
+ type: 'number',
228
+ minimum: -100,
229
+ maximum: 100,
230
+ description: 'shadows_highlights only. Default (when creating): 0.',
231
+ },
232
+ whitepoint: {
233
+ type: 'number',
234
+ minimum: -10,
235
+ maximum: 10,
236
+ description: 'shadows_highlights only. Shifts the white point: positive brightens the top end, ' +
237
+ 'negative pulls it down. Default (when creating): 0.',
238
+ },
239
+ radius: {
240
+ type: 'number',
241
+ minimum: 0,
242
+ maximum: 1500,
243
+ description: 'shadows_highlights: spatial falloff radius, 0.1..1500 (default 100 when creating). ' +
244
+ 'sharpen: unsharp-mask std-dev, 0..1500 (default 3.0 when creating). gaussian_blur: blur ' +
245
+ 'radius (std-dev) in pixels at full resolution, 0..1500 (default 1.5 when creating). All ' +
246
+ 'three are SPATIAL (radius-scaled on the proxy preview, see gimp_overview).',
247
+ },
248
+ compress: {
249
+ type: 'number',
250
+ minimum: 0,
251
+ maximum: 100,
252
+ description: 'shadows_highlights only. How far the shadows/highlights effect reaches toward the ' +
253
+ 'midtones: higher confines it to the darkest and brightest tones. Default (when creating): 50.',
254
+ },
255
+ shadows_ccorrect: {
256
+ type: 'number',
257
+ minimum: 0,
258
+ maximum: 100,
259
+ description: 'shadows_highlights only. Colour saturation kept in the lifted shadows (0 = none, 100 = ' +
260
+ 'full). Default (when creating): 100.',
261
+ },
262
+ highlights_ccorrect: {
263
+ type: 'number',
264
+ minimum: 0,
265
+ maximum: 100,
266
+ description: 'shadows_highlights only. Colour saturation kept in the recovered highlights (0 = none, ' +
267
+ '100 = full). Default (when creating): 50.',
268
+ },
269
+ scale: {
270
+ type: 'number',
271
+ minimum: 0,
272
+ maximum: 10,
273
+ description: 'saturation only. 1.0 = unchanged. Default (when creating): 1.0.',
274
+ },
275
+ vibrance: {
276
+ type: 'number',
277
+ minimum: -100,
278
+ maximum: 100,
279
+ description: 'vibrance only. THE vibrance control — a vibrance adjustment must set this field, not ' +
280
+ '`saturation` (a separate, plain multiplier for this type that normally stays at its ' +
281
+ 'default). Positive values boost muted/less-saturated colours more than already-saturated ' +
282
+ 'ones (skin tones move less than a bright sky); negative desaturates the same way in ' +
283
+ 'reverse. Default (when creating): 0.',
284
+ },
285
+ amount: {
286
+ type: 'number',
287
+ minimum: 0,
288
+ maximum: 300,
289
+ description: "sharpen only. GEGL's unsharp-mask strength MULTIPLIER, not a percent: 0.5 is the " +
290
+ 'default, 1.0 is strong, and values above ~3 are extreme. Default (when creating): 0.5.',
291
+ },
292
+ threshold: {
293
+ type: 'number',
294
+ minimum: 0,
295
+ maximum: 1,
296
+ description: 'sharpen only. Default (when creating): 0.',
297
+ },
298
+ strength: {
299
+ type: 'integer',
300
+ minimum: 1,
301
+ maximum: 32,
302
+ description: 'noise_reduction only. Neighbourhood size. Default (when creating): 4.',
303
+ },
304
+ },
305
+ required: ['image', 'type'],
306
+ };
307
+ async function gimpAddAdjustment(gimp, rawArgs) {
308
+ try {
309
+ const args = validateArgs(adjustSchema, rawArgs);
310
+ const result = await gimp.call('adjust', pickSchemaDeclaredKeys(adjustSchema, args));
311
+ const isNewVibranceWithNoVibrance = rawArgs.filter_id === undefined &&
312
+ rawArgs.type === 'vibrance' &&
313
+ rawArgs.vibrance === undefined;
314
+ return {
315
+ content: [
316
+ {
317
+ type: 'text',
318
+ text: `${result.filter_id !== undefined && rawArgs.filter_id !== undefined ? 'Updated' : 'Added'} ` +
319
+ `${result.type} filter "${result.name}" (id ${result.filter_id})` +
320
+ (result.mask ? ` confined to mask "${result.mask}".` : '.') +
321
+ (isNewVibranceWithNoVibrance
322
+ ? ' vibrance defaulted to 0 — only saturation changed; set `vibrance` to actually boost muted colours.'
323
+ : ''),
324
+ },
325
+ ],
326
+ structuredContent: result,
327
+ };
328
+ }
329
+ catch (error) {
330
+ return toolGimpErrorResult('Error applying GIMP adjustment', error);
331
+ }
332
+ }
333
+ export function createGimpAdjustmentTools(gimp) {
334
+ return [
335
+ {
336
+ tool: {
337
+ name: 'gimp_add_adjustment',
338
+ description: 'Headless GIMP: append (or, with filter_id, re-edit in place) a NON-DESTRUCTIVE ' +
339
+ 'adjustment filter — stays live and re-editable; nothing bakes into pixels until ' +
340
+ 'gimp_export, and gimp_filter op=delete removes it again. `type` picks the adjustment: ' +
341
+ 'curves, levels, exposure, brightness_contrast, hue_saturation, color_balance, ' +
342
+ 'color_temperature, shadows_highlights, saturation, vibrance, sharpen, noise_reduction, ' +
343
+ "gaussian_blur. A field another type uses is refused, naming this type's own fields. A " +
344
+ 'filter applies to ONE layer (`layer_id` or `layer`, else the selected or top layer), ' +
345
+ 'not to the flattened image — on a multi-layer document, say which layer. For type ' +
346
+ 'vibrance, the knob is the `vibrance` field — `saturation` on that type is a separate ' +
347
+ 'plain multiplier that normally stays untouched. curves: ONE filter per channel — add ' +
348
+ 'separate filters for red and blue rather than one filter for both; points are [input, ' +
349
+ 'output] 0-255 pairs including the endpoints. hue_saturation and color_balance likewise ' +
350
+ 'carry ONE `range` per filter. gaussian_blur: radius in pixels at full resolution; ' +
351
+ 'combine with a mask from gimp_create_mask for soft-focus or background-blur effects. A ' +
352
+ "re-edit (filter_id) MERGES: any field you omit keeps the filter's existing value, so " +
353
+ "re-editing {contrast: 50} on a brightness_contrast filter doesn't reset brightness. Only " +
354
+ 'filters Editmamei created can be re-edited; one added in the GIMP GUI is refused (delete ' +
355
+ 'and re-create it). `mask` (a channel name from gimp_create_mask) confines a NEW filter ' +
356
+ '— fixed at creation, cannot change on a re-edit. ORDER MATTERS: straighten / flip / ' +
357
+ 'resize the canvas first, then crop, THEN add any masked adjustment — rotate, flip, and ' +
358
+ 'resize all refuse outright once a masked adjustment filter exists, or any filter not ' +
359
+ 'created by Editmamei (for example one added in the GIMP GUI). ' +
360
+ "color_temperature's direction is " +
361
+ 'counter-intuitive: raising to_kelvin WARMS the image. Check the stack afterward with ' +
362
+ 'gimp_filter (op=list).',
363
+ inputSchema: adjustSchema,
364
+ outputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ filter_id: { type: 'number' },
368
+ name: { type: 'string' },
369
+ type: { type: 'string' },
370
+ mask: { type: ['string', 'null'] },
371
+ },
372
+ },
373
+ annotations: {
374
+ title: 'Add/Update GIMP Adjustment',
375
+ readOnlyHint: false,
376
+ destructiveHint: false,
377
+ idempotentHint: false,
378
+ openWorldHint: true,
379
+ },
380
+ },
381
+ handler: async (args) => gimpAddAdjustment(gimp, args),
382
+ },
383
+ ];
384
+ }
385
+ export const ADJUST_SCHEMA_FOR_TESTS = adjustSchema;