editmamei 1.7.1 → 1.9.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 +1790 -26
- package/dist/backends/gimp/bridge/ops.py +3013 -168
- 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/server.js +58 -13
- package/dist/core/settings.js +7 -0
- package/dist/core/tool-activity.js +15 -1
- package/dist/core/tool-groups.js +9 -1
- package/dist/core/tool-tiers.js +9 -1
- package/dist/delivery/client.js +3 -0
- package/dist/modules/gimp/index.js +12 -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 +118 -21
- package/dist/tools/filter-tools.js +2 -1
- package/dist/tools/gimp-adjustment-tools.js +7 -6
- package/dist/tools/gimp-checkpoint-tools.js +7 -1
- package/dist/tools/gimp-core-tools.js +10 -11
- package/dist/tools/gimp-document-tools.js +100 -18
- package/dist/tools/gimp-effect-tools.js +56 -10
- package/dist/tools/gimp-fill-tools.js +256 -0
- package/dist/tools/gimp-filter-tools.js +4 -1
- package/dist/tools/gimp-geometry-tools.js +2 -2
- package/dist/tools/gimp-inspect-tools.js +48 -10
- package/dist/tools/gimp-layer-tools.js +7 -24
- package/dist/tools/gimp-match-tools.js +133 -0
- package/dist/tools/gimp-result-text.js +104 -0
- package/dist/tools/gimp-retouch-tools.js +238 -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 +241 -0
- package/dist/tools/gimp-transform-layer-tools.js +215 -0
- package/dist/tools/gimp-verify-tools.js +13 -15
- 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 +12 -2
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/dist/tools/gimp-mask-tools.js +0 -102
|
@@ -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 ' +
|
|
@@ -278,9 +278,10 @@ const adjustSchema = {
|
|
|
278
278
|
maximum: 100,
|
|
279
279
|
description: 'vibrance only. THE vibrance control — a vibrance adjustment must set this field, not ' +
|
|
280
280
|
'`saturation` (a separate, plain multiplier for this type that normally stays at its ' +
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
'
|
|
281
|
+
"default). The scale follows Photoshop's Vibrance: +10 to +30 is a natural boost, +50 a " +
|
|
282
|
+
"strong one; negative values desaturate. Unlike Photoshop's, it lifts already-saturated " +
|
|
283
|
+
'colors (a blue sky, a bright shirt) about as much as muted ones, so check those areas ' +
|
|
284
|
+
'with a histogram above about +50. Default (when creating): 0.',
|
|
284
285
|
},
|
|
285
286
|
amount: {
|
|
286
287
|
type: 'number',
|
|
@@ -348,11 +349,11 @@ export function createGimpAdjustmentTools(gimp) {
|
|
|
348
349
|
'separate filters for red and blue rather than one filter for both; points are [input, ' +
|
|
349
350
|
'output] 0-255 pairs including the endpoints. hue_saturation and color_balance likewise ' +
|
|
350
351
|
'carry ONE `range` per filter. gaussian_blur: radius in pixels at full resolution; ' +
|
|
351
|
-
'combine with a mask
|
|
352
|
+
'combine with a saved-selection mask for soft-focus or background-blur effects. A ' +
|
|
352
353
|
"re-edit (filter_id) MERGES: any field you omit keeps the filter's existing value, so " +
|
|
353
354
|
"re-editing {contrast: 50} on a brightness_contrast filter doesn't reset brightness. Only " +
|
|
354
355
|
'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
|
|
356
|
+
'and re-create it). `mask` (a saved selection channel name) confines a NEW filter ' +
|
|
356
357
|
'— fixed at creation, cannot change on a re-edit. ORDER MATTERS: straighten / flip / ' +
|
|
357
358
|
'resize the canvas first, then crop, THEN add any masked adjustment — rotate, flip, and ' +
|
|
358
359
|
'resize all refuse outright once a masked adjustment filter exists, or any filter not ' +
|
|
@@ -6,6 +6,7 @@ import { validateArgs } from '../utils/validate.js';
|
|
|
6
6
|
import { toolGimpErrorResult, unknownDiscriminator } from '../utils/tool-helpers.js';
|
|
7
7
|
import { GimpError } from '../backends/gimp/errors.js';
|
|
8
8
|
import { GIMP_IMAGE_PROP } from './gimp-shared.js';
|
|
9
|
+
import { capLines, quoted } from './gimp-result-text.js';
|
|
9
10
|
export const MAX_CHECKPOINTS_PER_IMAGE = 5;
|
|
10
11
|
export const MAX_CHECKPOINTS_TOTAL = 20;
|
|
11
12
|
const checkpointSchema = {
|
|
@@ -303,7 +304,12 @@ async function checkpointList(gimp, args, store) {
|
|
|
303
304
|
content: [
|
|
304
305
|
{
|
|
305
306
|
type: 'text',
|
|
306
|
-
text: `${checkpoints.length} checkpoint(s)${scopedNote}.${goneNote}
|
|
307
|
+
text: `${checkpoints.length} checkpoint(s)${scopedNote}.${goneNote}` +
|
|
308
|
+
capLines(checkpoints.map((c) => `- ${quoted(c.checkpoint_id)}: image ${c.image}` +
|
|
309
|
+
`${c.open ? '' : ' (gone)'}${c.pending ? ', still being created' : ''}, ` +
|
|
310
|
+
`${c.created_at}, ${c.bytes.toLocaleString('en-US')} bytes`))
|
|
311
|
+
.map((line) => `\n${line}`)
|
|
312
|
+
.join(''),
|
|
307
313
|
},
|
|
308
314
|
],
|
|
309
315
|
structuredContent: { checkpoints },
|
|
@@ -90,9 +90,9 @@ in the real GIMP GUI afterward to see it directly.
|
|
|
90
90
|
stacking a second one for the same correction (see "Curve laws"
|
|
91
91
|
below).
|
|
92
92
|
6. **Finish** — \`gimp_save_xcf\` keeps every filter live and
|
|
93
|
-
re-editable; \`gimp_export\` writes a flattened deliverable
|
|
94
|
-
|
|
95
|
-
now.
|
|
93
|
+
re-editable; \`gimp_export\` writes a flattened deliverable, or a
|
|
94
|
+
layered .psd for finishing in Photoshop. Do both when the user wants
|
|
95
|
+
to keep working on it later AND ship a JPEG/PNG now.
|
|
96
96
|
|
|
97
97
|
## Curve laws (gimp_add_adjustment type=curves)
|
|
98
98
|
|
|
@@ -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,
|
|
@@ -63,7 +66,7 @@ const saveXcfSchema = {
|
|
|
63
66
|
type: 'string',
|
|
64
67
|
description: 'Absolute path ending in .xcf. Saves the LIVE GIMP document — every filter stays in ' +
|
|
65
68
|
"place and re-editable afterward (matches ps_save_psd's framing for .psd). For a " +
|
|
66
|
-
'flattened
|
|
69
|
+
'deliverable (flattened, or a layered .psd), use gimp_export instead.',
|
|
67
70
|
},
|
|
68
71
|
overwrite: {
|
|
69
72
|
type: 'boolean',
|
|
@@ -78,7 +81,7 @@ async function gimpSaveXcf(gimp, rawArgs) {
|
|
|
78
81
|
const args = validateArgs(saveXcfSchema, rawArgs);
|
|
79
82
|
const filePath = requireAbsoluteGimpPath('file_path', args.file_path);
|
|
80
83
|
if (extname(filePath).toLowerCase() !== '.xcf') {
|
|
81
|
-
throw new GimpError('invalid_argument', `"file_path" must end in .xcf, got "${filePath}" — use gimp_export for a
|
|
84
|
+
throw new GimpError('invalid_argument', `"file_path" must end in .xcf, got "${filePath}" — use gimp_export for a deliverable.`);
|
|
82
85
|
}
|
|
83
86
|
refuseExistingGimpFile(filePath, args.overwrite, existsSync);
|
|
84
87
|
const result = await gimp.call('export', {
|
|
@@ -99,16 +102,22 @@ async function gimpSaveXcf(gimp, rawArgs) {
|
|
|
99
102
|
return toolGimpErrorResult('Error saving GIMP document', error);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
|
-
const
|
|
105
|
+
const FLAT_EXPORT_FORMAT_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.tif', '.tiff'];
|
|
106
|
+
const LAYERED_EXPORT_FORMAT_EXTENSIONS = ['.psd'];
|
|
107
|
+
const EXPORT_FORMAT_EXTENSIONS = [
|
|
108
|
+
...FLAT_EXPORT_FORMAT_EXTENSIONS,
|
|
109
|
+
...LAYERED_EXPORT_FORMAT_EXTENSIONS,
|
|
110
|
+
];
|
|
103
111
|
const exportSchema = {
|
|
104
112
|
type: 'object',
|
|
105
113
|
properties: {
|
|
106
114
|
image: GIMP_IMAGE_PROP,
|
|
107
115
|
file_path: {
|
|
108
116
|
type: 'string',
|
|
109
|
-
description: 'Absolute path. The extension picks the format: jpg/jpeg, png, webp, tif/tiff
|
|
110
|
-
".xcf outright — that is gimp_save_xcf's job. Metadata
|
|
111
|
-
'ALWAYS stripped on export, unconditionally — never
|
|
117
|
+
description: 'Absolute path. The extension picks the format: jpg/jpeg, png, webp, tif/tiff (flattened), ' +
|
|
118
|
+
"or psd (layered). Refuses .xcf outright — that is gimp_save_xcf's job. Metadata " +
|
|
119
|
+
'(EXIF/XMP/IPTC/GPS, thumbnail) is ALWAYS stripped on export, unconditionally — never ' +
|
|
120
|
+
'carried over from the source.',
|
|
112
121
|
},
|
|
113
122
|
overwrite: {
|
|
114
123
|
type: 'boolean',
|
|
@@ -148,23 +157,59 @@ const exportSchema = {
|
|
|
148
157
|
type: 'integer',
|
|
149
158
|
enum: [8, 16],
|
|
150
159
|
default: 8,
|
|
151
|
-
description: 'PNG/TIFF only: output bit depth. JPEG/WebP are always 8-bit.',
|
|
160
|
+
description: 'PNG/TIFF/PSD only: output bit depth. JPEG/WebP are always 8-bit.',
|
|
161
|
+
},
|
|
162
|
+
keep_alpha: {
|
|
163
|
+
type: 'boolean',
|
|
164
|
+
default: false,
|
|
165
|
+
description: 'PNG/WebP only: keep transparency. The visible layers are merged onto a transparent ' +
|
|
166
|
+
'canvas, so areas no layer covers (a cut-out, say) stay transparent. Default false ' +
|
|
167
|
+
'flattens onto white. Refused for JPEG and TIFF.',
|
|
152
168
|
},
|
|
153
169
|
},
|
|
154
170
|
required: ['image', 'file_path'],
|
|
155
171
|
};
|
|
172
|
+
const LAYER_NAMES_SHOWN = 10;
|
|
173
|
+
function layerNames(names) {
|
|
174
|
+
const shown = names.slice(0, LAYER_NAMES_SHOWN).map((n) => `"${n}"`);
|
|
175
|
+
const more = names.length - shown.length;
|
|
176
|
+
return more > 0 ? `${shown.join(', ')} and ${more} more` : shown.join(', ');
|
|
177
|
+
}
|
|
178
|
+
function layeredExportText(result) {
|
|
179
|
+
const parts = [
|
|
180
|
+
`Exported ${result.bytes.toLocaleString()} bytes to ${result.path} as a layered PSD with ` +
|
|
181
|
+
`${result.layers} layers. Live filters were baked into their layers' pixels.`,
|
|
182
|
+
];
|
|
183
|
+
if (result.merged_groups.length > 0) {
|
|
184
|
+
parts.push(`Groups merged into one layer each, because PSD cannot store a filter on a group: ` +
|
|
185
|
+
`${layerNames(result.merged_groups)}.`);
|
|
186
|
+
}
|
|
187
|
+
if (result.rasterized_text.length > 0) {
|
|
188
|
+
parts.push(`Text layers written as pixels, not editable text: ${layerNames(result.rasterized_text)}.`);
|
|
189
|
+
}
|
|
190
|
+
if (result.may_blend_differently.length > 0) {
|
|
191
|
+
parts.push('Photoshop blends partly transparent layers and blend modes in gamma space, so these ' +
|
|
192
|
+
`may look slightly different there: ${layerNames(result.may_blend_differently)}.`);
|
|
193
|
+
}
|
|
194
|
+
parts.push('Metadata (EXIF/XMP/IPTC/GPS, thumbnail) was stripped.');
|
|
195
|
+
return parts.join(' ');
|
|
196
|
+
}
|
|
156
197
|
async function gimpExport(gimp, rawArgs) {
|
|
157
198
|
try {
|
|
158
199
|
const args = validateArgs(exportSchema, rawArgs);
|
|
159
200
|
const filePath = requireAbsoluteGimpPath('file_path', args.file_path);
|
|
160
201
|
const ext = extname(filePath).toLowerCase();
|
|
161
202
|
if (ext === '.xcf') {
|
|
162
|
-
throw new GimpError('invalid_argument', '"file_path" must not be a .xcf file — gimp_export writes a
|
|
203
|
+
throw new GimpError('invalid_argument', '"file_path" must not be a .xcf file — gimp_export writes a deliverable; use ' +
|
|
163
204
|
'gimp_save_xcf to save the live, re-editable document.');
|
|
164
205
|
}
|
|
165
206
|
if (!EXPORT_FORMAT_EXTENSIONS.includes(ext)) {
|
|
166
207
|
throw new GimpError('invalid_argument', `"file_path" must end in ${EXPORT_FORMAT_EXTENSIONS.join(', ')}, got "${ext || '(none)'}".`);
|
|
167
208
|
}
|
|
209
|
+
const keepAlpha = args.keep_alpha === true;
|
|
210
|
+
if (keepAlpha && ext !== '.png' && ext !== '.webp') {
|
|
211
|
+
throw new GimpError('invalid_argument', `"keep_alpha" only works with a .png or .webp file_path, got "${ext}".`);
|
|
212
|
+
}
|
|
168
213
|
refuseExistingGimpFile(filePath, args.overwrite, existsSync);
|
|
169
214
|
const bridgeArgs = { image: args.image, path: filePath };
|
|
170
215
|
if (ext === '.jpg' || ext === '.jpeg') {
|
|
@@ -182,8 +227,20 @@ async function gimpExport(gimp, rawArgs) {
|
|
|
182
227
|
bridgeArgs.compression = args.tiff_compression;
|
|
183
228
|
bridgeArgs.bit_depth = args.bit_depth;
|
|
184
229
|
}
|
|
185
|
-
|
|
230
|
+
else if (ext === '.psd') {
|
|
231
|
+
bridgeArgs.bit_depth = args.bit_depth;
|
|
232
|
+
}
|
|
233
|
+
if (keepAlpha)
|
|
234
|
+
bridgeArgs.keep_alpha = true;
|
|
186
235
|
const format = ext.replace('.', '');
|
|
236
|
+
if (ext === '.psd') {
|
|
237
|
+
const result = await gimp.call('export', bridgeArgs);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: 'text', text: layeredExportText(result) }],
|
|
240
|
+
structuredContent: { ...result, format },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const result = await gimp.call('export', bridgeArgs);
|
|
187
244
|
return {
|
|
188
245
|
content: [
|
|
189
246
|
{
|
|
@@ -240,7 +297,7 @@ export function createGimpDocumentTools(gimp) {
|
|
|
240
297
|
tool: {
|
|
241
298
|
name: 'gimp_close_document',
|
|
242
299
|
description: 'Headless GIMP: close an image WITHOUT saving. Save first with gimp_save_xcf (keeps filters live) or ' +
|
|
243
|
-
'gimp_export (
|
|
300
|
+
'gimp_export (deliverable) if the work should be kept.',
|
|
244
301
|
inputSchema: closeSchema,
|
|
245
302
|
outputSchema: { type: 'object', properties: { closed: { type: 'number' } } },
|
|
246
303
|
annotations: {
|
|
@@ -271,7 +328,8 @@ export function createGimpDocumentTools(gimp) {
|
|
|
271
328
|
'to overwrite an existing file unless overwrite: true. Save early: on a very large ' +
|
|
272
329
|
'document a save can take tens of seconds, and if any call times out the GIMP session ' +
|
|
273
330
|
'restarts and every unsaved open image and filter is lost — save before long operations, ' +
|
|
274
|
-
'not after. For a
|
|
331
|
+
'not after. For a shippable copy with metadata stripped (flattened, or a layered .psd), ' +
|
|
332
|
+
'use gimp_export instead.',
|
|
275
333
|
inputSchema: saveXcfSchema,
|
|
276
334
|
outputSchema: {
|
|
277
335
|
type: 'object',
|
|
@@ -290,12 +348,20 @@ export function createGimpDocumentTools(gimp) {
|
|
|
290
348
|
{
|
|
291
349
|
tool: {
|
|
292
350
|
name: 'gimp_export',
|
|
293
|
-
description: 'Headless GIMP: write a
|
|
294
|
-
'
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
'
|
|
298
|
-
'
|
|
351
|
+
description: 'Headless GIMP: write a deliverable to disk — every live filter is baked in, and ALL ' +
|
|
352
|
+
'metadata (EXIF/XMP/IPTC/GPS, thumbnail) is stripped unconditionally, never carried ' +
|
|
353
|
+
'over from the source. Format is picked from the file_path extension: jpg/jpeg, png, ' +
|
|
354
|
+
'webp and tif/tiff write a FLATTENED image; psd writes a LAYERED file for finishing in ' +
|
|
355
|
+
'Photoshop or another editor. In a PSD, layers keep their masks, opacity, blend modes, ' +
|
|
356
|
+
'visibility and groups; filters are baked into each layer; a group with its own filter ' +
|
|
357
|
+
'is merged into one layer; text layers become pixels. Photoshop blends partly ' +
|
|
358
|
+
'transparent layers and blend modes in gamma space, so those can look slightly ' +
|
|
359
|
+
'different there — the result lists them. Any other extension is refused, and .xcf is ' +
|
|
360
|
+
'refused outright — use gimp_save_xcf for the live, re-editable document instead. ' +
|
|
361
|
+
'Refuses to overwrite an existing file unless overwrite: true (with it, the existing ' +
|
|
362
|
+
'file is replaced). A flattened image is composited onto white by default; keep_alpha: ' +
|
|
363
|
+
'true (png or webp only) keeps transparency, so a cut-out exports with a transparent ' +
|
|
364
|
+
'background.',
|
|
299
365
|
inputSchema: exportSchema,
|
|
300
366
|
outputSchema: {
|
|
301
367
|
type: 'object',
|
|
@@ -303,6 +369,22 @@ export function createGimpDocumentTools(gimp) {
|
|
|
303
369
|
path: { type: 'string' },
|
|
304
370
|
bytes: { type: 'number' },
|
|
305
371
|
format: { type: ['string', 'null'] },
|
|
372
|
+
layers: { type: 'number', description: 'PSD only: layers written, groups included.' },
|
|
373
|
+
merged_groups: {
|
|
374
|
+
type: 'array',
|
|
375
|
+
items: { type: 'string' },
|
|
376
|
+
description: 'PSD only: groups merged into one layer to keep their own filter.',
|
|
377
|
+
},
|
|
378
|
+
rasterized_text: {
|
|
379
|
+
type: 'array',
|
|
380
|
+
items: { type: 'string' },
|
|
381
|
+
description: 'PSD only: text layers written as pixels.',
|
|
382
|
+
},
|
|
383
|
+
may_blend_differently: {
|
|
384
|
+
type: 'array',
|
|
385
|
+
items: { type: 'string' },
|
|
386
|
+
description: 'PSD only: visible layers below 100% opacity or with a non-normal blend mode.',
|
|
387
|
+
},
|
|
306
388
|
},
|
|
307
389
|
},
|
|
308
390
|
annotations: {
|
|
@@ -8,6 +8,9 @@ const EFFECT_TYPES = [
|
|
|
8
8
|
'lens_blur',
|
|
9
9
|
'add_noise',
|
|
10
10
|
'drop_shadow',
|
|
11
|
+
'outline',
|
|
12
|
+
'outer_glow',
|
|
13
|
+
'surface_blur',
|
|
11
14
|
];
|
|
12
15
|
const effectSchema = {
|
|
13
16
|
type: 'object',
|
|
@@ -41,7 +44,7 @@ const effectSchema = {
|
|
|
41
44
|
layer_id: GIMP_LAYER_ID_PROP,
|
|
42
45
|
mask: {
|
|
43
46
|
type: 'string',
|
|
44
|
-
description: 'Name of a
|
|
47
|
+
description: 'Name of a saved selection channel, confining a NEW filter to it. Only valid ' +
|
|
45
48
|
"when creating (no filter_id) — a filter's mask is fixed at creation. ORDER MATTERS: " +
|
|
46
49
|
'straighten / flip / resize the canvas first (gimp_transform_canvas, gimp_resize_image), ' +
|
|
47
50
|
'then crop, THEN add masked effects — rotate, flip, and resize all refuse outright once ' +
|
|
@@ -67,8 +70,11 @@ const effectSchema = {
|
|
|
67
70
|
'(default 25 when creating) — capped much lower than the other two effects here (measured ' +
|
|
68
71
|
'live: expensive at full resolution). drop_shadow: blur radius in pixels at full ' +
|
|
69
72
|
'resolution, 0..1500 (default 10 when creating), clipped to the layer bounds rather than ' +
|
|
70
|
-
'growing it.
|
|
71
|
-
'
|
|
73
|
+
'growing it. outer_glow: glow blur in pixels, 1..250 (default 20 when creating). ' +
|
|
74
|
+
'surface_blur: smoothing radius in pixels, 1..6 (default 4 when creating) — capped low ' +
|
|
75
|
+
'because its cost grows with the square of the radius (a 24 MP export takes ~17 s at 6). ' +
|
|
76
|
+
'lens_blur, drop_shadow, outer_glow and surface_blur are SPATIAL (radius-scaled on the ' +
|
|
77
|
+
'proxy preview, see gimp_overview). The bridge enforces the precise range for whichever ' +
|
|
72
78
|
'`type` you gave.',
|
|
73
79
|
},
|
|
74
80
|
softness: {
|
|
@@ -189,8 +195,38 @@ const effectSchema = {
|
|
|
189
195
|
type: 'number',
|
|
190
196
|
minimum: 0,
|
|
191
197
|
maximum: 1,
|
|
192
|
-
description: 'drop_shadow
|
|
193
|
-
'configurable yet.',
|
|
198
|
+
description: 'drop_shadow: default (when creating) 0.5. The shadow color is black and not ' +
|
|
199
|
+
'configurable yet. outer_glow: default 0.75 (its color is the `color` field).',
|
|
200
|
+
},
|
|
201
|
+
width: {
|
|
202
|
+
type: 'number',
|
|
203
|
+
minimum: 1,
|
|
204
|
+
maximum: 50,
|
|
205
|
+
description: 'outline only. Stroke thickness in pixels at full resolution, 1..50. Default (when ' +
|
|
206
|
+
"creating): 4. SPATIAL. The stroke grows outward from the layer's alpha edge (round " +
|
|
207
|
+
"corners) and the layer's own pixels are untouched.",
|
|
208
|
+
},
|
|
209
|
+
spread: {
|
|
210
|
+
type: 'number',
|
|
211
|
+
minimum: 0,
|
|
212
|
+
maximum: 50,
|
|
213
|
+
description: "outer_glow only. Pixels the glow's solid core grows past the alpha edge before it " +
|
|
214
|
+
'blurs, 0..50. Default (when creating): 4. SPATIAL.',
|
|
215
|
+
},
|
|
216
|
+
color: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
pattern: '^#[0-9a-fA-F]{6}$',
|
|
219
|
+
description: 'outline and outer_glow only. "#rrggbb" hex. Default (when creating): "#000000" for ' +
|
|
220
|
+
'outline, "#ffffff" for outer_glow.',
|
|
221
|
+
},
|
|
222
|
+
edge_preservation: {
|
|
223
|
+
type: 'number',
|
|
224
|
+
minimum: 0,
|
|
225
|
+
maximum: 1,
|
|
226
|
+
description: 'surface_blur only. How much edge contrast survives, 0..1: LOWER smooths harder (near 0 ' +
|
|
227
|
+
'starts to soften the edges too), HIGHER leaves more detail and noise. Default (when ' +
|
|
228
|
+
'creating): 0.08, which flattens fine noise and skin texture while keeping hard edges ' +
|
|
229
|
+
'sharp.',
|
|
194
230
|
},
|
|
195
231
|
},
|
|
196
232
|
required: ['image', 'type'],
|
|
@@ -222,7 +258,7 @@ export function createGimpEffectTools(gimp) {
|
|
|
222
258
|
name: 'gimp_add_effect',
|
|
223
259
|
description: 'Headless GIMP: append (or, with filter_id, re-edit in place) one of a small ' +
|
|
224
260
|
'allow-listed set of GEGL effect filters — vignette, black_white, motion_blur, ' +
|
|
225
|
-
'lens_blur, add_noise, drop_shadow — the same non-destructive, re-editable, maskable ' +
|
|
261
|
+
'lens_blur, add_noise, drop_shadow, outline, outer_glow, surface_blur — the same non-destructive, re-editable, maskable ' +
|
|
226
262
|
"filter mechanism gimp_add_adjustment uses, sharing the SAME stack: gimp_filter's " +
|
|
227
263
|
"op=list/set_visibility/delete manage this tool's filters exactly as it manages " +
|
|
228
264
|
"gimp_add_adjustment's. `type` picks the effect; a field another effect uses is " +
|
|
@@ -230,16 +266,26 @@ export function createGimpEffectTools(gimp) {
|
|
|
230
266
|
'or `layer`, else the selected or top layer), not to the flattened image. A re-edit ' +
|
|
231
267
|
"(filter_id) MERGES: any field you omit keeps the filter's existing value. Only " +
|
|
232
268
|
'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
|
|
269
|
+
'(delete and re-create it). `mask` (a saved selection channel name) confines a ' +
|
|
234
270
|
'NEW filter — fixed at creation, cannot change on a re-edit. ORDER MATTERS: straighten ' +
|
|
235
271
|
'/ flip / resize the canvas first, then crop, THEN add any masked effect — rotate, ' +
|
|
236
272
|
'flip, and resize all refuse outright once a masked filter exists, or any filter not ' +
|
|
237
273
|
'created by Editmamei. vignette, motion_blur, and drop_shadow stay locked to the ' +
|
|
238
274
|
'content through flip, resize, and an exact 90/180/270-degree rotate (their center/' +
|
|
239
275
|
'angle/offset fields are updated automatically, and gimp_filter op=list reflects the ' +
|
|
240
|
-
'new values afterward) — rotating by any OTHER angle is refused while
|
|
241
|
-
'
|
|
242
|
-
'
|
|
276
|
+
'new values afterward) — rotating by any OTHER angle is refused while a vignette, a ' +
|
|
277
|
+
'motion_blur, or a drop_shadow with a non-zero offset is present, rather than letting ' +
|
|
278
|
+
'it drift out of alignment. outline, outer_glow, surface_blur, lens_blur and a ' +
|
|
279
|
+
'zero-offset drop_shadow have no direction, so they survive any rotation. Resizing ' +
|
|
280
|
+
'scales their pixel sizes with the image (never below the 1 px minimum of outline ' +
|
|
281
|
+
'width, outer_glow radius and surface_blur radius). lens_blur is a ' +
|
|
282
|
+
'uniform soft-focus blur, not a depth-of-field falloff. outline puts a solid stroke ' +
|
|
283
|
+
"around a layer's transparent edge (meme text, sticker cut-outs); outer_glow adds a " +
|
|
284
|
+
'soft glow around it (glowing text); surface_blur smooths skin and other texture ' +
|
|
285
|
+
'while keeping hard edges. outline and outer_glow need a layer with transparency, ' +
|
|
286
|
+
'and attach to a text layer without rasterizing it: the text stays editable and the ' +
|
|
287
|
+
'effect follows each edit. For a plain ' +
|
|
288
|
+
'blur with no bokeh ' +
|
|
243
289
|
'highlight boost, use gimp_add_adjustment type=gaussian_blur instead. Check the stack ' +
|
|
244
290
|
'afterward with gimp_filter (op=list).',
|
|
245
291
|
inputSchema: effectSchema,
|