@supericons/mcp 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/auth.js +69 -0
- package/converter.js +8 -0
- package/generated/mcp-output-locales.json +12436 -0
- package/generated/motion-lab-baseline.json +886 -0
- package/hosted-search-client.js +198 -0
- package/index.js +1240 -0
- package/material-export.js +174 -0
- package/mcp-output-localization.js +132 -0
- package/motion-lab-client.js +347 -0
- package/motion-lab.js +21 -0
- package/package.json +63 -0
- package/public/cjk-search-terms.json +63474 -0
- package/public/multilingual-search-aliases.json +4307 -0
- package/public/product-facts.json +25 -0
- package/recommend-icons.js +707 -0
- package/remote-server.js +465 -0
- package/runtime/cjk-search-core.js +82 -0
- package/runtime/converter-workflow.js +593 -0
- package/runtime/generated-search-intent-rules.js +1190 -0
- package/runtime/icon-semantic-aliases.js +330 -0
- package/runtime/icon-taxonomy-seed.js +461 -0
- package/runtime/public-metadata-sanitizer.js +171 -0
- package/runtime/search-intent-core.js +130 -0
- package/search.js +375 -0
- package/semantic-registry.js +212 -0
- package/server.json +27 -0
- package/telemetry.js +85 -0
- package/variant-support.js +236 -0
- package/workflow-access.js +65 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { ColorMode, Hierarchical, PathSimplifyMode, vectorize } from '@neplex/vectorizer';
|
|
3
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
4
|
+
import { sanitizeSvgExportMarkup } from './public-metadata-sanitizer.js';
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_IMAGE_MIME_TYPES = new Set(['image/png']);
|
|
7
|
+
const MAX_CONVERTER_INPUT_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
9
|
+
|
|
10
|
+
const TRACE_CLASS_GUIDANCE = {
|
|
11
|
+
'general-color': {
|
|
12
|
+
bestFor: 'full-color artwork and logos when you are unsure which color route to trust first',
|
|
13
|
+
avoidFor: 'tiny mono icons where geometric crispness matters more than broad color preservation',
|
|
14
|
+
},
|
|
15
|
+
'flat-logo-color': {
|
|
16
|
+
bestFor: 'logos with flat fills, separated regions, and no gradients',
|
|
17
|
+
avoidFor: 'photographic, textured, or gradient-heavy inputs',
|
|
18
|
+
},
|
|
19
|
+
'tile-icon-color': {
|
|
20
|
+
bestFor: 'small colored icons, badges, and tile-like UI artwork',
|
|
21
|
+
avoidFor: 'large free-form logos with broad curves or heavy internal detail',
|
|
22
|
+
},
|
|
23
|
+
'tiny-line-icon': {
|
|
24
|
+
bestFor: 'very small interface icons with fine line work',
|
|
25
|
+
avoidFor: 'multi-color artwork or large logo marks',
|
|
26
|
+
},
|
|
27
|
+
'single-color-mark': {
|
|
28
|
+
bestFor: 'single-color logos, marks, and wordmarks',
|
|
29
|
+
avoidFor: 'true black-and-white masks with transparency cutouts',
|
|
30
|
+
},
|
|
31
|
+
'mono-mask': {
|
|
32
|
+
bestFor: 'high-contrast black and white inputs, masks, and silhouette-style artwork',
|
|
33
|
+
avoidFor: 'soft grayscale artwork or colorful logos',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const QUALITY_MODE_GUIDANCE = {
|
|
38
|
+
exact: 'Keeps more detail and is the safer default when quality matters more than file size.',
|
|
39
|
+
compact: 'Simplifies paths and reduces file size at the cost of some detail.',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const COLOR_MODE_GUIDANCE = {
|
|
43
|
+
color: 'Use when color regions matter and you want the output to preserve more than one tone.',
|
|
44
|
+
mono: 'Use when the source should collapse to a single foreground color or mask-style output.',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const UI_MODE_GUIDANCE = {
|
|
48
|
+
logo: 'Bias toward logo-style artwork with broader curves and free-form shapes.',
|
|
49
|
+
icon: 'Bias toward icon-like geometry and smaller UI artwork.',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function parseBase64Payload(input = '') {
|
|
53
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
54
|
+
throw new Error('Input must be a non-empty base64 string or data URL.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const trimmed = input.trim();
|
|
58
|
+
const dataUrlMatch = /^data:([^;]+);base64,(.+)$/i.exec(trimmed);
|
|
59
|
+
if (dataUrlMatch) {
|
|
60
|
+
return {
|
|
61
|
+
mimeType: dataUrlMatch[1].toLowerCase(),
|
|
62
|
+
buffer: Buffer.from(dataUrlMatch[2], 'base64'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
mimeType: null,
|
|
68
|
+
buffer: Buffer.from(trimmed, 'base64'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveTraceClassForUiMode(traceClass = 'general-color', requestedColorMode = 'color', uiMode = 'logo') {
|
|
73
|
+
const iconClasses = new Set(['tiny-line-icon', 'tile-icon-color']);
|
|
74
|
+
const logoClasses = new Set(['flat-logo-color', 'general-color', 'single-color-mark', 'mono-mask']);
|
|
75
|
+
|
|
76
|
+
if (uiMode === 'icon') {
|
|
77
|
+
if (iconClasses.has(traceClass)) return traceClass;
|
|
78
|
+
return requestedColorMode === 'mono' ? 'tiny-line-icon' : 'tile-icon-color';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (logoClasses.has(traceClass)) return traceClass;
|
|
82
|
+
if (requestedColorMode === 'mono') return 'single-color-mark';
|
|
83
|
+
return 'flat-logo-color';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getTraceConfig(qualityMode = 'exact', requestedColorMode = 'color', traceClass = 'general-color', uiMode = 'logo') {
|
|
87
|
+
const mode = qualityMode === 'compact' ? 'compact' : 'exact';
|
|
88
|
+
const isBinary = requestedColorMode === 'mono';
|
|
89
|
+
const resolvedTraceClass = resolveTraceClassForUiMode(traceClass, requestedColorMode, uiMode);
|
|
90
|
+
|
|
91
|
+
const configByClass = {
|
|
92
|
+
'general-color': {
|
|
93
|
+
compact: {
|
|
94
|
+
colorMode: isBinary ? ColorMode.Binary : ColorMode.Color,
|
|
95
|
+
hierarchical: Hierarchical.Stacked,
|
|
96
|
+
filterSpeckle: isBinary ? 4 : 6,
|
|
97
|
+
colorPrecision: isBinary ? 8 : 6,
|
|
98
|
+
layerDifference: isBinary ? 12 : 8,
|
|
99
|
+
mode: PathSimplifyMode.Spline,
|
|
100
|
+
cornerThreshold: 55,
|
|
101
|
+
lengthThreshold: isBinary ? 5 : 6,
|
|
102
|
+
maxIterations: 2,
|
|
103
|
+
spliceThreshold: 50,
|
|
104
|
+
pathPrecision: 4,
|
|
105
|
+
},
|
|
106
|
+
exact: {
|
|
107
|
+
colorMode: isBinary ? ColorMode.Binary : ColorMode.Color,
|
|
108
|
+
hierarchical: Hierarchical.Stacked,
|
|
109
|
+
filterSpeckle: isBinary ? 3 : 4,
|
|
110
|
+
colorPrecision: isBinary ? 8 : 8,
|
|
111
|
+
layerDifference: isBinary ? 10 : 5,
|
|
112
|
+
mode: PathSimplifyMode.Spline,
|
|
113
|
+
cornerThreshold: 60,
|
|
114
|
+
lengthThreshold: isBinary ? 4 : 4,
|
|
115
|
+
maxIterations: 3,
|
|
116
|
+
spliceThreshold: 45,
|
|
117
|
+
pathPrecision: 5,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
'flat-logo-color': {
|
|
121
|
+
compact: {
|
|
122
|
+
colorMode: ColorMode.Color,
|
|
123
|
+
hierarchical: Hierarchical.Stacked,
|
|
124
|
+
filterSpeckle: 5,
|
|
125
|
+
colorPrecision: 7,
|
|
126
|
+
layerDifference: 7,
|
|
127
|
+
mode: PathSimplifyMode.Spline,
|
|
128
|
+
cornerThreshold: 56,
|
|
129
|
+
lengthThreshold: 5,
|
|
130
|
+
maxIterations: 2,
|
|
131
|
+
spliceThreshold: 48,
|
|
132
|
+
pathPrecision: 4,
|
|
133
|
+
},
|
|
134
|
+
exact: {
|
|
135
|
+
colorMode: ColorMode.Color,
|
|
136
|
+
hierarchical: Hierarchical.Stacked,
|
|
137
|
+
filterSpeckle: 3,
|
|
138
|
+
colorPrecision: 8,
|
|
139
|
+
layerDifference: 5,
|
|
140
|
+
mode: PathSimplifyMode.Spline,
|
|
141
|
+
cornerThreshold: 60,
|
|
142
|
+
lengthThreshold: 4,
|
|
143
|
+
maxIterations: 3,
|
|
144
|
+
spliceThreshold: 42,
|
|
145
|
+
pathPrecision: 5,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
'tile-icon-color': {
|
|
149
|
+
compact: {
|
|
150
|
+
colorMode: ColorMode.Color,
|
|
151
|
+
hierarchical: Hierarchical.Cutout,
|
|
152
|
+
filterSpeckle: 5,
|
|
153
|
+
colorPrecision: 7,
|
|
154
|
+
layerDifference: 8,
|
|
155
|
+
mode: PathSimplifyMode.Spline,
|
|
156
|
+
cornerThreshold: 54,
|
|
157
|
+
lengthThreshold: 5,
|
|
158
|
+
maxIterations: 2,
|
|
159
|
+
spliceThreshold: 48,
|
|
160
|
+
pathPrecision: 4,
|
|
161
|
+
},
|
|
162
|
+
exact: {
|
|
163
|
+
colorMode: ColorMode.Color,
|
|
164
|
+
hierarchical: Hierarchical.Cutout,
|
|
165
|
+
filterSpeckle: 3,
|
|
166
|
+
colorPrecision: 8,
|
|
167
|
+
layerDifference: 6,
|
|
168
|
+
mode: PathSimplifyMode.Spline,
|
|
169
|
+
cornerThreshold: 58,
|
|
170
|
+
lengthThreshold: 4,
|
|
171
|
+
maxIterations: 3,
|
|
172
|
+
spliceThreshold: 42,
|
|
173
|
+
pathPrecision: 5,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
'tiny-line-icon': {
|
|
177
|
+
compact: {
|
|
178
|
+
colorMode: ColorMode.Binary,
|
|
179
|
+
hierarchical: Hierarchical.Stacked,
|
|
180
|
+
filterSpeckle: 0,
|
|
181
|
+
colorPrecision: 8,
|
|
182
|
+
layerDifference: 12,
|
|
183
|
+
mode: PathSimplifyMode.Spline,
|
|
184
|
+
cornerThreshold: 52,
|
|
185
|
+
lengthThreshold: 2,
|
|
186
|
+
maxIterations: 2,
|
|
187
|
+
spliceThreshold: 16,
|
|
188
|
+
pathPrecision: 6,
|
|
189
|
+
},
|
|
190
|
+
exact: {
|
|
191
|
+
colorMode: ColorMode.Binary,
|
|
192
|
+
hierarchical: Hierarchical.Stacked,
|
|
193
|
+
filterSpeckle: 0,
|
|
194
|
+
colorPrecision: 8,
|
|
195
|
+
layerDifference: 10,
|
|
196
|
+
mode: PathSimplifyMode.Spline,
|
|
197
|
+
cornerThreshold: 46,
|
|
198
|
+
lengthThreshold: 1,
|
|
199
|
+
maxIterations: 3,
|
|
200
|
+
spliceThreshold: 12,
|
|
201
|
+
pathPrecision: 7,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
'single-color-mark': {
|
|
205
|
+
compact: {
|
|
206
|
+
colorMode: ColorMode.Binary,
|
|
207
|
+
hierarchical: Hierarchical.Stacked,
|
|
208
|
+
filterSpeckle: 1,
|
|
209
|
+
colorPrecision: 8,
|
|
210
|
+
layerDifference: 12,
|
|
211
|
+
mode: PathSimplifyMode.Spline,
|
|
212
|
+
cornerThreshold: 54,
|
|
213
|
+
lengthThreshold: 2,
|
|
214
|
+
maxIterations: 2,
|
|
215
|
+
spliceThreshold: 18,
|
|
216
|
+
pathPrecision: 6,
|
|
217
|
+
},
|
|
218
|
+
exact: {
|
|
219
|
+
colorMode: ColorMode.Binary,
|
|
220
|
+
hierarchical: Hierarchical.Stacked,
|
|
221
|
+
filterSpeckle: 0,
|
|
222
|
+
colorPrecision: 8,
|
|
223
|
+
layerDifference: 10,
|
|
224
|
+
mode: PathSimplifyMode.Spline,
|
|
225
|
+
cornerThreshold: 48,
|
|
226
|
+
lengthThreshold: 1,
|
|
227
|
+
maxIterations: 3,
|
|
228
|
+
spliceThreshold: 14,
|
|
229
|
+
pathPrecision: 7,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
'mono-mask': {
|
|
233
|
+
compact: {
|
|
234
|
+
colorMode: ColorMode.Binary,
|
|
235
|
+
hierarchical: Hierarchical.Stacked,
|
|
236
|
+
filterSpeckle: 1,
|
|
237
|
+
colorPrecision: 8,
|
|
238
|
+
layerDifference: 12,
|
|
239
|
+
mode: PathSimplifyMode.Spline,
|
|
240
|
+
cornerThreshold: 52,
|
|
241
|
+
lengthThreshold: 2,
|
|
242
|
+
maxIterations: 2,
|
|
243
|
+
spliceThreshold: 18,
|
|
244
|
+
pathPrecision: 6,
|
|
245
|
+
},
|
|
246
|
+
exact: {
|
|
247
|
+
colorMode: ColorMode.Binary,
|
|
248
|
+
hierarchical: Hierarchical.Stacked,
|
|
249
|
+
filterSpeckle: 0,
|
|
250
|
+
colorPrecision: 8,
|
|
251
|
+
layerDifference: 10,
|
|
252
|
+
mode: PathSimplifyMode.Spline,
|
|
253
|
+
cornerThreshold: 46,
|
|
254
|
+
lengthThreshold: 1,
|
|
255
|
+
maxIterations: 3,
|
|
256
|
+
spliceThreshold: 14,
|
|
257
|
+
pathPrecision: 7,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const traceConfig = configByClass[resolvedTraceClass] || configByClass['general-color'];
|
|
263
|
+
return {
|
|
264
|
+
resolvedMode: mode,
|
|
265
|
+
resolvedTraceClass,
|
|
266
|
+
config: traceConfig[mode] || traceConfig.exact,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function countMatches(text = '', pattern) {
|
|
271
|
+
const matches = text.match(pattern);
|
|
272
|
+
return matches ? matches.length : 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function extractViewBox(svg = '') {
|
|
276
|
+
const match = svg.match(/viewBox="([^"]+)"/i);
|
|
277
|
+
return match ? match[1] : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function validateInputBuffer(buffer, label = 'Input') {
|
|
281
|
+
if (!buffer?.length) {
|
|
282
|
+
throw new Error(`${label} payload is empty.`);
|
|
283
|
+
}
|
|
284
|
+
if (buffer.length > MAX_CONVERTER_INPUT_BYTES) {
|
|
285
|
+
throw new Error(`${label} exceeds the ${Math.round(MAX_CONVERTER_INPUT_BYTES / (1024 * 1024))}MB MCP limit.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeBackground(background = 'transparent') {
|
|
290
|
+
if (!background || background === 'transparent') return null;
|
|
291
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(background)) return background;
|
|
292
|
+
throw new Error('Background must be `transparent` or a hex color like `#ffffff`.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function readPngHeader(buffer) {
|
|
296
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 33) {
|
|
297
|
+
throw new Error('PNG input is too small to inspect.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (let index = 0; index < PNG_SIGNATURE.length; index += 1) {
|
|
301
|
+
if (buffer[index] !== PNG_SIGNATURE[index]) {
|
|
302
|
+
throw new Error('Input is not a valid PNG file.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const chunkType = buffer.toString('ascii', 12, 16);
|
|
307
|
+
if (chunkType !== 'IHDR') {
|
|
308
|
+
throw new Error('PNG header is invalid or missing IHDR.');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const colorType = buffer.readUInt8(25);
|
|
312
|
+
const colorTypeMap = {
|
|
313
|
+
0: { label: 'grayscale', hasAlpha: false, monochromeFriendly: true },
|
|
314
|
+
2: { label: 'rgb', hasAlpha: false, monochromeFriendly: false },
|
|
315
|
+
3: { label: 'indexed', hasAlpha: false, monochromeFriendly: false },
|
|
316
|
+
4: { label: 'grayscale-alpha', hasAlpha: true, monochromeFriendly: true },
|
|
317
|
+
6: { label: 'rgba', hasAlpha: true, monochromeFriendly: false },
|
|
318
|
+
};
|
|
319
|
+
const colorInfo = colorTypeMap[colorType] || {
|
|
320
|
+
label: `unknown-${colorType}`,
|
|
321
|
+
hasAlpha: false,
|
|
322
|
+
monochromeFriendly: false,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
width: buffer.readUInt32BE(16),
|
|
327
|
+
height: buffer.readUInt32BE(20),
|
|
328
|
+
bitDepth: buffer.readUInt8(24),
|
|
329
|
+
colorType,
|
|
330
|
+
colorModel: colorInfo.label,
|
|
331
|
+
hasAlpha: colorInfo.hasAlpha,
|
|
332
|
+
monochromeFriendly: colorInfo.monochromeFriendly,
|
|
333
|
+
compressionMethod: buffer.readUInt8(26),
|
|
334
|
+
filterMethod: buffer.readUInt8(27),
|
|
335
|
+
interlaceMethod: buffer.readUInt8(28),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildConverterInspection(header, sizeBytes) {
|
|
340
|
+
const { width, height, hasAlpha, monochromeFriendly, colorModel } = header;
|
|
341
|
+
const maxDimension = Math.max(width, height);
|
|
342
|
+
const minDimension = Math.min(width, height);
|
|
343
|
+
|
|
344
|
+
const isMicro = maxDimension <= 32;
|
|
345
|
+
const isSmall = maxDimension <= 64;
|
|
346
|
+
const isLarge = maxDimension >= 512;
|
|
347
|
+
const isSquareish = Math.abs(width - height) <= Math.max(8, Math.round(maxDimension * 0.1));
|
|
348
|
+
|
|
349
|
+
const risks = [];
|
|
350
|
+
const rationale = [];
|
|
351
|
+
|
|
352
|
+
let colorMode = monochromeFriendly ? 'mono' : 'color';
|
|
353
|
+
let qualityMode = 'exact';
|
|
354
|
+
let uiMode = isSmall ? 'icon' : 'logo';
|
|
355
|
+
let traceClass = 'general-color';
|
|
356
|
+
|
|
357
|
+
if (colorMode === 'mono') {
|
|
358
|
+
if (isSmall) {
|
|
359
|
+
traceClass = 'tiny-line-icon';
|
|
360
|
+
rationale.push('The PNG is structurally small, so the icon-biased mono trace class is the safest starting point.');
|
|
361
|
+
} else if (hasAlpha) {
|
|
362
|
+
traceClass = 'mono-mask';
|
|
363
|
+
rationale.push('The PNG carries alpha with a monochrome-friendly color model, which fits mask-style tracing better than flat logo tracing.');
|
|
364
|
+
} else {
|
|
365
|
+
traceClass = 'single-color-mark';
|
|
366
|
+
rationale.push('The PNG looks monochrome-friendly without transparency, which is a good fit for single-color marks and wordmarks.');
|
|
367
|
+
}
|
|
368
|
+
} else if (isSmall && isSquareish) {
|
|
369
|
+
traceClass = 'tile-icon-color';
|
|
370
|
+
uiMode = 'icon';
|
|
371
|
+
rationale.push('The PNG is small and icon-like, so a tile/icon color route is a safer first pass than a broad logo route.');
|
|
372
|
+
} else {
|
|
373
|
+
traceClass = 'general-color';
|
|
374
|
+
rationale.push('The PNG uses a color model that benefits from the most forgiving full-color starting route.');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isMicro) {
|
|
378
|
+
risks.push('Very small PNG inputs can lose detail quickly during tracing. Expect to compare one or two settings before committing.');
|
|
379
|
+
}
|
|
380
|
+
if (isLarge) {
|
|
381
|
+
risks.push('Large raster inputs often trace into heavier SVG output. Expect larger path counts and a bigger file size.');
|
|
382
|
+
}
|
|
383
|
+
if (!monochromeFriendly) {
|
|
384
|
+
risks.push('Color-rich PNGs are more sensitive to traceClass choice. Start with the recommended settings, then compare if the result feels too noisy or too simplified.');
|
|
385
|
+
}
|
|
386
|
+
if (colorModel === 'indexed') {
|
|
387
|
+
risks.push('Indexed-color PNGs can behave unpredictably when palette boundaries are rough or anti-aliased.');
|
|
388
|
+
}
|
|
389
|
+
if (hasAlpha && !monochromeFriendly) {
|
|
390
|
+
risks.push('Soft transparency edges can produce extra small shapes in the traced SVG.');
|
|
391
|
+
}
|
|
392
|
+
if (!risks.length) {
|
|
393
|
+
risks.push('No structural red flags were detected from the PNG header alone, but this inspection does not see semantic content inside the artwork.');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
confidence: 'medium',
|
|
398
|
+
scale: isMicro ? 'micro' : isSmall ? 'small' : isLarge ? 'large' : 'medium',
|
|
399
|
+
likelyFit: monochromeFriendly
|
|
400
|
+
? (isSmall ? 'small mono icon' : 'single-color logo or mark')
|
|
401
|
+
: (isSmall ? 'small colored icon' : 'logo or illustration'),
|
|
402
|
+
structuralNotes: [
|
|
403
|
+
`Header inspection sees a ${width}x${height} PNG using ${colorModel}${hasAlpha ? ' with alpha' : ''}.`,
|
|
404
|
+
'This preflight reads file structure, not semantic image content, so recommendations are strong starting points rather than perfect classifications.',
|
|
405
|
+
],
|
|
406
|
+
risks,
|
|
407
|
+
recommendedSettings: {
|
|
408
|
+
colorMode,
|
|
409
|
+
qualityMode,
|
|
410
|
+
traceClass,
|
|
411
|
+
uiMode,
|
|
412
|
+
},
|
|
413
|
+
rationale,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function getConverterMcpOptions() {
|
|
418
|
+
return {
|
|
419
|
+
proOnly: true,
|
|
420
|
+
limits: {
|
|
421
|
+
maxInputBytes: MAX_CONVERTER_INPUT_BYTES,
|
|
422
|
+
maxSvgToPngWidth: 2048,
|
|
423
|
+
minSvgToPngWidth: 16,
|
|
424
|
+
},
|
|
425
|
+
svgToPng: {
|
|
426
|
+
targetWidthRange: { min: 16, max: 2048, default: 512 },
|
|
427
|
+
backgrounds: ['transparent', '#ffffff', '#000000', 'custom-hex'],
|
|
428
|
+
outputs: ['png-base64', 'png-data-url'],
|
|
429
|
+
guidance: {
|
|
430
|
+
whenToUse: 'Use SVG-to-PNG when you already trust the SVG source and only need raster output at a target width.',
|
|
431
|
+
defaults: {
|
|
432
|
+
targetWidth: '512 is a safe general-purpose default for previews and product documentation.',
|
|
433
|
+
background: 'Keep `transparent` unless the PNG needs to sit on a forced background color.',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
pngToSvg: {
|
|
438
|
+
qualityModes: ['exact', 'compact'],
|
|
439
|
+
colorModes: ['color', 'mono'],
|
|
440
|
+
traceClasses: ['general-color', 'flat-logo-color', 'tile-icon-color', 'tiny-line-icon', 'single-color-mark', 'mono-mask'],
|
|
441
|
+
uiModes: ['logo', 'icon'],
|
|
442
|
+
outputs: ['svg-text'],
|
|
443
|
+
guidance: {
|
|
444
|
+
qualityModes: QUALITY_MODE_GUIDANCE,
|
|
445
|
+
colorModes: COLOR_MODE_GUIDANCE,
|
|
446
|
+
uiModes: UI_MODE_GUIDANCE,
|
|
447
|
+
traceClasses: TRACE_CLASS_GUIDANCE,
|
|
448
|
+
},
|
|
449
|
+
starterCombinations: [
|
|
450
|
+
{
|
|
451
|
+
label: 'Safe full-color default',
|
|
452
|
+
when: 'You have a logo or illustration and do not yet trust a narrower trace class.',
|
|
453
|
+
settings: { colorMode: 'color', qualityMode: 'exact', traceClass: 'general-color', uiMode: 'logo' },
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
label: 'Flat logo pass',
|
|
457
|
+
when: 'The PNG is a clean logo with solid fills and no gradients.',
|
|
458
|
+
settings: { colorMode: 'color', qualityMode: 'exact', traceClass: 'flat-logo-color', uiMode: 'logo' },
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
label: 'Tiny interface icon',
|
|
462
|
+
when: 'The PNG is a very small UI icon where crisp geometry matters more than color complexity.',
|
|
463
|
+
settings: { colorMode: 'mono', qualityMode: 'exact', traceClass: 'tiny-line-icon', uiMode: 'icon' },
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
label: 'Single-color wordmark or brand mark',
|
|
467
|
+
when: 'The PNG is a logo, wordmark, or mark that uses a single foreground color.',
|
|
468
|
+
settings: { colorMode: 'mono', qualityMode: 'exact', traceClass: 'single-color-mark', uiMode: 'logo' },
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
label: 'Small colored icon or badge',
|
|
472
|
+
when: 'The PNG is a compact color icon, badge, or tile-style UI asset.',
|
|
473
|
+
settings: { colorMode: 'color', qualityMode: 'exact', traceClass: 'tile-icon-color', uiMode: 'icon' },
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
label: 'High-contrast mask or silhouette',
|
|
477
|
+
when: 'The PNG is a black-and-white silhouette, stencil, or alpha-cutout mask.',
|
|
478
|
+
settings: { colorMode: 'mono', qualityMode: 'exact', traceClass: 'mono-mask', uiMode: 'logo' },
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
workflow: {
|
|
483
|
+
recommendedOrder: [
|
|
484
|
+
'inspect_converter_input',
|
|
485
|
+
'inspect_converter_options',
|
|
486
|
+
'convert_png_to_svg or convert_svg_to_png',
|
|
487
|
+
],
|
|
488
|
+
note: 'Inspect the input first when tracing PNG to SVG so the agent can justify its starting settings before converting.',
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function inspectConverterInput({ imageBase64, mimeType }) {
|
|
494
|
+
const { mimeType: parsedMimeType, buffer } = parseBase64Payload(imageBase64);
|
|
495
|
+
validateInputBuffer(buffer, 'PNG input');
|
|
496
|
+
const effectiveMimeType = (mimeType || parsedMimeType || 'image/png').toLowerCase();
|
|
497
|
+
|
|
498
|
+
if (!SUPPORTED_IMAGE_MIME_TYPES.has(effectiveMimeType)) {
|
|
499
|
+
throw new Error(`Unsupported image type "${effectiveMimeType}". Converter MCP currently accepts PNG only.`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const header = readPngHeader(buffer);
|
|
503
|
+
const assessment = buildConverterInspection(header, buffer.length);
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
format: 'png',
|
|
507
|
+
input: {
|
|
508
|
+
mimeType: effectiveMimeType,
|
|
509
|
+
sizeBytes: buffer.length,
|
|
510
|
+
width: header.width,
|
|
511
|
+
height: header.height,
|
|
512
|
+
bitDepth: header.bitDepth,
|
|
513
|
+
colorType: header.colorType,
|
|
514
|
+
colorModel: header.colorModel,
|
|
515
|
+
hasAlpha: header.hasAlpha,
|
|
516
|
+
interlaced: header.interlaceMethod === 1,
|
|
517
|
+
},
|
|
518
|
+
assessment,
|
|
519
|
+
nextStep: {
|
|
520
|
+
recommendedTool: 'convert_png_to_svg',
|
|
521
|
+
recommendedSettings: assessment.recommendedSettings,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function convertPngToSvg({ imageBase64, mimeType, qualityMode = 'exact', colorMode = 'color', traceClass = 'general-color', uiMode = 'logo' }) {
|
|
527
|
+
const { mimeType: parsedMimeType, buffer } = parseBase64Payload(imageBase64);
|
|
528
|
+
validateInputBuffer(buffer, 'PNG input');
|
|
529
|
+
const effectiveMimeType = (mimeType || parsedMimeType || 'image/png').toLowerCase();
|
|
530
|
+
|
|
531
|
+
if (!SUPPORTED_IMAGE_MIME_TYPES.has(effectiveMimeType)) {
|
|
532
|
+
throw new Error(`Unsupported image type "${effectiveMimeType}". Converter MCP currently accepts PNG only.`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const { resolvedMode, resolvedTraceClass, config } = getTraceConfig(qualityMode, colorMode, traceClass, uiMode);
|
|
536
|
+
const startedAt = Date.now();
|
|
537
|
+
const svg = sanitizeSvgExportMarkup(await vectorize(buffer, config), { preserveBranding: false });
|
|
538
|
+
const elapsedMs = Date.now() - startedAt;
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
svg,
|
|
542
|
+
warnings: qualityMode === 'auto'
|
|
543
|
+
? ['`auto` is not modeled in MCP yet; it currently resolves to `exact` for fidelity-first output.']
|
|
544
|
+
: [],
|
|
545
|
+
metrics: {
|
|
546
|
+
elapsedMs,
|
|
547
|
+
sizeBytes: Buffer.byteLength(svg, 'utf8'),
|
|
548
|
+
pathCount: countMatches(svg, /<path\b/gi),
|
|
549
|
+
shapeCount: countMatches(svg, /<(path|rect|circle|ellipse|polygon|polyline|line)\b/gi),
|
|
550
|
+
viewBox: extractViewBox(svg),
|
|
551
|
+
},
|
|
552
|
+
request: {
|
|
553
|
+
qualityMode: resolvedMode,
|
|
554
|
+
colorMode,
|
|
555
|
+
traceClass: resolvedTraceClass,
|
|
556
|
+
uiMode,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function convertSvgToPng({ svg, targetWidth = 512, background = 'transparent' }) {
|
|
562
|
+
if (typeof svg !== 'string' || !svg.trim()) {
|
|
563
|
+
throw new Error('SVG input must be a non-empty string.');
|
|
564
|
+
}
|
|
565
|
+
if (!Number.isFinite(targetWidth) || targetWidth < 16 || targetWidth > 2048) {
|
|
566
|
+
throw new Error('`targetWidth` must be a number between 16 and 2048.');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const resolvedBackground = normalizeBackground(background);
|
|
570
|
+
const startedAt = Date.now();
|
|
571
|
+
const resvg = new Resvg(svg, {
|
|
572
|
+
fitTo: { mode: 'width', value: Math.round(targetWidth) },
|
|
573
|
+
background: resolvedBackground || undefined,
|
|
574
|
+
});
|
|
575
|
+
const rendered = resvg.render();
|
|
576
|
+
const pngBuffer = rendered.asPng();
|
|
577
|
+
const elapsedMs = Date.now() - startedAt;
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
pngBase64: pngBuffer.toString('base64'),
|
|
581
|
+
pngDataUrl: `data:image/png;base64,${pngBuffer.toString('base64')}`,
|
|
582
|
+
metrics: {
|
|
583
|
+
elapsedMs,
|
|
584
|
+
sizeBytes: pngBuffer.byteLength,
|
|
585
|
+
width: rendered.width,
|
|
586
|
+
height: rendered.height,
|
|
587
|
+
},
|
|
588
|
+
request: {
|
|
589
|
+
targetWidth: Math.round(targetWidth),
|
|
590
|
+
background: resolvedBackground || 'transparent',
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|