image-edit-tools 1.0.3 → 1.0.5

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 CHANGED
@@ -16,15 +16,35 @@ npm install image-edit-tools
16
16
 
17
17
  ## Quick Start (Code)
18
18
  ```typescript
19
- import { crop, resize, pad } from 'image-edit-tools';
19
+ import { crop, resize, addText, composite, pipeline } from 'image-edit-tools';
20
20
 
21
+ // Basic resize
21
22
  const result = await resize('/path/to/img.jpg', { width: 800 });
22
23
  if (!result.ok) {
23
24
  console.error(result.error);
24
25
  return;
25
26
  }
26
- // Returns a Buffer.
27
- // Can be saved, piped, or passed explicitly!
27
+
28
+ // Add text overlay note the { layers: [...] } wrapper
29
+ const withText = await addText(result.data, {
30
+ layers: [
31
+ { text: 'Hello!', x: 40, y: 40, fontSize: 48, color: '#FFFFFF' },
32
+ ],
33
+ });
34
+
35
+ // Composite images — same { layers: [...] } pattern
36
+ const merged = await composite(result.data, {
37
+ layers: [
38
+ { input: '/path/to/overlay.png', left: 0, top: 0, blend: 'over' },
39
+ ],
40
+ });
41
+
42
+ // Pipeline: chain multiple operations in one call
43
+ const card = await pipeline('/path/to/photo.jpg', [
44
+ { op: 'resize', width: 1080, height: 1080 },
45
+ { op: 'adjust', brightness: 10, contrast: 5 },
46
+ { op: 'addText', layers: [{ text: 'Draft', x: 540, y: 540, fontSize: 64, color: '#FF0000', anchor: 'top-center' }] },
47
+ ]);
28
48
  ```
29
49
 
30
50
  ## Running the MCP Server
@@ -1 +1 @@
1
- {"version":3,"file":"add-text.d.ts","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAyB,MAAM,aAAa,CAAC;AAqDxF,wBAAsB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA6FvG"}
1
+ {"version":3,"file":"add-text.d.ts","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAyB,MAAM,aAAa,CAAC;AA2DxF,wBAAsB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CA4HvG"}
@@ -33,25 +33,29 @@ function escapeXml(text) {
33
33
  .replace(/"/g, '"')
34
34
  .replace(/'/g, ''');
35
35
  }
36
- function getAnchorProps(anchor = 'top-left') {
36
+ function getAnchorProps(anchor = 'top-left', fontSize = 24) {
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
- let dominantBaseline = 'hanging'; // top
41
- if (yAlign === 'bottom')
42
- dominantBaseline = 'auto'; // bottom is harder, usually means using y directly on baseline, but we can do mathematical offset. We'll rely on SVG baselines.
43
- else if (yAlign === 'middle' || yAlign === 'center')
44
- dominantBaseline = 'middle';
45
- else if (yAlign === 'auto')
46
- dominantBaseline = 'auto';
47
- // Sharp's librsvg supports dominant-baseline: text-before-edge (top), middle, alphabetic (bottom)
48
- const baselineMap = { top: 'text-before-edge', middle: 'middle', bottom: 'alphabetic', center: 'middle' };
40
+ // librsvg does NOT reliably support dominant-baseline values other than 'auto' (alphabetic).
41
+ // Instead of relying on dominant-baseline, we compute a y-offset to position text correctly.
42
+ // With 'auto' (alphabetic baseline), y = text baseline (bottom of caps).
43
+ // To make y = text top, we shift down by ~0.8 * fontSize.
44
+ // To make y = text middle, we shift down by ~0.35 * fontSize.
45
+ let yOffset = 0;
46
+ if (yAlign === 'top') {
47
+ yOffset = Math.round(fontSize * 0.8);
48
+ }
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)
49
53
  let textAnchor = 'start';
50
54
  if (xAlign === 'center')
51
55
  textAnchor = 'middle';
52
56
  else if (xAlign === 'right')
53
57
  textAnchor = 'end';
54
- return { textAnchor, dominantBaseline: baselineMap[yAlign] || 'text-before-edge' };
58
+ return { textAnchor, yOffset };
55
59
  }
56
60
  export async function addText(input, options) {
57
61
  try {
@@ -64,6 +68,8 @@ export async function addText(input, options) {
64
68
  let defs = '';
65
69
  let svgBody = '';
66
70
  let fontImports = new Set();
71
+ const warnings = [];
72
+ let contentBottom = 0;
67
73
  for (let i = 0; i < options.layers.length; i++) {
68
74
  const layer = options.layers[i];
69
75
  const fontSize = layer.fontSize ?? 24;
@@ -76,18 +82,21 @@ export async function addText(input, options) {
76
82
  const lineHeight = layer.lineHeight ?? 1.2;
77
83
  const totalHeight = lines.length * fontSize * lineHeight;
78
84
  const approxMaxWidth = Math.max(...lines.map(l => l.length * fontSize * 0.6));
79
- const { textAnchor, dominantBaseline } = getAnchorProps(layer.anchor);
85
+ const { textAnchor, yOffset } = getAnchorProps(layer.anchor, fontSize);
86
+ const renderY = layer.y + yOffset;
80
87
  let align = textAnchor;
81
88
  if (layer.align) {
82
89
  align = layer.align === 'left' ? 'start' : layer.align === 'right' ? 'end' : 'middle';
83
90
  }
84
- const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: ${dominantBaseline};`;
91
+ // Always use dominant-baseline: auto (alphabetic) the only value librsvg reliably supports
92
+ const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;`;
85
93
  let layerSvg = '';
86
94
  if (layer.background) {
87
95
  const bg = layer.background;
88
96
  const pad = bg.padding ?? 0;
89
97
  const bgOpacity = bg.opacity ?? 1.0;
90
98
  const radius = bg.borderRadius ?? 0;
99
+ // Background rect is positioned relative to the *intended* y (layer.y), not renderY
91
100
  let rectX = layer.x - pad;
92
101
  let rectY = layer.y - pad;
93
102
  if (textAnchor === 'middle') {
@@ -96,21 +105,44 @@ export async function addText(input, options) {
96
105
  else if (textAnchor === 'end') {
97
106
  rectX = layer.x - approxMaxWidth - pad;
98
107
  }
99
- if (dominantBaseline === 'middle') {
108
+ // Adjust for anchor vertical alignment
109
+ const parts = (layer.anchor ?? 'top-left').split('-');
110
+ const vAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
111
+ if (vAlign === 'middle' || vAlign === 'center') {
100
112
  rectY = layer.y - (totalHeight / 2) - pad;
101
113
  }
102
- else if (dominantBaseline === 'alphabetic') { // bottom
114
+ else if (vAlign === 'bottom') {
103
115
  rectY = layer.y - totalHeight - pad + fontSize;
104
116
  }
105
117
  layerSvg += `<rect x="${rectX}" y="${rectY}" width="${approxMaxWidth + pad * 2}" height="${totalHeight + pad * 2}" fill="${bg.color}" opacity="${bgOpacity}" rx="${radius}" ry="${radius}" />`;
106
118
  }
107
- layerSvg += `<text x="${layer.x}" y="${layer.y}" style="${style}">`;
119
+ layerSvg += `<text x="${layer.x}" y="${renderY}" style="${style}">`;
108
120
  lines.forEach((line, idx) => {
109
121
  let dy = idx === 0 ? 0 : fontSize * lineHeight;
110
122
  layerSvg += `<tspan x="${layer.x}" dy="${dy}">${escapeXml(line)}</tspan>`;
111
123
  });
112
124
  layerSvg += `</text>`;
113
125
  svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
126
+ // Compute bounding box for overflow detection (using intended y, not renderY)
127
+ let boxX = layer.x;
128
+ let boxY = layer.y;
129
+ if (textAnchor === 'middle')
130
+ boxX -= approxMaxWidth / 2;
131
+ else if (textAnchor === 'end')
132
+ boxX -= approxMaxWidth;
133
+ const anchorParts = (layer.anchor ?? 'top-left').split('-');
134
+ const vAlignBox = anchorParts.length === 2 ? anchorParts[0] : anchorParts[0] === 'center' ? 'middle' : anchorParts[0];
135
+ if (vAlignBox === 'middle' || vAlignBox === 'center')
136
+ boxY -= totalHeight / 2;
137
+ else if (vAlignBox === 'bottom')
138
+ boxY -= totalHeight - fontSize;
139
+ const boxBottom = boxY + totalHeight;
140
+ const boxRight = boxX + approxMaxWidth;
141
+ if (boxBottom > contentBottom)
142
+ contentBottom = boxBottom;
143
+ if (boxX < 0 || boxY < 0 || boxRight > width || boxBottom > height) {
144
+ warnings.push(`Text layer ${i} ("${layer.text.slice(0, 20)}...") extends beyond canvas bounds.`);
145
+ }
114
146
  }
115
147
  const fontStyle = fontImports.size > 0 ? `<style>${Array.from(fontImports).join('\n')}</style>` : '';
116
148
  const svgString = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
@@ -121,7 +153,9 @@ export async function addText(input, options) {
121
153
  const output = await sharp(buffer)
122
154
  .composite([{ input: Buffer.from(svgString), blend: 'over' }])
123
155
  .toBuffer();
124
- return ok(output);
156
+ const result = ok(output, warnings);
157
+ result.bounds = { contentBottom: Math.round(contentBottom) };
158
+ return result;
125
159
  }
126
160
  catch (e) {
127
161
  const msg = e.message || '';
@@ -1 +1 @@
1
- {"version":3,"file":"add-text.js","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"add-text.js","sourceRoot":"","sources":["../../src/ops/add-text.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../src/ops/composite.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIjF,wBAAsB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAkD9G"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../src/ops/composite.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIjF,wBAAsB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAwE9G"}
@@ -31,13 +31,32 @@ export async function composite(input, options) {
31
31
  };
32
32
  };
33
33
  const overlays = await Promise.all(options.layers.map(loadLayer));
34
+ // Detect potentially problematic opaque layers
35
+ const warnings = [];
36
+ const baseMeta = await sharp(buffer).metadata();
37
+ const canvasArea = (baseMeta.width ?? 1) * (baseMeta.height ?? 1);
38
+ for (let i = 0; i < options.layers.length; i++) {
39
+ const layer = options.layers[i];
40
+ const layerOpacity = layer.opacity ?? 1.0;
41
+ if (layerOpacity >= 0.9) {
42
+ try {
43
+ const layerBuf = await loadImage(layer.image);
44
+ const layerMeta = await sharp(layerBuf).metadata();
45
+ const layerArea = (layerMeta.width ?? 0) * (layerMeta.height ?? 0);
46
+ if (layerArea / canvasArea > 0.25) {
47
+ warnings.push(`Layer ${i} is opaque (opacity=${layerOpacity}) and covers ${Math.round(layerArea / canvasArea * 100)}% of the canvas. It may hide content underneath.`);
48
+ }
49
+ }
50
+ catch { /* skip analysis for unreadable layers */ }
51
+ }
52
+ }
34
53
  let output = buffer;
35
54
  if (overlays.length > 0) {
36
55
  output = await sharp(buffer)
37
56
  .composite(overlays)
38
57
  .toBuffer();
39
58
  }
40
- return ok(output);
59
+ return ok(output, warnings);
41
60
  }
42
61
  catch (e) {
43
62
  const msg = e.message || '';
@@ -1 +1 @@
1
- {"version":3,"file":"composite.js","sourceRoot":"","sources":["../../src/ops/composite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composite.js","sourceRoot":"","sources":["../../src/ops/composite.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export type ImageInput = Buffer | string;
11
11
  export type Ok<T> = {
12
12
  ok: true;
13
13
  data: T;
14
+ warnings?: string[];
14
15
  };
15
16
  export type Err = {
16
17
  ok: false;
@@ -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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"}
@@ -1,4 +1,4 @@
1
1
  import { Ok, Err, ErrorCode } from '../types.js';
2
- export declare const ok: <T>(data: T) => Ok<T>;
2
+ export declare const ok: <T>(data: T, warnings?: string[]) => Ok<T>;
3
3
  export declare const err: (error: string, code: ErrorCode) => Err;
4
4
  //# sourceMappingURL=result.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/utils/result.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEhD,eAAO,MAAM,EAAE,YAAa,CAAC,KAAG,GAAG,CAAC,CAAyB,CAAA;AAC7D,eAAO,MAAM,GAAG,UAAW,MAAM,QAAQ,SAAS,KAAG,GAAmC,CAAA"}
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/utils/result.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEhD,eAAO,MAAM,EAAE,YAAa,CAAC,aAAa,MAAM,EAAE,KAAG,GAAG,CAAC,CAIxD,CAAA;AACD,eAAO,MAAM,GAAG,UAAW,MAAM,QAAQ,SAAS,KAAG,GAAmC,CAAA"}
@@ -1,3 +1,8 @@
1
- export const ok = (data) => ({ ok: true, data });
1
+ export const ok = (data, warnings) => {
2
+ const result = { ok: true, data };
3
+ if (warnings && warnings.length > 0)
4
+ result.warnings = warnings;
5
+ return result;
6
+ };
2
7
  export const err = (error, code) => ({ ok: false, error, code });
3
8
  //# sourceMappingURL=result.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/utils/result.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,EAAE,GAAG,CAAI,IAAO,EAAS,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;AAC7D,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAe,EAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/utils/result.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,EAAE,GAAG,CAAI,IAAO,EAAE,QAAmB,EAAS,EAAE;IAC3D,MAAM,MAAM,GAAU,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzC,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAChE,OAAO,MAAM,CAAC;AAChB,CAAC,CAAA;AACD,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAe,EAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA"}
package/docs/AGENTS.md CHANGED
@@ -11,6 +11,8 @@ Welcome, fellow AI agent! `image-edit-tools` is designed specifically for you to
11
11
  - **Pipelines**: Instead of calling `image_crop`, then `image_resize`, then `image_convert` using three separate tool calls (which transfers images back and forth 3 times), use the `image_pipeline` tool! Pass an array of operations. It is dramatically faster and avoids memory overhead.
12
12
  - **Analysis First**: When instructed to edit an image based on its content (e.g. "Blur the person's face"), first call `image_detect_faces`. Parse the coordinates, then construct an `image_blur_region` payload using exactly those coordinates.
13
13
  - **No Side Effects**: All tools are pure functions. They compute and return the image. If the user asks you to modify a file, you must take the data URI returned by the tool and write it back to the file system using your filesystem tools.
14
+ - **Icons, not Emoji**: The `image_add_text` tool uses SVG rendering (librsvg), which **cannot render color emoji**. If you need icons or emoji, create them as separate PNG/SVG images and use `image_composite` to layer them onto the canvas at the desired position.
15
+ - **Check Warnings**: Responses may include a `warnings` array. Check it for overlap, overflow, or layer ordering issues.
14
16
 
15
17
  ## Example Chain (Watermarking)
16
18
  1. **Goal**: Add a bottom-right watermark.
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Instagram Card News Generator
3
3
  *
4
- * Demonstrates: resize, pad, addText, composite, convert, pipeline
5
- * Output: 1080×1080 Instagram carousel slides (cover + 3 content slides + closing)
4
+ * Demonstrates: addText, composite, sharp SVG generation
5
+ * Output: 1080x1080 Instagram carousel slides (cover + 3 content + closing)
6
+ *
7
+ * NOTE: librsvg cannot render color emoji. This example uses composite()
8
+ * with SVG-generated icon shapes instead of emoji in addText().
6
9
  *
7
10
  * Usage:
8
11
  * npx tsx examples/instagram-card-news.ts
@@ -12,7 +15,6 @@ import { writeFileSync, mkdirSync, existsSync } from 'fs';
12
15
  import { join, dirname } from 'path';
13
16
  import { fileURLToPath } from 'url';
14
17
  import { addText } from '../src/ops/add-text.js';
15
- import { composite } from '../src/ops/composite.js';
16
18
 
17
19
  const __filename = fileURLToPath(import.meta.url);
18
20
  const __dirname = dirname(__filename);
@@ -23,131 +25,151 @@ if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true });
23
25
  // ── Design Tokens ────────────────────────────────────────────────────────────
24
26
  const SIZE = 1080;
25
27
  const PALETTE = {
26
- bg: '#0F172A', // slate-900
27
- accent: '#3B82F6', // blue-500
28
- surface: '#1E293B', // slate-800
29
- text: '#F8FAFC', // slate-50
30
- muted: '#94A3B8', // slate-400
31
- highlight:'#F59E0B', // amber-500
28
+ bg: '#0F172A',
29
+ accent: '#3B82F6',
30
+ surface: '#1E293B',
31
+ text: '#F8FAFC',
32
+ muted: '#94A3B8',
33
+ highlight:'#F59E0B',
34
+ purple: '#8B5CF6',
32
35
  };
33
36
 
34
- // ── Helper: create a solid-color 1080×1080 canvas ────────────────────────────
37
+ // ── SVG Shape Helpers ────────────────────────────────────────────────────────
35
38
  async function canvas(color: string): Promise<Buffer> {
36
- return sharp({
37
- create: { width: SIZE, height: SIZE, channels: 4, background: color }
38
- }).png().toBuffer();
39
+ return sharp({ create: { width: SIZE, height: SIZE, channels: 4, background: color } }).png().toBuffer();
39
40
  }
40
41
 
41
- // ── Helper: create a rounded rectangle shape ─────────────────────────────────
42
- async function roundedRect(
43
- w: number, h: number, color: string, radius: number, opacity = 1
44
- ): Promise<Buffer> {
45
- const svg = `<svg width="${w}" height="${h}">
46
- <rect width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="${color}" opacity="${opacity}"/>
47
- </svg>`;
42
+ async function roundedRect(w: number, h: number, color: string, radius: number, opacity = 1): Promise<Buffer> {
43
+ const svg = `<svg width="${w}" height="${h}"><rect width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="${color}" opacity="${opacity}"/></svg>`;
48
44
  return sharp(Buffer.from(svg)).png().toBuffer();
49
45
  }
50
46
 
51
- // ── Helper: create a circle ──────────────────────────────────────────────────
52
- async function circle(r: number, color: string): Promise<Buffer> {
47
+ async function svgCircle(r: number, color: string): Promise<Buffer> {
53
48
  const d = r * 2;
54
- const svg = `<svg width="${d}" height="${d}">
55
- <circle cx="${r}" cy="${r}" r="${r}" fill="${color}"/>
56
- </svg>`;
49
+ const svg = `<svg width="${d}" height="${d}"><circle cx="${r}" cy="${r}" r="${r}" fill="${color}"/></svg>`;
57
50
  return sharp(Buffer.from(svg)).png().toBuffer();
58
51
  }
59
52
 
60
- // ── Helper: create a gradient banner ─────────────────────────────────────────
61
- async function gradientBanner(w: number, h: number): Promise<Buffer> {
53
+ async function gradientBanner(w: number, h: number, rx = 0): Promise<Buffer> {
62
54
  const svg = `<svg width="${w}" height="${h}">
63
- <defs>
64
- <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
65
- <stop offset="0%" stop-color="${PALETTE.accent}"/>
66
- <stop offset="100%" stop-color="#8B5CF6"/>
67
- </linearGradient>
68
- </defs>
69
- <rect width="${w}" height="${h}" rx="24" ry="24" fill="url(#g)"/>
55
+ <defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
56
+ <stop offset="0%" stop-color="${PALETTE.accent}"/>
57
+ <stop offset="100%" stop-color="${PALETTE.purple}"/>
58
+ </linearGradient></defs>
59
+ <rect width="${w}" height="${h}" rx="${rx}" fill="url(#g)"/>
70
60
  </svg>`;
71
61
  return sharp(Buffer.from(svg)).png().toBuffer();
72
62
  }
73
63
 
74
- // ── Helper: number badge ─────────────────────────────────────────────────────
75
64
  async function numberBadge(num: number): Promise<Buffer> {
76
- const size = 80;
77
- const svg = `<svg width="${size}" height="${size}">
78
- <circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="${PALETTE.accent}"/>
79
- <text x="${size/2}" y="${size/2}" text-anchor="middle" dominant-baseline="central"
65
+ const s = 80;
66
+ const svg = `<svg width="${s}" height="${s}">
67
+ <circle cx="${s/2}" cy="${s/2}" r="${s/2}" fill="${PALETTE.accent}"/>
68
+ <text x="${s/2}" y="${s/2}" text-anchor="middle" dominant-baseline="central"
80
69
  font-family="sans-serif" font-weight="bold" font-size="36" fill="white">${num}</text>
81
70
  </svg>`;
82
71
  return sharp(Buffer.from(svg)).png().toBuffer();
83
72
  }
84
73
 
74
+ // ── Icon Helpers (SVG shapes to replace emoji) ───────────────────────────────
75
+ async function iconArrow(size: number): Promise<Buffer> {
76
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
77
+ <polygon points="10,24 38,24 28,14" fill="${PALETTE.accent}" stroke="${PALETTE.accent}" stroke-width="2" stroke-linejoin="round"/>
78
+ <polygon points="10,24 38,24 28,34" fill="${PALETTE.accent}" stroke="${PALETTE.accent}" stroke-width="2" stroke-linejoin="round"/>
79
+ <rect x="10" y="20" width="28" height="8" rx="4" fill="${PALETTE.accent}"/>
80
+ </svg>`;
81
+ return sharp(Buffer.from(svg)).png().toBuffer();
82
+ }
83
+
84
+ async function iconCrop(size: number): Promise<Buffer> {
85
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
86
+ <path d="M12,4 L12,36 L44,36" stroke="${PALETTE.accent}" stroke-width="4" fill="none" stroke-linecap="round"/>
87
+ <path d="M36,44 L36,12 L4,12" stroke="${PALETTE.purple}" stroke-width="4" fill="none" stroke-linecap="round"/>
88
+ </svg>`;
89
+ return sharp(Buffer.from(svg)).png().toBuffer();
90
+ }
91
+
92
+ async function iconPalette(size: number): Promise<Buffer> {
93
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
94
+ <circle cx="24" cy="24" r="20" fill="none" stroke="${PALETTE.highlight}" stroke-width="3"/>
95
+ <circle cx="16" cy="18" r="4" fill="#FF6B6B"/>
96
+ <circle cx="28" cy="14" r="4" fill="#51CF66"/>
97
+ <circle cx="34" cy="24" r="4" fill="#339AF0"/>
98
+ <circle cx="18" cy="30" r="4" fill="${PALETTE.highlight}"/>
99
+ </svg>`;
100
+ return sharp(Buffer.from(svg)).png().toBuffer();
101
+ }
102
+
103
+ async function iconDrop(size: number): Promise<Buffer> {
104
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
105
+ <path d="M24,6 Q24,6 36,26 A14,14 0 1,1 12,26 Q24,6 24,6Z" fill="${PALETTE.accent}" opacity="0.8"/>
106
+ </svg>`;
107
+ return sharp(Buffer.from(svg)).png().toBuffer();
108
+ }
109
+
110
+ async function iconLink(size: number): Promise<Buffer> {
111
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
112
+ <path d="M18,30 L30,18" stroke="${PALETTE.purple}" stroke-width="4" stroke-linecap="round"/>
113
+ <path d="M22,34 L14,34 A8,8 0 0,1 14,18 L20,18" stroke="${PALETTE.purple}" stroke-width="4" fill="none" stroke-linecap="round"/>
114
+ <path d="M26,14 L34,14 A8,8 0 0,1 34,30 L28,30" stroke="${PALETTE.purple}" stroke-width="4" fill="none" stroke-linecap="round"/>
115
+ </svg>`;
116
+ return sharp(Buffer.from(svg)).png().toBuffer();
117
+ }
118
+
119
+ async function iconTerminal(size: number): Promise<Buffer> {
120
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
121
+ <rect x="4" y="8" width="40" height="32" rx="4" fill="#0D1117" stroke="${PALETTE.accent}" stroke-width="2"/>
122
+ <path d="M12,20 L20,26 L12,32" stroke="#51CF66" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
123
+ <line x1="24" y1="32" x2="36" y2="32" stroke="${PALETTE.muted}" stroke-width="3" stroke-linecap="round"/>
124
+ </svg>`;
125
+ return sharp(Buffer.from(svg)).png().toBuffer();
126
+ }
127
+
128
+ async function iconBot(size: number): Promise<Buffer> {
129
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
130
+ <rect x="10" y="16" width="28" height="24" rx="6" fill="${PALETTE.accent}"/>
131
+ <circle cx="24" cy="12" r="4" fill="${PALETTE.accent}"/>
132
+ <line x1="24" y1="8" x2="24" y2="4" stroke="${PALETTE.accent}" stroke-width="3" stroke-linecap="round"/>
133
+ <circle cx="18" cy="26" r="3" fill="white"/>
134
+ <circle cx="30" cy="26" r="3" fill="white"/>
135
+ <rect x="18" y="33" width="12" height="3" rx="1.5" fill="white"/>
136
+ </svg>`;
137
+ return sharp(Buffer.from(svg)).png().toBuffer();
138
+ }
139
+
140
+ async function iconStar(size: number): Promise<Buffer> {
141
+ const svg = `<svg width="${size}" height="${size}" viewBox="0 0 48 48">
142
+ <polygon points="24,4 29,18 44,18 32,28 36,42 24,34 12,42 16,28 4,18 19,18"
143
+ fill="${PALETTE.highlight}" stroke="${PALETTE.highlight}" stroke-width="1"/>
144
+ </svg>`;
145
+ return sharp(Buffer.from(svg)).png().toBuffer();
146
+ }
147
+
85
148
  // ═══════════════════════════════════════════════════════════════════════════════
86
149
  // SLIDE 1: Cover
87
150
  // ═══════════════════════════════════════════════════════════════════════════════
88
151
  async function slide1_cover(): Promise<Buffer> {
89
152
  let bg = await canvas(PALETTE.bg);
90
153
 
91
- // Accent gradient bar at top
92
154
  const topBar = await gradientBanner(SIZE, 8);
93
- bg = await sharp(bg).composite([{ input: topBar, top: 0, left: 0 }]).png().toBuffer();
155
+ const circle1 = await svgCircle(200, PALETTE.accent + '15');
156
+ const circle2 = await svgCircle(140, PALETTE.highlight + '10');
157
+ const card = await roundedRect(900, 500, PALETTE.surface, 32, 0.9);
158
+ const arrow = await iconArrow(64);
94
159
 
95
- // Decorative circles
96
- const circle1 = await circle(200, PALETTE.accent + '15');
97
- const circle2 = await circle(140, PALETTE.highlight + '10');
98
160
  bg = await sharp(bg).composite([
161
+ { input: topBar, top: 0, left: 0 },
99
162
  { input: circle1, top: -60, left: -60 },
100
163
  { input: circle2, top: 800, left: 880 },
164
+ { input: card, top: 260, left: 90 },
165
+ { input: arrow, top: 170, left: SIZE / 2 - 32 },
101
166
  ]).png().toBuffer();
102
167
 
103
- // Surface card
104
- const card = await roundedRect(900, 500, PALETTE.surface, 32, 0.9);
105
- bg = await sharp(bg).composite([{ input: card, top: 260, left: 90 }]).png().toBuffer();
106
-
107
- // Main title text
108
168
  const result = await addText(bg, { layers: [
109
- {
110
- text: '🚀',
111
- x: SIZE / 2, y: 180,
112
- fontSize: 80,
113
- color: PALETTE.text,
114
- anchor: 'top-center',
115
- },
116
- {
117
- text: 'Image Edit Tools',
118
- x: SIZE / 2, y: 340,
119
- fontSize: 56,
120
- color: PALETTE.text,
121
- anchor: 'top-center',
122
- },
123
- {
124
- text: 'AI 에이전트를 위한',
125
- x: SIZE / 2, y: 430,
126
- fontSize: 36,
127
- color: PALETTE.highlight,
128
- anchor: 'top-center',
129
- },
130
- {
131
- text: '이미지 편집 SDK',
132
- x: SIZE / 2, y: 480,
133
- fontSize: 36,
134
- color: PALETTE.highlight,
135
- anchor: 'top-center',
136
- },
137
- {
138
- text: 'TypeScript · Sharp · MCP',
139
- x: SIZE / 2, y: 580,
140
- fontSize: 28,
141
- color: PALETTE.muted,
142
- anchor: 'top-center',
143
- },
144
- {
145
- text: '← 스와이프하여 알아보기',
146
- x: SIZE / 2, y: 950,
147
- fontSize: 24,
148
- color: PALETTE.muted,
149
- anchor: 'top-center',
150
- },
169
+ { text: 'Image Edit Tools', x: SIZE / 2, y: 340, fontSize: 56, color: PALETTE.text, anchor: 'top-center' },
170
+ { text: 'AI Agent Image Editing SDK', x: SIZE / 2, y: 420, fontSize: 32, color: PALETTE.highlight, anchor: 'top-center' },
171
+ { text: 'TypeScript / Sharp / MCP', x: SIZE / 2, y: 520, fontSize: 28, color: PALETTE.muted, anchor: 'top-center' },
172
+ { text: 'Swipe to learn more', x: SIZE / 2, y: 950, fontSize: 24, color: PALETTE.muted, anchor: 'top-center' },
151
173
  ]});
152
174
  if (!result.ok) throw new Error(result.error);
153
175
  return result.data;
@@ -159,21 +181,21 @@ async function slide1_cover(): Promise<Buffer> {
159
181
  async function slide2_features(): Promise<Buffer> {
160
182
  let bg = await canvas(PALETTE.bg);
161
183
 
162
- // Header stripe
163
184
  const stripe = await gradientBanner(SIZE, 120);
164
185
  bg = await sharp(bg).composite([{ input: stripe, top: 0, left: 0 }]).png().toBuffer();
165
186
 
166
- // Feature cards
167
187
  const features = [
168
- { icon: 'A', title: 'Crop + Resize', desc: '절대/비율/종횡비 크롭' },
169
- { icon: 'B', title: 'Adjust + Filter', desc: '밝기, 대비, 채도, 온도' },
170
- { icon: 'C', title: 'Watermark', desc: '텍스트/이미지 워터마크' },
171
- { icon: 'D', title: 'Pipeline', desc: '체인 파이프라인 처리' },
188
+ { icon: iconCrop, title: 'Crop + Resize', desc: 'Absolute, ratio, aspect crop' },
189
+ { icon: iconPalette, title: 'Adjust + Filter', desc: 'Brightness, contrast, hue' },
190
+ { icon: iconDrop, title: 'Watermark', desc: 'Text or image watermark' },
191
+ { icon: iconLink, title: 'Pipeline', desc: 'Chain operations together' },
172
192
  ];
173
193
 
174
194
  const cardW = 420, cardH = 180, gap = 40;
175
195
  const startY = 180;
196
+ const iconSize = 40;
176
197
 
198
+ // Create feature cards and composite icons
177
199
  for (let i = 0; i < features.length; i++) {
178
200
  const col = i % 2;
179
201
  const row = Math.floor(i / 2);
@@ -181,12 +203,16 @@ async function slide2_features(): Promise<Buffer> {
181
203
  const y = startY + row * (cardH + gap);
182
204
 
183
205
  const card = await roundedRect(cardW, cardH, PALETTE.surface, 20);
184
- bg = await sharp(bg).composite([{ input: card, top: y, left: x }]).png().toBuffer();
206
+ const icon = await features[i].icon(iconSize);
207
+ bg = await sharp(bg).composite([
208
+ { input: card, top: y, left: x },
209
+ { input: icon, top: y + 25, left: x + 25 },
210
+ ]).png().toBuffer();
185
211
  }
186
212
 
187
- // Add text for header and each feature
213
+ // Text layers (no emoji)
188
214
  const textLayers = [
189
- { text: '주요 기능', x: SIZE / 2, y: 50, fontSize: 40, color: '#FFFFFF', anchor: 'top-center' as const },
215
+ { text: 'Key Features', x: SIZE / 2, y: 40, fontSize: 40, color: '#FFFFFF', anchor: 'top-center' as const },
190
216
  ];
191
217
 
192
218
  for (let i = 0; i < features.length; i++) {
@@ -196,13 +222,11 @@ async function slide2_features(): Promise<Buffer> {
196
222
  const y = startY + row * (cardH + gap);
197
223
 
198
224
  textLayers.push(
199
- { text: features[i].icon, x: x + 30, y: y + 30, fontSize: 40, color: PALETTE.text, anchor: 'top-left' as const },
200
- { text: features[i].title, x: x + 90, y: y + 35, fontSize: 28, color: PALETTE.text, anchor: 'top-left' as const },
201
- { text: features[i].desc, x: x + 30, y: y + 100, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' as const },
225
+ { text: features[i].title, x: x + 80, y: y + 35, fontSize: 28, color: PALETTE.text, anchor: 'top-left' as const },
226
+ { text: features[i].desc, x: x + 25, y: y + 110, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' as const },
202
227
  );
203
228
  }
204
229
 
205
- // Bottom slide counter
206
230
  textLayers.push(
207
231
  { text: '2 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' as const },
208
232
  );
@@ -218,9 +242,12 @@ async function slide2_features(): Promise<Buffer> {
218
242
  async function slide3_code(): Promise<Buffer> {
219
243
  let bg = await canvas(PALETTE.bg);
220
244
 
221
- // Code block background
222
245
  const codeCard = await roundedRect(920, 500, '#0D1117', 20);
223
- bg = await sharp(bg).composite([{ input: codeCard, top: 250, left: 80 }]).png().toBuffer();
246
+ const termIcon = await iconTerminal(48);
247
+ bg = await sharp(bg).composite([
248
+ { input: codeCard, top: 250, left: 80 },
249
+ { input: termIcon, top: 95, left: SIZE / 2 - 24 },
250
+ ]).png().toBuffer();
224
251
 
225
252
  const codeLines = [
226
253
  'import resize, addText from',
@@ -237,19 +264,9 @@ async function slide3_code(): Promise<Buffer> {
237
264
  ')',
238
265
  ];
239
266
 
240
- const codeLayers = [
241
- {
242
- text: '💻 코드 예시',
243
- x: SIZE / 2, y: 100,
244
- fontSize: 44, color: PALETTE.text,
245
- anchor: 'top-center' as const,
246
- },
247
- {
248
- text: '단 몇 줄로 이미지 편집 완료',
249
- x: SIZE / 2, y: 170,
250
- fontSize: 26, color: PALETTE.muted,
251
- anchor: 'top-center' as const,
252
- },
267
+ const codeLayers: Parameters<typeof addText>[1]['layers'] = [
268
+ { text: 'Code Example', x: SIZE / 2, y: 160, fontSize: 40, color: PALETTE.text, anchor: 'top-center' },
269
+ { text: 'Edit images in just a few lines', x: SIZE / 2, y: 215, fontSize: 24, color: PALETTE.muted, anchor: 'top-center' },
253
270
  ];
254
271
 
255
272
  codeLines.forEach((line, i) => {
@@ -260,19 +277,14 @@ async function slide3_code(): Promise<Buffer> {
260
277
  if (line.includes('width') || line.includes('height') || line.includes('layers')) color = '#FFA657';
261
278
 
262
279
  codeLayers.push({
263
- text: line,
264
- x: 120,
265
- y: 285 + i * 38,
266
- fontSize: 24,
267
- color,
268
- anchor: 'top-left' as const,
280
+ text: line, x: 120, y: 285 + i * 38,
281
+ fontSize: 24, color, anchor: 'top-left',
269
282
  });
270
283
  });
271
284
 
272
- codeLayers.push({
273
- text: '3 / 5', x: SIZE / 2, y: 980,
274
- fontSize: 20, color: PALETTE.muted, anchor: 'top-center' as const,
275
- });
285
+ codeLayers.push(
286
+ { text: '3 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' },
287
+ );
276
288
 
277
289
  const res = await addText(bg, { layers: codeLayers });
278
290
  if (!res.ok) throw new Error(res.error);
@@ -285,13 +297,12 @@ async function slide3_code(): Promise<Buffer> {
285
297
  async function slide4_mcp(): Promise<Buffer> {
286
298
  let bg = await canvas(PALETTE.bg);
287
299
 
288
- // Badges
289
300
  const badge1 = await numberBadge(1);
290
301
  const badge2 = await numberBadge(2);
291
302
  const badge3 = await numberBadge(3);
292
-
293
- // Step cards
294
303
  const stepCard = await roundedRect(820, 140, PALETTE.surface, 16);
304
+ const botIcon = await iconBot(56);
305
+
295
306
  bg = await sharp(bg).composite([
296
307
  { input: stepCard, top: 280, left: 130 },
297
308
  { input: stepCard, top: 480, left: 130 },
@@ -299,22 +310,23 @@ async function slide4_mcp(): Promise<Buffer> {
299
310
  { input: badge1, top: 310, left: 60 },
300
311
  { input: badge2, top: 510, left: 60 },
301
312
  { input: badge3, top: 710, left: 60 },
313
+ { input: botIcon, top: 65, left: SIZE / 2 - 28 },
302
314
  ]).png().toBuffer();
303
315
 
304
316
  const res = await addText(bg, { layers: [
305
- { text: '🤖 MCP 연동', x: SIZE / 2, y: 80, fontSize: 44, color: PALETTE.text, anchor: 'top-center' as const },
306
- { text: 'AI 에이전트에서 바로 사용', x: SIZE / 2, y: 150, fontSize: 26, color: PALETTE.muted, anchor: 'top-center' as const },
317
+ { text: 'MCP Integration', x: SIZE / 2, y: 140, fontSize: 44, color: PALETTE.text, anchor: 'top-center' },
318
+ { text: 'Use directly from AI agents', x: SIZE / 2, y: 200, fontSize: 26, color: PALETTE.muted, anchor: 'top-center' },
307
319
 
308
- { text: 'npm install', x: 170, y: 310, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' as const },
309
- { text: 'image-edit-tools 설치', x: 170, y: 355, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' as const },
320
+ { text: 'npm install', x: 170, y: 310, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' },
321
+ { text: 'Install image-edit-tools', x: 170, y: 355, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' },
310
322
 
311
- { text: 'MCP 설정 추가', x: 170, y: 510, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' as const },
312
- { text: 'claude_desktop_config.json 수정', x: 170, y: 555, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' as const },
323
+ { text: 'Add MCP config', x: 170, y: 510, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' },
324
+ { text: 'Edit claude_desktop_config.json', x: 170, y: 555, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' },
313
325
 
314
- { text: 'AI에게 요청', x: 170, y: 710, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' as const },
315
- { text: ' 사진 1080x1080으로 크롭해줘', x: 170, y: 755, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' as const },
326
+ { text: 'Ask the AI', x: 170, y: 710, fontSize: 26, color: PALETTE.highlight, anchor: 'top-left' },
327
+ { text: 'Crop this photo to 1080x1080', x: 170, y: 755, fontSize: 22, color: PALETTE.muted, anchor: 'top-left' },
316
328
 
317
- { text: '4 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' as const },
329
+ { text: '4 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' },
318
330
  ]});
319
331
  if (!res.ok) throw new Error(res.error);
320
332
  return res.data;
@@ -326,25 +338,22 @@ async function slide4_mcp(): Promise<Buffer> {
326
338
  async function slide5_cta(): Promise<Buffer> {
327
339
  let bg = await canvas(PALETTE.bg);
328
340
 
329
- // Large decorative gradient circle
330
- const bigCircle = await circle(300, PALETTE.accent + '20');
331
- bg = await sharp(bg).composite([
332
- { input: bigCircle, top: SIZE / 2 - 300, left: SIZE / 2 - 300 },
333
- ]).png().toBuffer();
334
-
335
- // CTA button shape
341
+ const bigCircle = await svgCircle(300, PALETTE.accent + '20');
336
342
  const ctaBtn = await roundedRect(500, 80, PALETTE.accent, 40);
343
+ const star = await iconStar(80);
344
+
337
345
  bg = await sharp(bg).composite([
346
+ { input: bigCircle, top: SIZE / 2 - 300, left: SIZE / 2 - 300 },
347
+ { input: star, top: 220, left: SIZE / 2 - 40 },
338
348
  { input: ctaBtn, top: 650, left: SIZE / 2 - 250 },
339
349
  ]).png().toBuffer();
340
350
 
341
351
  const res = await addText(bg, { layers: [
342
- { text: '', x: SIZE / 2, y: 250, fontSize: 100, color: PALETTE.text, anchor: 'top-center' as const },
343
- { text: '지금 시작하세요', x: SIZE / 2, y: 400, fontSize: 48, color: PALETTE.text, anchor: 'top-center' as const },
344
- { text: 'npm install image-edit-tools', x: SIZE / 2, y: 500, fontSize: 30, color: PALETTE.highlight, anchor: 'top-center' as const },
345
- { text: '설치하기', x: SIZE / 2, y: 670, fontSize: 28, color: '#FFFFFF', anchor: 'top-center' as const },
346
- { text: 'github.com/swimmingkiim/image-edit-tools', x: SIZE / 2, y: 820, fontSize: 22, color: PALETTE.muted, anchor: 'top-center' as const },
347
- { text: '5 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' as const },
352
+ { text: 'Get Started', x: SIZE / 2, y: 380, fontSize: 48, color: PALETTE.text, anchor: 'top-center' },
353
+ { text: 'npm install image-edit-tools', x: SIZE / 2, y: 480, fontSize: 30, color: PALETTE.highlight, anchor: 'top-center' },
354
+ { text: 'Install Now', x: SIZE / 2, y: 670, fontSize: 28, color: '#FFFFFF', anchor: 'top-center' },
355
+ { text: 'github.com/swimmingkiim/image-edit-tools', x: SIZE / 2, y: 820, fontSize: 22, color: PALETTE.muted, anchor: 'top-center' },
356
+ { text: '5 / 5', x: SIZE / 2, y: 980, fontSize: 20, color: PALETTE.muted, anchor: 'top-center' },
348
357
  ]});
349
358
  if (!res.ok) throw new Error(res.error);
350
359
  return res.data;
@@ -354,7 +363,7 @@ async function slide5_cta(): Promise<Buffer> {
354
363
  // Main
355
364
  // ═══════════════════════════════════════════════════════════════════════════════
356
365
  async function main() {
357
- console.log('📸 Generating Instagram card news slides...\n');
366
+ console.log('Generating Instagram card news slides...\n');
358
367
 
359
368
  const slides = [
360
369
  { name: 'slide-1-cover', fn: slide1_cover },
@@ -368,10 +377,10 @@ async function main() {
368
377
  const buf = await slide.fn();
369
378
  const outPath = join(OUTPUT_DIR, `${slide.name}.png`);
370
379
  writeFileSync(outPath, buf);
371
- console.log(` ${slide.name}.png (${(buf.length / 1024).toFixed(1)} KB)`);
380
+ console.log(` OK ${slide.name}.png (${(buf.length / 1024).toFixed(1)} KB)`);
372
381
  }
373
382
 
374
- console.log(`\n🎉 Done! ${slides.length} slides saved to examples/output/`);
383
+ console.log(`\nDone! ${slides.length} slides saved to examples/output/`);
375
384
  }
376
385
 
377
386
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-edit-tools",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Deterministic image editing SDK for AI agents. Ships with MCP tools.",
5
5
  "author": "swimmingkiim",
6
6
  "homepage": "https://github.com/swimmingkiim/image-edit-tools#readme",
@@ -33,23 +33,29 @@ function escapeXml(text: string): string {
33
33
  .replace(/'/g, '&apos;');
34
34
  }
35
35
 
36
- function getAnchorProps(anchor: TextAnchor = 'top-left'): { textAnchor: string, dominantBaseline: 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
- let dominantBaseline = 'hanging'; // top
42
- if (yAlign === 'bottom') dominantBaseline = 'auto'; // bottom is harder, usually means using y directly on baseline, but we can do mathematical offset. We'll rely on SVG baselines.
43
- else if (yAlign === 'middle' || yAlign === 'center') dominantBaseline = 'middle';
44
- else if (yAlign === 'auto') dominantBaseline = 'auto';
45
- // Sharp's librsvg supports dominant-baseline: text-before-edge (top), middle, alphabetic (bottom)
46
- const baselineMap: Record<string, string> = { top: 'text-before-edge', middle: 'middle', bottom: 'alphabetic', center: 'middle' };
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, dominantBaseline: baselineMap[yAlign] || 'text-before-edge' };
58
+ return { textAnchor, yOffset };
53
59
  }
54
60
 
55
61
  export async function addText(input: ImageInput, options: { layers: TextLayer[] }): Promise<ImageResult> {
@@ -66,6 +72,8 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
66
72
  let defs = '';
67
73
  let svgBody = '';
68
74
  let fontImports = new Set<string>();
75
+ const warnings: string[] = [];
76
+ let contentBottom = 0;
69
77
 
70
78
  for (let i = 0; i < options.layers.length; i++) {
71
79
  const layer = options.layers[i];
@@ -80,14 +88,16 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
80
88
  const totalHeight = lines.length * fontSize * lineHeight;
81
89
  const approxMaxWidth = Math.max(...lines.map(l => l.length * fontSize * 0.6));
82
90
 
83
- const { textAnchor, dominantBaseline } = getAnchorProps(layer.anchor);
91
+ const { textAnchor, yOffset } = getAnchorProps(layer.anchor, fontSize);
92
+ const renderY = layer.y + yOffset;
84
93
 
85
94
  let align = textAnchor;
86
95
  if (layer.align) {
87
96
  align = layer.align === 'left' ? 'start' : layer.align === 'right' ? 'end' : 'middle';
88
97
  }
89
98
 
90
- const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: ${dominantBaseline};`;
99
+ // Always use dominant-baseline: auto (alphabetic) the only value librsvg reliably supports
100
+ const style = `font-family: ${fontFamily}; font-size: ${fontSize}px; fill: ${color}; opacity: ${opacity}; text-anchor: ${align}; dominant-baseline: auto;`;
91
101
 
92
102
  let layerSvg = '';
93
103
 
@@ -97,6 +107,7 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
97
107
  const bgOpacity = bg.opacity ?? 1.0;
98
108
  const radius = bg.borderRadius ?? 0;
99
109
 
110
+ // Background rect is positioned relative to the *intended* y (layer.y), not renderY
100
111
  let rectX = layer.x - pad;
101
112
  let rectY = layer.y - pad;
102
113
 
@@ -106,16 +117,19 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
106
117
  rectX = layer.x - approxMaxWidth - pad;
107
118
  }
108
119
 
109
- if (dominantBaseline === 'middle') {
120
+ // Adjust for anchor vertical alignment
121
+ const parts = (layer.anchor ?? 'top-left').split('-');
122
+ const vAlign = parts.length === 2 ? parts[0] : parts[0] === 'center' ? 'middle' : parts[0];
123
+ if (vAlign === 'middle' || vAlign === 'center') {
110
124
  rectY = layer.y - (totalHeight / 2) - pad;
111
- } else if (dominantBaseline === 'alphabetic') { // bottom
125
+ } else if (vAlign === 'bottom') {
112
126
  rectY = layer.y - totalHeight - pad + fontSize;
113
127
  }
114
128
 
115
129
  layerSvg += `<rect x="${rectX}" y="${rectY}" width="${approxMaxWidth + pad * 2}" height="${totalHeight + pad * 2}" fill="${bg.color}" opacity="${bgOpacity}" rx="${radius}" ry="${radius}" />`;
116
130
  }
117
131
 
118
- layerSvg += `<text x="${layer.x}" y="${layer.y}" style="${style}">`;
132
+ layerSvg += `<text x="${layer.x}" y="${renderY}" style="${style}">`;
119
133
  lines.forEach((line, idx) => {
120
134
  let dy = idx === 0 ? 0 : fontSize * lineHeight;
121
135
  layerSvg += `<tspan x="${layer.x}" dy="${dy}">${escapeXml(line)}</tspan>`;
@@ -123,6 +137,27 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
123
137
  layerSvg += `</text>`;
124
138
 
125
139
  svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
140
+
141
+ // Compute bounding box for overflow detection (using intended y, not renderY)
142
+ let boxX = layer.x;
143
+ let boxY = layer.y;
144
+ if (textAnchor === 'middle') boxX -= approxMaxWidth / 2;
145
+ else if (textAnchor === 'end') boxX -= approxMaxWidth;
146
+
147
+ const anchorParts = (layer.anchor ?? 'top-left').split('-');
148
+ const vAlignBox = anchorParts.length === 2 ? anchorParts[0] : anchorParts[0] === 'center' ? 'middle' : anchorParts[0];
149
+ if (vAlignBox === 'middle' || vAlignBox === 'center') boxY -= totalHeight / 2;
150
+ else if (vAlignBox === 'bottom') boxY -= totalHeight - fontSize;
151
+
152
+ const boxBottom = boxY + totalHeight;
153
+ const boxRight = boxX + approxMaxWidth;
154
+ if (boxBottom > contentBottom) contentBottom = boxBottom;
155
+
156
+ if (boxX < 0 || boxY < 0 || boxRight > width || boxBottom > height) {
157
+ warnings.push(
158
+ `Text layer ${i} ("${layer.text.slice(0, 20)}...") extends beyond canvas bounds.`
159
+ );
160
+ }
126
161
  }
127
162
 
128
163
  const fontStyle = fontImports.size > 0 ? `<style>${Array.from(fontImports).join('\n')}</style>` : '';
@@ -137,7 +172,9 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
137
172
  .composite([{ input: Buffer.from(svgString), blend: 'over' }])
138
173
  .toBuffer();
139
174
 
140
- return ok(output);
175
+ const result = ok(output, warnings);
176
+ (result as any).bounds = { contentBottom: Math.round(contentBottom) };
177
+ return result;
141
178
  } catch (e: any) {
142
179
  const msg = e.message || '';
143
180
  if (msg.includes('HTTP')) return err(msg, ErrorCode.FETCH_FAILED);
@@ -38,6 +38,28 @@ export async function composite(input: ImageInput, options: { layers: CompositeL
38
38
 
39
39
  const overlays = await Promise.all(options.layers.map(loadLayer));
40
40
 
41
+ // Detect potentially problematic opaque layers
42
+ const warnings: string[] = [];
43
+ const baseMeta = await sharp(buffer).metadata();
44
+ const canvasArea = (baseMeta.width ?? 1) * (baseMeta.height ?? 1);
45
+
46
+ for (let i = 0; i < options.layers.length; i++) {
47
+ const layer = options.layers[i];
48
+ const layerOpacity = layer.opacity ?? 1.0;
49
+ if (layerOpacity >= 0.9) {
50
+ try {
51
+ const layerBuf = await loadImage(layer.image);
52
+ const layerMeta = await sharp(layerBuf).metadata();
53
+ const layerArea = (layerMeta.width ?? 0) * (layerMeta.height ?? 0);
54
+ if (layerArea / canvasArea > 0.25) {
55
+ warnings.push(
56
+ `Layer ${i} is opaque (opacity=${layerOpacity}) and covers ${Math.round(layerArea / canvasArea * 100)}% of the canvas. It may hide content underneath.`
57
+ );
58
+ }
59
+ } catch { /* skip analysis for unreadable layers */ }
60
+ }
61
+ }
62
+
41
63
  let output = buffer;
42
64
  if (overlays.length > 0) {
43
65
  output = await sharp(buffer)
@@ -45,7 +67,7 @@ export async function composite(input: ImageInput, options: { layers: CompositeL
45
67
  .toBuffer();
46
68
  }
47
69
 
48
- return ok(output);
70
+ return ok(output, warnings);
49
71
  } catch (e: any) {
50
72
  const msg = e.message || '';
51
73
  if (msg.includes('HTTP')) return err(msg, ErrorCode.FETCH_FAILED);
package/src/types.ts CHANGED
@@ -11,7 +11,7 @@ export type ImageInput = Buffer | string;
11
11
 
12
12
  // ─── Result ───────────────────────────────────────────────────────────────────
13
13
 
14
- export type Ok<T> = { ok: true; data: T };
14
+ export type Ok<T> = { ok: true; data: T; warnings?: string[] };
15
15
  export type Err = { ok: false; error: string; code: ErrorCode };
16
16
  export type Result<T> = Ok<T> | Err;
17
17
  export type ImageResult = Result<Buffer>;
@@ -1,4 +1,8 @@
1
1
  import { Ok, Err, ErrorCode } from '../types.js'
2
2
 
3
- export const ok = <T>(data: T): Ok<T> => ({ ok: true, data })
3
+ export const ok = <T>(data: T, warnings?: string[]): Ok<T> => {
4
+ const result: Ok<T> = { ok: true, data };
5
+ if (warnings && warnings.length > 0) result.warnings = warnings;
6
+ return result;
7
+ }
4
8
  export const err = (error: string, code: ErrorCode): Err => ({ ok: false, error, code })
@@ -53,4 +53,25 @@ describe('addText', () => {
53
53
  if (result.ok) return
54
54
  expect(result.code).toBe('INVALID_INPUT')
55
55
  })
56
+
57
+ it('warns when text extends beyond canvas bounds', async () => {
58
+ const result = await addText(sampleJpeg, {
59
+ layers: [{ text: 'Way out of bounds', x: 9999, y: 9999, fontSize: 48 }]
60
+ })
61
+ expect(result.ok).toBe(true)
62
+ if (!result.ok) return
63
+ expect(result.warnings).toBeDefined()
64
+ expect(result.warnings!.length).toBeGreaterThan(0)
65
+ expect(result.warnings![0]).toContain('beyond canvas bounds')
66
+ })
67
+
68
+ it('includes bounds.contentBottom in result', async () => {
69
+ const result = await addText(sampleJpeg, {
70
+ layers: [{ text: 'Hello', x: 50, y: 50, fontSize: 24 }]
71
+ })
72
+ expect(result.ok).toBe(true)
73
+ if (!result.ok) return
74
+ expect((result as any).bounds).toBeDefined()
75
+ expect((result as any).bounds.contentBottom).toBeGreaterThan(50)
76
+ })
56
77
  })
@@ -55,4 +55,16 @@ describe('composite', () => {
55
55
  if (result.ok) return
56
56
  expect(result.code).toBe('INVALID_INPUT')
57
57
  })
58
+
59
+ it('warns when opaque layer covers large area', async () => {
60
+ // sampleJpeg is 400x300 = 120000 pixels, logoPng is 100x100 = 10000 pixels (8.3% — no warning)
61
+ const result = await composite(sampleJpeg, {
62
+ layers: [{ image: sampleJpeg, x: 0, y: 0 }]
63
+ })
64
+ expect(result.ok).toBe(true)
65
+ if (!result.ok) return
66
+ expect(result.warnings).toBeDefined()
67
+ expect(result.warnings!.length).toBeGreaterThan(0)
68
+ expect(result.warnings![0]).toContain('may hide content')
69
+ })
58
70
  })