1 image to all assets needed
untuk generate asset,
---
name: asset-starter
description: >
Generate a complete brand asset kit from a single logo mark PNG. Use this skill whenever
the user uploads a logo image and wants to export assets for a website, app, or PWA —
including SVG variants (fill, outline, dark/light), PNG icons (192, 512, maskable,
apple-touch), favicon.ico, WEBP, wordmark (mark + brand name), and horizontal/square
banners. Trigger this skill when the user mentions any of: "generate assets", "logo kit",
"SVG variants", "favicon", "PWA icons", "apple touch icon", "maskable icon", "banner",
"wordmark", "brand assets", or uploads a logo and asks for multiple export formats.
Also trigger when the user says "asset-starter" or "/asset-starter".
---
# Asset Starter
Generate a full brand asset kit from one logo mark PNG. No external services needed —
runs entirely on PIL + OpenCV in the container.
## What this skill produces
### SVG (7 files)
- logo-mark-fill-black.svg — mark filled #0D0D0D, transparent bg
- logo-mark-fill-white.svg — mark filled #FFFFFF, transparent bg
- logo-mark-outline-black.svg — stroke only, #0D0D0D
- logo-mark-outline-white.svg — stroke only, #FFFFFF
- logo-wordmark-light.svg — mark + brand name, dark-on-transparent
- logo-wordmark-dark.svg — mark + brand name, white-on-transparent
- favicon.svg — same as fill-black, named for <link rel="icon">
### PNG / ICO (5 files)
- icon-192.png — 192×192, transparent bg (PWA manifest)
- icon-512.png — 512×512, transparent bg (PWA manifest)
- maskable-icon-512.png — 512×512, white bg, 18% safe-zone padding
- apple-touch-icon.png — 180×180, white bg
- favicon.ico — multi-resolution: 16, 32, 48, 64, 128, 256 px
### WEBP (3 files)
- icon-192.webp, icon-512.webp, maskable-icon-512.webp
### Wordmark PNG + WEBP (4 files)
- logo-wordmark-light.png/.webp — 1200px wide, transparent bg
- logo-wordmark-dark.png/.webp — 1200px wide, dark #1A1A1A bg
### Banner PNG + WEBP + SVG (9 files)
- banner-dark.png/.webp/.svg — 1920×640, black bg, white content
- banner-light.png/.webp/.svg — 1920×640, white bg, dark content
- banner-sq-dark.png/.webp — 1080×1080, square, black bg
Total: ~28 files
---
## Step 0 — Gather requirements
Before generating anything, confirm these details with the user. Ask in a single message — do NOT generate assets until all answers are received.
Required questions:
1. Logo mark PNG — already uploaded? If not, ask for it.
2. Logo background — is the logo on a dark/black bg (use THRESH_BINARY) or light/white bg (use THRESH_BINARY_INV)? Claude can usually detect this from the image.
3. Brand name — what text goes in wordmarks and banners? (e.g. "Rahman Resource")
4. Tagline / description — short line shown below brand name in banners. (e.g. "free resource for nextjs and convex projects only"). If none, banners will be brand-name only.
5. Mark size in icons — default is large (pad 5%). Ask only if user seems to want something specific.
6. Wordmark font style — default: Poppins Light. Ask only if user mentions it.
Shortcut: If the user already provided the PNG and answered most questions in their message, extract what you can and ask only for what's missing (max 1–2 follow-up questions).
Banner behavior: Banners always show BRAND + TAGLINE in a safe two-line layout that auto-wraps and never clips. Pure mark files (SVG marks, icons, favicon) never include text.
---
## Step 1 — Build RGBA masks from source PNG
```python
import cv2, numpy as np
from PIL import Image
src_pil = Image.open(SRC).convert("RGB")
src_np = np.array(src_pil)
gray = cv2.cvtColor(src_np, cv2.COLOR_RGB2GRAY)
, mask = cv2.threshold(gray, 200, 255, cv2.THRESHBINARY_INV)
H, W = mask.shape
def make_rgba(r, g, b, alpha_mask):
arr = np.zeros((H, W, 4), dtype=np.uint8)
arr[:,:,0]=r; arr[:,:,1]=g; arr[:,:,2]=b; arr[:,:,3]=alpha_mask
return Image.fromarray(arr, "RGBA")
logo_black = make_rgba(13, 13, 13, mask)
logo_white = make_rgba(255, 255, 255, mask)
```
Threshold rule: < 200 = logo pixel, >= 200 = background. Works for logos on
white or near-white backgrounds. If the logo is on a dark background, invert: use
cv2.THRESH_BINARY instead of cv2.THRESH_BINARY_INV.
---
## Step 2 — Trace SVG path via OpenCV contours
```python
contours, = cv2.findContours(mask, cv2.RETREXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
main_cnt = contours[0]
approx = cv2.approxPolyDP(main_cnt, 1.5, True).reshape(-1, 2).tolist()
d_path = "M " + " L ".join(f"{x},{y}" for x, y in approx) + " Z"
viewBox = f"0 0 {W} {H}"
SW = max(W, H) * 0.012 # stroke width for outline variants
```
epsilon=1.5 gives ~80–120 anchor points — enough fidelity without bloating file size.
For logos with very thin strokes or sharp serifs, lower to 0.8. For simple geometric
marks, raise to 3.0.
If the logo has multiple disconnected shapes (e.g. an icon + separate dot), use
cv2.RETR_CCOMP and loop over all contours above a minimum area threshold:
```python
contours, hier = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
path_ds = [contour_to_d(c) for c in contours if cv2.contourArea(c) > 500]
```
---
## Step 3 — SVG builders
```python
def build_p(fill="#000000", stroke=None, sw=SW):
fa = f'fill="{fill}" fill-rule="evenodd"'
sa = (f'stroke="{stroke}" stroke-width="{sw:.1f}" '
f'stroke-linejoin="round" stroke-linecap="round"') if stroke else ""
return f' <path {fa} {sa} d="{d_path}"/>'
def mark_svg(fill, stroke=None, sw=SW, bg=None):
bg_el = f' <rect width="100%" height="100%" fill="{bg}"/>' if bg else ""
return (f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{viewBox}">\n'
f'{bg_el}\n{build_p(fill, stroke, sw)}\n</svg>')
```
Wordmark SVG — mark stacked above brand name text:
```python
TEXT_LABEL = "Brand Name" # ← replace with actual brand name
TEXT_SIZE = H * 0.11
LETTER_SPC = W * 0.01
TOTAL_H = H + H*0.07 + TEXT_SIZE * 1.3
def wordmark_svg(mark_fill, text_fill, bg=None):
bg_el = f' <rect width="100%" height="100%" fill="{bg}"/>' if bg else ""
text_y = H + H*0.07 + TEXT_SIZE
return (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {W:.0f} {TOTAL_H:.0f}">\n'
f'{bg_el}\n{build_p(mark_fill)}\n'
f' <text x="{W/2:.0f}" y="{text_y:.0f}"\n'
f' font-family="\'Poppins\', \'Helvetica Neue\', Arial, sans-serif"\n'
f' font-size="{TEXT_SIZE:.0f}" font-weight="300"\n'
f' letter-spacing="{LETTER_SPC:.1f}" text-anchor="middle"\n'
f' fill="{text_fill}">{TEXT_LABEL}</text>\n'
f'</svg>')
```
Banner SVG — horizontal layout, mark-left + text-right:
```python
def banner_svg(mark_fill, text_fill, bg, bw=1920, bh=640):
scale = int(bh * 0.55) / max(W, H)
sw, sh = W scale, H scale
left_pad = int(bw * 0.075)
mark_y = (bh - sh) / 2
text_x = left_pad + sw + int(bw * 0.04)
fnt_sz = bh * 0.225
text_y = bh / 2 + fnt_sz * 0.35
transform = f'translate({left_pad},{mark_y:.1f}) scale({scale:.4f})'
bg_el = f'<rect width="{bw}" height="{bh}" fill="{bg}"/>'
return (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {bw} {bh}">\n'
f' {bg_el}\n'
f' <g transform="{transform}">{build_p(mark_fill)}</g>\n'
f' <text x="{text_x:.1f}" y="{text_y:.1f}"\n'
f' font-family="\'Poppins\', \'Helvetica Neue\', Arial, sans-serif"\n'
f' font-size="{fnt_sz:.1f}" font-weight="300"\n'
f' fill="{text_fill}">{TEXT_LABEL}</text>\n'
f'</svg>')
```
---
## Step 4 — Raster PNG helpers (PIL only, no cairosvg)
```python
def resize_logo(logo_img, size, pad_ratio=0.12, bg=None):
inner = int(size (1 - 2 pad_ratio))
lcp = logo_img.copy()
lcp.thumbnail((inner, inner), Image.LANCZOS)
rw, rh = lcp.size
canvas = Image.new("RGBA", (size, size), bg if bg else (0,0,0,0))
canvas.paste(lcp, ((size-rw)//2, (size-rh)//2), lcp)
return canvas
```
Padding rules:
| Asset | pad_ratio | Background |
|----------------------|-------------|------------------|
| icon-192, icon-512 | 0.05 | transparent |
| maskable-icon-512 | 0.18 | #FFFFFF |
| apple-touch-icon | 0.05 | #FFFFFF |
| favicon.ico base | 0.05 | #FFFFFF |
---
## Step 5 — Wordmark raster (PIL with Poppins)
```python
from PIL import ImageFont, ImageDraw
FONT_LIGHT = "/usr/share/fonts/truetype/google-fonts/Poppins-Light.ttf"
def make_wordmark_pil(logo_img, text_color, bg_color=None, width=1200):
mark_w = int(width * 0.38)
lcp = logo_img.copy()
lcp.thumbnail((mark_w, mark_w), Image.LANCZOS)
mw, mh = lcp.size
fnt_sz = int(width * 0.057)
fnt = ImageFont.truetype(FONT_LIGHT, fnt_sz)
gap = int(mh * 0.10)
canvas = Image.new("RGBA", (width, mh + gap + int(fnt_sz*1.35)),
bg_color if bg_color else (0,0,0,0))
canvas.paste(lcp, ((width-mw)//2, 0), lcp)
draw = ImageDraw.Draw(canvas)
bb = draw.textbbox((0,0), TEXT_LABEL, font=fnt)
tx = (width - (bb[2]-bb[0])) // 2
draw.text((tx, mh+gap), TEXT_LABEL, font=fnt, fill=text_color)
return canvas
```
---
## Step 6 — Banner raster (PIL) — safe two-line layout
Banners use a two-line text layout (brand name + tagline) that auto-sizes fonts and wraps the tagline to prevent clipping. Never hardcode font sizes — always compute them dynamically.
```python
def get_font(size):
for path in [
"/usr/share/fonts/truetype/google-fonts/Poppins-Light.ttf",
"/usr/share/fonts/truetype/google-fonts/Poppins-Regular.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]:
try: return ImageFont.truetype(path, size)
except: pass
return ImageFont.load_default()
def get_font_bold(size):
for path in [
"/usr/share/fonts/truetype/google-fonts/Poppins-SemiBold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
]:
try: return ImageFont.truetype(path, size)
except: pass
return get_font(size)
def make_banner(logo_img, text_color, tagline_color, bg_color, width=1920, height=640):
canvas = Image.new("RGBA", (width, height), bg_color)
mark_max = int(height * 0.52)
lcp = logo_img.copy()
lcp.thumbnail((mark_max, mark_max), Image.LANCZOS)
mw, mh = lcp.size
left_pad = int(width * 0.07)
canvas.paste(lcp, (left_pad, (height-mh)//2), lcp)
text_x = left_pad + mw + int(width * 0.04)
avail_w = int(width * 0.94) - text_x
draw = ImageDraw.Draw(canvas)
# Brand name: shrink until it fits avail_w
brand_sz = int(height * 0.20)
while brand_sz > 20:
fnt = get_font_bold(brand_sz)
bb = draw.textbbox((0,0), BRAND, font=fnt)
if (bb[2]-bb[0]) <= avail_w: break
brand_sz -= 4
fnt_brand = get_font_bold(brand_sz)
# Tagline: 42% of brand size, word-wrapped
tag_sz = max(int(brand_sz * 0.42), 18)
fnt_tag = get_font(tag_sz)
words = TAGLINE.split()
lines, line = [], ""
for w in words:
test = (line + " " + w).strip()
bb = draw.textbbox((0,0), test, font=fnt_tag)
if (bb[2]-bb[0]) <= avail_w: line = test
else:
if line: lines.append(line)
line = w
if line: lines.append(line)
# Measure and vertically center text block
bb_b = draw.textbbox((0,0), BRAND, font=fnt_brand)
bh = bb_b[3] - bb_b[1]
bb_t = draw.textbbox((0,0), lines[0] if lines else "", font=fnt_tag)
th = bb_t[3] - bb_t[1]
gap_bt = int(brand_sz * 0.25)
gap_tl = int(tag_sz * 0.30)
n = len(lines)
total_h = bh + (gap_bt + n*th + (n-1)*gap_tl if TAGLINE else 0)
ty = (height - total_h) // 2
draw.text((text_x, ty), BRAND, font=fnt_brand, fill=text_color)
if TAGLINE:
y = ty + bh + gap_bt
for ln in lines:
draw.text((text_x, y), ln, font=fnt_tag, fill=tagline_color)
y += th + gap_tl
return canvas
```
Produce three variants:
```python
banner_dark = make_banner(logo_white, (255,255,255,255), (180,180,180,255), (10,10,10,255), 1920, 640)
banner_light = make_banner(logo_black, (13,13,13,255), (80,80,80,255), (255,255,255,255), 1920, 640)
banner_sq_dark = make_banner(logo_white, (255,255,255,255), (180,180,180,255), (10,10,10,255), 1080, 1080)
```
---
## Step 7 — Save all files
```python
import os
OUT = "/mnt/user-data/outputs"
# SVG
for name, content in svg_defs.items():
with open(os.path.join(OUT, name), "w") as f: f.write(content)
# PNG
icon192.save(os.path.join(OUT, "icon-192.png"), "PNG")
icon512.save(os.path.join(OUT, "icon-512.png"), "PNG")
mask512.save(os.path.join(OUT, "maskable-icon-512.png"), "PNG")
apple.save(os.path.join(OUT, "apple-touch-icon.png"), "PNG")
# ICO (multi-resolution)
ico_base.save(os.path.join(OUT, "favicon.ico"), format="ICO",
sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)])
# WEBP
icon192.save(os.path.join(OUT, "icon-192.webp"), "WEBP", quality=90)
icon512.save(os.path.join(OUT, "icon-512.webp"), "WEBP", quality=90)
mask512.save(os.path.join(OUT, "maskable-icon-512.webp"), "WEBP", quality=90)
# Wordmark
wm_light.save(os.path.join(OUT, "logo-wordmark-light.png"), "PNG")
wm_dark.save(os.path.join(OUT, "logo-wordmark-dark.png"), "PNG")
wm_light.save(os.path.join(OUT, "logo-wordmark-light.webp"), "WEBP", quality=92)
wm_dark.save(os.path.join(OUT, "logo-wordmark-dark.webp"), "WEBP", quality=92)
# Banners
for img, name, fmt, kw in [
(banner_dark, "banner-dark.png", "PNG", {}),
(banner_dark, "banner-dark.webp", "WEBP", {"quality":92}),
(banner_light, "banner-light.png", "PNG", {}),
(banner_light, "banner-light.webp", "WEBP", {"quality":92}),
(banner_sq_dark, "banner-sq-dark.png", "PNG", {}),
(banner_sq_dark, "banner-sq-dark.webp", "WEBP", {"quality":92}),
]:
img.save(os.path.join(OUT, name), fmt, **kw)
```
---
## Step 8 — Present files
After all files are saved, call present_files once with ALL output files in a single call — never split into multiple batches. Order them for visual impact:
```
banners (PNG first, then WEBP, then SVG)
→ wordmarks (PNG, WEBP, SVG)
→ SVG marks (fill-white, fill-black, outline-black, outline-white, favicon.svg)
→ PNG icons (icon-512, icon-192, maskable-icon-512, apple-touch-icon)
→ WEBP icons (icon-512, icon-192, maskable-icon-512)
→ favicon.ico
```
Example (all paths listed in one present_files call):
```python
present_files([
OUT/banner-dark.png, OUT/banner-light.png, OUT/banner-sq-dark.png,
OUT/banner-dark.webp, OUT/banner-light.webp, OUT/banner-sq-dark.webp,
OUT/banner-dark.svg, OUT/banner-light.svg,
OUT/logo-wordmark-dark.png, OUT/logo-wordmark-light.png,
OUT/logo-wordmark-dark.webp, OUT/logo-wordmark-light.webp,
OUT/logo-wordmark-dark.svg, OUT/logo-wordmark-light.svg,
OUT/logo-mark-fill-white.svg, OUT/logo-mark-fill-black.svg,
OUT/logo-mark-outline-black.svg, OUT/logo-mark-outline-white.svg,
OUT/favicon.svg,
OUT/icon-512.png, OUT/icon-192.png,
OUT/maskable-icon-512.png, OUT/apple-touch-icon.png,
OUT/icon-512.webp, OUT/icon-192.webp, OUT/maskable-icon-512.webp,
OUT/favicon.ico,
])
```
Then print a summary table of all generated files with sizes.
---
## Edge cases & troubleshooting
| Symptom | Fix |
|---|---|
| Mask captures noise/artifacts | Raise threshold: cv2.threshold(gray, 230, ...) |
| Logo on dark/black background | Switch to cv2.THRESH_BINARY (not _INV) |
| SVG path looks jagged | Lower epsilon: cv2.approxPolyDP(cnt, 0.8, ...) |
| SVG path too blocky/simplified | Lower epsilon: try 0.51.0 |
| Multiple disconnected shapes | Use cv2.RETR_CCOMP and loop all contours |
| Wordmark text clipped/too large | Adjust fnt_sz = int(width * 0.04) (lower = smaller) |
| Font not found | Fallback: ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", fnt_sz) |
| Banner text overflows right edge | Use dynamic sizing loop: shrink brand_sz until textbbox width <= avail_w |
| Tagline wraps badly / overflows | Use word-wrap loop with avail_w guard — never hardcode tagline font size |
| No tagline provided | Set TAGLINE = "" and skip tagline drawing block entirely |
---
## manifest.json snippet (Next.js / PWA)
After generating, offer this snippet to the user:
```json
{
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/maskable-icon-512.png","sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
```
And the <head> tags:
```html
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
```