editmamei 0.17.4 → 0.18.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/NOTICES.md +43 -0
- package/README.md +91 -8
- package/dist/api/extendscript/_helpers.js +8 -0
- 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/clients/json-config.js +1 -1
- package/dist/core/server.js +92 -24
- package/dist/core/tool-groups.js +178 -0
- package/dist/core/tool-tiers.js +34 -48
- package/dist/delivery/bundle.js +8 -0
- package/dist/delivery/config.js +23 -0
- package/dist/delivery/provision.js +30 -2
- package/dist/delivery/store.js +40 -1
- package/dist/detection/detect-active-doc.js +84 -0
- package/dist/detection/detection-client.js +62 -0
- package/dist/detection/face-contour-geometry.js +137 -0
- package/dist/detection/face-detector.js +34 -0
- package/dist/detection/face-feature-geometry.js +180 -0
- package/dist/detection/geometry.js +35 -0
- package/dist/detection/landmark-detection-client.js +38 -0
- package/dist/detection/landmark-detector.js +63 -0
- package/dist/detection/landmark-spec.js +29 -0
- package/dist/detection/mesh-face.js +36 -0
- package/dist/detection/object-detector.js +135 -0
- package/dist/detection/runtime.js +46 -0
- package/dist/license/env-activation.js +46 -7
- package/dist/models/dfine-s-coco-quant.onnx +0 -0
- package/dist/models/ultraface-rfb-320.onnx +0 -0
- package/dist/modules/ce/index.js +23 -5
- package/dist/perception/facets.js +148 -0
- package/dist/perception/region-precompute.js +122 -0
- package/dist/perception/region-scorer.js +151 -0
- package/dist/perception/scene-model.js +156 -0
- package/dist/perception/select-recipes.js +493 -0
- package/dist/perception/sky-ground-flood.js +234 -0
- package/dist/perception/sky-mask-transfer.js +77 -0
- package/dist/platform/macos-executor.js +0 -1
- package/dist/platform/windows-executor.js +6 -1
- package/dist/preview/coordinate-grid.js +250 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/events.js +2 -1
- package/dist/telemetry/sanitize.js +15 -1
- package/dist/tools/adjustment-tools.js +46 -51
- package/dist/tools/detection-tools.js +209 -0
- package/dist/tools/document-tools.js +41 -30
- package/dist/tools/filter-tools.js +616 -101
- package/dist/tools/guide-tools.js +186 -0
- package/dist/tools/history-tools.js +1 -39
- package/dist/tools/image-tools.js +88 -0
- package/dist/tools/inspect-tools.js +86 -0
- package/dist/tools/layer-properties-tools.js +195 -106
- package/dist/tools/layer-tools.js +55 -14
- package/dist/tools/layer-transform-tools.js +236 -73
- package/dist/tools/metadata-tools.js +2 -32
- package/dist/tools/object-tools.js +193 -0
- package/dist/tools/overview-tools.js +69 -43
- package/dist/tools/path-tools.js +274 -0
- package/dist/tools/portrait-tools.js +146 -0
- package/dist/tools/preview-tools.js +78 -3
- package/dist/tools/retouch-tools.js +45 -33
- package/dist/tools/scene-tools.js +401 -0
- package/dist/tools/selection-tools.js +766 -228
- package/dist/tools/text-on-object-tools.js +225 -0
- package/dist/tools/text-tools.js +47 -56
- package/dist/tools/transform-canvas-tools.js +138 -0
- package/dist/tools/vector-mask-tools.js +116 -0
- package/dist/version.js +1 -1
- package/package.json +4 -2
|
@@ -603,13 +603,32 @@ const shadowsHighlightsSchema = {
|
|
|
603
603
|
},
|
|
604
604
|
},
|
|
605
605
|
};
|
|
606
|
+
const APPLY_ADJUSTMENT_TYPES = ['shadows_highlights', 'equalize', 'color_lookup'];
|
|
607
|
+
const APPLY_ADJUSTMENT_INPUT_SCHEMA = {
|
|
608
|
+
type: 'object',
|
|
609
|
+
properties: {
|
|
610
|
+
type: {
|
|
611
|
+
type: 'string',
|
|
612
|
+
enum: [...APPLY_ADJUSTMENT_TYPES],
|
|
613
|
+
description: 'Which destructive adjustment to bake (none exist as adjustment layers in PS). ' +
|
|
614
|
+
'shadows_highlights: one-pass shadow/highlight recovery (shadow_amount, highlight_amount, …). ' +
|
|
615
|
+
'equalize: parameter-free histogram equalization. ' +
|
|
616
|
+
'color_lookup: bake a 3DLUT grade — cl_lut_name REQUIRED. ' +
|
|
617
|
+
'All auto-duplicate the active layer by default (apply_to_active_layer:true to bake in place).',
|
|
618
|
+
},
|
|
619
|
+
...shadowsHighlightsSchema.properties,
|
|
620
|
+
...equalizeSchema.properties,
|
|
621
|
+
...colorLookupBakeSchema.properties,
|
|
622
|
+
},
|
|
623
|
+
required: ['type'],
|
|
624
|
+
};
|
|
606
625
|
export function createAdjustmentTools(connection, snippetClient) {
|
|
607
626
|
return [
|
|
608
627
|
{
|
|
609
628
|
tool: {
|
|
610
|
-
name: '
|
|
611
|
-
description: "
|
|
612
|
-
inputSchema:
|
|
629
|
+
name: 'photoshop_apply_adjustment',
|
|
630
|
+
description: "Apply a DESTRUCTIVE tonal adjustment that Photoshop does NOT offer as an adjustment layer — chosen via `type`. Runs on a DUPLICATE of the active layer by default (auto-duplicate-first per Bundle O — the original is preserved; revert by deleting the copy); pass `apply_to_active_layer: true` to bake in place. Auto-rasterizes text/smart-object layers. `shadows_highlights` recovers blown highlights + crushed shadows in one pass (defaults match Adobe's dialog: 35 shadow amount, +20 color correction); `equalize` stretches/flattens the histogram (parameter-free); `color_lookup` bakes a 3DLUT grade (cl_lut_name required — leaf name of a file in Presets/3DLUTs/ or an absolute .cube/.3dl/.look path). For EDITABLE tonal/color work, prefer photoshop_add_adjustment_layer.",
|
|
631
|
+
inputSchema: APPLY_ADJUSTMENT_INPUT_SCHEMA,
|
|
613
632
|
outputSchema: {
|
|
614
633
|
type: 'object',
|
|
615
634
|
properties: {
|
|
@@ -622,52 +641,6 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
622
641
|
highlight_radius: { type: 'number' },
|
|
623
642
|
color_correction: { type: 'number' },
|
|
624
643
|
midtone_contrast: { type: 'number' },
|
|
625
|
-
target_was_copy: { type: 'boolean' },
|
|
626
|
-
target_layer_name: { type: 'string' },
|
|
627
|
-
original_layer_name: { type: 'string' },
|
|
628
|
-
context: { type: 'object' },
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
annotations: {
|
|
632
|
-
title: 'Shadows/Highlights (destructive, auto-duplicates)',
|
|
633
|
-
destructiveHint: true,
|
|
634
|
-
idempotentHint: false,
|
|
635
|
-
},
|
|
636
|
-
},
|
|
637
|
-
handler: async (args) => applyShadowsHighlights(connection, snippetClient, args),
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
tool: {
|
|
641
|
-
name: 'photoshop_apply_equalize',
|
|
642
|
-
description: "Equalize the active layer's tonal distribution — stretches the histogram so it covers the full 0-255 range and tries to flatten the distribution. **Photoshop does not ship Equalize as an adjustment layer** — it only exists as the destructive Image > Adjustments > Equalize command. By default this tool applies it to a DUPLICATE of the active layer (auto-duplicate-first per Bundle O), so the original is preserved and you can revert by deleting the copy. Pass `apply_to_active_layer: true` to bake into the original. Auto-rasterizes text/smart-object layers. Parameter-free — the operation IS the action.",
|
|
643
|
-
inputSchema: equalizeSchema,
|
|
644
|
-
outputSchema: {
|
|
645
|
-
type: 'object',
|
|
646
|
-
properties: {
|
|
647
|
-
filter: { type: 'string' },
|
|
648
|
-
target_was_copy: { type: 'boolean' },
|
|
649
|
-
target_layer_name: { type: 'string' },
|
|
650
|
-
original_layer_name: { type: 'string' },
|
|
651
|
-
context: { type: 'object' },
|
|
652
|
-
},
|
|
653
|
-
},
|
|
654
|
-
annotations: {
|
|
655
|
-
title: 'Equalize (destructive, auto-duplicates)',
|
|
656
|
-
destructiveHint: true,
|
|
657
|
-
idempotentHint: false,
|
|
658
|
-
},
|
|
659
|
-
},
|
|
660
|
-
handler: async (args) => applyEqualize(connection, snippetClient, args),
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
tool: {
|
|
664
|
-
name: 'photoshop_apply_color_lookup',
|
|
665
|
-
description: 'Apply a 3DLUT preset as a destructive color grade, baked onto a duplicate of the active layer (the original is preserved). Pass cl_lut_name as the leaf name of any LUT in Photoshop\'s Presets/3DLUTs/ folder (e.g. "FallColors.look", "Kodak 5205 Fuji 3510 (by Adobe).cube", "TealOrangePlusContrast.3DL") or an absolute path to a .cube/.3dl/.look file. Use for color grading where you don\'t need the LUT to stay editable. NOTE: the non-destructive `add_adjustment_layer type=color_lookup` form CANNOT load a LUT via scripting (PS requires an internal parsed-profile blob no scripting API generates; see docs/20260603-color-lookup-limitation.md) — this bake form is the only working LUT path. For an editable grade, compose selective_color + color_balance + curves to mimic the look.',
|
|
666
|
-
inputSchema: colorLookupBakeSchema,
|
|
667
|
-
outputSchema: {
|
|
668
|
-
type: 'object',
|
|
669
|
-
properties: {
|
|
670
|
-
filter: { type: 'string' },
|
|
671
644
|
lut_path: { type: 'string' },
|
|
672
645
|
lut_format: { type: 'string' },
|
|
673
646
|
target_was_copy: { type: 'boolean' },
|
|
@@ -677,12 +650,12 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
677
650
|
},
|
|
678
651
|
},
|
|
679
652
|
annotations: {
|
|
680
|
-
title: 'Apply
|
|
653
|
+
title: 'Apply Adjustment (destructive, auto-duplicates)',
|
|
681
654
|
destructiveHint: true,
|
|
682
655
|
idempotentHint: false,
|
|
683
656
|
},
|
|
684
657
|
},
|
|
685
|
-
handler: async (args) =>
|
|
658
|
+
handler: async (args) => applyAdjustment(connection, snippetClient, args),
|
|
686
659
|
},
|
|
687
660
|
{
|
|
688
661
|
tool: {
|
|
@@ -716,6 +689,28 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
716
689
|
},
|
|
717
690
|
];
|
|
718
691
|
}
|
|
692
|
+
async function applyAdjustment(connection, snippetClient, rawArgs) {
|
|
693
|
+
const type = rawArgs.type;
|
|
694
|
+
const { type: _omit, ...rest } = rawArgs;
|
|
695
|
+
switch (type) {
|
|
696
|
+
case 'shadows_highlights':
|
|
697
|
+
return applyShadowsHighlights(connection, snippetClient, rest);
|
|
698
|
+
case 'equalize':
|
|
699
|
+
return applyEqualize(connection, snippetClient, rest);
|
|
700
|
+
case 'color_lookup':
|
|
701
|
+
return applyColorLookup(connection, snippetClient, rest);
|
|
702
|
+
default:
|
|
703
|
+
return {
|
|
704
|
+
content: [
|
|
705
|
+
{
|
|
706
|
+
type: 'text',
|
|
707
|
+
text: `Error: unknown adjustment type "${String(type)}". Allowed: ${APPLY_ADJUSTMENT_TYPES.join(', ')}.`,
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
isError: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
719
714
|
const FORWARDED_KEYS = new Set([
|
|
720
715
|
'curves_preset',
|
|
721
716
|
'black_point',
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { decode, encode } from 'jpeg-js';
|
|
2
|
+
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { detectActiveDoc } from '../detection/detect-active-doc.js';
|
|
4
|
+
import { OnnxDetectionClient, } from '../detection/detection-client.js';
|
|
5
|
+
const detectSchema = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
target: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
enum: ['faces', 'objects', 'both'],
|
|
11
|
+
default: 'both',
|
|
12
|
+
description: "What to detect: 'faces' (forward-facing faces), 'objects' (COCO-80 classes: person, dog, car, chair, …), or 'both'.",
|
|
13
|
+
},
|
|
14
|
+
max_dimension: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
default: 1024,
|
|
17
|
+
minimum: 256,
|
|
18
|
+
maximum: 4096,
|
|
19
|
+
description: 'Long-edge pixels of the JPEG the detectors run on. Smaller is faster; the models downscale internally so 1024 is plenty. Returned coordinates are always in full document pixels regardless.',
|
|
20
|
+
},
|
|
21
|
+
face_threshold: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
default: 0.7,
|
|
24
|
+
minimum: 0,
|
|
25
|
+
maximum: 1,
|
|
26
|
+
description: 'Minimum face confidence (0–1).',
|
|
27
|
+
},
|
|
28
|
+
object_threshold: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
default: 0.4,
|
|
31
|
+
minimum: 0,
|
|
32
|
+
maximum: 1,
|
|
33
|
+
description: 'Minimum object confidence (0–1).',
|
|
34
|
+
},
|
|
35
|
+
max_objects: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
default: 50,
|
|
38
|
+
minimum: 1,
|
|
39
|
+
maximum: 300,
|
|
40
|
+
description: 'Cap on returned objects, highest-confidence first.',
|
|
41
|
+
},
|
|
42
|
+
annotate: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
default: true,
|
|
45
|
+
description: 'Return an annotated preview with the detected boxes drawn (faces cyan, objects magenta) so you can visually confirm the detections.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
function drawBoxes(bytes, boxes) {
|
|
50
|
+
const img = decode(bytes, { useTArray: true, formatAsRGBA: true });
|
|
51
|
+
const { width: w, height: h, data } = img;
|
|
52
|
+
const thickness = Math.max(2, Math.round(Math.max(w, h) / 400));
|
|
53
|
+
const setPx = (x, y, r, g, b) => {
|
|
54
|
+
if (x < 0 || y < 0 || x >= w || y >= h)
|
|
55
|
+
return;
|
|
56
|
+
const i = (y * w + x) * 4;
|
|
57
|
+
data[i] = r;
|
|
58
|
+
data[i + 1] = g;
|
|
59
|
+
data[i + 2] = b;
|
|
60
|
+
data[i + 3] = 255;
|
|
61
|
+
};
|
|
62
|
+
for (const { bbox, rgb } of boxes) {
|
|
63
|
+
const x1 = Math.round(bbox[0]);
|
|
64
|
+
const y1 = Math.round(bbox[1]);
|
|
65
|
+
const x2 = Math.round(bbox[2]);
|
|
66
|
+
const y2 = Math.round(bbox[3]);
|
|
67
|
+
for (let x = x1; x <= x2; x++)
|
|
68
|
+
for (let k = 0; k < thickness; k++) {
|
|
69
|
+
setPx(x, y1 + k, rgb[0], rgb[1], rgb[2]);
|
|
70
|
+
setPx(x, y2 - k, rgb[0], rgb[1], rgb[2]);
|
|
71
|
+
}
|
|
72
|
+
for (let y = y1; y <= y2; y++)
|
|
73
|
+
for (let k = 0; k < thickness; k++) {
|
|
74
|
+
setPx(x1 + k, y, rgb[0], rgb[1], rgb[2]);
|
|
75
|
+
setPx(x2 - k, y, rgb[0], rgb[1], rgb[2]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return encode({ data, width: w, height: h }, 88).data;
|
|
79
|
+
}
|
|
80
|
+
function summarizeObjects(objects) {
|
|
81
|
+
if (!objects || objects.length === 0)
|
|
82
|
+
return '';
|
|
83
|
+
const counts = new Map();
|
|
84
|
+
for (const o of objects)
|
|
85
|
+
counts.set(o.label, (counts.get(o.label) ?? 0) + 1);
|
|
86
|
+
return [...counts.entries()].map(([l, n]) => (n > 1 ? `${l}×${n}` : l)).join(', ');
|
|
87
|
+
}
|
|
88
|
+
async function detect(connection, client, rawArgs) {
|
|
89
|
+
try {
|
|
90
|
+
const args = validateArgs(detectSchema, rawArgs);
|
|
91
|
+
const target = args.target ?? 'both';
|
|
92
|
+
const annotate = args.annotate ?? true;
|
|
93
|
+
const wantFaces = target === 'faces' || target === 'both';
|
|
94
|
+
const wantObjects = target === 'objects' || target === 'both';
|
|
95
|
+
const det = await detectActiveDoc(connection, client, {
|
|
96
|
+
faces: wantFaces,
|
|
97
|
+
objects: wantObjects,
|
|
98
|
+
maxDimension: args.max_dimension,
|
|
99
|
+
faceThreshold: args.face_threshold,
|
|
100
|
+
objectThreshold: args.object_threshold,
|
|
101
|
+
maxObjects: args.max_objects,
|
|
102
|
+
});
|
|
103
|
+
const mapped = det.result;
|
|
104
|
+
const content = [];
|
|
105
|
+
if (annotate) {
|
|
106
|
+
try {
|
|
107
|
+
const boxes = [
|
|
108
|
+
...(det.raw.faces ?? []).map((f) => ({
|
|
109
|
+
bbox: f.bbox,
|
|
110
|
+
rgb: [0, 220, 255],
|
|
111
|
+
})),
|
|
112
|
+
...(det.raw.objects ?? []).map((o) => ({
|
|
113
|
+
bbox: o.bbox,
|
|
114
|
+
rgb: [255, 0, 220],
|
|
115
|
+
})),
|
|
116
|
+
];
|
|
117
|
+
const annotated = drawBoxes(det.exportBytes, boxes);
|
|
118
|
+
content.push({
|
|
119
|
+
type: 'image',
|
|
120
|
+
data: annotated.toString('base64'),
|
|
121
|
+
mimeType: 'image/jpeg',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const faceCount = mapped.faces?.length ?? 0;
|
|
128
|
+
const objCount = mapped.objects?.length ?? 0;
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (wantFaces)
|
|
131
|
+
parts.push(`${faceCount} face${faceCount === 1 ? '' : 's'}`);
|
|
132
|
+
if (wantObjects) {
|
|
133
|
+
const tally = summarizeObjects(mapped.objects);
|
|
134
|
+
parts.push(`${objCount} object${objCount === 1 ? '' : 's'}${tally ? ` (${tally})` : ''}`);
|
|
135
|
+
}
|
|
136
|
+
content.push({
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: `Detected ${parts.join(', ')} in the active document (${det.docWidth}×${det.docHeight}). ` +
|
|
139
|
+
`All bounding boxes are [x1, y1, x2, y2] in document pixels.`,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
content,
|
|
143
|
+
structuredContent: {
|
|
144
|
+
image: mapped.image,
|
|
145
|
+
backends: mapped.backends,
|
|
146
|
+
...(mapped.faces ? { faces: mapped.faces } : {}),
|
|
147
|
+
...(mapped.objects ? { objects: mapped.objects } : {}),
|
|
148
|
+
context: det.context,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text', text: `Error during detection: ${msg}` }],
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function createDetectionTools(connection, _snippetClient, client = new OnnxDetectionClient()) {
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
tool: {
|
|
164
|
+
name: 'photoshop_detect',
|
|
165
|
+
description: 'Locate faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in the active document using LOCAL on-device computer vision — the image is never sent anywhere. Returns labeled bounding boxes in DOCUMENT-pixel space plus an annotated preview for visual confirmation. Use this to gain semantic scene awareness ("there are 2 people and a dog") and real coordinates before any spatially-targeted edit — far more reliable than estimating positions from a preview. `target` selects faces / objects / both. Read-only: renders a throwaway duplicate, never modifies the working document. Boxes are [x1, y1, x2, y2]. Validate surprising results against the annotated image before acting on them.',
|
|
166
|
+
inputSchema: detectSchema,
|
|
167
|
+
outputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
image: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: { width: { type: 'number' }, height: { type: 'number' } },
|
|
173
|
+
},
|
|
174
|
+
backends: { type: 'object' },
|
|
175
|
+
faces: {
|
|
176
|
+
type: 'array',
|
|
177
|
+
items: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
bbox: { type: 'array', items: { type: 'number' } },
|
|
181
|
+
confidence: { type: 'number' },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
objects: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
items: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
label: { type: 'string' },
|
|
191
|
+
class_id: { type: 'number' },
|
|
192
|
+
bbox: { type: 'array', items: { type: 'number' } },
|
|
193
|
+
confidence: { type: 'number' },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
context: { type: 'object' },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
annotations: {
|
|
201
|
+
title: 'Detect Faces & Objects',
|
|
202
|
+
readOnlyHint: true,
|
|
203
|
+
idempotentHint: true,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
handler: async (args) => detect(connection, client, args),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
@@ -132,6 +132,22 @@ const exportPngSchema = {
|
|
|
132
132
|
},
|
|
133
133
|
required: ['output_path'],
|
|
134
134
|
};
|
|
135
|
+
const EXPORT_FORMATS = ['jpeg', 'png'];
|
|
136
|
+
const exportInputSchema = {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
format: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
enum: [...EXPORT_FORMATS],
|
|
142
|
+
description: 'Output image format. jpeg: quality 0-100, optional convert_to_srgb + embed_color_profile. ' +
|
|
143
|
+
'png: transparent_background (alpha) or flattened-on-white, compression 0-9. ' +
|
|
144
|
+
'Both take output_path (required) and optional long_edge_px (downscale only).',
|
|
145
|
+
},
|
|
146
|
+
...exportJpegSchema.properties,
|
|
147
|
+
...exportPngSchema.properties,
|
|
148
|
+
},
|
|
149
|
+
required: ['format', 'output_path'],
|
|
150
|
+
};
|
|
135
151
|
export function createDocumentTools(connection, snippetClient) {
|
|
136
152
|
return [
|
|
137
153
|
{
|
|
@@ -232,9 +248,9 @@ export function createDocumentTools(connection, snippetClient) {
|
|
|
232
248
|
},
|
|
233
249
|
{
|
|
234
250
|
tool: {
|
|
235
|
-
name: '
|
|
236
|
-
description: 'Export the active document
|
|
237
|
-
inputSchema:
|
|
251
|
+
name: 'photoshop_export',
|
|
252
|
+
description: 'Export the active document to a flattened image file — JPEG or PNG, chosen via `format`. Operates on a duplicate, so the working document is unchanged. Optionally downscales so the long edge equals long_edge_px. JPEG: quality 0-100 (Save-As scale), optional sRGB convert + profile embed. PNG: transparent background (alpha preserved) or flattened onto white, compression 0-9.',
|
|
253
|
+
inputSchema: exportInputSchema,
|
|
238
254
|
outputSchema: {
|
|
239
255
|
type: 'object',
|
|
240
256
|
properties: {
|
|
@@ -244,47 +260,42 @@ export function createDocumentTools(connection, snippetClient) {
|
|
|
244
260
|
height_px: { type: 'number' },
|
|
245
261
|
quality: { type: 'number' },
|
|
246
262
|
quality_ps_scale: { type: 'number' },
|
|
247
|
-
context: { type: 'object' },
|
|
248
|
-
},
|
|
249
|
-
required: ['success'],
|
|
250
|
-
},
|
|
251
|
-
annotations: {
|
|
252
|
-
title: 'Export JPEG (pipeline)',
|
|
253
|
-
destructiveHint: true,
|
|
254
|
-
openWorldHint: true,
|
|
255
|
-
idempotentHint: true,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
handler: async (args) => exportJpegPipeline(connection, snippetClient, args),
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
tool: {
|
|
262
|
-
name: 'photoshop_export_png',
|
|
263
|
-
description: 'Export the active document as a PNG. Choose transparent background (alpha preserved) or flattened onto white. Optionally resizes (downscale only). Operates on a duplicate — working document is unchanged.',
|
|
264
|
-
inputSchema: exportPngSchema,
|
|
265
|
-
outputSchema: {
|
|
266
|
-
type: 'object',
|
|
267
|
-
properties: {
|
|
268
|
-
success: { type: 'boolean' },
|
|
269
|
-
exported_to: { type: 'string' },
|
|
270
|
-
width_px: { type: 'number' },
|
|
271
|
-
height_px: { type: 'number' },
|
|
272
263
|
transparent: { type: 'boolean' },
|
|
273
264
|
context: { type: 'object' },
|
|
274
265
|
},
|
|
275
266
|
required: ['success'],
|
|
276
267
|
},
|
|
277
268
|
annotations: {
|
|
278
|
-
title: 'Export
|
|
269
|
+
title: 'Export Image (pipeline)',
|
|
279
270
|
destructiveHint: true,
|
|
280
271
|
openWorldHint: true,
|
|
281
272
|
idempotentHint: true,
|
|
282
273
|
},
|
|
283
274
|
},
|
|
284
|
-
handler: async (args) =>
|
|
275
|
+
handler: async (args) => exportImage(connection, snippetClient, args),
|
|
285
276
|
},
|
|
286
277
|
];
|
|
287
278
|
}
|
|
279
|
+
async function exportImage(connection, snippetClient, rawArgs) {
|
|
280
|
+
const format = rawArgs.format;
|
|
281
|
+
const { format: _omit, ...rest } = rawArgs;
|
|
282
|
+
switch (format) {
|
|
283
|
+
case 'jpeg':
|
|
284
|
+
return exportJpegPipeline(connection, snippetClient, rest);
|
|
285
|
+
case 'png':
|
|
286
|
+
return exportPngPipeline(connection, snippetClient, rest);
|
|
287
|
+
default:
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'text',
|
|
292
|
+
text: `Error: unknown export format "${String(format)}". Allowed: ${EXPORT_FORMATS.join(', ')}.`,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
isError: true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
288
299
|
async function createDocument(connection, snippetClient, rawArgs) {
|
|
289
300
|
try {
|
|
290
301
|
const args = validateArgs(createDocumentSchema, rawArgs);
|