mcp-png-to-svg
v0.1.0
Published
MCP server that converts PNG logos to animation-ready centerline SVG (for stroke-dashoffset path drawing).
Maintainers
Readme
mcp-png-to-svg
An MCP server that converts a PNG logo into a small number of clean, animation-ready centerline SVG paths — the kind you can actually drive with stroke-dashoffset for path-drawing animations in React Native Reanimated, SVG strokeDasharray, GSAP DrawSVG, or Lottie.
Most PNG→SVG tracers (potrace, imagetracerjs, vtracer fill mode) produce fill-based tracing that explodes into thousands of micro-polygons. Those are useless for stroke animation. This tool does centerline tracing — Guo-Hall thinning + tangent-based stroke linking — so that two intertwined wavy lines come out as two <path> elements, not two thousand.
Install
Add one entry to your Claude Code .mcp.json:
{
"mcpServers": {
"png-to-svg": {
"command": "npx",
"args": ["-y", "mcp-png-to-svg"]
}
}
}That's it. No system dependencies, no Python, no Inkscape. Node 18.17+ required.
First-run cold start is 3–5 s while npx fetches sharp's prebuilt binaries.
What it's for
- Logos and icons with clean strokes
- Hand-drawn sketches and signatures
- Letter glyphs, monograms
- Anything you'd animate by "drawing" it in mid-air
What it's NOT for
- Photos
- Filled illustrations with large solid regions
- Screenshots with text blocks
The tool refuses these with a clear NOT_LINE_ART error rather than returning 27,000 garbage paths. If you hit the refusal, your source is telling you to find or create a vector asset — centerline tracing cannot meaningfully help you.
Tool: png_to_animatable_svg
Input
| Field | Type | Default | Notes |
|------------------------|--------------------|--------------------|-------|
| inputPath | string | — | File path. One of inputPath / inputBase64 is required. |
| inputBase64 | string | — | Base64 PNG bytes. |
| outputPath | string | next to input | SVG output path. |
| maxPaths | number | 12 | Hard ceiling. Exceeded → error. |
| minBranchLength | number | 5 | Prune skeleton spurs shorter than this (pixels). |
| simplifyTolerance | number | 1.5 | RDP tolerance in pixels. |
| junctionMergeRadius | number | 4% of diagonal | Collapse junction clusters smaller than this. |
| strokeWidth | number | 2 | SVG stroke width. |
| threshold | "otsu" | number | "otsu" | Binarization threshold. |
Output
{
svg?: string; // inlined if <= 8 KB
outputPath?: string; // always set if the SVG was written to disk
pathCount: number;
totalLength: number;
viewBox: string;
suggestedDurationMs: number;
warnings: string[];
usage: string; // copy-paste Reanimated snippet
}All emitted paths have fill="none", stroke="currentColor", and — crucially — pathLength="1", so your animation math is just strokeDashoffset: 1 - progress, regardless of the path's real length.
React Native Reanimated snippet
import { useEffect } from "react";
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from "react-native-reanimated";
import Svg, { Path } from "react-native-svg";
const AnimPath = Animated.createAnimatedComponent(Path);
export function LogoDraw() {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1400 });
}, []);
const props = useAnimatedProps(() => ({
strokeDasharray: "1 1",
// NOTE: iOS drops numeric updates; coerce to string.
strokeDashoffset: String(1 - progress.value),
}));
return (
<Svg viewBox="-8 -8 416 416" width={240} height={240}>
<AnimPath
d="M49,120L85,146L...L351,280"
pathLength={1}
{...props}
stroke="#D4AF37"
strokeWidth={2}
fill="none"
strokeLinecap="round"
/>
{/* ...one AnimPath per <path> in the generated SVG... */}
</Svg>
);
}Pipeline
sharp→ grayscale → Otsu threshold → binary mask- Morphological close (heal AA pinholes)
- Guo-Hall 1-pixel thinning
- Spur pruning (remove short anti-alias branches)
- 8-connected skeleton → multigraph (endpoints + junctions)
- Junction cluster collapse (real crossings rarely thin to a single pixel)
- Tangent-based stroke linking — at each junction, pair edge stubs by straightest-through tangent; walk pairings to produce long continuous polylines
- Ramer–Douglas–Peucker simplification
- Emit
<path>elements withpathLength="1", sorted longest-first
Error codes
| Code | Meaning |
|------------------|---------|
| MISSING_INPUT | Neither inputPath nor inputBase64 was provided. |
| AMBIGUOUS_INPUT| Both inputPath and inputBase64 were provided. |
| INPUT_NOT_FOUND| File at inputPath does not exist. |
| NOT_LINE_ART | Too much ink coverage — looks like a photo or filled shape. |
| TOO_MANY_PATHS | Tracing produced more paths than maxPaths. Simplify the source or raise the ceiling. |
| OUTPUT_TOO_LARGE | inputBase64 was used without outputPath and the SVG exceeds 8 KB. |
Limitations
- Variable-width strokes (calligraphic logos) lose their width variation — centerline is inherently uniform. If your logo depends on varying stroke weight, use the original vector asset instead.
- Filled regions inside line art (e.g. a solid dot on top of an "i") will skeletonize to their own small strokes; use
minBranchLengthto suppress them. - Text in complex fonts will produce many paths — intentionally. This tool is for icons and logos, not for rasterized type.
License
MIT
