agentleFS
tarikhamiltontarikhamilton/claude-skills/skills/svg-icon-render/SKILL.md

svg-icon-render

Skill0 starsChanged 5 months agoMITView on GitHub

Render a flat SVG icon from a free icon pack (Lucide, Heroicons, Tabler, Feather, Bootstrap, Phosphor, Material Symbols) to a PNG at any size and color, using macOS qlmanage + PIL. Use when the user says "render an icon", "make an SVG icon", "convert SVG to PNG", "icon for the stream deck", "render Lucide / Heroicons / Tabler / Feather / Phosphor / Material Symbols icon", "give me an icon in <color>", or describes a utility/primitive icon (arrow, refresh, camera, music, settings, etc.) they want as a flat PNG asset. Icon-only — for full Stream Deck button updates see `streamdeck-grid-update`. For AI-illustrated or brand iOS-style icons see `gemini-icon`.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    All seven packs are free for commercial use.

    line 118
  2. Very specific:

    Stroke width is set on the SVG, not via CLI: If you want a heavier Lucide stroke, edit stroke-width="2" → stroke-width="2.6" in the SVG …

    line 140
  3. Very specific:

    qlmanage caches: If you re-render the same filename and get the previous result, append a unique suffix to the SVG path or use a fresh temp …

    line 146

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

SVG icon → PNG render

Renders a single icon from one of seven bundled SVG icon packs to a flat PNG at any size, any color, on any background (or transparent). Built around macOS-native qlmanage so there's nothing to install — qlmanage's WebKit renderer handles modern SVG features (currentColor, clip-path, gradients) better than the Python-native libs.

When to use this vs. other icon skills

  • Multi-button Stream Deck change that needs icons → streamdeck-grid-update. It owns the manifest edit AND calls this skill (or its inline PIL recipes) for the icon layer.
  • Branded / iOS-style / illustrated / "designed" icon → gemini-icon. AI-generated, fits product/brand identity. This skill is for utility primitives (arrows, refresh, system glyphs).
  • Single workspace launcher button → streamdeck-workspace-button. End-to-end; pulls icons via brand favicon, not this skill.
  • Anything else — "I need an icon at 128×128 white-on-transparent of a refresh arrow" → this skill.

The pipeline (why three steps)

SVG pack file → force_color() → qlmanage thumbnail → PIL alpha-key + tint → PNG
  1. force_color(svg, "#000000") rewrites currentColor to literal black and strips width=/height= from the root <svg> (otherwise qlmanage honors the pack's 24×24 / 16×16 / 256×256 viewport literally — see Pitfalls). Black is chosen for maximum contrast against qlmanage's white background; the final tint is applied later.
  2. qlmanage -t -s <size> -o <dir> <svg> renders to PNG at the requested size. WebKit-backed; handles every pack tested.
  3. PIL alpha-key + tint — convert the black-on-white render into the requested color: alpha = 255 - luminance, RGB = tint color. Replaces the dropped-near-white-pixels trick from /tmp/sd-icons-v2.py so the same pipeline works for any color including pure white (which would otherwise erase itself).

render.py in this skill folder implements the whole thing. Copy or import it. Smoke-test: python3 render.py /tmp/out.png renders the bundled Lucide arrow-right as white-on-dark 288×288.

Calling the renderer

from render import render
from pathlib import Path

# Stream Deck convention (dark utility background, 288 px)
render(
    svg_source=Path.home() / ".claude/skills/svg-icon-render/samples/lucide/arrow-right.svg",
    out_path=Path.home() / ".config/aerospace/icons/move-right.png",
    color="#3b82f6",           # tailwind blue-500
    size=288,
    background=(26, 26, 26, 255),  # #1a1a1a → utility-icon background
)

# Transparent PNG (for layered design / icon library)
render("path/to/icon.svg", Path("/tmp/icon.png"),
       color="#111827", size=512, background=None)

# Raw SVG string (no file on disk)
render("<svg viewBox='0 0 24 24'><path d='M5 12h14' stroke='currentColor' stroke-width='2'/></svg>",
       Path("/tmp/x.png"), color="#fff")

Parameters:

  • color — hex (#fff or #ffffff). The icon's tint.
  • size — output px (square). 288 = Stream Deck. 512 = generic app icon. 64 = small UI.
  • background — RGBA tuple or None for transparent. (26, 26, 26, 255) is the Stream Deck utility convention.
  • pad_ratio — default 0.10 (10% inset all around). Set lower (e.g. 0.02) when the source SVG already has whitespace baked in.
  • white_threshold — default 240. Pixels with mean luminance above this become fully transparent. Lower if your icon contains intentional off-white tones.

Picking a pack — style guide

PackStyleWhen to reach for it
Lucide2px stroke, square caps, 24×24Default for utility/system icons. Already used on Tarik's Stream Deck (move-*, join-*, reset, flip). Outlined, abstract, neutral.
Feather2px stroke, 24×24Lucide's parent project — basically the same look with a smaller catalog. Use if Lucide doesn't have a glyph; otherwise prefer Lucide (actively maintained).
Tabler1.5px stroke, 24×24Largest free outline catalog (4500+). Slightly thinner stroke than Lucide; reach for it when Lucide is missing a niche glyph.
Heroicons (outline)1.5px stroke, 24×24, "Apple-y" roundedTailwind makers'. Slightly softer look than Lucide. Pair with Tailwind UIs.
Heroicons (solid)Filled, no stroke, 24×24iOS-style filled glyphs. Use for status pills, badge icons, "weight in the corner" UIs — feels heavier than outline.
Bootstrap IconsFilled, mostly fill-only, 16×16Bootstrap-native. Tiny viewBox (renders fine — viewBox scales) but lower visual density than the 24×24 packs at small sizes.
Phosphor256×256, six weightsDistinctive friendly rounded corners. Six weights (thin/light/regular/bold/duotone/fill). Use when the design needs personality. Each weight is a separate npm package; this skill bundles regular and fill.
Material Symbols48×48 (or icon font), three stylesGoogle's. Recognizable as "Google design." Three styles (outlined/rounded/sharp). Best when matching Google product surfaces or Android-feeling UIs. Apache 2.0 licensed (vs ISC/MIT for the others).

Heuristic: for Tarik's Stream Deck system glyphs, default to Lucide. For "this should feel like an iOS button," reach for Heroicons solid or Phosphor fill. For breadth ("does anyone have an icon for X?"), check Tabler first.

Bundled samples

Under samples/<pack>/ — 5 SVGs per pack (arrow, refresh, camera, music, settings) so future agents have a reference without network. Pack-by-pack arrow filename (catalog conventions differ):

  • lucide/arrow-right.svg
  • heroicons-outline/arrow-right.svg (also arrow-path = refresh)
  • heroicons-solid/arrow-right.svg
  • tabler/arrow-right.svg
  • feather/arrow-right.svg
  • bootstrap/arrow-right.svg (refresh is arrow-clockwise, settings is gear)
  • phosphor-regular/arrow-right.svg (refresh is arrow-clockwise)
  • phosphor-fill/arrow-right.svg
  • material-symbols-outlined/arrow_forward.svg (Google uses snake_case, refresh is refresh, camera is photo_camera)

To fetch additional icons from a pack on demand:

# Lucide — every icon name is kebab-case
curl -sSfL "https://cdn.jsdelivr.net/npm/lucide-static/icons/zap.svg" -o samples/lucide/zap.svg

# Heroicons — /24/outline/ or /24/solid/
curl -sSfL "https://cdn.jsdelivr.net/npm/heroicons/24/outline/bell.svg" -o /tmp/bell.svg

# Tabler — /outline/ or /filled/
curl -sSfL "https://cdn.jsdelivr.net/npm/@tabler/icons/icons/outline/zap.svg" -o /tmp/zap.svg

# Feather
curl -sSfL "https://cdn.jsdelivr.net/npm/feather-icons/dist/icons/zap.svg" -o /tmp/zap.svg

# Bootstrap Icons
curl -sSfL "https://cdn.jsdelivr.net/npm/bootstrap-icons/icons/lightning.svg" -o /tmp/lightning.svg

# Phosphor — regular | thin | light | bold | duotone | fill (fill names end in -fill.svg)
curl -sSfL "https://cdn.jsdelivr.net/npm/@phosphor-icons/core/assets/regular/lightning.svg" -o /tmp/lightning.svg
curl -sSfL "https://cdn.jsdelivr.net/npm/@phosphor-icons/core/assets/fill/lightning-fill.svg" -o /tmp/lightning.svg

# Material Symbols — outlined | rounded | sharp; names are snake_case
curl -sSfL "https://cdn.jsdelivr.net/npm/@material-symbols/svg-400/outlined/bolt.svg" -o /tmp/bolt.svg

Licenses (per pack — verify before redistributing)

All seven packs are free for commercial use. License terms differ:

  • Lucide — ISC. Attribution not required. Bundled lucide-static.
  • Heroicons — MIT. Attribution not required. Tailwind Labs.
  • Tabler Icons — MIT.
  • Feather Icons — MIT.
  • Bootstrap Icons — MIT.
  • Phosphor Icons — MIT.
  • Material Symbols — Apache 2.0. Attribution preserved in package header.

Redistributing 5 sample SVGs from each pack inside this skill is fine under all of the above. If shipping a product that bundles the full pack, re-check the upstream LICENSE file (most use the package.json#license field).

Pitfalls (caught while building this skill)

  • Forcing fill="<color>" on a stroke-only SVG fills closed paths solid. Lucide arrow-right looks like a filled triangle blob if you set fill="#fff" on its root. Solution in render.py: only substitute currentColor, don't override the root's stroke-vs-fill choice. The pack's authors already made that call.
  • qlmanage honors width="24" literally — -s 288 only sets the canvas, not the render scale. If the SVG has width="24" height="24" attributes, you get a 24-pixel icon centered in 288 pixels of whitespace. Strip them before rendering (render.py does this). The viewBox alone tells WebKit how to scale.
  • Pure-white tint erases itself. The original /tmp/sd-icons-v2.py worked because it used vivid stroke colors (blue, green, amber, pink) far from white, so dropping near-white pixels left only the colored strokes. Trying the same with color="#ffffff" deletes the icon. Solution: render in BLACK on white, then alpha-key (luminance → alpha) and tint to the target color in PIL. render() always does this regardless of the requested color.
  • Multi-color SVGs (favicons, illustrations) need different handling. This pipeline assumes flat single-color icons. If the source SVG has multiple fill="…" colors (rare in icon packs, common in favicons), it'll all collapse to the tint color. For brand favicons → see streamdeck-grid-update operation H's "Branded icons" recipe (LANCZOS resize, no recolor). For illustrated icons → gemini-icon.
  • Stroke width is set on the SVG, not via CLI. If you want a heavier Lucide stroke, edit stroke-width="2" → stroke-width="2.6" in the SVG before rendering. There's no CLI flag for this.
  • Bootstrap Icons' viewBox is 0 0 16 16, not 24. No-op for the renderer (viewBox is just scale), but the visual weight is heavier — you may want a smaller pad_ratio (e.g. 0.05) since the icon already uses more of the box.
  • Material Symbols' viewBox is 0 -960 960 960 (negative Y origin). Looks weird but renders correctly — WebKit handles it.
  • qlmanage caches. If you re-render the same filename and get the previous result, append a unique suffix to the SVG path or use a fresh temp dir (render.py uses tempfile.TemporaryDirectory so each call is clean).

Cross-references

  • streamdeck-grid-update — Stream Deck manifest edits. Calls into this skill for icon work. Owns the Images/ copy and the on-disk-side wiring.
  • gemini-icon — AI-generated branded/illustrated icons. Use when "draw me a custom icon" rather than "render this glyph from a free pack."
  • streamdeck-workspace-button — single workspace launcher button (uses brand favicons, not pack icons).
  • Reference render: /tmp/sd-icons-v2.py is the original Stream Deck batch that established the qlmanage-based pipeline; this skill generalized it (any size/color/bg, render-then-tint instead of drop-near-white, multiple packs instead of inline Lucide path strings).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.