Back to Blog
2026-03-22

Text-to-SVG: Generate Vector Graphics from Text Prompts with AI

Generate clean SVG vector graphics from text prompts using diffusion models + vectorization pipelines. No Illustrator, no manual tracing — fully programmable vector output.

SVG generation from text prompts is one of the most underrated applications of AI in design. Instead of rasterizing to a PNG, you can produce scalable vector paths that work at any resolution, integrate with CSS animations, and export directly into Figma or Illustrator.

This guide covers the complete pipeline: text prompt → raster generation → vectorization → clean SVG, with a direct SVG path generation approach using diffusion guidance.

Two Approaches to Text-to-SVG

Approach 1: Raster-to-Vector Pipeline Generate a raster image (PNG) from your prompt, then vectorize it using traced paths. Reliable, works with any image generator.

Approach 2: Direct SVG Generation Use DiffVG or SVGCraft-style methods that optimize SVG paths directly. Higher quality, more complex setup.

We'll cover both.

Setup

pip install diffusers transformers torch Pillow cairosvg svgwrite
# For vectorization
pip install vtracer  # Rust-based vectorizer, much faster than potrace
# Or: sudo apt install potrace (classic open-source tracer)

Approach 1: Raster → Vector Pipeline

Step 1: Generate the Raster Image

import torch
from diffusers import StableDiffusionPipeline
from PIL import Image

def generate_raster(
    prompt: str,
    negative_prompt: str = "photo, realistic, complex background, gradient, noise",
    model_id: str = "runwayml/stable-diffusion-v1-5",  # Or any local model
    steps: int = 30,
    guidance: float = 7.5,
    size: int = 512
) -> Image.Image:
    """
    Generate a raster image optimized for vectorization.
    Key: use negative prompts to encourage flat, clean shapes.
    """
    pipe = StableDiffusionPipeline.from_pretrained(
        model_id,
        torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
    )
    pipe = pipe.to("cuda" if torch.cuda.is_available() else "cpu")
    pipe.enable_attention_slicing()
    
    # Prompt engineering for vectorization-friendly output
    vector_prompt = f"{prompt}, flat design, minimal, clean lines, solid colors, icon style, white background, svg illustration"
    
    image = pipe(
        prompt=vector_prompt,
        negative_prompt=negative_prompt,
        num_inference_steps=steps,
        guidance_scale=guidance,
        height=size,
        width=size
    ).images[0]
    
    return image

# Generate a logo-style image
img = generate_raster(
    "a mountain peak with sun rays, minimalist logo",
    size=512
)
img.save("mountain_logo.png")

Step 2: Preprocess for Vectorization

Better preprocessing → cleaner vector paths:

from PIL import Image, ImageFilter, ImageEnhance
import numpy as np

def preprocess_for_vectorization(
    image: Image.Image,
    reduce_colors: int = 8,         # Posterize to N colors
    sharpen: bool = True,
    background_threshold: int = 240  # Pixels lighter than this → pure white
) -> Image.Image:
    """
    Clean up raster image to make vectorization produce fewer, cleaner paths.
    """
    img = image.convert("RGB")
    
    # Remove near-white background noise
    arr = np.array(img)
    mask = np.all(arr > background_threshold, axis=2)
    arr[mask] = [255, 255, 255]
    img = Image.fromarray(arr)
    
    # Posterize to reduce number of distinct color regions
    img = img.convert("P", palette=Image.ADAPTIVE, colors=reduce_colors)
    img = img.convert("RGB")
    
    if sharpen:
        img = img.filter(ImageFilter.SHARPEN)
        enhancer = ImageEnhance.Contrast(img)
        img = enhancer.enhance(1.5)
    
    return img

clean_img = preprocess_for_vectorization(img, reduce_colors=6)
clean_img.save("mountain_logo_clean.png")

Step 3: Vectorize to SVG

import subprocess
import vtracer

def vectorize_to_svg(
    input_path: str,
    output_path: str,
    method: str = "vtracer",   # "vtracer" or "potrace"
    color_precision: int = 6,   # vtracer: number of color quantization steps
    filter_speckle: int = 4,    # Remove noise smaller than N pixels
    curve_fitting: str = "spline"  # spline, polygon, pixel
) -> str:
    """
    Convert raster image to SVG using vtracer or potrace.
    Returns the SVG content as a string.
    """
    if method == "vtracer":
        vtracer.convert_image_to_svg_py(
            input_path,
            output_path,
            colormode="color",
            hierarchical="stacked",
            mode=curve_fitting,
            filter_speckle=filter_speckle,
            color_precision=color_precision,
            layer_difference=16,
            corner_threshold=60,
            length_threshold=4.0,
            max_iterations=10,
            splice_threshold=45,
            path_precision=3
        )
    elif method == "potrace":
        # Potrace works on BMP, convert first
        bmp_path = input_path.replace(".png", ".bmp")
        Image.open(input_path).convert("1").save(bmp_path)
        subprocess.run([
            "potrace", "--svg", "--output", output_path, bmp_path
        ], check=True)
    
    with open(output_path, "r") as f:
        svg_content = f.read()
    
    return svg_content

svg = vectorize_to_svg(
    "mountain_logo_clean.png",
    "mountain_logo.svg",
    method="vtracer",
    color_precision=8,
    filter_speckle=6
)
print(f"SVG generated: {len(svg)} chars")

Approach 2: Direct SVG Path Generation

For icon-style output, you can generate SVG paths programmatically using geometric primitives guided by an LLM:

import svgwrite
import json
import requests

def generate_svg_with_llm(
    prompt: str,
    canvas_size: int = 200,
    ollama_model: str = "llama3.2:3b"
) -> str:
    """
    Use local LLM to describe geometric SVG paths, then render them.
    """
    system = f"""You are an SVG designer. Given a description, output ONLY a JSON array of SVG shapes.
Canvas size: {canvas_size}x{canvas_size}. Center is ({canvas_size//2}, {canvas_size//2}).

Shape types:
- circle: {{"type":"circle","cx":100,"cy":100,"r":40,"fill":"#3B82F6"}}
- rect: {{"type":"rect","x":60,"y":60,"width":80,"height":80,"fill":"#F59E0B","rx":8}}
- polygon: {{"type":"polygon","points":"100,20 140,80 60,80","fill":"#10B981"}}
- path: {{"type":"path","d":"M 50 100 L 150 100 L 100 50 Z","fill":"#EF4444"}}

Output JSON array only. 3-8 shapes. No explanation."""
    
    try:
        resp = requests.post(
            "http://localhost:11434/api/generate",
            json={"model": ollama_model, "prompt": prompt, "system": system,
                  "stream": False, "options": {"temperature": 0.5}},
            timeout=20
        )
        raw = resp.json()["response"]
        start = raw.find("[")
        end = raw.rfind("]") + 1
        shapes = json.loads(raw[start:end])
    except Exception:
        # Fallback: simple geometric shapes for common prompts
        shapes = [
            {"type": "circle", "cx": 100, "cy": 100, "r": 60, "fill": "#3B82F6"},
            {"type": "circle", "cx": 100, "cy": 100, "r": 40, "fill": "#60A5FA"},
        ]
    
    # Render shapes to SVG
    dwg = svgwrite.Drawing(size=(canvas_size, canvas_size))
    dwg.add(dwg.rect(insert=(0, 0), size=(canvas_size, canvas_size), fill="white"))
    
    for shape in shapes:
        if shape["type"] == "circle":
            dwg.add(dwg.circle(center=(shape["cx"], shape["cy"]), r=shape["r"],
                              fill=shape.get("fill", "#000")))
        elif shape["type"] == "rect":
            dwg.add(dwg.rect(insert=(shape["x"], shape["y"]),
                            size=(shape["width"], shape["height"]),
                            fill=shape.get("fill", "#000"),
                            rx=shape.get("rx", 0)))
        elif shape["type"] == "polygon":
            dwg.add(dwg.polygon(points=shape["points"].split(),
                               fill=shape.get("fill", "#000")))
        elif shape["type"] == "path":
            dwg.add(dwg.path(d=shape["d"], fill=shape.get("fill", "#000")))
    
    return dwg.tostring()

svg_content = generate_svg_with_llm("a simple wifi signal icon", canvas_size=200)
with open("wifi_icon.svg", "w") as f:
    f.write(svg_content)
print("Icon generated: wifi_icon.svg")

Post-Processing: Clean SVG Optimization

Raw vectorization output is often bloated. Optimize before use:

def optimize_svg(input_path: str, output_path: str):
    """
    Simplify SVG using svgo-style path reduction.
    Install: npm install -g svgo
    """
    result = subprocess.run(
        ["svgo", "--input", input_path, "--output", output_path,
         "--config", '{"plugins": ["removeDoctype", "removeComments", "mergePaths", "convertPathData"]}'],
        capture_output=True, text=True
    )
    
    before = os.path.getsize(input_path)
    after = os.path.getsize(output_path)
    reduction = (1 - after/before) * 100
    print(f"Optimized: {before:,} → {after:,} bytes ({reduction:.1f}% reduction)")

# Full pipeline
img = generate_raster("a compass rose, flat design icon, navy blue")
clean = preprocess_for_vectorization(img, reduce_colors=4)
clean.save("/tmp/compass_clean.png")
svg = vectorize_to_svg("/tmp/compass_clean.png", "compass.svg")
optimize_svg("compass.svg", "compass_optimized.svg")

Quality Tips

  • More reduction colors = fewer SVG paths (faster to trace, simpler output)
  • filter_speckle=8 removes small islands from rough backgrounds
  • White background in the generated image prevents background paths from bleeding into shapes
  • For logos, 2–4 colors at the raster stage produces the cleanest vector result
  • curve_fitting=spline is best for organic shapes; use polygon for geometric/technical icons

The Vector Workspace includes this full text-to-SVG pipeline plus icon set generation, logo creation, image-to-vector conversion, pattern generation, and batch vectorization — 1,015 lines of vector tooling ready to run.

→ Get Vector Workspace on the Shop