#!/usr/bin/env python3
"""Turns Lucide SVGs into Android vector drawables (res/drawable/lucide_*.xml).
Lucide draws 24x24 strokes 2 wide with round caps and joins; the drawables keep
that, in black, and Compose tints them. Usage: lucide.py <lucide svg dir> <res/drawable dir> name...
"""
import os
import sys
import xml.etree.ElementTree as ET
def num(value, default=0.0):
return float(value) if value is not None else default
def fmt(value):
text = f"{value:.3f}".rstrip("0").rstrip(".")
return text or "0"
def rect(el):
x, y = num(el.get("x")), num(el.get("y"))
w, h = num(el.get("width")), num(el.get("height"))
rx = el.get("rx")
ry = el.get("ry")
rx = num(rx if rx is not None else ry)
ry = num(ry if ry is not None else rx)
rx, ry = min(rx, w / 2), min(ry, h / 2)
if rx == 0:
return f"M{fmt(x)},{fmt(y)}h{fmt(w)}v{fmt(h)}h{fmt(-w)}Z"
return (
f"M{fmt(x + rx)},{fmt(y)}h{fmt(w - 2 * rx)}a{fmt(rx)},{fmt(ry)} 0 0 1 {fmt(rx)},{fmt(ry)}"
f"v{fmt(h - 2 * ry)}a{fmt(rx)},{fmt(ry)} 0 0 1 {fmt(-rx)},{fmt(ry)}"
f"h{fmt(-(w - 2 * rx))}a{fmt(rx)},{fmt(ry)} 0 0 1 {fmt(-rx)},{fmt(-ry)}"
f"v{fmt(-(h - 2 * ry))}a{fmt(rx)},{fmt(ry)} 0 0 1 {fmt(rx)},{fmt(-ry)}Z"
)
def ellipse(cx, cy, rx, ry):
return (
f"M{fmt(cx - rx)},{fmt(cy)}a{fmt(rx)},{fmt(ry)} 0 1 0 {fmt(2 * rx)},0"
f"a{fmt(rx)},{fmt(ry)} 0 1 0 {fmt(-2 * rx)},0Z"
)
def points(el, close):
values = el.get("points").replace(",", " ").split()
pairs = [(values[i], values[i + 1]) for i in range(0, len(values) - 1, 2)]
data = "M" + " L".join(f"{x},{y}" for x, y in pairs)
return data + ("Z" if close else "")
def path_data(el):
tag = el.tag.split("}")[-1]
if tag == "path":
return el.get("d")
if tag == "rect":
return rect(el)
if tag == "circle":
r = num(el.get("r"))
return ellipse(num(el.get("cx")), num(el.get("cy")), r, r)
if tag == "ellipse":
return ellipse(num(el.get("cx")), num(el.get("cy")), num(el.get("rx")), num(el.get("ry")))
if tag == "line":
return f"M{el.get('x1')},{el.get('y1')} L{el.get('x2')},{el.get('y2')}"
if tag == "polyline":
return points(el, False)
if tag == "polygon":
return points(el, True)
return None
def convert(svg_path):
root = ET.parse(svg_path).getroot()
paths = []
for el in root.iter():
data = path_data(el)
if data:
fill = el.get("fill")
paths.append((data, fill not in (None, "none")))
out = [
'<?xml version="1.0" encoding="utf-8"?>',
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
' android:width="24dp" android:height="24dp"',
' android:viewportWidth="24" android:viewportHeight="24">',
]
for data, filled in paths:
fill = ' android:fillColor="#FF000000"' if filled else ""
out.append(
f' <path android:pathData="{data}"{fill} android:strokeColor="#FF000000" android:strokeWidth="2"'
' android:strokeLineCap="round" android:strokeLineJoin="round" />'
)
out.append("</vector>")
return "\n".join(out) + "\n"
def main():
source, target, names = sys.argv[1], sys.argv[2], sys.argv[3:]
os.makedirs(target, exist_ok=True)
for name in names:
svg = os.path.join(source, f"{name}.svg")
if not os.path.exists(svg):
print(f"missing: {name}", file=sys.stderr)
continue
out = os.path.join(target, "lucide_" + name.replace("-", "_") + ".xml")
with open(out, "w") as handle:
handle.write(convert(svg))
if __name__ == "__main__":
main()