image-edit-tools 1.0.3 → 1.0.4
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 +23 -3
- package/dist/ops/add-text.d.ts.map +1 -1
- package/dist/ops/add-text.js +23 -1
- package/dist/ops/add-text.js.map +1 -1
- package/dist/ops/composite.d.ts.map +1 -1
- package/dist/ops/composite.js +20 -1
- package/dist/ops/composite.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/result.d.ts +1 -1
- package/dist/utils/result.d.ts.map +1 -1
- package/dist/utils/result.js +6 -1
- package/dist/utils/result.js.map +1 -1
- package/docs/AGENTS.md +2 -0
- package/examples/instagram-card-news.ts +166 -157
- package/package.json +1 -1
- package/src/ops/add-text.ts +23 -1
- package/src/ops/composite.ts +23 -1
- package/src/types.ts +1 -1
- package/src/utils/result.ts +5 -1
- package/tests/unit/add-text.test.ts +21 -0
- package/tests/unit/composite.test.ts +12 -0
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,
|
|
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
|
-
|
|
27
|
-
//
|
|
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,
|
|
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,CAmHvG"}
|
package/dist/ops/add-text.js
CHANGED
|
@@ -64,6 +64,8 @@ export async function addText(input, options) {
|
|
|
64
64
|
let defs = '';
|
|
65
65
|
let svgBody = '';
|
|
66
66
|
let fontImports = new Set();
|
|
67
|
+
const warnings = [];
|
|
68
|
+
let contentBottom = 0;
|
|
67
69
|
for (let i = 0; i < options.layers.length; i++) {
|
|
68
70
|
const layer = options.layers[i];
|
|
69
71
|
const fontSize = layer.fontSize ?? 24;
|
|
@@ -111,6 +113,24 @@ export async function addText(input, options) {
|
|
|
111
113
|
});
|
|
112
114
|
layerSvg += `</text>`;
|
|
113
115
|
svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
|
|
116
|
+
// Compute bounding box for overflow detection
|
|
117
|
+
let boxX = layer.x;
|
|
118
|
+
let boxY = layer.y;
|
|
119
|
+
if (textAnchor === 'middle')
|
|
120
|
+
boxX -= approxMaxWidth / 2;
|
|
121
|
+
else if (textAnchor === 'end')
|
|
122
|
+
boxX -= approxMaxWidth;
|
|
123
|
+
if (dominantBaseline === 'middle')
|
|
124
|
+
boxY -= totalHeight / 2;
|
|
125
|
+
else if (dominantBaseline === 'alphabetic')
|
|
126
|
+
boxY -= totalHeight - fontSize;
|
|
127
|
+
const boxBottom = boxY + totalHeight;
|
|
128
|
+
const boxRight = boxX + approxMaxWidth;
|
|
129
|
+
if (boxBottom > contentBottom)
|
|
130
|
+
contentBottom = boxBottom;
|
|
131
|
+
if (boxX < 0 || boxY < 0 || boxRight > width || boxBottom > height) {
|
|
132
|
+
warnings.push(`Text layer ${i} ("${layer.text.slice(0, 20)}...") extends beyond canvas bounds.`);
|
|
133
|
+
}
|
|
114
134
|
}
|
|
115
135
|
const fontStyle = fontImports.size > 0 ? `<style>${Array.from(fontImports).join('\n')}</style>` : '';
|
|
116
136
|
const svgString = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -121,7 +141,9 @@ export async function addText(input, options) {
|
|
|
121
141
|
const output = await sharp(buffer)
|
|
122
142
|
.composite([{ input: Buffer.from(svgString), blend: 'over' }])
|
|
123
143
|
.toBuffer();
|
|
124
|
-
|
|
144
|
+
const result = ok(output, warnings);
|
|
145
|
+
result.bounds = { contentBottom: Math.round(contentBottom) };
|
|
146
|
+
return result;
|
|
125
147
|
}
|
|
126
148
|
catch (e) {
|
|
127
149
|
const msg = e.message || '';
|
package/dist/ops/add-text.js.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/ops/composite.js
CHANGED
|
@@ -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":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA2C,SAAS,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAiB,EAAE,OAAqC;IACtF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QAEtC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACtD,OAAO,GAAG,CAAC,uBAAuB,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;QAC/D,CAAC;QAED,MAAM,SAAS,GAAG,KAAK,EAAE,KAAqB,EAAiC,EAAE;YAC/E,IAAI,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAE5C,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;gBAC3E,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC;qBAC7B,WAAW,EAAE;qBACb,SAAS,CAAC;oBACT;wBACE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC;wBACpE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;wBACzC,IAAI,EAAE,IAAI;wBACV,KAAK,EAAE,SAAS;qBACjB;iBACF,CAAC;qBACD,QAAQ,EAAE,CAAC;YAChB,CAAC;YAED,OAAO;gBACL,KAAK,EAAE,QAAQ;gBACf,IAAI,EAAE,KAAK,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrD,GAAG,EAAE,KAAK,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACpD,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,MAAM;aAC7B,CAAC;QACJ,CAAC,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;QAElE,IAAI,MAAM,GAAG,MAAM,CAAC;QACpB,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;iBACzB,SAAS,CAAC,QAAQ,CAAC;iBACnB,QAAQ,EAAE,CAAC;QAChB,CAAC;QAED,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"composite.js","sourceRoot":"","sources":["../../src/ops/composite.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;AAEA;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAIzC,MAAM,MAAM,EAAE,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/result.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/utils/result.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export const ok = (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
|
package/dist/utils/result.js.map
CHANGED
|
@@ -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,
|
|
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:
|
|
5
|
-
* Output:
|
|
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',
|
|
27
|
-
accent: '#3B82F6',
|
|
28
|
-
surface: '#1E293B',
|
|
29
|
-
text: '#F8FAFC',
|
|
30
|
-
muted: '#94A3B8',
|
|
31
|
-
highlight:'#F59E0B',
|
|
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
|
-
// ──
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
77
|
-
const svg = `<svg width="${
|
|
78
|
-
<circle cx="${
|
|
79
|
-
<text x="${
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
169
|
-
{ icon:
|
|
170
|
-
{ icon:
|
|
171
|
-
{ icon:
|
|
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
|
-
|
|
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
|
-
//
|
|
213
|
+
// Text layers (no emoji)
|
|
188
214
|
const textLayers = [
|
|
189
|
-
{ text: '
|
|
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].
|
|
200
|
-
{ text: features[i].
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
306
|
-
{ text: '
|
|
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'
|
|
309
|
-
{ text: 'image-edit-tools
|
|
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
|
|
312
|
-
{ text: 'claude_desktop_config.json
|
|
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
|
|
315
|
-
{ text: '
|
|
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'
|
|
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
|
-
|
|
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: '
|
|
343
|
-
{ text: '
|
|
344
|
-
{ text: '
|
|
345
|
-
{ text: '
|
|
346
|
-
{ text: '
|
|
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('
|
|
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(`
|
|
380
|
+
console.log(` OK ${slide.name}.png (${(buf.length / 1024).toFixed(1)} KB)`);
|
|
372
381
|
}
|
|
373
382
|
|
|
374
|
-
console.log(`\
|
|
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
package/src/ops/add-text.ts
CHANGED
|
@@ -66,6 +66,8 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
66
66
|
let defs = '';
|
|
67
67
|
let svgBody = '';
|
|
68
68
|
let fontImports = new Set<string>();
|
|
69
|
+
const warnings: string[] = [];
|
|
70
|
+
let contentBottom = 0;
|
|
69
71
|
|
|
70
72
|
for (let i = 0; i < options.layers.length; i++) {
|
|
71
73
|
const layer = options.layers[i];
|
|
@@ -123,6 +125,24 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
123
125
|
layerSvg += `</text>`;
|
|
124
126
|
|
|
125
127
|
svgBody += `<g style="isolation: isolate">${layerSvg}</g>`;
|
|
128
|
+
|
|
129
|
+
// Compute bounding box for overflow detection
|
|
130
|
+
let boxX = layer.x;
|
|
131
|
+
let boxY = layer.y;
|
|
132
|
+
if (textAnchor === 'middle') boxX -= approxMaxWidth / 2;
|
|
133
|
+
else if (textAnchor === 'end') boxX -= approxMaxWidth;
|
|
134
|
+
if (dominantBaseline === 'middle') boxY -= totalHeight / 2;
|
|
135
|
+
else if (dominantBaseline === 'alphabetic') boxY -= totalHeight - fontSize;
|
|
136
|
+
|
|
137
|
+
const boxBottom = boxY + totalHeight;
|
|
138
|
+
const boxRight = boxX + approxMaxWidth;
|
|
139
|
+
if (boxBottom > contentBottom) contentBottom = boxBottom;
|
|
140
|
+
|
|
141
|
+
if (boxX < 0 || boxY < 0 || boxRight > width || boxBottom > height) {
|
|
142
|
+
warnings.push(
|
|
143
|
+
`Text layer ${i} ("${layer.text.slice(0, 20)}...") extends beyond canvas bounds.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
const fontStyle = fontImports.size > 0 ? `<style>${Array.from(fontImports).join('\n')}</style>` : '';
|
|
@@ -137,7 +157,9 @@ export async function addText(input: ImageInput, options: { layers: TextLayer[]
|
|
|
137
157
|
.composite([{ input: Buffer.from(svgString), blend: 'over' }])
|
|
138
158
|
.toBuffer();
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
const result = ok(output, warnings);
|
|
161
|
+
(result as any).bounds = { contentBottom: Math.round(contentBottom) };
|
|
162
|
+
return result;
|
|
141
163
|
} catch (e: any) {
|
|
142
164
|
const msg = e.message || '';
|
|
143
165
|
if (msg.includes('HTTP')) return err(msg, ErrorCode.FETCH_FAILED);
|
package/src/ops/composite.ts
CHANGED
|
@@ -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>;
|
package/src/utils/result.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Ok, Err, ErrorCode } from '../types.js'
|
|
2
2
|
|
|
3
|
-
export const ok = <T>(data: T): Ok<T> =>
|
|
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
|
})
|