<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[magic aigc]]></title><description><![CDATA[magic aigc]]></description><link>https://magicaigc.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 02:00:01 GMT</lastBuildDate><atom:link href="https://magicaigc.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your Traced SVG Opens in Figma. That Doesn't Mean It's Ready to Ship]]></title><description><![CDATA[A stdlib Python audit plus a render-back diff for AI-generated and auto-traced vectors, with the numbers from a real tracer run]]></description><link>https://magicaigc.hashnode.dev/your-traced-svg-opens-in-figma-that-doesn-t-mean-it-s-ready-to-ship</link><guid isPermaLink="true">https://magicaigc.hashnode.dev/your-traced-svg-opens-in-figma-that-doesn-t-mean-it-s-ready-to-ship</guid><category><![CDATA[SVG]]></category><category><![CDATA[Python]]></category><category><![CDATA[frontend]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[magic aigc]]></dc:creator><pubDate>Fri, 23 Oct 2026 17:08:20 GMT</pubDate><content:encoded><![CDATA[<p><em>Disclosure: I'm part of the team that runs imagesv2.ai, which includes an SVG generator I mention near the end. Everything here works on any SVG, whichever tool produced it.</em></p>
<p>Image models draw pixels. When a tool hands you an SVG of a generated logo or icon, there is almost always a tracing step in between: a vectorizer walks the raster, groups pixels by color, and fits curves around each patch. That step is genuinely useful. It is also where most "the SVG looked fine" bugs come from.</p>
<p>The <code>.svg</code> extension tells you very little. The file might be four clean shapes, or 229 slightly different grays, or a PNG wearing an <code>&lt;svg&gt;</code> costume. It might scale in a responsive layout, or crop the moment CSS touches it. You only find out when it hits production.</p>
<p>This post builds two small checks you can run before an SVG goes into a repo, a design system or a CMS:</p>
<ol>
<li>a <strong>structural audit</strong> that reads the XML (standard library only), and</li>
<li>a <strong>render-back diff</strong> that rasterizes the SVG and compares it with the image it was traced from.</li>
</ol>
<p>Then we fix what they find, including one trap in a very popular optimizer.</p>
<h2>The test files</h2>
<p>To keep this reproducible, I generated test rasters with Pillow instead of using model output: <code>flat.png</code> (a flat three-color logo on white, anti-aliased) and <code>shaded.png</code> (the same logo with a soft gradient, blur and grain, the "make it look premium" version). Both were traced with <a href="https://github.com/visioncortex/vtracer">vtracer</a>, an open-source color tracer, through a three-line wrapper (<code>trace.py</code>) that calls <code>vtracer.convert_image_to_svg_py()</code> in color/spline mode with <code>filter_speckle=4</code>, <code>color_precision=6</code> and a configurable <code>splice_threshold</code> (vtracer's default is 45). The anti-aliasing matters: real renders have soft edges, and soft edges are where tracers produce extra in-between colors.</p>
<p>A third file, <code>wrapped.svg</code>, is just a base64 PNG inside an <code>&lt;image&gt;</code> tag, which is more common in the wild than it should be. All numbers below come from these synthetic files, not from a benchmark.</p>
<h2>Check 1: read the structure</h2>
<pre><code class="language-python">#!/usr/bin/env python3
"""Pre-ship audit for traced / AI-generated SVG files. Stdlib only."""
import re, sys
import xml.etree.ElementTree as ET
from collections import Counter

NUM = re.compile(r"-?\d*\.?\d+(?:e-?\d+)?")

def local(tag):                      # '{http://www.w3.org/2000/svg}path' -&gt; 'path'
    return tag.rsplit("}", 1)[-1]

def hex_to_rgb(h):
    h = h.lstrip("#")
    if len(h) == 3:
        h = "".join(c * 2 for c in h)
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))

def near_duplicates(colors, tol=12):
    """Pairs of fills that differ by &lt;= tol per channel: usually anti-aliasing debris."""
    rgb = [(c, hex_to_rgb(c)) for c in colors if re.fullmatch(r"#[0-9a-fA-F]{3,6}", c)]
    pairs = 0
    for i in range(len(rgb)):
        for j in range(i + 1, len(rgb)):
            if max(abs(a - b) for a, b in zip(rgb[i][1], rgb[j][1])) &lt;= tol:
                pairs += 1
    return pairs

def audit(path):
    root = ET.parse(path).getroot()
    els = list(root.iter())
    tags = Counter(local(e.tag) for e in els)
    fills = Counter((e.get("fill") or "").upper() for e in els if local(e.tag) == "path")
    fills.pop("", None)
    d_chars = sum(len(e.get("d", "")) for e in els if local(e.tag) == "path")
    w, h = root.get("width"), root.get("height")
    problems, blockers = [], []

    if not root.get("viewBox"):
        blockers.append(f"no viewBox (width={w}, height={h}): resizing crops or pads instead of scaling")
    if tags["image"]:
        blockers.append(f"{tags['image']} embedded &lt;image&gt;: raster inside, not a real vector")
    if tags["path"] &gt; 500:
        problems.append(f"{tags['path']} paths: too busy to hand-edit")
    if len(fills) &gt; 16:
        problems.append(f"{len(fills)} distinct fills: palette not flat")
    dup = near_duplicates(list(fills))
    if dup:
        problems.append(f"{dup} near-duplicate fill pairs (&lt;=12/channel apart)")
    if tags["text"] == 0:
        problems.append("no &lt;text&gt;: any lettering is outlines, not retypeable")
    unsafe = [local(e.tag) for e in els if local(e.tag) in ("script", "foreignObject")]
    unsafe += [k for e in els for k in e.attrib if k.lower().startswith("on")]
    if unsafe:
        blockers.append(f"active content: {sorted(set(unsafe))}")

    # A first shape covering the whole canvas is almost always a traced background
    first = next((e for e in els if local(e.tag) == "path"), None)
    if first is not None and w and h:
        xs_ys = [float(n) for n in NUM.findall(first.get("d", ""))]
        if xs_ys and max(xs_ys) &gt;= float(w.rstrip("px")) * 0.99 and min(xs_ys) &lt;= 1:
            problems.append(f"first path spans the canvas (fill {first.get('fill')}): traced background")

    print(f"{path}: {tags['path']} paths, {len(fills)} fills, "
          f"{d_chars / 1000:.0f}k chars of path data")
    for p in blockers:
        print("  BLOCK", p)
    for p in problems:
        print("  warn ", p)
    return not blockers

if __name__ == "__main__":
    ok = all([audit(p) for p in sys.argv[1:]])
    sys.exit(0 if ok else 1)
</code></pre>
<p>Output on the three raw files:</p>
<pre><code class="language-text">$ python svg_audit.py flat.svg shaded.svg wrapped.svg
flat.svg: 5 paths, 5 fills, 9k chars of path data
  BLOCK no viewBox (width=1024, height=1024): resizing crops or pads instead of scaling
  warn  no &lt;text&gt;: any lettering is outlines, not retypeable
  warn  first path spans the canvas (fill #FEFEFE): traced background
shaded.svg: 229 paths, 205 fills, 275k chars of path data
  BLOCK no viewBox (width=1024, height=1024): resizing crops or pads instead of scaling
  warn  205 distinct fills: palette not flat
  warn  1551 near-duplicate fill pairs (&lt;=12/channel apart)
  warn  no &lt;text&gt;: any lettering is outlines, not retypeable
  warn  first path spans the canvas (fill #E8E8E8): traced background
wrapped.svg: 0 paths, 0 fills, 0k chars of path data
  BLOCK no viewBox (width=1024, height=1024): resizing crops or pads instead of scaling
  BLOCK 1 embedded &lt;image&gt;: raster inside, not a real vector
  warn  no &lt;text&gt;: any lettering is outlines, not retypeable
</code></pre>
<p>What each line is telling you:</p>
<ul>
<li><strong>No <code>viewBox</code>.</strong> The tracer wrote <code>width="1024" height="1024"</code> and nothing else. Without a <code>viewBox</code> there is no coordinate system to scale <em>from</em>, so when the element is resized the drawing doesn't follow: you get cropping or empty space. This is the one I'd fail a build on.</li>
<li><strong>Embedded <code>&lt;image&gt;</code>.</strong> No paths at all. It will "open in Figma", and it will blur like any PNG.</li>
<li><strong>205 fills, 1,551 near-duplicate pairs.</strong> The gradient and grain became hundreds of almost-identical grays. Nobody can recolor that by hand, and "swap the brand blue" now means editing dozens of fills.</li>
<li><strong>No <code>&lt;text&gt;</code>.</strong> In a traced file, letters are outlines. That's fine for a logo mark; it's a problem if someone expects to fix a typo in the tagline.</li>
<li><strong>Full-canvas first path.</strong> Tracers usually emit the background as one big shape at the bottom of the stack. You'll want to delete it for a transparent asset, but read Check 3 before you do.</li>
</ul>
<h2>Check 2: render it back and diff</h2>
<p>Structure tells you whether the file is <em>workable</em>. It doesn't tell you whether it <em>looks like the source</em>. For that, rasterize the SVG with a real renderer (<a href="https://github.com/linebender/resvg">resvg</a>, via the <code>resvg_py</code> binding) and compare pixels:</p>
<pre><code class="language-python">#!/usr/bin/env python3
"""Render an SVG back to pixels and compare it with the raster it was traced from."""
import io, sys
import resvg_py
from PIL import Image, ImageChops

svg_path, png_path = sys.argv[1], sys.argv[2]
bg = sys.argv[3] if len(sys.argv) &gt; 3 else "#ff00ff"   # what transparent areas should look like
src = Image.open(png_path).convert("RGB")
data = resvg_py.svg_to_bytes(svg_string=open(svg_path).read(), width=src.width)
# default magenta: any area the SVG leaves empty is counted as wrong
out = Image.new("RGBA", src.size, bg)
out.alpha_composite(Image.open(io.BytesIO(bytes(data))).convert("RGBA").resize(src.size))
diff = ImageChops.difference(src, out.convert("RGB")).convert("L")
bad = sum(diff.histogram()[48:]) / (src.width * src.height)   # &gt;48/255 = visibly off
print(f"{svg_path}: {bad:.2%} of pixels visibly different from {png_path}")
diff.point(lambda v: 255 if v &gt; 48 else 0).save(svg_path.rsplit(".", 1)[0] + "_diff.png")
sys.exit(1 if bad &gt; 0.01 else 0)
</code></pre>
<pre><code class="language-text">$ python render_diff.py flat.svg flat.png
flat.svg: 0.80% of pixels visibly different from flat.png
</code></pre>
<p>0.80% sounds harmless, and the script's 1% threshold passed it. Then I opened <code>flat_diff.png</code>: almost all of that error was one place, a flattened arc along the top of the circle where the spline fitter had cut a corner. On a logo, that's the first thing anyone notices.</p>
<p>Lowering vtracer's <code>splice_threshold</code> from its default of 45 to 20 fixed it:</p>
<pre><code class="language-text">$ python trace.py flat.png flat_s20.svg 20 &amp;&amp; python render_diff.py flat_s20.svg flat.png
flat_s20.svg: 0.17% of pixels visibly different from flat.png
</code></pre>
<p>Two lessons. A global percentage hides local damage, so always look at the mask, not just the number. And tracer settings matter as much as the prompt; don't assume the defaults suit your artwork.</p>
<h2>Check 3: the background trap</h2>
<p>Deleting the full-canvas background shape is the obvious way to get a transparent SVG. Here's a tiny fixer that also adds the missing <code>viewBox</code> and removes the fixed size so CSS decides:</p>
<pre><code class="language-python">#!/usr/bin/env python3
"""Add a viewBox, make the SVG responsive, optionally drop a traced background."""
import sys
import xml.etree.ElementTree as ET

SVG = "http://www.w3.org/2000/svg"
ET.register_namespace("", SVG)           # keep plain &lt;svg&gt;, not &lt;ns0:svg&gt;

src, dst = sys.argv[1], sys.argv[2]
drop_bg = "--drop-bg" in sys.argv
tree = ET.parse(src)
root = tree.getroot()

w = float(root.get("width", "0").rstrip("px"))
h = float(root.get("height", "0").rstrip("px"))
if not root.get("viewBox") and w and h:
    root.set("viewBox", f"0 0 {w:g} {h:g}")
for attr in ("width", "height"):         # let CSS / the container decide the size
    root.attrib.pop(attr, None)

if drop_bg:
    first = root.find(f"{{{SVG}}}path")
    if first is not None:
        root.remove(first)
        print("removed background path, fill", first.get("fill"))

tree.write(dst, encoding="utf-8", xml_declaration=True)
</code></pre>
<p>On the flat logo it works. Rendered over white, the cleaned file is still 0.15% off the source. But on the shaded version, after flattening its palette to six colors (more on that below), the same fix produced this:</p>
<pre><code class="language-text">$ python render_diff.py shaded_q6_min.svg shaded_q6.png
shaded_q6_min.svg: 41.00% of pixels visibly different from shaded_q6.png
</code></pre>
<p>The diff run without a background color (magenta stands in for "transparent") showed a hollow ring around the circle. Tracers like vtracer stack shapes, and the background color was doing double duty: it was also the visible color of a band <em>inside</em> the artwork. Remove the bottom shape and that band becomes a hole.</p>
<p>So: only drop the background after a render-back diff on magenta, and if a hole appears, recolor that region instead of deleting it.</p>
<h2>Fixing the rest</h2>
<p><strong>Precision and size.</strong> Tracers write coordinates like <code>23.903061523437486</code>. One decimal place is plenty at this scale:</p>
<pre><code class="language-text">$ npx -y svgo@4 -p 1 flat_fixed.svg -o flat_min.svg
</code></pre>
<p>That took the flat logo from 9,727 to 1,287 bytes, and the render diff stayed at 0.15%.</p>
<p><strong>The optimizer trap.</strong> In svgo 3, the default preset includes <code>removeViewBox</code>, which strips the <code>viewBox</code> when <code>width</code> and <code>height</code> are present. To check, I put <code>width</code> and <code>height</code> back on the cleaned file (<code>wh.svg</code>) and ran both major versions (output trimmed):</p>
<pre><code class="language-text">$ npx -y svgo@3 -p 1 wh.svg -o - | head -c 100
&lt;svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"&gt;&lt;path fill="#1F6FEA" d="M604.4 21
$ npx -y svgo@4 -p 1 wh.svg -o - | head -c 100
&lt;svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"&gt;&lt;path fil
</code></pre>
<p>svgo 3 silently threw the <code>viewBox</code> away; svgo 4 kept it. The file still renders at its fixed size, so nothing looks broken until someone puts the icon in a flexible container.</p>
<p>If your pipeline pins svgo 3 (plenty do, through older build plugins), either disable that plugin, or remove <code>width</code>/<code>height</code> first like <code>fix_svg.py</code> does. Then run the audit <em>after</em> optimizing, not before.</p>
<p><strong>Busy palettes start upstream.</strong> Quantizing <code>shaded.png</code> to six colors with Pillow before tracing cut 205 fills to 5. The path data, though, was still 64k characters, because a radial gradient quantizes into wobbly concentric bands. You can't optimize your way out of shading. The real fix is asking for flat artwork in the first place: flat fills, a small palette, bold outlines, plain background.</p>
<h2>Where generated SVGs fit</h2>
<p>That last point is also how our own <a href="https://imagesv2.ai/gpt-image-2-svg">GPT Image 2 SVG generator</a> is designed. Its Visual Asset mode generates an image and then traces it, and the page tells you to prompt for flat colors and a plain background, warns that gradients and textures turn into hundreds or thousands of paths, notes that letters become outlines, and says the background usually ends up as a separate shape you can delete. In other words: everything above still applies, so run both checks on its output too.</p>
<p>For eyeballing a suspicious file, a viewer with a checkerboard background and click-to-source is handy. Our <a href="https://imagesv2.ai/svg-viewer">browser-based SVG viewer</a> does that, rendering client-side with a Pick mode that jumps from a shape to its line in the source. Inkscape's XML editor or browser devtools work too.</p>
<h2>What the audit doesn't catch (yet)</h2>
<ul>
<li>Fills set through <code>style=""</code> or CSS classes rather than <code>fill=""</code> attributes</li>
<li>Gradients in <code>&lt;defs&gt;</code>, strokes, and <code>&lt;use&gt;</code> references</li>
<li>Thresholds (500 paths, 16 fills, 1% diff) that are guesses tuned on a handful of files; adjust them for your assets</li>
</ul>
<p>Both scripts exit non-zero on failure, so they drop into CI as-is:</p>
<pre><code class="language-bash">python svg_audit.py assets/icons/*.svg
for f in assets/icons/*.svg; do python render_diff.py "$f" "sources/$(basename "${f%.svg}").png"; done
</code></pre>
<hr />
<p>What's the worst thing you've found inside an SVG that "looked fine" — and which check would have caught it before it shipped?</p>
]]></content:encoded></item></channel></rss>