editmamei 1.7.1 → 1.8.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.
- package/README.md +3 -3
- package/dist/backends/gimp/bridge/lib.py +833 -4
- package/dist/backends/gimp/bridge/ops.py +1943 -110
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/config.js +7 -0
- package/dist/core/settings.js +7 -0
- package/dist/core/tool-activity.js +13 -1
- package/dist/core/tool-groups.js +7 -1
- package/dist/core/tool-tiers.js +7 -1
- package/dist/modules/gimp/index.js +8 -2
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/sanitize.js +15 -1
- package/dist/tools/adjustment-tools.js +9 -1
- package/dist/tools/document-tools.js +50 -9
- package/dist/tools/filter-tools.js +2 -1
- package/dist/tools/gimp-adjustment-tools.js +3 -3
- package/dist/tools/gimp-core-tools.js +7 -8
- package/dist/tools/gimp-document-tools.js +4 -1
- package/dist/tools/gimp-effect-tools.js +2 -2
- package/dist/tools/gimp-geometry-tools.js +2 -2
- package/dist/tools/gimp-inspect-tools.js +35 -5
- package/dist/tools/gimp-layer-tools.js +5 -24
- package/dist/tools/gimp-match-tools.js +133 -0
- package/dist/tools/gimp-selection-tools.js +374 -0
- package/dist/tools/gimp-shared.js +12 -0
- package/dist/tools/gimp-text-tools.js +181 -0
- package/dist/tools/gimp-transform-layer-tools.js +215 -0
- package/dist/tools/gimp-verify-tools.js +2 -13
- package/dist/tools/image-placement-tools.js +6 -1
- package/dist/tools/sky-tools.js +2 -1
- package/dist/utils/gimp-path.js +18 -22
- package/dist/utils/local-path.js +99 -0
- package/dist/utils/operation-timeouts.js +7 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/tools/gimp-mask-tools.js +0 -102
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/config.js
CHANGED
|
@@ -50,6 +50,13 @@ const KEYS = {
|
|
|
50
50
|
},
|
|
51
51
|
coerce: coerceBool,
|
|
52
52
|
},
|
|
53
|
+
'automation.allow_execute_script': {
|
|
54
|
+
get: (s) => s.automation.allow_execute_script,
|
|
55
|
+
set: (s, v) => {
|
|
56
|
+
s.automation.allow_execute_script = v;
|
|
57
|
+
},
|
|
58
|
+
coerce: coerceBool,
|
|
59
|
+
},
|
|
53
60
|
ps_path: {
|
|
54
61
|
get: (s) => s.ps_path,
|
|
55
62
|
set: (s, v) => {
|
package/dist/core/settings.js
CHANGED
|
@@ -21,6 +21,7 @@ function defaults(installId) {
|
|
|
21
21
|
return {
|
|
22
22
|
telemetry: { usage: true, diagnostics: false, install_id: installId },
|
|
23
23
|
privacy: { send_previews_to_llm: true },
|
|
24
|
+
automation: { allow_execute_script: true },
|
|
24
25
|
ps_path: null,
|
|
25
26
|
update_check: true,
|
|
26
27
|
editor: 'auto',
|
|
@@ -34,6 +35,7 @@ function coerce(raw, installId) {
|
|
|
34
35
|
const r = raw;
|
|
35
36
|
const t = (r.telemetry ?? {});
|
|
36
37
|
const p = (r.privacy ?? {});
|
|
38
|
+
const a = (r.automation ?? {});
|
|
37
39
|
return {
|
|
38
40
|
telemetry: {
|
|
39
41
|
usage: typeof t.usage === 'boolean' ? t.usage : base.telemetry.usage,
|
|
@@ -47,6 +49,11 @@ function coerce(raw, installId) {
|
|
|
47
49
|
? p.send_previews_to_llm
|
|
48
50
|
: base.privacy.send_previews_to_llm,
|
|
49
51
|
},
|
|
52
|
+
automation: {
|
|
53
|
+
allow_execute_script: typeof a.allow_execute_script === 'boolean'
|
|
54
|
+
? a.allow_execute_script
|
|
55
|
+
: base.automation.allow_execute_script,
|
|
56
|
+
},
|
|
50
57
|
ps_path: typeof r.ps_path === 'string' ? r.ps_path : null,
|
|
51
58
|
update_check: typeof r.update_check === 'boolean' ? r.update_check : base.update_check,
|
|
52
59
|
editor: typeof r.editor === 'string' && EDITOR_PINS.includes(r.editor)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const RECIPE_TOOLS = new Set([
|
|
2
|
+
'ps_template_apply',
|
|
3
|
+
'ps_batch',
|
|
4
|
+
'ps_sequence',
|
|
5
|
+
'ps_play_action',
|
|
6
|
+
]);
|
|
1
7
|
export const READ_ONLY_TOOLS = new Set([
|
|
2
8
|
'ps_sequence',
|
|
3
9
|
'ps_batch',
|
|
@@ -29,6 +35,7 @@ export const READ_ONLY_TOOLS = new Set([
|
|
|
29
35
|
'gimp_get_preview',
|
|
30
36
|
'gimp_get_histogram',
|
|
31
37
|
'gimp_compare',
|
|
38
|
+
'gimp_get_selection_preview',
|
|
32
39
|
]);
|
|
33
40
|
export const KEPT_WORK_TOOLS = new Set([
|
|
34
41
|
'ps_export',
|
|
@@ -109,14 +116,19 @@ export const MUTATING_TOOLS = new Set([
|
|
|
109
116
|
'gimp_crop_document',
|
|
110
117
|
'gimp_resize_image',
|
|
111
118
|
'gimp_transform_canvas',
|
|
112
|
-
'gimp_create_mask',
|
|
113
119
|
'gimp_checkpoint',
|
|
114
120
|
'gimp_layer',
|
|
115
121
|
'gimp_bake',
|
|
116
122
|
'gimp_create_document',
|
|
117
123
|
'gimp_place_image',
|
|
118
124
|
'gimp_canvas',
|
|
125
|
+
'gimp_text',
|
|
119
126
|
'gimp_convert_image_mode',
|
|
127
|
+
'gimp_select',
|
|
128
|
+
'gimp_modify_selection',
|
|
129
|
+
'gimp_layer_mask',
|
|
130
|
+
'gimp_transform_layer',
|
|
131
|
+
'gimp_match_layer',
|
|
120
132
|
]);
|
|
121
133
|
export const RAW_DEVELOP_TOOL = 'ps_develop_raw';
|
|
122
134
|
export const CAMERA_RAW_TOOL = 'ps_apply_camera_raw';
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -187,14 +187,20 @@ export const TOOL_GROUPS = {
|
|
|
187
187
|
gimp_add_adjustment: 'adjust',
|
|
188
188
|
gimp_filter: 'filter',
|
|
189
189
|
gimp_add_effect: 'filter',
|
|
190
|
-
gimp_create_mask: 'masks',
|
|
191
190
|
gimp_checkpoint: 'document',
|
|
192
191
|
gimp_layer: 'layers',
|
|
193
192
|
gimp_bake: 'layers',
|
|
193
|
+
gimp_transform_layer: 'layers',
|
|
194
194
|
gimp_create_document: 'document',
|
|
195
195
|
gimp_place_image: 'layers',
|
|
196
196
|
gimp_canvas: 'document',
|
|
197
|
+
gimp_text: 'layers',
|
|
197
198
|
gimp_convert_image_mode: 'document',
|
|
199
|
+
gimp_select: 'select',
|
|
200
|
+
gimp_modify_selection: 'select',
|
|
201
|
+
gimp_layer_mask: 'masks',
|
|
202
|
+
gimp_get_selection_preview: 'verify',
|
|
203
|
+
gimp_match_layer: 'layers',
|
|
198
204
|
};
|
|
199
205
|
export function groupOf(toolName) {
|
|
200
206
|
const group = TOOL_GROUPS[toolName];
|
package/dist/core/tool-tiers.js
CHANGED
|
@@ -102,17 +102,23 @@ export const TOOL_TIERS = {
|
|
|
102
102
|
gimp_crop_document: 'community',
|
|
103
103
|
gimp_resize_image: 'community',
|
|
104
104
|
gimp_transform_canvas: 'community',
|
|
105
|
-
gimp_create_mask: 'community',
|
|
106
105
|
gimp_get_preview: 'community',
|
|
107
106
|
gimp_get_histogram: 'community',
|
|
108
107
|
gimp_compare: 'community',
|
|
109
108
|
gimp_checkpoint: 'community',
|
|
110
109
|
gimp_layer: 'community',
|
|
111
110
|
gimp_bake: 'community',
|
|
111
|
+
gimp_transform_layer: 'community',
|
|
112
112
|
gimp_create_document: 'community',
|
|
113
113
|
gimp_place_image: 'community',
|
|
114
114
|
gimp_canvas: 'community',
|
|
115
|
+
gimp_text: 'community',
|
|
115
116
|
gimp_convert_image_mode: 'community',
|
|
117
|
+
gimp_select: 'community',
|
|
118
|
+
gimp_modify_selection: 'dev',
|
|
119
|
+
gimp_layer_mask: 'community',
|
|
120
|
+
gimp_get_selection_preview: 'community',
|
|
121
|
+
gimp_match_layer: 'dev',
|
|
116
122
|
};
|
|
117
123
|
export function tierOf(toolName) {
|
|
118
124
|
const tier = TOOL_TIERS[toolName];
|
|
@@ -8,11 +8,14 @@ import { createGimpAdjustmentTools } from '../../tools/gimp-adjustment-tools.js'
|
|
|
8
8
|
import { createGimpEffectTools } from '../../tools/gimp-effect-tools.js';
|
|
9
9
|
import { createGimpFilterTools } from '../../tools/gimp-filter-tools.js';
|
|
10
10
|
import { createGimpGeometryTools } from '../../tools/gimp-geometry-tools.js';
|
|
11
|
-
import { createGimpMaskTools } from '../../tools/gimp-mask-tools.js';
|
|
12
11
|
import { createGimpVerifyTools } from '../../tools/gimp-verify-tools.js';
|
|
13
12
|
import { createGimpCheckpointTools } from '../../tools/gimp-checkpoint-tools.js';
|
|
14
13
|
import { createGimpLayerTools } from '../../tools/gimp-layer-tools.js';
|
|
14
|
+
import { createGimpTransformLayerTools } from '../../tools/gimp-transform-layer-tools.js';
|
|
15
15
|
import { createGimpComposeTools } from '../../tools/gimp-compose-tools.js';
|
|
16
|
+
import { createGimpTextTools } from '../../tools/gimp-text-tools.js';
|
|
17
|
+
import { createGimpSelectionTools } from '../../tools/gimp-selection-tools.js';
|
|
18
|
+
import { createGimpMatchTools } from '../../tools/gimp-match-tools.js';
|
|
16
19
|
export const gimpFactories = [
|
|
17
20
|
createGimpCoreTools,
|
|
18
21
|
createGimpDocumentTools,
|
|
@@ -21,11 +24,14 @@ export const gimpFactories = [
|
|
|
21
24
|
createGimpEffectTools,
|
|
22
25
|
createGimpFilterTools,
|
|
23
26
|
createGimpGeometryTools,
|
|
24
|
-
createGimpMaskTools,
|
|
25
27
|
createGimpVerifyTools,
|
|
26
28
|
createGimpCheckpointTools,
|
|
27
29
|
createGimpLayerTools,
|
|
30
|
+
createGimpTransformLayerTools,
|
|
28
31
|
createGimpComposeTools,
|
|
32
|
+
createGimpTextTools,
|
|
33
|
+
createGimpSelectionTools,
|
|
34
|
+
createGimpMatchTools,
|
|
29
35
|
];
|
|
30
36
|
export const gimpModule = {
|
|
31
37
|
manifest: { id: 'gimp', name: 'Editmamei GIMP Tools', abi: KERNEL_ABI },
|
|
Binary file
|
|
@@ -23,8 +23,22 @@ function homedirPrefixes() {
|
|
|
23
23
|
return [...set];
|
|
24
24
|
}
|
|
25
25
|
const SINGLE_SEG_POSIX = /(^|\s)\/[^\s"'/]+(?=$|\s)/;
|
|
26
|
+
const WORD = String.raw `[^\s"'\\/:]+`;
|
|
27
|
+
const DIR = String.raw `${WORD}(?: ${WORD})*`;
|
|
28
|
+
const TILDE_WORD = String.raw `[^\s"'\\/]+`;
|
|
29
|
+
const TILDE_DIR = String.raw `${TILDE_WORD}(?: ${TILDE_WORD})*`;
|
|
30
|
+
const POSIX_WORD = String.raw `[^\s"'/]+`;
|
|
31
|
+
const POSIX_DIR = String.raw `${POSIX_WORD}(?: ${POSIX_WORD})*`;
|
|
32
|
+
const PATH_TOKEN = new RegExp([
|
|
33
|
+
String.raw `[A-Za-z]:[\\/](?:${DIR}[\\/])*[^\s"']*`,
|
|
34
|
+
String.raw `\\\\(?:${DIR}[\\/])*[^\s"']+`,
|
|
35
|
+
String.raw `file:\/\/[^\s"']*`,
|
|
36
|
+
String.raw `(?<![\w~])~(?:[\\/]${TILDE_DIR}(?=[\\/]))*[\\/]${TILDE_WORD}[\\/]?`,
|
|
37
|
+
String.raw `\/${POSIX_WORD}(?:\/${POSIX_DIR}(?=\/))*\/${POSIX_WORD}\/?`,
|
|
38
|
+
String.raw `(?:^|\s)\/[^\s"'/]+(?=$|\s)`,
|
|
39
|
+
].join('|'), 'gi');
|
|
26
40
|
function basenamePaths(s) {
|
|
27
|
-
return s.replace(
|
|
41
|
+
return s.replace(PATH_TOKEN, (m) => {
|
|
28
42
|
const single = SINGLE_SEG_POSIX.exec(m);
|
|
29
43
|
if (single)
|
|
30
44
|
return `${single[1]}/…`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { looksLikePath, requireLocalPath } from '../utils/local-path.js';
|
|
4
5
|
import { getPendingRawDevelop } from '../core/raw-develop-state.js';
|
|
5
6
|
import { CAMERA_RAW_TOOL } from '../core/tool-activity.js';
|
|
6
7
|
const addAdjustmentLayerSchema = {
|
|
@@ -802,6 +803,11 @@ const FORWARDED_KEYS = new Set([
|
|
|
802
803
|
'pos_levels',
|
|
803
804
|
'thr_level',
|
|
804
805
|
]);
|
|
806
|
+
function lutNameOrPath(value) {
|
|
807
|
+
return typeof value === 'string' && looksLikePath(value)
|
|
808
|
+
? requireLocalPath('cl_lut_name', value)
|
|
809
|
+
: value;
|
|
810
|
+
}
|
|
805
811
|
async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
806
812
|
try {
|
|
807
813
|
const args = validateArgs(addAdjustmentLayerSchema, rawArgs);
|
|
@@ -816,6 +822,8 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
816
822
|
if (args[key] !== undefined)
|
|
817
823
|
params[key] = args[key];
|
|
818
824
|
}
|
|
825
|
+
if (typeof params.cl_lut_name === 'string')
|
|
826
|
+
params.cl_lut_name = lutNameOrPath(params.cl_lut_name);
|
|
819
827
|
if (type === 'color_lookup' && !params.cl_lut_name) {
|
|
820
828
|
return {
|
|
821
829
|
content: [
|
|
@@ -875,7 +883,7 @@ async function applyColorLookup(connection, snippetClient, rawArgs) {
|
|
|
875
883
|
snippet: 'applyColorLookup',
|
|
876
884
|
errorPrefix: 'Error applying color lookup',
|
|
877
885
|
params: (args) => ({
|
|
878
|
-
lutName: args.cl_lut_name,
|
|
886
|
+
lutName: lutNameOrPath(args.cl_lut_name),
|
|
879
887
|
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
880
888
|
}),
|
|
881
889
|
successText: (result) => `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
|
|
@@ -2,6 +2,7 @@ import { runScript } from '../utils/run-script.js';
|
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
3
|
import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, } from '../utils/operation-timeouts.js';
|
|
4
4
|
import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
|
|
5
|
+
import { checkOutputPath, recordOutputPath, requireLocalPath } from '../utils/local-path.js';
|
|
5
6
|
import { purgeSceneChannels } from '../perception/region-precompute.js';
|
|
6
7
|
import { Logger } from '../utils/logger.js';
|
|
7
8
|
const logger = new Logger('document-tools');
|
|
@@ -76,7 +77,7 @@ const openDocumentSchema = {
|
|
|
76
77
|
properties: {
|
|
77
78
|
file_path: {
|
|
78
79
|
type: 'string',
|
|
79
|
-
description:
|
|
80
|
+
description: "Absolute path to the file to open, on this computer's own drives (network shares are refused).",
|
|
80
81
|
},
|
|
81
82
|
suppress_dialogs: {
|
|
82
83
|
type: 'boolean',
|
|
@@ -96,7 +97,14 @@ const savePsdSchema = {
|
|
|
96
97
|
properties: {
|
|
97
98
|
output_path: {
|
|
98
99
|
type: 'string',
|
|
99
|
-
description: 'Absolute output path including filename, e.g. E:\\Photos\\Edit\\shell_01.psd',
|
|
100
|
+
description: 'Absolute output path including filename, ending in .psd, e.g. E:\\Photos\\Edit\\shell_01.psd',
|
|
101
|
+
},
|
|
102
|
+
overwrite: {
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
105
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
106
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
107
|
+
default: false,
|
|
100
108
|
},
|
|
101
109
|
maximize_compatibility: {
|
|
102
110
|
type: 'boolean',
|
|
@@ -116,7 +124,14 @@ const exportJpegSchema = {
|
|
|
116
124
|
properties: {
|
|
117
125
|
output_path: {
|
|
118
126
|
type: 'string',
|
|
119
|
-
description: 'Absolute output path including filename.',
|
|
127
|
+
description: 'Absolute output path including filename, ending in .jpg.',
|
|
128
|
+
},
|
|
129
|
+
overwrite: {
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
132
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
133
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
134
|
+
default: false,
|
|
120
135
|
},
|
|
121
136
|
quality: {
|
|
122
137
|
type: 'integer',
|
|
@@ -149,7 +164,14 @@ const exportPngSchema = {
|
|
|
149
164
|
properties: {
|
|
150
165
|
output_path: {
|
|
151
166
|
type: 'string',
|
|
152
|
-
description: 'Absolute output path including filename.',
|
|
167
|
+
description: 'Absolute output path including filename, ending in .png.',
|
|
168
|
+
},
|
|
169
|
+
overwrite: {
|
|
170
|
+
type: 'boolean',
|
|
171
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
172
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
173
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
174
|
+
default: false,
|
|
153
175
|
},
|
|
154
176
|
transparent_background: {
|
|
155
177
|
type: 'boolean',
|
|
@@ -531,7 +553,7 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
531
553
|
let filePath;
|
|
532
554
|
try {
|
|
533
555
|
const args = validateArgs(openDocumentSchema, rawArgs);
|
|
534
|
-
filePath = args.file_path;
|
|
556
|
+
filePath = requireLocalPath('file_path', args.file_path);
|
|
535
557
|
const suppressDialogs = args.suppress_dialogs;
|
|
536
558
|
const bitDepth = args.bit_depth;
|
|
537
559
|
const script = await snippetClient.build('openDocumentPipeline', {
|
|
@@ -581,8 +603,13 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
581
603
|
}
|
|
582
604
|
async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
583
605
|
let args;
|
|
606
|
+
let outputPath;
|
|
584
607
|
try {
|
|
585
608
|
args = validateArgs(savePsdSchema, rawArgs);
|
|
609
|
+
outputPath = checkOutputPath('output_path', args.output_path, {
|
|
610
|
+
extensions: ['.psd'],
|
|
611
|
+
overwrite: args.overwrite === true,
|
|
612
|
+
});
|
|
586
613
|
}
|
|
587
614
|
catch (error) {
|
|
588
615
|
return toolErrorResult('Error saving PSD', error);
|
|
@@ -605,11 +632,13 @@ async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
|
605
632
|
snippet: 'savePsdAsCopy',
|
|
606
633
|
errorPrefix: 'Error saving PSD',
|
|
607
634
|
params: (args) => ({
|
|
608
|
-
outputPath
|
|
635
|
+
outputPath,
|
|
609
636
|
maximizeCompat: args.maximize_compatibility,
|
|
610
637
|
}),
|
|
611
638
|
successText: (result) => `PSD saved:\n${JSON.stringify(result, null, 2)}`,
|
|
612
639
|
});
|
|
640
|
+
if (!res.isError)
|
|
641
|
+
recordOutputPath(outputPath);
|
|
613
642
|
if (!res.isError && typeof res.structuredContent === 'object' && res.structuredContent !== null) {
|
|
614
643
|
res.structuredContent.scene_channels_purged = purged;
|
|
615
644
|
}
|
|
@@ -618,7 +647,10 @@ async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
|
618
647
|
async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
619
648
|
try {
|
|
620
649
|
const args = validateArgs(exportJpegSchema, rawArgs);
|
|
621
|
-
const outputPath = args.output_path
|
|
650
|
+
const outputPath = checkOutputPath('output_path', args.output_path, {
|
|
651
|
+
extensions: ['.jpg'],
|
|
652
|
+
overwrite: args.overwrite === true,
|
|
653
|
+
});
|
|
622
654
|
const qualityPct = args.quality;
|
|
623
655
|
const quality = Math.round((qualityPct / 100) * 12);
|
|
624
656
|
const longEdgePx = args.long_edge_px;
|
|
@@ -630,6 +662,7 @@ async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
|
630
662
|
const script = await snippetClient.build('exportJpegPipeline', params);
|
|
631
663
|
const result = (await runScript(connection, script));
|
|
632
664
|
const structured = { ...result, quality: qualityPct, quality_ps_scale: quality };
|
|
665
|
+
recordOutputPath(outputPath);
|
|
633
666
|
return {
|
|
634
667
|
content: [
|
|
635
668
|
{
|
|
@@ -645,7 +678,8 @@ async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
|
645
678
|
}
|
|
646
679
|
}
|
|
647
680
|
async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
648
|
-
|
|
681
|
+
let outputPath;
|
|
682
|
+
const res = await runSnippetTool({
|
|
649
683
|
connection,
|
|
650
684
|
snippetClient,
|
|
651
685
|
rawArgs,
|
|
@@ -654,8 +688,12 @@ async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
|
654
688
|
errorPrefix: 'Error exporting PNG',
|
|
655
689
|
params: (args) => {
|
|
656
690
|
const longEdgePx = args.long_edge_px;
|
|
691
|
+
outputPath = checkOutputPath('output_path', args.output_path, {
|
|
692
|
+
extensions: ['.png'],
|
|
693
|
+
overwrite: args.overwrite === true,
|
|
694
|
+
});
|
|
657
695
|
const params = {
|
|
658
|
-
outputPath
|
|
696
|
+
outputPath,
|
|
659
697
|
transparentBg: args.transparent_background,
|
|
660
698
|
compression: args.compression,
|
|
661
699
|
};
|
|
@@ -665,4 +703,7 @@ async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
|
665
703
|
},
|
|
666
704
|
successText: (result) => `PNG exported:\n${JSON.stringify(result, null, 2)}`,
|
|
667
705
|
});
|
|
706
|
+
if (!res.isError && outputPath !== undefined)
|
|
707
|
+
recordOutputPath(outputPath);
|
|
708
|
+
return res;
|
|
668
709
|
}
|
|
@@ -3,6 +3,7 @@ import { validateArgs } from '../utils/validate.js';
|
|
|
3
3
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
4
4
|
import { resolveGatedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
5
5
|
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp, asSmartFilterProp, unknownDiscriminator, } from '../utils/tool-helpers.js';
|
|
6
|
+
import { requireLocalPath } from '../utils/local-path.js';
|
|
6
7
|
import { LAYER_BLEND_MODES } from '../utils/blend-modes.js';
|
|
7
8
|
import { runSmartFilterOp } from './smart-object-tools.js';
|
|
8
9
|
const gaussianBlurSchema = {
|
|
@@ -1652,7 +1653,7 @@ async function applyDisplace(connection, snippetClient, rawArgs) {
|
|
|
1652
1653
|
snippet: 'applyDisplace',
|
|
1653
1654
|
errorPrefix: 'Error applying Displace',
|
|
1654
1655
|
params: (args) => ({
|
|
1655
|
-
mapPath: args.map_path,
|
|
1656
|
+
mapPath: requireLocalPath('map_path', args.map_path),
|
|
1656
1657
|
horizontalScale: args.horizontal_scale ?? 10,
|
|
1657
1658
|
verticalScale: args.vertical_scale ?? 10,
|
|
1658
1659
|
displacementMap: args.displacement_map ?? 'stretch_to_fit',
|
|
@@ -55,7 +55,7 @@ const adjustSchema = {
|
|
|
55
55
|
},
|
|
56
56
|
mask: {
|
|
57
57
|
type: 'string',
|
|
58
|
-
description: 'Name of a
|
|
58
|
+
description: 'Name of a saved selection channel, confining a NEW filter to it. Only valid ' +
|
|
59
59
|
"when creating (no filter_id) — a filter's mask is fixed at creation. ORDER MATTERS: " +
|
|
60
60
|
'straighten / flip / resize the canvas first (gimp_transform_canvas, gimp_resize_image), ' +
|
|
61
61
|
'then crop, THEN add masked adjustments — rotate, flip, and resize all refuse outright ' +
|
|
@@ -348,11 +348,11 @@ export function createGimpAdjustmentTools(gimp) {
|
|
|
348
348
|
'separate filters for red and blue rather than one filter for both; points are [input, ' +
|
|
349
349
|
'output] 0-255 pairs including the endpoints. hue_saturation and color_balance likewise ' +
|
|
350
350
|
'carry ONE `range` per filter. gaussian_blur: radius in pixels at full resolution; ' +
|
|
351
|
-
'combine with a mask
|
|
351
|
+
'combine with a saved-selection mask for soft-focus or background-blur effects. A ' +
|
|
352
352
|
"re-edit (filter_id) MERGES: any field you omit keeps the filter's existing value, so " +
|
|
353
353
|
"re-editing {contrast: 50} on a brightness_contrast filter doesn't reset brightness. Only " +
|
|
354
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
|
|
355
|
+
'and re-create it). `mask` (a saved selection channel name) confines a NEW filter ' +
|
|
356
356
|
'— fixed at creation, cannot change on a re-edit. ORDER MATTERS: straighten / flip / ' +
|
|
357
357
|
'resize the canvas first, then crop, THEN add any masked adjustment — rotate, flip, and ' +
|
|
358
358
|
'resize all refuse outright once a masked adjustment filter exists, or any filter not ' +
|
|
@@ -145,13 +145,12 @@ workflow avoids.
|
|
|
145
145
|
|
|
146
146
|
## Masks
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
instead.
|
|
148
|
+
A selection can be saved into a NAMED channel. Pass the name of a
|
|
149
|
+
channel saved in the image as \`mask\` when you CREATE a filter
|
|
150
|
+
with \`gimp_add_adjustment\` or \`gimp_add_effect\` to confine it.
|
|
151
|
+
A filter's mask is fixed at creation — re-editing it by
|
|
152
|
+
\`filter_id\` cannot change which mask it uses; delete and
|
|
153
|
+
re-create it with a new mask instead.
|
|
155
154
|
|
|
156
155
|
## Order matters: geometry before masked adjustments
|
|
157
156
|
|
|
@@ -169,7 +168,7 @@ every geometry op.
|
|
|
169
168
|
## Layers and composites
|
|
170
169
|
|
|
171
170
|
A filter applies to ONE layer. \`gimp_layer\` creates, groups, reorders,
|
|
172
|
-
|
|
171
|
+
and restyles layers and merges or flattens them;
|
|
173
172
|
\`gimp_place_image\` adds another file as a new layer;
|
|
174
173
|
\`gimp_create_document\` starts a blank canvas. Address layers by the
|
|
175
174
|
\`layer_id\` these tools and \`gimp_inspect\` report — names can repeat.
|
|
@@ -40,7 +40,10 @@ async function gimpOpenDocument(gimp, rawArgs) {
|
|
|
40
40
|
type: 'text',
|
|
41
41
|
text: `Opened ${basename(filePath)} as image ${result.image} ` +
|
|
42
42
|
`(${result.width}x${result.height}, ${result.base_type}, ${result.precision}). ` +
|
|
43
|
-
`${result.layers.length} layer(s): ${result.layers.join(', ') || '(none)'}
|
|
43
|
+
`${result.layers.length} layer(s): ${result.layers.join(', ') || '(none)'}.` +
|
|
44
|
+
(result.orientation_applied !== undefined
|
|
45
|
+
? ` Rotated upright for EXIF orientation ${result.orientation_applied}.`
|
|
46
|
+
: ''),
|
|
44
47
|
},
|
|
45
48
|
],
|
|
46
49
|
structuredContent: result,
|
|
@@ -41,7 +41,7 @@ const effectSchema = {
|
|
|
41
41
|
layer_id: GIMP_LAYER_ID_PROP,
|
|
42
42
|
mask: {
|
|
43
43
|
type: 'string',
|
|
44
|
-
description: 'Name of a
|
|
44
|
+
description: 'Name of a saved selection channel, confining a NEW filter to it. Only valid ' +
|
|
45
45
|
"when creating (no filter_id) — a filter's mask is fixed at creation. ORDER MATTERS: " +
|
|
46
46
|
'straighten / flip / resize the canvas first (gimp_transform_canvas, gimp_resize_image), ' +
|
|
47
47
|
'then crop, THEN add masked effects — rotate, flip, and resize all refuse outright once ' +
|
|
@@ -230,7 +230,7 @@ export function createGimpEffectTools(gimp) {
|
|
|
230
230
|
'or `layer`, else the selected or top layer), not to the flattened image. A re-edit ' +
|
|
231
231
|
"(filter_id) MERGES: any field you omit keeps the filter's existing value. Only " +
|
|
232
232
|
'filters Editmamei created can be re-edited; one added in the GIMP GUI is refused ' +
|
|
233
|
-
'(delete and re-create it). `mask` (a channel name
|
|
233
|
+
'(delete and re-create it). `mask` (a saved selection channel name) confines a ' +
|
|
234
234
|
'NEW filter — fixed at creation, cannot change on a re-edit. ORDER MATTERS: straighten ' +
|
|
235
235
|
'/ flip / resize the canvas first, then crop, THEN add any masked effect — rotate, ' +
|
|
236
236
|
'flip, and resize all refuse outright once a masked filter exists, or any filter not ' +
|
|
@@ -38,7 +38,7 @@ const resizeSchema = {
|
|
|
38
38
|
},
|
|
39
39
|
required: ['image'],
|
|
40
40
|
};
|
|
41
|
-
const EFFECT_UPDATE_FAILURES_PROP = {
|
|
41
|
+
export const EFFECT_UPDATE_FAILURES_PROP = {
|
|
42
42
|
type: 'array',
|
|
43
43
|
items: { type: 'string' },
|
|
44
44
|
description: 'Names of effect filters (position/direction/size-dependent ones tracked through geometry ' +
|
|
@@ -75,7 +75,7 @@ const transformCanvasSchema = {
|
|
|
75
75
|
},
|
|
76
76
|
required: ['image', 'op'],
|
|
77
77
|
};
|
|
78
|
-
function effectUpdateFailuresNote(failures) {
|
|
78
|
+
export function effectUpdateFailuresNote(failures) {
|
|
79
79
|
if (!failures || failures.length === 0)
|
|
80
80
|
return '';
|
|
81
81
|
const plural = failures.length > 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateArgs } from '../utils/validate.js';
|
|
2
2
|
import { toolGimpErrorResult, unknownDiscriminator } from '../utils/tool-helpers.js';
|
|
3
3
|
import { pickSchemaDeclaredKeys } from './gimp-shared.js';
|
|
4
|
-
const INSPECT_WHATS = ['documents', 'document', 'layers', 'channels', 'filter'];
|
|
4
|
+
const INSPECT_WHATS = ['documents', 'document', 'layers', 'channels', 'filter', 'fonts'];
|
|
5
5
|
const inspectSchema = {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
@@ -20,12 +20,19 @@ const inspectSchema = {
|
|
|
20
20
|
"allows duplicate layer names) and flag `is_text_layer`. 'document'/'layers' cap the tree " +
|
|
21
21
|
'at 2000 nodes total and report `truncated: true` if the cap was hit, alongside ' +
|
|
22
22
|
'`top_level_count` and `total_nodes`; if it times out the GIMP session restarts and ' +
|
|
23
|
-
|
|
23
|
+
"unsaved work is lost, so save (gimp_save_xcf) first. A text layer's node also reports " +
|
|
24
|
+
"its text, font, font_size (points), color and alignment. 'fonts' lists the installed " +
|
|
25
|
+
'font names (no image needed), optionally narrowed by `filter`.',
|
|
24
26
|
},
|
|
25
27
|
image: {
|
|
26
28
|
type: 'integer',
|
|
27
29
|
description: 'Image id, as returned by gimp_open_document. Required for every `what` except ' +
|
|
28
|
-
"'documents'.",
|
|
30
|
+
"'documents' and 'fonts'.",
|
|
31
|
+
},
|
|
32
|
+
filter: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: "what='fonts' only: case-insensitive substring to narrow the font names by. The list is " +
|
|
35
|
+
'sorted and capped at 200 names; `total` counts every match.',
|
|
29
36
|
},
|
|
30
37
|
filter_id: {
|
|
31
38
|
type: 'integer',
|
|
@@ -75,6 +82,22 @@ async function gimpInspect(gimp, rawArgs) {
|
|
|
75
82
|
structuredContent: { what, documents },
|
|
76
83
|
};
|
|
77
84
|
}
|
|
85
|
+
if (what === 'fonts') {
|
|
86
|
+
const result = await gimp.call('fonts', pickSchemaDeclaredKeys(inspectSchema, args));
|
|
87
|
+
const shown = result.fonts.length;
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `${result.total} font(s) installed` +
|
|
93
|
+
(args.filter ? ` matching "${String(args.filter).slice(0, 100)}"` : '') +
|
|
94
|
+
(shown < result.total ? `, showing the first ${shown}` : '') +
|
|
95
|
+
`: ${result.fonts.join(', ') || '(none)'}.`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
structuredContent: { what, ...result },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
78
101
|
if (what === 'document' || what === 'layers' || what === 'channels' || what === 'filter') {
|
|
79
102
|
const result = await gimp.call('describe', pickSchemaDeclaredKeys(inspectSchema, args));
|
|
80
103
|
return {
|
|
@@ -100,8 +123,11 @@ export function createGimpInspectTools(gimp) {
|
|
|
100
123
|
"WITH coverage (selected_pixels/fraction), the cost 'document' skips, and can stop " +
|
|
101
124
|
'early on a document with many named channels (`truncated: true`, `channels_skipped`). ' +
|
|
102
125
|
"'filter' describes one filter by filter_id, in the same shape gimp_filter (op=list) " +
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
"reports it in. 'fonts' lists the installed font names (no image needed), sorted, " +
|
|
127
|
+
'optionally narrowed by `filter` (substring), capped at 200 with a `total`. Layer tree ' +
|
|
128
|
+
'nodes are addressed by `layer_id` (canonical — GIMP allows duplicate layer names) and ' +
|
|
129
|
+
"flag `is_text_layer`; a text layer's node also reports its text, font, font_size " +
|
|
130
|
+
"(points), color and alignment. 'document'/'layers' cap the tree at " +
|
|
105
131
|
'2000 nodes total and report `truncated: true` if the cap was hit, alongside ' +
|
|
106
132
|
'`top_level_count` and `total_nodes`; if it times out the GIMP session restarts and ' +
|
|
107
133
|
'unsaved work is lost, so save (gimp_save_xcf) first.',
|
|
@@ -110,6 +136,10 @@ export function createGimpInspectTools(gimp) {
|
|
|
110
136
|
type: 'object',
|
|
111
137
|
properties: {
|
|
112
138
|
what: { type: 'string' },
|
|
139
|
+
fonts: { type: 'array', items: { type: 'string' } },
|
|
140
|
+
total: { type: 'number' },
|
|
141
|
+
returned: { type: 'number' },
|
|
142
|
+
default: { type: ['string', 'null'] },
|
|
113
143
|
documents: {
|
|
114
144
|
type: 'array',
|
|
115
145
|
items: { type: 'object', properties: { image: { type: 'number' } } },
|