Files
CleanMM/scripts/generate_icon.py

230 lines
8.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Atlas for Mac App Icon Generator
Brand: Calm Authority
Concept: A stylized globe with meridian lines overlaid on a deep-teal-to-emerald
gradient, with a darker premium backdrop and a refined mint accent arc representing the "atlas" mapping metaphor.
Generates all required macOS app icon sizes from a programmatic SVG.
"""
import subprocess
import os
import json
import tempfile
ICON_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"Apps", "AtlasApp", "Sources", "AtlasApp", "Assets.xcassets", "AppIcon.appiconset"
)
# macOS icon sizes needed
SIZES = [16, 32, 64, 128, 256, 512, 1024]
def generate_svg(size=1024):
"""Generate the Atlas app icon as SVG."""
return f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" width="{size}" height="{size}">
<defs>
<!-- Brand gradient: darker premium teal to deep emerald -->
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#031B1A"/>
<stop offset="50%" stop-color="#0A5C56"/>
<stop offset="100%" stop-color="#073936"/>
</linearGradient>
<!-- Inner glow from top-left -->
<radialGradient id="innerGlow" cx="0.3" cy="0.25" r="0.7">
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.16"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</radialGradient>
<!-- Globe gradient -->
<radialGradient id="globeGrad" cx="0.4" cy="0.35" r="0.55">
<stop offset="0%" stop-color="#A7F3D0" stop-opacity="0.38"/>
<stop offset="60%" stop-color="#5EEAD4" stop-opacity="0.22"/>
<stop offset="100%" stop-color="#0A5C56" stop-opacity="0.10"/>
</radialGradient>
<!-- Mint accent gradient -->
<linearGradient id="mintGrad" x1="0" y1="0" x2="1" y2="0.5">
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.98"/>
<stop offset="100%" stop-color="#6EE7B7" stop-opacity="0.82"/>
</linearGradient>
<!-- Clip to rounded square -->
<clipPath id="roundClip">
<rect x="0" y="0" width="{size}" height="{size}" rx="{int(size * 0.22)}" ry="{int(size * 0.22)}"/>
</clipPath>
</defs>
<g clip-path="url(#roundClip)">
<!-- Background -->
<rect width="{size}" height="{size}" fill="url(#bgGrad)"/>
<rect width="{size}" height="{size}" fill="url(#innerGlow)"/>
<!-- Globe circle -->
<circle cx="{size//2}" cy="{size//2}" r="{int(size * 0.32)}"
fill="url(#globeGrad)" stroke="#CCFBF1" stroke-width="{max(1, size//256)}" stroke-opacity="0.24"/>
<!-- Meridian lines (longitude) -->
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.24">
<!-- Vertical center line -->
<line x1="{size//2}" y1="{int(size*0.18)}" x2="{size//2}" y2="{int(size*0.82)}"/>
<!-- Elliptical meridians -->
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.12)}" ry="{int(size*0.32)}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.24)}" ry="{int(size*0.32)}"/>
</g>
<!-- Latitude lines (horizontal) -->
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.18">
<line x1="{int(size*0.18)}" y1="{size//2}" x2="{int(size*0.82)}" y2="{size//2}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.12)}"/>
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.22)}"/>
</g>
<!-- Mint accent arc the "mapping" highlight -->
<path d="M {int(size*0.28)} {int(size*0.58)}
Q {int(size*0.5)} {int(size*0.35)}, {int(size*0.72)} {int(size*0.42)}"
fill="none" stroke="url(#mintGrad)" stroke-width="{max(2, int(size*0.018))}"
stroke-linecap="round" stroke-opacity="0.92"/>
<!-- Small mint dot at arc start -->
<circle cx="{int(size*0.28)}" cy="{int(size*0.58)}" r="{max(2, int(size*0.009))}"
fill="#A7F3D0" opacity="0.95"/>
<!-- Small mint dot at arc end -->
<circle cx="{int(size*0.72)}" cy="{int(size*0.42)}" r="{max(2, int(size*0.009))}"
fill="#A7F3D0" opacity="0.95"/>
<!-- Subtle sparkle at top-right of globe -->
<g transform="translate({int(size*0.62)}, {int(size*0.28)})" opacity="0.5">
<line x1="0" y1="-{int(size*0.02)}" x2="0" y2="{int(size*0.02)}"
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
<line x1="-{int(size*0.02)}" y1="0" x2="{int(size*0.02)}" y2="0"
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
</g>
<!-- Bottom subtle reflection -->
<rect x="0" y="{int(size*0.75)}" width="{size}" height="{int(size*0.25)}"
fill="url(#bgGrad)" opacity="0.3"/>
</g>
</svg>'''
def main():
os.makedirs(ICON_DIR, exist_ok=True)
# Write SVG
svg_content = generate_svg(1024)
svg_path = os.path.join(ICON_DIR, "icon_1024.svg")
with open(svg_path, "w") as f:
f.write(svg_content)
print(f"SVG written to {svg_path}")
# Try to convert to PNG using sips (built-in macOS tool) via a temp file
# First, check if we have rsvg-convert or cairosvg
converters = []
# Check for rsvg-convert (from librsvg)
if subprocess.run(["which", "rsvg-convert"], capture_output=True).returncode == 0:
converters.append("rsvg-convert")
# Check for python cairosvg
try:
import cairosvg
converters.append("cairosvg")
except ImportError:
pass
# Check for Inkscape
if subprocess.run(["which", "inkscape"], capture_output=True).returncode == 0:
converters.append("inkscape")
images = {}
if "rsvg-convert" in converters:
print("Using rsvg-convert for PNG generation...")
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
subprocess.run([
"rsvg-convert", "-w", str(s), "-h", str(s),
svg_path, "-o", out
], check=True)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
elif "cairosvg" in converters:
print("Using cairosvg for PNG generation...")
import cairosvg
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
cairosvg.svg2png(
bytestring=svg_content.encode(),
write_to=out,
output_width=s,
output_height=s
)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
elif "inkscape" in converters:
print("Using Inkscape for PNG generation...")
for s in SIZES:
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
subprocess.run([
"inkscape", svg_path,
"--export-type=png",
f"--export-filename={out}",
f"--export-width={s}",
f"--export-height={s}"
], check=True, capture_output=True)
images[f"icon_{s}x{s}.png"] = s
print(f" Generated {s}x{s}")
else:
print("WARNING: No SVG-to-PNG converter found.")
print("Install one of: librsvg (brew install librsvg), cairosvg (pip install cairosvg), or Inkscape")
print(f"Then run: cd {ICON_DIR} && rsvg-convert -w 1024 -h 1024 icon_1024.svg -o icon_1024x1024.png")
print("SVG file is ready for manual conversion.")
# Still write Contents.json with expected filenames
for s in SIZES:
images[f"icon_{s}x{s}.png"] = s
# Write Contents.json for Xcode
icon_images = []
for s in [16, 32, 128, 256, 512]:
# 1x
icon_images.append({
"filename": f"icon_{s}x{s}.png",
"idiom": "mac",
"scale": "1x",
"size": f"{s}x{s}"
})
# 2x
icon_images.append({
"filename": f"icon_{s*2}x{s*2}.png",
"idiom": "mac",
"scale": "2x",
"size": f"{s}x{s}"
})
contents = {
"images": icon_images,
"info": {
"author": "atlas-icon-generator",
"version": 1
}
}
contents_path = os.path.join(ICON_DIR, "Contents.json")
with open(contents_path, "w") as f:
json.dump(contents, f, indent=2)
print(f"Contents.json written to {contents_path}")
print("Done!")
if __name__ == "__main__":
main()