Eliminates raw pixel assets and hand-written vector paths from KMP Compose projects. Compiles raster images (PNG/JPG) and SVGs into Kotlin ImageVector code via a local, deterministic toolchain (quantize → trace → normalize → codegen). Agents are forbidden from writing path coordinates by hand — all vector generation is delegated to scripts/convert_image_to_imagevector.py. Use whenever a project needs an icon, logo, or flat illustration as a Compose asset, or when replacing PNG icons with vect...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-imagevector-generator --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Kmp Imagevector Generator?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-imagevector-generator)More formats (shields.io, HTML) on the badges page.
---
name: kmp-imagevector-generator
description: >-
Eliminates raw pixel assets and hand-written vector paths from KMP Compose projects.
Compiles raster images (PNG/JPG) and SVGs into Kotlin ImageVector code via a local,
deterministic toolchain (quantize → trace → normalize → codegen). Agents are forbidden
from writing path coordinates by hand — all vector generation is delegated to
scripts/convert_image_to_imagevector.py. Use whenever a project needs an icon, logo,
or flat illustration as a Compose asset, or when replacing PNG icons with vectors.
license: Apache-2.0
metadata:
author: kmp-agent-skills
last-updated: '2026-07-08'
keywords:
- ImageVector
- vector icons
- raster to vector
- SVG to Compose
- icon generation
- vtracer
- potrace
- vector tracing
- asset pipeline
- logo vector
- arc to bezier
- arc flattening
- SVG arc command
- picosvg
- stroke to fill
- stroke width
- outline icon
---
## Hard Rules (never violated)
> **Rule 1 — NEVER hand-write ImageVector path data.** You are FORBIDDEN from writing
> `moveTo`/`lineTo`/`curveTo`/`quadTo` coordinates or any `ImageVector.Builder` body
> yourself. Float coordinates hallucinated in-context produce broken art and burn tokens.
> Path data comes ONLY from `scripts/convert_image_to_imagevector.py`. If the script
> cannot run, stop and report — do not approximate.
> **Rule 2 — Never read generated icon file bodies.** Generated `*.kt` icon files are
> opaque artifacts (like Roborazzi PNGs). Read only the script's report line. Wire the
> icon by its property name (`Icon(imageVector = BrandLogo, …)`).
> **Rule 3 — Regenerate, never edit.** Generated files carry a
> `// GENERATED by convert_image_to_imagevector` header and are audit-enforced. Changes
> go through re-tracing the source, never manual tweaks.
The architecture audit enforces these: `handwritten imagevector [HIGH]` flags builder
blocks without the GENERATED header; `raster asset in commonMain [MEDIUM]` flags PNG/JPG
icons that should be vectors (photos under `assets/photos/` are exempt).
---
## When to Use This Skill
- A project needs an icon, logo, or flat illustration as a Compose asset
- Replacing PNG/JPG icons in `composeResources` with vectors
- A designer hands over a raster mockup containing extractable flat art
- Converting an SVG icon set into Kotlin ImageVector code
**Trigger keywords:** ImageVector, vector icon, convert image, trace image, SVG to
Compose, PNG to vector, icon from image, logo vector, vectorize, raster to vector,
image asset, icon asset, compile icon, vtracer, potrace, extract logo, extract icon,
app icon vector, icon pipeline, no PNG icons, heroicons, hero icons, outline icon,
solid icon, mini icon, micro icon, icon set, fetch icon, download icon, remote svg.
**Freshness rule:** vtracer's Python wheel API and Compose's `ImageVector.Builder`
signatures evolve — recheck both before upgrading the toolchain.
---
## Recommendation First
Default to **SVG input + `--color-mode semantic`** for icons: single color-agnostic
layer, tinted at the call site via `AppTheme.colors.X`, canonical 24×24 viewport.
Why:
- semantic icons adapt to dark/light mode for free — no per-mode assets
- SVG input needs zero dependencies; raster tracing is the fallback, not the default
- one `by lazy` property per icon file keeps startup cost zero and diffs reviewable
Use `--color-mode literal` only for multi-color brand art (logos, illustrations) whose
colors must not change with the theme.
---
## The Pipeline
```
PNG/JPG ─► ①quantize (Pillow) ─► ②trace (vtracer|potrace) ─► SVG ─┐
SVG ──────────────────────────────► ⓪normalize (picosvg, if needed) ─┴─► ③normalize ─► ④Kotlin
```
| Stage | Tool | Notes |
|---|---|---|
| ⓪ Pre-normalize | picosvg (optional; only invoked when a stroke is detected) | Flattens `stroke`/`stroke-width` into real filled outline paths via Skia; install hint if missing |
| ① Quantize | Pillow (median-cut, `--colors N`) | Entropy gate rejects photographs up front |
| ② Trace | vtracer (preferred, full color) / potrace (fallback, mono) | Detected at runtime; install hint if missing |
| ③ Normalize | Built-in parser (zero deps) | Absolute coords, S/T reflection, arc-to-cubic flattening, uniform rescale to `--viewport`, `--max-nodes` budget (default 400) |
| ④ Codegen | Built-in template | `ImageVector.Builder` + `by lazy`, GENERATED header, one file per asset |
Arcs (`A`/`a`) are flattened into cubic Beziers automatically — most hand-authored icon
sets (Heroicons, Feather, Lucide) use arcs for any rounded/circular element, so this is
not an edge case. `ImageVector.Builder` has no native arc primitive, same reason tracers
only ever emit cubics; the parser now does the same conversion for hand-authored SVGs
rather than refusing them.
**Stroke-based SVGs (`fill="none"` + `stroke="..."`) are pre-normalized via picosvg.**
This parser only ever reads `fill` — a stroke-based path's `d` is a centerline, not an
outline, and filling it directly produces a solid blob with **no error**, indistinguishable
from a correct conversion. Stroke-only icon families are common, not an edge case:
Heroicons Outline, Feather, Lucide, Tabler, and Material Symbols Outlined all draw every
icon this way. When a `stroke="..."` attribute with a real value is detected anywhere in
the SVG, the script runs picosvg (`SVG.fromstring(text).topicosvg()`) first — it uses
Skia to correctly turn the stroke into a filled outline (caps, joins, miters), which
this script's own flat regex parser has no way to do itself. If picosvg isn't installed,
the script refuses with an install hint (`pip install picosvg`) rather than silently
filling the centerline.
---
## Workflow (the ONLY allowed flow)
1. Save the source to `assets/raw/` (raster) or anywhere (`.svg`).
2. Run the script:
```bash
python3 ~/.claude/skills/kmp-imagevector-generator/scripts/convert_image_to_imagevector.py \
logo.svg --name BrandLogo --group-id com.example.app \
--output composeApp/src/commonMain/kotlin/com/example/app/core/designsystem/icons
```
If the script is not at `~/.claude/skills/` (Codex CLI, Gemini CLI, or a repo-relative
install), use the path relative to wherever this skill was installed:
```bash
python3 skills/kmp-imagevector-generator/scripts/convert_image_to_imagevector.py \
logo.svg --name BrandLogo --group-id com.example.app \
--output composeApp/src/commonMain/kotlin/com/example/app/core/designsystem/icons
```
3. Read ONLY the report line (`layers / nodes / viewport / color-mode`).
4. Wire the reference:
```kotlin
Icon(imageVector = BrandLogo, contentDescription = "Logo") // literal
Icon(imageVector = SearchIcon, contentDescription = null,
tint = AppTheme.colors.onSurfaceVariant) // semantic
```
5. Optionally register in a project `AppIcons` object so call sites use `AppIcons.Search`.
**Flags:** `--name` (PascalCase property), `--viewport` (default 24 for icons; use the
art's aspect box for logos), `--colors` (raster quantization, default 6), `--max-nodes`
(budget, default 400 — the script refuses bloated vectors), `--color-mode semantic|literal`,
`--package` (full Kotlin package for the generated file; defaults to
`<group-id>.core.designsystem.icons` — that default is the
`kmp-compose-design-system` skill's own module convention, **not** universal.
This script must work standalone for any project structure; pass `--package` explicitly
when the consumer project doesn't use `:core:designsystem`, rather than hand-editing the
generated file's package line afterward).
**Dependencies:** none for plain filled SVG input. Raster input: `pip install vtracer`
(preferred) or `brew install potrace` + `pip install Pillow` (mono fallback). Stroke-based
SVG input (`fill="none"` + `stroke="..."`): `pip install picosvg` — only required when
the source actually uses strokes; the script detects this and only asks for it then.
---
## Remote SVG Sources (e.g. Heroicons)
The pipeline accepts any well-formed SVG regardless of origin. To use an icon from a
remote icon set:
1. Fetch the raw SVG to a local file first — never point the converter at a live URL,
and never fetch from a rendered icon-browser page (e.g. `heroicons.com` itself is a
JS app; it returns HTML, not SVG). Use the raw source repo instead.
2. Validate the icon name and variant against `references/heroicons-catalog.md` before
constructing the URL — it lists the 4 variant keywords (Outline, Solid, Mini, Micro)
and the full 324-name catalog, snapshotted from the upstream repo. Don't guess a name;
if it's not in the list, re-fetch the live directory listing (command included in that
file) rather than hallucinating a plausible-sounding one.
3. Run the same converter as any other SVG input (Step 2 of the Workflow above) —
`--color-mode semantic` is almost always correct since Heroicons ship as single-color
`currentColor` strokes/fills.
```bash
curl -sL "https://raw.githubusercontent.com/tailwindlabs/heroicons/master/optimized/24/outline/bell.svg" \
-o /tmp/bell.svg
python3 skills/kmp-imagevector-generator/scripts/convert_image_to_imagevector.py \
/tmp/bell.svg --name Bell --group-id com.example.app --color-mode semantic \
--output composeApp/src/commonMain/kotlin/com/example/app/core/designsystem/icons
```
---
## What the Script Refuses (by design)
| Refusal | Why |
|---|---|
| Photographic input (entropy gate) | Tracing photos produces garbage vectors — keep photos as raster under `assets/photos/` |
| Node budget exceeded (`--max-nodes`) | Bloated vectors hurt binary size and recomposition; simplify the art or reduce `--colors` |
| Stroke-based SVG with picosvg not installed | This parser only reads `fill`; without picosvg it cannot correctly turn a stroke into an outline (cap/join/miter geometry) — refuses with an install hint rather than silently filling the centerline |
Arc commands (`A`/`a`) are **not** refused — they're flattened into cubic Beziers
automatically (see The Pipeline above). This used to be a refusal; it broke ~75% of
real-world icon sets like Heroicons, which use arcs for every rounded/circular element.
---
## Common Anti-Patterns
- Hand-writing `ImageVector.Builder` blocks or `moveTo/curveTo` coordinates in-context — hallucinated floats produce broken art; always delegate to the script (`handwritten imagevector [HIGH]` audit finding)
- Shipping PNG/JPG icons in `commonMain/composeResources` when a vector pipeline exists — flagged as `raster asset in commonMain [MEDIUM]`; photos belong under `assets/photos/`
- Editing a generated icon file by hand — changes are lost on re-trace; regenerate from the source instead
- Using `--color-mode literal` for tintable icons — bakes one theme's color in and breaks dark mode; semantic single-layer + call-site tint adapts for free
- Reading the generated file body into context to "verify" it — the report line and a Roborazzi golden are the verification; the path data is opaque
- Tracing a full-screen mockup in one pass — crop the individual asset first; the tracer vectorizes everything it sees
- Hand-editing the generated file's `package` line because the project doesn't use `:core:designsystem` — pass `--package` and regenerate instead; still a hand-edit of a GENERATED file even though it's "just" the package declaration
- Trusting a ✅ success line as proof the icon looks right when the source SVG uses strokes and picosvg wasn't installed at the time — before this fix, this parser filled the stroke's centerline silently, producing a wrong icon with a normal-looking report line. Always sanity-check a first stroke-based conversion visually (Roborazzi golden or preview) rather than assuming the report line alone is sufficient
- Assuming a filled `<path fill="...">` extracted from an SVG is definitely the intended shape — if the source used `stroke`, check that picosvg actually ran (or was needed at all) rather than trusting the file exists
---
## Testing
The toolchain core (SVG parse → normalize → codegen) is pure Python and covered by
repo tests (`tests/test_skill_scripts.py`):
- path parser: absolute/relative commands, H/V expansion, S/T control-point reflection, implicit lineto after M, arc-to-cubic flattening (including the packed-flag parsing gotcha, e.g. `"1110"` = large-arc=1, sweep=1, x=10)
- viewport rescale: uniform scale + centering into the canonical square
- codegen: GENERATED header present, `by lazy` property name, layer count, semantic merge to a single layer
- budget: node count over `--max-nodes` exits with an error
- stroke detection: `stroke="..."` with a real value (not `none`/empty/`transparent`) is caught; a full stroke→picosvg→fill conversion test runs when picosvg is installed (it's in `requirements-dev.txt` so CI exercises the real path, not just the detection heuristic)
Rendering fidelity is verified downstream with Roborazzi: capture the generated icon at
24/48 dp via `/kmp-record-design-baselines` and review with `/kmp-audit-screenshots`.
---
## Output Style
When asked to add an icon/logo/vector asset, respond in this order:
1. Identify the source (SVG provided? raster? needs cropping?)
2. Run the script — show the command and the report line only
3. Show the one-line wiring snippet (`Icon(imageVector = …, tint = …)`)
4. If the script refused — photo or node budget — relay the refusal and the fix.
Never work around a refusal by writing paths manually.
Never print generated path data into the conversation.
---
## Related Skills
- `kmp-compose-design-system` — semantic tint tokens (`AppTheme.colors.X`) applied at the icon call site; `/kmp-generate-palette` for the token map itself
- `kmp-roborazzi` — golden captures of generated icons are the fidelity test
- `kmp-layout-system` — wireframes reference icons by `[label]`; this skill turns the labels into real assets
- `kmp-shared-resources` — for assets that must stay raster (photos), and string/font resources
---
## References Directory
| File | Purpose | When to use |
|---|---|---|
| `references/heroicons-catalog.md` | The 4 Heroicons variant keywords (Outline/Solid/Mini/Micro) with repo path templates, plus the full 324-name icon catalog snapshot | Validate an icon name + variant before constructing a fetch URL — see "Remote SVG Sources" above |
---
## Changelog
| Date | Change |
|---|---|
| 2026-07-08 | **Fixed a real, serious defect**: this parser only ever read `fill` — it had zero handling for `stroke`/`stroke-width`, and silently filled a stroke's centerline instead, producing a wrong-looking icon with a completely normal ✅ success report line, indistinguishable from a correct conversion. Stroke-only icon families (Heroicons Outline, Feather, Lucide, Tabler, Material Symbols Outlined) are the norm for "outline style" sets, not an edge case. Fixed by detecting `stroke="..."` with a real value and pre-normalizing via picosvg (`SVG.fromstring(text).topicosvg()`), which uses Skia to correctly convert the stroke into a filled outline (cap/join/miter geometry that would be a serious undertaking to reimplement). If picosvg isn't installed, the script now refuses explicitly with an install hint instead of silently mis-converting. `picosvg` added to `requirements-dev.txt` so CI exercises the real normalization path, not just the detection heuristic. Verified end-to-end against real Heroicons Outline icons. 5 new tests. |
| 2026-07-08 | Added a `--package` flag — found via a real consumer-project report that the generated file's package was hardcoded to `<group-id>.core.designsystem.icons` with no override, forcing every project onto the `kmp-compose-design-system` skill's own module convention even when it doesn't apply. `--package` overrides it explicitly (default unchanged, fully backward compatible); new anti-pattern against hand-editing the package line instead. 2 new tests. |
| 2026-07-08 | Compared the arc-flattening implementation against picosvg's `arc_to_cubic.py` (itself adapted from FontTools/Blink) and backported 2 precision refinements: (1) a `+0.001` epsilon in the segment-count `ceil()` — without it, floating-point trig roundoff on an arc whose sweep should be an exact 90°-multiple can compute `dtheta` as e.g. `1.5707963267948972` instead of exactly `π/2`, producing one unnecessary extra cubic segment (verified against a real reproduction case, not just a synthetic one); (2) a zero-radius arc (`rx==0`/`ry==0`) now emits a real `lineTo` instead of a cubic whose control points merely sit on the straight line — cheaper against `--max-nodes` for the same visual result. 2 new tests. |
| 2026-07-08 | **Fixed a real blocker**: arc commands (`A`/`a`) were rejected outright, but ~75% of a 32-icon real-world Heroicons test batch failed for exactly that reason — most icon sets use arcs for every rounded/circular element. Implemented proper arc-to-cubic-Bezier flattening in `parse_path` (standard SVG spec endpoint-to-center parameterization, split into ≤90° sub-segments), including correct handling of packed arc flags (e.g. `"1110"` = large-arc=1, sweep=1, x=10 — a classic gotcha a naive float tokenizer misreads as one number). Verified end-to-end against real Heroicons SVGs (bell, user-circle, envelope, wifi, users) fetched live. Also fixed an unrelated but real codegen bug found in the same pass: the semantic-mode fill comment was embedded inside the `path(fill = ...)` argument list, so `//` commented out the closing `) {` and broke every semantic-mode icon's generated Kotlin syntax. 4 new tests (arc flattening, packed-flag parsing, semicircle endpoint accuracy, fill-comment regression). |
| 2026-07-07 | Added a "Remote SVG Sources" workflow for fetching icons from remote sets (e.g. Heroicons) as a local file before conversion — never fetch a live URL directly or scrape a rendered icon-browser page. New `references/heroicons-catalog.md`: the 4 variant keywords (Outline, Solid, Mini, Micro) with repo path templates, and the full 324-name Heroicons catalog snapshot with a re-fetch command for freshness. |
| 2026-07-03 | Added a repo-relative fallback path for the converter script — `~/.claude/skills/...` only resolves in a Claude Code install; Codex CLI and Gemini CLI installs need the `skills/...` relative path (see INSTALL.md). |
| 2026-07-03 | Initial release — raster/SVG → ImageVector local toolchain (quantize/trace/normalize/codegen), hard rules forbidding hand-written path data, semantic vs literal color modes, node budget, entropy gate, audit enforcement (handwritten imagevector, raster asset in commonMain). |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!