image-edit-tools 1.0.4 → 1.0.6
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/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +90 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/ops/add-text.d.ts.map +1 -1
- package/dist/ops/add-text.js +52 -19
- package/dist/ops/add-text.js.map +1 -1
- package/dist/ops/clip-to-shape.d.ts +3 -0
- package/dist/ops/clip-to-shape.d.ts.map +1 -0
- package/dist/ops/clip-to-shape.js +58 -0
- package/dist/ops/clip-to-shape.js.map +1 -0
- package/dist/ops/draw-shape.d.ts +3 -0
- package/dist/ops/draw-shape.d.ts.map +1 -0
- package/dist/ops/draw-shape.js +54 -0
- package/dist/ops/draw-shape.js.map +1 -0
- package/dist/ops/drop-shadow.d.ts +3 -0
- package/dist/ops/drop-shadow.d.ts.map +1 -0
- package/dist/ops/drop-shadow.js +54 -0
- package/dist/ops/drop-shadow.js.map +1 -0
- package/dist/ops/gradient-overlay.d.ts +3 -0
- package/dist/ops/gradient-overlay.d.ts.map +1 -0
- package/dist/ops/gradient-overlay.js +49 -0
- package/dist/ops/gradient-overlay.js.map +1 -0
- package/dist/ops/pipeline.d.ts.map +1 -1
- package/dist/ops/pipeline.js +16 -0
- package/dist/ops/pipeline.js.map +1 -1
- package/dist/ops/rotate.d.ts +3 -0
- package/dist/ops/rotate.d.ts.map +1 -0
- package/dist/ops/rotate.js +25 -0
- package/dist/ops/rotate.js.map +1 -0
- package/dist/types.d.ts +93 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/mcp/tools.ts +86 -0
- package/src/ops/add-text.ts +55 -16
- package/src/ops/clip-to-shape.ts +63 -0
- package/src/ops/draw-shape.ts +58 -0
- package/src/ops/drop-shadow.ts +60 -0
- package/src/ops/gradient-overlay.ts +62 -0
- package/src/ops/pipeline.ts +9 -0
- package/src/ops/rotate.ts +27 -0
- package/src/types.ts +100 -1
- package/tests/unit/clip-to-shape.test.ts +36 -0
- package/tests/unit/draw-shape.test.ts +34 -0
- package/tests/unit/drop-shadow.test.ts +42 -0
- package/tests/unit/gradient-overlay.test.ts +29 -0
- package/tests/unit/rotate.test.ts +42 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { ErrorCode } from '../types.js';
|
|
3
|
+
import { loadImage } from '../utils/load-image.js';
|
|
4
|
+
import { err, ok } from '../utils/result.js';
|
|
5
|
+
import { getImageMetadata } from '../utils/validate.js';
|
|
6
|
+
function buildGradientSvg(width, height, direction, color, opacity, coverage) {
|
|
7
|
+
// Map direction to SVG linearGradient coordinates
|
|
8
|
+
const dirMap = {
|
|
9
|
+
'top': { x1: '0%', y1: '0%', x2: '0%', y2: '100%' },
|
|
10
|
+
'bottom': { x1: '0%', y1: '100%', x2: '0%', y2: '0%' },
|
|
11
|
+
'left': { x1: '0%', y1: '0%', x2: '100%', y2: '0%' },
|
|
12
|
+
'right': { x1: '100%', y1: '0%', x2: '0%', y2: '0%' },
|
|
13
|
+
'top-left': { x1: '0%', y1: '0%', x2: '100%', y2: '100%' },
|
|
14
|
+
'top-right': { x1: '100%', y1: '0%', x2: '0%', y2: '100%' },
|
|
15
|
+
'bottom-left': { x1: '0%', y1: '100%', x2: '100%', y2: '0%' },
|
|
16
|
+
'bottom-right': { x1: '100%', y1: '100%', x2: '0%', y2: '0%' },
|
|
17
|
+
};
|
|
18
|
+
const d = dirMap[direction] ?? dirMap['bottom'];
|
|
19
|
+
const stopOffset = Math.round(coverage * 100);
|
|
20
|
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
21
|
+
<defs>
|
|
22
|
+
<linearGradient id="g" x1="${d.x1}" y1="${d.y1}" x2="${d.x2}" y2="${d.y2}">
|
|
23
|
+
<stop offset="0%" stop-color="${color}" stop-opacity="${opacity}"/>
|
|
24
|
+
<stop offset="${stopOffset}%" stop-color="${color}" stop-opacity="0"/>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
</defs>
|
|
27
|
+
<rect width="${width}" height="${height}" fill="url(#g)"/>
|
|
28
|
+
</svg>`;
|
|
29
|
+
}
|
|
30
|
+
export async function gradientOverlay(input, options = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const buffer = await loadImage(input);
|
|
33
|
+
const meta = await getImageMetadata(buffer);
|
|
34
|
+
const { width, height } = meta;
|
|
35
|
+
const direction = options.direction ?? 'bottom';
|
|
36
|
+
const color = options.color ?? '#000000';
|
|
37
|
+
const opacity = options.opacity ?? 0.7;
|
|
38
|
+
const coverage = options.coverage ?? 0.5;
|
|
39
|
+
const svg = buildGradientSvg(width, height, direction, color, opacity, coverage);
|
|
40
|
+
const output = await sharp(buffer)
|
|
41
|
+
.composite([{ input: Buffer.from(svg), blend: 'over' }])
|
|
42
|
+
.toBuffer();
|
|
43
|
+
return ok(output);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return err(e.message || 'Gradient overlay failed', ErrorCode.PROCESSING_FAILED);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=gradient-overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradient-overlay.js","sourceRoot":"","sources":["../../src/ops/gradient-overlay.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/ops/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAa,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/ops/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAa,MAAM,aAAa,CAAC;AAiBpF,wBAAsB,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsD3H"}
|
package/dist/ops/pipeline.js
CHANGED
|
@@ -10,6 +10,10 @@ import { composite } from './composite.js';
|
|
|
10
10
|
import { watermark } from './watermark.js';
|
|
11
11
|
import { convert } from './convert.js';
|
|
12
12
|
import { optimize } from './optimize.js';
|
|
13
|
+
import { rotate } from './rotate.js';
|
|
14
|
+
import { gradientOverlay } from './gradient-overlay.js';
|
|
15
|
+
import { clipToShape } from './clip-to-shape.js';
|
|
16
|
+
import { dropShadow } from './drop-shadow.js';
|
|
13
17
|
export async function pipeline(input, operations) {
|
|
14
18
|
let currentImage = input;
|
|
15
19
|
for (let i = 0; i < operations.length; i++) {
|
|
@@ -50,6 +54,18 @@ export async function pipeline(input, operations) {
|
|
|
50
54
|
case 'optimize':
|
|
51
55
|
result = await optimize(currentImage, op);
|
|
52
56
|
break;
|
|
57
|
+
case 'rotate':
|
|
58
|
+
result = await rotate(currentImage, op);
|
|
59
|
+
break;
|
|
60
|
+
case 'gradientOverlay':
|
|
61
|
+
result = await gradientOverlay(currentImage, op);
|
|
62
|
+
break;
|
|
63
|
+
case 'clipToShape':
|
|
64
|
+
result = await clipToShape(currentImage, op);
|
|
65
|
+
break;
|
|
66
|
+
case 'dropShadow':
|
|
67
|
+
result = await dropShadow(currentImage, op);
|
|
68
|
+
break;
|
|
53
69
|
case 'removeBg': {
|
|
54
70
|
const { removeBg } = await import('./remove-bg.js');
|
|
55
71
|
result = await removeBg(currentImage, op);
|
package/dist/ops/pipeline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/ops/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,SAAS,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/ops/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotate.d.ts","sourceRoot":"","sources":["../../src/ops/rotate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIhF,wBAAsB,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAqB5F"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { ErrorCode } from '../types.js';
|
|
3
|
+
import { loadImage } from '../utils/load-image.js';
|
|
4
|
+
import { err, ok } from '../utils/result.js';
|
|
5
|
+
export async function rotate(input, options) {
|
|
6
|
+
try {
|
|
7
|
+
const buffer = await loadImage(input);
|
|
8
|
+
const angle = options.angle % 360;
|
|
9
|
+
if (angle === 0)
|
|
10
|
+
return ok(buffer);
|
|
11
|
+
const bgColor = options.background ?? { r: 0, g: 0, b: 0, alpha: 0 };
|
|
12
|
+
const background = typeof bgColor === 'string'
|
|
13
|
+
? bgColor
|
|
14
|
+
: bgColor;
|
|
15
|
+
const output = await sharp(buffer)
|
|
16
|
+
.rotate(angle, { background })
|
|
17
|
+
.png()
|
|
18
|
+
.toBuffer();
|
|
19
|
+
return ok(output);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return err(e.message || 'Rotation failed', ErrorCode.PROCESSING_FAILED);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=rotate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotate.js","sourceRoot":"","sources":["../../src/ops/rotate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0C,SAAS,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAiB,EAAE,OAAsB;IACpE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC;QAElC,IAAI,KAAK,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QAEnC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACrE,MAAM,UAAU,GAAG,OAAO,OAAO,KAAK,QAAQ;YAC5C,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,OAAO,CAAC;QAEZ,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;aAC/B,MAAM,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC;aAC7B,GAAG,EAAE;aACL,QAAQ,EAAE,CAAC;QAEd,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,OAAO,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,iBAAiB,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -121,6 +121,20 @@ export interface TextLayer {
|
|
|
121
121
|
maxWidth?: number;
|
|
122
122
|
lineHeight?: number;
|
|
123
123
|
background?: TextBackground;
|
|
124
|
+
/** Letter spacing in pixels. Default: 0 */
|
|
125
|
+
letterSpacing?: number;
|
|
126
|
+
/** Text stroke (outline) */
|
|
127
|
+
stroke?: {
|
|
128
|
+
color: string;
|
|
129
|
+
width: number;
|
|
130
|
+
};
|
|
131
|
+
/** Text shadow */
|
|
132
|
+
textShadow?: {
|
|
133
|
+
color: string;
|
|
134
|
+
offsetX: number;
|
|
135
|
+
offsetY: number;
|
|
136
|
+
blur?: number;
|
|
137
|
+
};
|
|
124
138
|
}
|
|
125
139
|
export type BlendMode = 'over' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten';
|
|
126
140
|
export interface CompositeLayer {
|
|
@@ -199,6 +213,76 @@ export interface ImageMetadata {
|
|
|
199
213
|
density?: number;
|
|
200
214
|
exif?: Record<string, unknown>;
|
|
201
215
|
}
|
|
216
|
+
export interface RotateOptions {
|
|
217
|
+
/** Rotation angle in degrees (0–360). Positive = clockwise. */
|
|
218
|
+
angle: number;
|
|
219
|
+
/** Background color for exposed areas. Default: 'transparent' */
|
|
220
|
+
background?: string;
|
|
221
|
+
}
|
|
222
|
+
export type GradientDirection = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
223
|
+
export interface GradientOverlayOptions {
|
|
224
|
+
/** Direction the gradient fades FROM (opaque end). Default: 'bottom' */
|
|
225
|
+
direction?: GradientDirection;
|
|
226
|
+
/** Gradient color. Default: '#000000' */
|
|
227
|
+
color?: string;
|
|
228
|
+
/** Opacity of the opaque end. 0–1. Default: 0.7 */
|
|
229
|
+
opacity?: number;
|
|
230
|
+
/** How much of the image the gradient covers. 0–1. Default: 0.5 */
|
|
231
|
+
coverage?: number;
|
|
232
|
+
}
|
|
233
|
+
export type ClipShape = 'circle' | 'ellipse' | 'rounded-rect';
|
|
234
|
+
export interface ClipToShapeOptions {
|
|
235
|
+
shape: ClipShape;
|
|
236
|
+
/** Border radius for 'rounded-rect'. Default: 32 */
|
|
237
|
+
borderRadius?: number;
|
|
238
|
+
}
|
|
239
|
+
export type ShapeType = 'rect' | 'circle' | 'ellipse' | 'line';
|
|
240
|
+
export interface DrawShapeOptions {
|
|
241
|
+
/** Canvas width */
|
|
242
|
+
width: number;
|
|
243
|
+
/** Canvas height */
|
|
244
|
+
height: number;
|
|
245
|
+
/** Shape to draw */
|
|
246
|
+
shape: ShapeType;
|
|
247
|
+
/** Fill color. Default: 'transparent' */
|
|
248
|
+
fill?: string;
|
|
249
|
+
/** Fill opacity. 0–1. Default: 1 */
|
|
250
|
+
fillOpacity?: number;
|
|
251
|
+
/** Stroke color */
|
|
252
|
+
stroke?: string;
|
|
253
|
+
/** Stroke width. Default: 0 */
|
|
254
|
+
strokeWidth?: number;
|
|
255
|
+
/** Border radius (rect only). Default: 0 */
|
|
256
|
+
borderRadius?: number;
|
|
257
|
+
/** Circle/ellipse center X. Defaults to canvas center */
|
|
258
|
+
cx?: number;
|
|
259
|
+
/** Circle/ellipse center Y. Defaults to canvas center */
|
|
260
|
+
cy?: number;
|
|
261
|
+
/** Circle radius or ellipse rx */
|
|
262
|
+
r?: number;
|
|
263
|
+
/** Ellipse ry */
|
|
264
|
+
ry?: number;
|
|
265
|
+
/** Line start X */
|
|
266
|
+
x1?: number;
|
|
267
|
+
/** Line start Y */
|
|
268
|
+
y1?: number;
|
|
269
|
+
/** Line end X */
|
|
270
|
+
x2?: number;
|
|
271
|
+
/** Line end Y */
|
|
272
|
+
y2?: number;
|
|
273
|
+
}
|
|
274
|
+
export interface DropShadowOptions {
|
|
275
|
+
/** Shadow color. Default: 'rgba(0,0,0,0.5)' */
|
|
276
|
+
color?: string;
|
|
277
|
+
/** Shadow offset X. Default: 4 */
|
|
278
|
+
offsetX?: number;
|
|
279
|
+
/** Shadow offset Y. Default: 4 */
|
|
280
|
+
offsetY?: number;
|
|
281
|
+
/** Blur radius. Default: 8 */
|
|
282
|
+
blur?: number;
|
|
283
|
+
/** Expand canvas to fit shadow. Default: true */
|
|
284
|
+
expand?: boolean;
|
|
285
|
+
}
|
|
202
286
|
export type PipelineOperation = ({
|
|
203
287
|
op: 'crop';
|
|
204
288
|
} & CropOptions) | ({
|
|
@@ -226,7 +310,15 @@ export type PipelineOperation = ({
|
|
|
226
310
|
op: 'optimize';
|
|
227
311
|
} & OptimizeOptions) | ({
|
|
228
312
|
op: 'removeBg';
|
|
229
|
-
} & RemoveBgOptions)
|
|
313
|
+
} & RemoveBgOptions) | ({
|
|
314
|
+
op: 'rotate';
|
|
315
|
+
} & RotateOptions) | ({
|
|
316
|
+
op: 'gradientOverlay';
|
|
317
|
+
} & GradientOverlayOptions) | ({
|
|
318
|
+
op: 'clipToShape';
|
|
319
|
+
} & ClipToShapeOptions) | ({
|
|
320
|
+
op: 'dropShadow';
|
|
321
|
+
} & DropShadowOptions);
|
|
230
322
|
export interface BatchOptions {
|
|
231
323
|
concurrency?: number;
|
|
232
324
|
onProgress?: (done: number, total: number) => void;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,3 +22,8 @@ export { detectFaces } from './ops/detect-faces.js';
|
|
|
22
22
|
export { extractText } from './ops/extract-text.js';
|
|
23
23
|
export { pipeline } from './ops/pipeline.js';
|
|
24
24
|
export { batch } from './ops/batch.js';
|
|
25
|
+
export { rotate } from './ops/rotate.js';
|
|
26
|
+
export { gradientOverlay } from './ops/gradient-overlay.js';
|
|
27
|
+
export { clipToShape } from './ops/clip-to-shape.js';
|
|
28
|
+
export { drawShape } from './ops/draw-shape.js';
|
|
29
|
+
export { dropShadow } from './ops/drop-shadow.js';
|
package/src/mcp/tools.ts
CHANGED
|
@@ -219,6 +219,80 @@ export const allTools: Tool[] = [
|
|
|
219
219
|
},
|
|
220
220
|
required: ['images', 'operation', 'options']
|
|
221
221
|
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'image_rotate',
|
|
225
|
+
description: 'Rotates an image by an arbitrary angle. Exposed areas are transparent by default.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
image: { type: 'string' },
|
|
230
|
+
angle: { type: 'number', description: 'Rotation angle in degrees (0-360), clockwise' },
|
|
231
|
+
background: { type: 'string', description: 'Background color for exposed areas. Default: transparent' }
|
|
232
|
+
},
|
|
233
|
+
required: ['image', 'angle']
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'image_gradient_overlay',
|
|
238
|
+
description: 'Applies a gradient overlay for text readability. Great for placing text over photos.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
image: { type: 'string' },
|
|
243
|
+
direction: { type: 'string', enum: ['top','bottom','left','right','top-left','top-right','bottom-left','bottom-right'] },
|
|
244
|
+
color: { type: 'string', description: 'Gradient color. Default: #000000' },
|
|
245
|
+
opacity: { type: 'number', description: '0-1. Default: 0.7' },
|
|
246
|
+
coverage: { type: 'number', description: '0-1 how much of image is covered. Default: 0.5' }
|
|
247
|
+
},
|
|
248
|
+
required: ['image']
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'image_clip_to_shape',
|
|
253
|
+
description: 'Clips an image to a shape: circle, ellipse, or rounded-rect. Perfect for profile photos.',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {
|
|
257
|
+
image: { type: 'string' },
|
|
258
|
+
shape: { type: 'string', enum: ['circle', 'ellipse', 'rounded-rect'] },
|
|
259
|
+
borderRadius: { type: 'number', description: 'For rounded-rect. Default: 32' }
|
|
260
|
+
},
|
|
261
|
+
required: ['image', 'shape']
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'image_draw_shape',
|
|
266
|
+
description: 'Creates a new image containing a shape (rect, circle, ellipse, line). Use with composite to layer.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
width: { type: 'number' }, height: { type: 'number' },
|
|
271
|
+
shape: { type: 'string', enum: ['rect', 'circle', 'ellipse', 'line'] },
|
|
272
|
+
fill: { type: 'string' }, fillOpacity: { type: 'number' },
|
|
273
|
+
stroke: { type: 'string' }, strokeWidth: { type: 'number' },
|
|
274
|
+
borderRadius: { type: 'number' },
|
|
275
|
+
cx: { type: 'number' }, cy: { type: 'number' }, r: { type: 'number' }, ry: { type: 'number' },
|
|
276
|
+
x1: { type: 'number' }, y1: { type: 'number' }, x2: { type: 'number' }, y2: { type: 'number' }
|
|
277
|
+
},
|
|
278
|
+
required: ['width', 'height', 'shape']
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'image_drop_shadow',
|
|
283
|
+
description: 'Adds a drop shadow behind the image. Expands canvas to fit shadow.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
image: { type: 'string' },
|
|
288
|
+
color: { type: 'string', description: 'Shadow color. Default: rgba(0,0,0,0.5)' },
|
|
289
|
+
offsetX: { type: 'number', description: 'Default: 4' },
|
|
290
|
+
offsetY: { type: 'number', description: 'Default: 4' },
|
|
291
|
+
blur: { type: 'number', description: 'Blur radius. Default: 8' },
|
|
292
|
+
expand: { type: 'boolean', description: 'Expand canvas. Default: true' }
|
|
293
|
+
},
|
|
294
|
+
required: ['image']
|
|
295
|
+
}
|
|
222
296
|
}
|
|
223
297
|
];
|
|
224
298
|
|
|
@@ -260,6 +334,18 @@ export async function handleTool(name: string, args: Record<string, any>): Promi
|
|
|
260
334
|
const txt = await api.extractText(image, args);
|
|
261
335
|
return JSON.stringify(txt);
|
|
262
336
|
}
|
|
337
|
+
else if (name === 'image_rotate') result = await api.rotate(image, args as any);
|
|
338
|
+
else if (name === 'image_gradient_overlay') result = await api.gradientOverlay(image, args as any);
|
|
339
|
+
else if (name === 'image_clip_to_shape') result = await api.clipToShape(image, args as any);
|
|
340
|
+
else if (name === 'image_draw_shape') {
|
|
341
|
+
result = await api.drawShape(args as any);
|
|
342
|
+
if (result && result.ok && Buffer.isBuffer(result.data)) {
|
|
343
|
+
const b64 = result.data.toString('base64');
|
|
344
|
+
return JSON.stringify({ ok: true, data: `data:image/png;base64,${b64}` });
|
|
345
|
+
}
|
|
346
|
+
return JSON.stringify(result);
|
|
347
|
+
}
|
|
348
|
+
else if (name === 'image_drop_shadow') result = await api.dropShadow(image, args as any);
|
|
263
349
|
else {
|
|
264
350
|
return JSON.stringify({ error: `Tool ${name} not implemented`, code: 'INVALID_INPUT' });
|
|
265
351
|
}
|
package/src/ops/add-text.ts
CHANGED
|
@@ -33,23 +33,29 @@ function escapeXml(text: string): string {
|
|
|
33
33
|
.replace(/'/g, ''');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function getAnchorProps(anchor: TextAnchor = 'top-left'): { textAnchor: string,
|
|
36
|
+
function getAnchorProps(anchor: TextAnchor = 'top-left', fontSize: number = 24): { textAnchor: string, yOffset: number } {
|
|
37
37
|
const parts = anchor.split('-');
|
|
38
38
|
const yAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
39
39
|
const xAlign = parts.length === 2 ? parts[1] : parts[0] === 'center' ? 'center' : 'left';
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
41
|
+
// librsvg does NOT reliably support dominant-baseline values other than 'auto' (alphabetic).
|
|
42
|
+
// Instead of relying on dominant-baseline, we compute a y-offset to position text correctly.
|
|
43
|
+
// With 'auto' (alphabetic baseline), y = text baseline (bottom of caps).
|
|
44
|
+
// To make y = text top, we shift down by ~0.8 * fontSize.
|
|
45
|
+
// To make y = text middle, we shift down by ~0.35 * fontSize.
|
|
46
|
+
let yOffset = 0;
|
|
47
|
+
if (yAlign === 'top') {
|
|
48
|
+
yOffset = Math.round(fontSize * 0.8);
|
|
49
|
+
} else if (yAlign === 'middle' || yAlign === 'center') {
|
|
50
|
+
yOffset = Math.round(fontSize * 0.35);
|
|
51
|
+
}
|
|
52
|
+
// 'bottom' / 'auto' → yOffset = 0 (alphabetic baseline is already at y)
|
|
47
53
|
|
|
48
54
|
let textAnchor = 'start';
|
|
49
55
|
if (xAlign === 'center') textAnchor = 'middle';
|
|
50
56
|
else if (xAlign === 'right') textAnchor = 'end';
|
|
51
57
|
|
|
52
|
-
return { textAnchor,
|
|
58
|
+
return { textAnchor, yOffset };
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export async function addText(input: ImageInput, options: { layers: TextLayer[] }): Promise<ImageResult> {
|
|
@@ -82,14 +88,26 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
82
88
|
const totalHeight = lines.length * fontSize * lineHeight;
|
|
83
89
|
const approxMaxWidth = Math.max(...lines.map(l => l.length * fontSize * 0.6));
|
|
84
90
|
|
|
85
|
-
const { textAnchor,
|
|
91
|
+
const { textAnchor, yOffset } = getAnchorProps(layer.anchor, fontSize);
|
|
92
|
+
const renderY = layer.y + yOffset;
|
|
86
93
|
|
|
87
94
|
let align = textAnchor;
|
|
88
95
|
if (layer.align) {
|
|
89
96
|
align = layer.align === 'left' ? 'start' : layer.align === 'right' ? 'end' : 'middle';
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
// Always use dominant-baseline: auto (alphabetic) — the only value librsvg reliably supports
|
|
100
|
+
let style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;`;
|
|
101
|
+
|
|
102
|
+
// Letter spacing
|
|
103
|
+
if (layer.letterSpacing) {
|
|
104
|
+
style += ` letter-spacing: ${layer.letterSpacing}px;`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stroke (outline) — paint-order renders stroke behind fill
|
|
108
|
+
if (layer.stroke) {
|
|
109
|
+
style += ` stroke: ${layer.stroke.color}; stroke-width: ${layer.stroke.width}px; paint-order: stroke;`;
|
|
110
|
+
}
|
|
93
111
|
|
|
94
112
|
let layerSvg = '';
|
|
95
113
|
|
|
@@ -99,6 +117,7 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
99
117
|
const bgOpacity = bg.opacity ?? 1.0;
|
|
100
118
|
const radius = bg.borderRadius ?? 0;
|
|
101
119
|
|
|
120
|
+
// Background rect is positioned relative to the *intended* y (layer.y), not renderY
|
|
102
121
|
let rectX = layer.x - pad;
|
|
103
122
|
let rectY = layer.y - pad;
|
|
104
123
|
|
|
@@ -108,16 +127,33 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
108
127
|
rectX = layer.x - approxMaxWidth - pad;
|
|
109
128
|
}
|
|
110
129
|
|
|
111
|
-
|
|
130
|
+
// Adjust for anchor vertical alignment
|
|
131
|
+
const parts = (layer.anchor ?? 'top-left').split('-');
|
|
132
|
+
const vAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
|
|
133
|
+
if (vAlign === 'middle' || vAlign === 'center') {
|
|
112
134
|
rectY = layer.y - (totalHeight / 2) - pad;
|
|
113
|
-
} else if (
|
|
135
|
+
} else if (vAlign === 'bottom') {
|
|
114
136
|
rectY = layer.y - totalHeight - pad + fontSize;
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
layerSvg += `<rect x="${rectX}" y="${rectY}" width="${approxMaxWidth + pad * 2}" height="${totalHeight + pad * 2}" fill="${bg.color}" opacity="${bgOpacity}" rx="${radius}" ry="${radius}" />`;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
|
|
142
|
+
// Text shadow: render a duplicate text behind the main text
|
|
143
|
+
if (layer.textShadow) {
|
|
144
|
+
const ts = layer.textShadow;
|
|
145
|
+
const shadowStyle = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${ts.color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;${layer.letterSpacing ? ` letter-spacing: ${layer.letterSpacing}px;` : ''}`;
|
|
146
|
+
const sx = layer.x + ts.offsetX;
|
|
147
|
+
const sy = renderY + ts.offsetY;
|
|
148
|
+
layerSvg += `<text x="${sx}" y="${sy}" style="${shadowStyle}">`;
|
|
149
|
+
lines.forEach((line, idx) => {
|
|
150
|
+
let dy = idx === 0 ? 0 : fontSize * lineHeight;
|
|
151
|
+
layerSvg += `<tspan x="${sx}" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
152
|
+
});
|
|
153
|
+
layerSvg += `</text>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
layerSvg += `<text x="${layer.x}" y="${renderY}" style="${style}">`;
|
|
121
157
|
lines.forEach((line, idx) => {
|
|
122
158
|
let dy = idx === 0 ? 0 : fontSize * lineHeight;
|
|
123
159
|
layerSvg += `<tspan x="${layer.x}" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
@@ -126,13 +162,16 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
126
162
|
|
|
127
163
|
svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
|
|
128
164
|
|
|
129
|
-
// Compute bounding box for overflow detection
|
|
165
|
+
// Compute bounding box for overflow detection (using intended y, not renderY)
|
|
130
166
|
let boxX = layer.x;
|
|
131
167
|
let boxY = layer.y;
|
|
132
168
|
if (textAnchor === 'middle') boxX -= approxMaxWidth / 2;
|
|
133
169
|
else if (textAnchor === 'end') boxX -= approxMaxWidth;
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
|
|
171
|
+
const anchorParts = (layer.anchor ?? 'top-left').split('-');
|
|
172
|
+
const vAlignBox = anchorParts.length === 2 ? anchorParts[0] : anchorParts[0] === 'center' ? 'middle' : anchorParts[0];
|
|
173
|
+
if (vAlignBox === 'middle' || vAlignBox === 'center') boxY -= totalHeight / 2;
|
|
174
|
+
else if (vAlignBox === 'bottom') boxY -= totalHeight - fontSize;
|
|
136
175
|
|
|
137
176
|
const boxBottom = boxY + totalHeight;
|
|
138
177
|
const boxRight = boxX + approxMaxWidth;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { ClipToShapeOptions, ImageInput, ImageResult, ErrorCode } from '../types.js';
|
|
3
|
+
import { loadImage } from '../utils/load-image.js';
|
|
4
|
+
import { err, ok } from '../utils/result.js';
|
|
5
|
+
import { getImageMetadata } from '../utils/validate.js';
|
|
6
|
+
|
|
7
|
+
function buildClipSvg(width: number, height: number, options: ClipToShapeOptions): string {
|
|
8
|
+
const { shape } = options;
|
|
9
|
+
let clipContent: string;
|
|
10
|
+
|
|
11
|
+
switch (shape) {
|
|
12
|
+
case 'circle': {
|
|
13
|
+
const r = Math.min(width, height) / 2;
|
|
14
|
+
const cx = width / 2;
|
|
15
|
+
const cy = height / 2;
|
|
16
|
+
clipContent = `<circle cx="${cx}" cy="${cy}" r="${r}"/>`;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'ellipse': {
|
|
20
|
+
clipContent = `<ellipse cx="${width / 2}" cy="${height / 2}" rx="${width / 2}" ry="${height / 2}"/>`;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case 'rounded-rect': {
|
|
24
|
+
const radius = options.borderRadius ?? 32;
|
|
25
|
+
clipContent = `<rect width="${width}" height="${height}" rx="${radius}" ry="${radius}"/>`;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
default:
|
|
29
|
+
clipContent = `<rect width="${width}" height="${height}"/>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create an SVG mask: white shape on black = keep shape area
|
|
33
|
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
34
|
+
${clipContent.replace('/>', ' fill="white"/>')}
|
|
35
|
+
</svg>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function clipToShape(input: ImageInput, options: ClipToShapeOptions): Promise<ImageResult> {
|
|
39
|
+
try {
|
|
40
|
+
const buffer = await loadImage(input);
|
|
41
|
+
const meta = await getImageMetadata(buffer);
|
|
42
|
+
const { width, height } = meta;
|
|
43
|
+
|
|
44
|
+
const maskSvg = buildClipSvg(width, height, options);
|
|
45
|
+
const mask = await sharp(Buffer.from(maskSvg))
|
|
46
|
+
.resize(width, height)
|
|
47
|
+
.grayscale()
|
|
48
|
+
.toBuffer();
|
|
49
|
+
|
|
50
|
+
const output = await sharp(buffer)
|
|
51
|
+
.ensureAlpha()
|
|
52
|
+
.composite([{
|
|
53
|
+
input: mask,
|
|
54
|
+
blend: 'dest-in'
|
|
55
|
+
}])
|
|
56
|
+
.png()
|
|
57
|
+
.toBuffer();
|
|
58
|
+
|
|
59
|
+
return ok(output);
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
return err(e.message || 'Clip to shape failed', ErrorCode.PROCESSING_FAILED);
|
|
62
|
+
}
|
|
63
|
+
}
|