Turning AI-generated brand marks into production SVGs
For the cost of about an hour of focused work and a couple of dollars in API spend, the pipeline produces a working brand-mark family.
AI image models are good enough now to land a brand mark in just a few minutes. The mark comes out as a raster PNG — beautiful and locked, but not yet in a shape you can actually ship.
A production identity needs vectors. It needs surface-explicit variants — on-light, on-dark, mono-black, mono-white — for the surfaces a brand actually lives on. It needs a favicon set with raster fallbacks for the browsers and OS-es that won’t render SVG at the favicon slot. It needs fills at the exact brand hex values, not the slightly-off versions a neural tracer hands you. And it needs a file size you can put on a web page without apologizing for it.
The path from raster to that production family looks short on paper. In practice it’s full of small mistakes that each cost some API credits and an hour of fumbling to figure out. The point of writing the pipeline down is that the second person to walk it shouldn’t have to learn the same mistakes the first one did.
For the cost of about an hour and $1.50–$2.50 in API spend, the following pipeline produces a complete primary-mark family: a primary on-light variant, a primary on-dark variant, mono-black, mono-white, a sigil, and the favicon set. Most of that hour is QC and variant generation rather than the API calls themselves.
When this pipeline applies
The starting state assumed here:
A locked AI-generated primary mark as a raster PNG (gpt-image-2, Flux, Midjourney, whatever)
A locked brand palette of 3–5 hex values
An expectation of shipping production SVGs to a brand asset library
The pipeline does NOT apply if:
The mark is destined for billboard-scale signage or embroidery — those need a designer cleanup pass in Illustrator on top of this output
The mark is silhouette-only — Potrace handles those in one step, no neural tracer needed
The mark only ever lives on one surface — just chroma-key a transparent PNG and skip vectoring
Tools
All command-line, all standard:
magick(ImageMagick 7+) — flattening, chroma-key, color quantizationrsvg-convert— SVG → PNG render for QCsvgo— multipass SVG optimization (Node global)python3for a short palette-snapping script (standard lib only)curl— for the Vectorizer.AI API
Subscriptions
The pipeline uses the Vectorizer.AI API. The cheapest API tier is around $13 CAD/month for 50 credits.
One trap worth flagging up front: Vectorizer.AI sells web-app and API subscriptions as separate SKUs at the same price. The web-app subscription gives unlimited browser uploads and zero API credits. Subscribing to the wrong one and then watching every API call 402 is a special kind of frustrating. Verify with the account endpoint before assuming API calls work:
curl -sS https://vectorizer.ai/api/v1/account \
-u "$VECTORIZER_AI_APP_ID:$VECTORIZER_AI_API_SECRET"The API plan name looks like vec_999m_50 (50 credits) with a non-zero credits value. The web-app plan name is unlimited_999m. If you see the second one, the API will not work no matter how many calls you make.
The pipeline
Eight steps, in order. A couple of them depend on what the earlier ones produced, so the order isn’t optional even if it looks like it.
Step 1 — Verify the source PNG has a solid background
This is one of the most expensive mistakes in the pipeline, and also one of the easiest to avoid. Vectorizer.AI’s neural tracer treats anti-aliased alpha-edge pixels as a distinct color category, and it emits a separate path for every band of partial transparency. If you feed it a chroma-keyed transparent PNG, the output will be 5,000+ paths and a 900 KB SVG that isn’t really shippable.
file path/to/source.png
# Want: PNG image data, 1024 x 1024, 8-bit/color RGB, non-interlaced
# AVOID: 8-bit/color RGBA (the A = transparency)If the source is RGBA, re-flatten against a solid color before tracing:
magick source.png -background "#1A0E0F" -flatten flat.pngThe flatten color doesn’t have to be a brand color. Vectorizer.AI will trace it as one of the output colors, and you can either drop it from the SVG after or accept it as the “on this surface” variant.
Step 2 — Vectorize via the Vectorizer.AI API
curl -sS https://vectorizer.ai/api/v1/vectorize \
-u "$VECTORIZER_AI_APP_ID:$VECTORIZER_AI_API_SECRET" \
-F image=@source.png \
-F mode=production \
-F output.file_format=svg \
-F processing.max_colors=5 \
-F output.svg.version=svg_1_1 \
-F output.svg.fixed_size=true \
-o raw-output.svg \
-w "HTTP %{http_code} | %{size_download}B | %{time_total}s\n"A few notes on those parameters:
mode=production— 1.0 credits, no watermark, unrestricted output.mode=testis free but embeds a 2,600-path watermark mesh that ruins visual QC. Test mode is only useful to confirm auth.processing.max_colors— set to one more than your palette size. For a 4-color palette setmax_colors=5. Setting it equal to palette size will quantize the least-frequent color out. For pure mono sources usemax_colors=2.output.svg.fixed_size=true— gives you a viewBox-locked SVG that scales predictably.output.svg.version=svg_1_1— broader compatibility than svg_2_0, which matters for email clients and print pipelines.
Expect 70–130 KB output, 80–150 paths, 8–12 seconds per call. If you see 500+ KB or 1000+ paths, the source was probably transparent or has noisy edges. Re-flatten and try again.
Step 3 — Palette-snap every fill to exact brand hex
Vectorizer.AI’s output colors drift slightly from the source. A pure #FF2D87 accent will come back as #fc1e6a, plus a handful of near-cream variants from anti-aliased edges. LAB-distance snapping cleans this up deterministically.
"""palette-snap.py <input.svg> <output.svg>
Snaps every fill/stroke hex value to the nearest brand-palette color in LAB."""
import re, sys
from pathlib import Path
PALETTE = {
# Edit per brand
"#1A0E0F": "ink",
"#F5E9D3": "paper",
"#FF2D87": "accent1",
"#8B1A2B": "accent2",
"#FFFFFF": "white",
}
def hex_to_rgb(s):
h = s.lstrip("#")
if len(h) == 3: h = "".join(c*2 for c in h)
return int(h[0:2],16), int(h[2:4],16), int(h[4:6],16)
def rgb_to_lab(rgb):
r,g,b = (c/255 for c in rgb)
def linear(c): return c/12.92 if c <= 0.04045 else ((c+0.055)/1.055)**2.4
r,g,b = linear(r), linear(g), linear(b)
x = (r*0.4124564 + g*0.3575761 + b*0.1804375) / 0.95047
y = (r*0.2126729 + g*0.7151522 + b*0.0721750)
z = (r*0.0193339 + g*0.1191920 + b*0.9503041) / 1.08883
def f(t): return t**(1/3) if t > 0.008856 else (7.787*t + 16/116)
fx, fy, fz = f(x), f(y), f(z)
return (116*fy - 16, 500*(fx-fy), 200*(fy-fz))
PALETTE_LAB = {h: rgb_to_lab(hex_to_rgb(h)) for h in PALETTE}
def nearest(hex_str):
lab = rgb_to_lab(hex_to_rgb(hex_str))
return min(PALETTE_LAB, key=lambda h: sum((a-b)**2 for a,b in zip(lab, PALETTE_LAB[h])))
HEX_RE = re.compile(r'#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b')
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
content = src.read_text()
stats = {}
def replace(m):
orig = m.group(0).upper()
if len(orig) == 4: orig = "#" + "".join(c*2 for c in orig[1:])
snapped = nearest(orig)
stats[f"{orig} -> {snapped}"] = stats.get(f"{orig} -> {snapped}", 0) + 1
return snapped
dst.write_text(HEX_RE.sub(replace, content))
for k, v in sorted(stats.items(), key=lambda kv: -kv[1]):
print(f" {v:5d} {k}")Run it:
python3 palette-snap.py raw-output.svg snapped.svgThe script prints what it snapped to what. Eyeball it. If a near-charcoal color got snapped to your accent or vice versa, the palette has a gap or the source has AA noise. Tune max_colors and re-vectorize.
Step 4 — SVGO multipass compression
svgo --multipass --precision=2 snapped.svg -o optimized.svgExpect 40–45% size reduction. SVGO merges multiple <path> elements into one path with M move-to jumps, so grep -c '<path' will misleadingly report 1. The render is identical.
Step 5 — Render to PNG and visually QC against the source
Don’t skip this step. Every pipeline run can introduce a regression that only shows up by eye, and the earlier it gets caught the cheaper it is to fix.
rsvg-convert optimized.svg -w 512 -o qc.pngOpen the PNG. Compare against the source. Look for:
Vanishing details (a cream-on-cream feature that disappeared into the background)
Wrong-colored regions (a crimson zone rendered pink because palette-snap pulled it to the wrong neighbor)
Missing paths (something got SVGO’d away)
Bounding-box drift (the mark shifted in canvas)
If anything’s wrong, fix the source step — re-vectorize with different params, edit the palette dict — don’t try to hand-patch the optimized SVG.
Step 6 — Build the surface variants
A production family needs:
<brand>-mark-on-light.svg— mark in dark on the brand’s light surface color, with a solid<rect>background baked in<brand>-mark-on-dark.svg— mark in light on the brand’s dark surface color, ditto<brand>-mark-mono-black.svg— single-color black, transparent background<brand>-mark-mono-white.svg— single-color white, transparent backgroundOptionally:
<brand>-mark-transparent.svg— the cleanest version, no background
The fast path: if your vectorized SVG uses class-based fills (class="u-cream", class="u-pink"), every variant is a one-line <style> swap plus a <rect> background. Path data stays identical across all five variants. Each file ends up around 11 KB because the path data is shared semantically.
If the SVG uses inline fill="#..." attributes instead, either run a regex pass once to convert them to classes, or make N copies and find-replace per variant. The first approach is much less work as soon as you need more than two variants.
Bake a <rect width="100%" height="100%" fill="#..."/> background into the on-light and on-dark files. It’s tempting to leave the surface color to the consumer’s CSS, but downstream consumers will eventually forget to set it, and the mark will vanish on the wrong surface when they do.
Step 7 — Build the favicon
The favicon is the one place in a brand system where prefers-color-scheme inside the SVG is the right abstraction. Browser chrome IS the surface. It changes when the user changes their OS theme. The favicon needs to follow it.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<style>
.u-cream { fill: #F5E9D3; }
.u-pink { fill: #FF2D87; }
.u-bg { fill: #1A0E0F; }
@media (prefers-color-scheme: dark) {
.u-cream { fill: #1A0E0F; }
.u-bg { fill: #F5E9D3; }
}
</style>
<rect class="u-bg" width="1024" height="1024"/>
<!-- the rest of the mark paths -->
</svg>Then generate the raster fallbacks at the standard sizes:
for size in 16 32 48 180; do
rsvg-convert mark-favicon.svg -w $size -h $size -o favicon-${size}.png
doneOpen the 16/32 PNGs and look at them.
Primary marks with internal detail — aviators, monocles, decorative hardware, grip wraps — rarely survive at 16–32 px. They tend to render as muddy pixel blobs at those sizes. There are three reasonable ways to handle that:
Substitute a simpler mark at small sizes. If the brand has a sigil or monogram variant, use it for the 16/32 PNG fallbacks. Different SVG sources, same family. Document the substitution.
Hand-redraw a 16-px variant. A dedicated favicon mark with the silhouette plus one feature.
Accept a recognizable color blob. If the brand’s color signature is distinctive enough, even a muddy 16-px favicon reads as “that brand” by hue alone. Document it as a deliberate tradeoff.
Step 8 — Write the README
Save a README.md next to the production SVG family that documents:
The variant matrix — file → which surface → which use case
The locked palette hex values
Provenance — source PNG, generation params, scripts run
What’s deferred and why (wordmarks composited from real type instead of traced, designer cleanup pass pending)
A future agent or designer landing on the directory cold will save half a day reading this rather than reverse-engineering it.
Gotchas worth pre-reading
These are the gotchas that have actually cost time in practice. Most of them only bite once, but each one tends to cost some real time the first time you run into it.
The subscription mix-up. Vectorizer.AI sells web-app and API plans that look almost identical, cost the same, and sit side-by-side on the pricing page. The only reliable way to tell which one your account ended up on is to hit the account endpoint and read the plan name back. Skipping that check usually shows up as every API call returning 402, until it eventually clicks that the wrong subscription is the one that got bought.
Transparent PNG fragmentation. Feeding a chroma-keyed transparent PNG into the tracer tends to produce something like 5,000 paths and a 900 KB SVG that isn’t really usable in production. The neural tracer treats every band of anti-aliased halo as a distinct color and emits a separate path for each one. About three credits is the going rate for learning that lesson, so the safer move is to always flatten against a solid color before tracing.
max_colors quantization. If you set max_colors equal to your palette size, the API will quantize the least-frequent color out of the output entirely. A crimson zone can come back as a hot pink, and the snap script in Step 3 can’t recover what was never traced in the first place. The safer default is to set max_colors to palette size plus one and let palette-snap clean up the drift afterward.
Wordmarks traced as polygons. If the source PNG has text rendered into it — a “PROPER NAME LTD” lockup beneath the mark, for instance — Vectorizer.AI will trace each letter as a polygon. Traced letterforms muddy at small sizes in ways that look unprofessional even before anyone can articulate why. The fix is to strip the wordmark paths from the SVG and re-add the wordmark as a real <text> element with the actual font, composited into a lockup at the use site.
The fast way to strip them: each wordmark letter has a transform="translate(X,Y)" where Y falls inside a narrow band — say 800–920 for letters along the bottom of a 1024-pixel canvas. A regex over that Y range removes them in one pass.
pattern = re.compile(
r'<path\b[^>]*?\btransform="translate\(\s*[0-9.]+\s*,\s*(?:8[0-9][0-9]|9[01][0-9])\s*\)"[^>]*?/>',
re.DOTALL,
)
cleaned = pattern.sub('', svg_content)Adjust the Y range for your canvas size and wordmark position.
Primary mark doesn’t survive 16–32 px. Covered in Step 7. Use a simpler sigil at small sizes, and document the substitution in the README so the next person knows the favicon and the primary mark aren’t drawn from the same SVG source.
What the pipeline gives up
Vectorizer.AI’s output is about 30× cleaner than vtracer’s on the same source — one unified silhouette vs. 31 fragments — but it is still not anchor-minimized. A typical Vectorizer.AI mark has 130–150 anchors where a hand-cleaned version would have 40–60. At digital sizes and most physical sizes this is invisible. At billboard scale, storefront signage, or embroidery runs above ~100 units, the lumpy bezier curvature shows.
The pipeline as documented produces digital-grade and most-physical-grade output. If the brand commits to a 30-foot sign or an embroidery run of meaningful volume, add a designer cleanup pass on the primary mark in Illustrator. Budget 30–60 minutes per mark, around $100–150 for a freelance designer.
Worked example
Imagine a fictional brand Meridian, a wayfinding studio. Mark: a stylized compass rose. Palette: 4 colors.
PALETTE = {
"#1A2540": "navy",
"#D9A857": "gold",
"#F0E6D2": "paper",
"#0A0A0F": "ink",
}The run:
# 1. Verify source
file meridian-compass.png
# PNG image data, 1024 x 1024, 8-bit/color RGB — good
# 2. Vectorize
curl -sS https://vectorizer.ai/api/v1/vectorize \
-u "$VECTORIZER_AI_APP_ID:$VECTORIZER_AI_API_SECRET" \
-F image=@meridian-compass.png \
-F mode=production \
-F output.file_format=svg \
-F processing.max_colors=5 \
-F output.svg.version=svg_1_1 \
-F output.svg.fixed_size=true \
-o meridian-raw.svg
# 3. Palette snap (edit PALETTE dict in the script first)
python3 palette-snap.py meridian-raw.svg meridian-snapped.svg
# 4. SVGO
svgo --multipass --precision=2 meridian-snapped.svg -o meridian.svg
# 5. QC
rsvg-convert meridian.svg -w 512 -o /tmp/qc.png
# Open qc.png, compare to source. Looks correct? Continue.
# 6. Build surface variants — duplicate file, swap <style>
# Each variant becomes meridian-on-light.svg, meridian-on-dark.svg, etc.
# 7. Favicon
rsvg-convert meridian-favicon.svg -w 32 -h 32 -o meridian-favicon-32.png
# 32-px favicon reads as a compass rose, not a blob: ship it.
# Otherwise substitute a simpler sigil at 16/32.
# 8. README — document the variant matrix, palette, provenance.For the cost of around an hour and 4–8 API credits ($1–2 USD), you end up with a primary mark, a sigil, a horizontal lockup, four surface variants, and the full favicon set.
Recommended directory layout
<brand>/svg/
├── production/
│ ├── primary/
│ │ ├── <brand>-primary-on-light.svg
│ │ ├── <brand>-primary-on-dark.svg
│ │ ├── <brand>-primary-mono-black.svg
│ │ ├── <brand>-primary-mono-white.svg
│ │ └── <brand>-primary-transparent.svg
│ ├── sigil/
│ │ ├── <brand>-sigil-on-light.svg
│ │ └── <brand>-sigil-on-dark.svg
│ ├── horizontal/
│ │ └── <brand>-horizontal-on-dark.svg
│ ├── favicon/
│ │ ├── <brand>-favicon.svg
│ │ ├── <brand>-favicon-16.png
│ │ ├── <brand>-favicon-32.png
│ │ ├── <brand>-favicon-48.png
│ │ └── <brand>-favicon-180.png
│ └── README.md
└── vectorizer-ai/
├── raw/ # untouched API output
├── snapped/ # palette-snapped, pre-SVGO
├── optimized/ # SVGO'd, ready to copy to production/
└── _failed-*/ # quarantined failed runsKeep the working files in vectorizer-ai/ — useful for forensics and re-runs. Don’t import from there at use sites. Always import from production/.
Closing
The thing that makes this pipeline work isn’t any single step on its own. It’s the order, combined with knowing which mistakes cost real money along the way. Most of that cost gets paid by the first person to walk through each gotcha, and the point of writing it down is that the second person shouldn’t have to.
For the cost of about an hour of focused work and a couple of dollars in API spend, the pipeline produces a working brand-mark family that includes a primary mark, a sigil, a horizontal lockup, four surface variants, and a full favicon set. Worth knowing about, if you’re going to be doing this kind of work more than once.


