177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Generate Montana .icns from СИМВОЛ_ВРЕМЕНИ.PNG — exact colors, 3D volume, rounded corners."""
|
||
|
||
from PIL import Image, ImageDraw, ImageFilter
|
||
import subprocess, os, shutil
|
||
|
||
SIZE = 1024
|
||
CORNER = 228 # macOS squircle ~22%
|
||
|
||
|
||
def rounded_rect_mask(size, radius):
|
||
scale = 4
|
||
big = Image.new('L', (size[0]*scale, size[1]*scale), 0)
|
||
d = ImageDraw.Draw(big)
|
||
d.rounded_rectangle([(0, 0), (size[0]*scale-1, size[1]*scale-1)], radius=radius*scale, fill=255)
|
||
return big.resize(size, Image.LANCZOS)
|
||
|
||
|
||
def extract_symbol(src):
|
||
"""Extract only the gold symbol as RGBA with transparent background."""
|
||
w, h = src.size
|
||
px = src.load()
|
||
|
||
# Find bbox of non-black pixels
|
||
threshold = 25
|
||
min_x, min_y, max_x, max_y = w, h, 0, 0
|
||
for y in range(h):
|
||
for x in range(w):
|
||
r, g, b = px[x, y][:3]
|
||
if r > threshold or g > threshold or b > threshold:
|
||
min_x = min(min_x, x)
|
||
min_y = min(min_y, y)
|
||
max_x = max(max_x, x)
|
||
max_y = max(max_y, y)
|
||
|
||
print(f"Symbol bbox: ({min_x}, {min_y}, {max_x+1}, {max_y+1})")
|
||
|
||
# Extract just the symbol with transparency
|
||
sw = max_x - min_x + 1
|
||
sh = max_y - min_y + 1
|
||
symbol = Image.new('RGBA', (sw, sh), (0, 0, 0, 0))
|
||
spx = symbol.load()
|
||
|
||
for y in range(sh):
|
||
for x in range(sw):
|
||
r, g, b = px[min_x + x, min_y + y][:3]
|
||
brightness = max(r, g, b)
|
||
if brightness > threshold:
|
||
spx[x, y] = (r, g, b, 255)
|
||
else:
|
||
spx[x, y] = (0, 0, 0, 0)
|
||
|
||
return symbol
|
||
|
||
|
||
def create_icon(source_path):
|
||
src = Image.open(source_path).convert('RGBA')
|
||
symbol = extract_symbol(src)
|
||
cw, ch = symbol.size
|
||
print(f"Symbol size: {cw}x{ch}")
|
||
|
||
# Scale symbol to ~55% of icon
|
||
target = int(SIZE * 0.55)
|
||
scale = target / max(cw, ch)
|
||
new_w = int(cw * scale)
|
||
new_h = int(ch * scale)
|
||
symbol = symbol.resize((new_w, new_h), Image.LANCZOS)
|
||
|
||
# === BACKGROUND: pure black with subtle depth gradient ===
|
||
canvas = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 255))
|
||
|
||
# Subtle radial gradient from center (very subtle warmth)
|
||
for y in range(SIZE):
|
||
for x in range(SIZE):
|
||
dx = (x - SIZE * 0.45) / SIZE
|
||
dy = (y - SIZE * 0.4) / SIZE
|
||
dist = (dx*dx + dy*dy) ** 0.5
|
||
v = max(0, int(10 * (1 - dist * 1.5)))
|
||
canvas.putpixel((x, y), (v, v, v + 1, 255))
|
||
|
||
# === SYMBOL PLACEMENT ===
|
||
sx = (SIZE - new_w) // 2
|
||
sy = (SIZE - new_h) // 2
|
||
|
||
# Drop shadow
|
||
shadow = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||
shadow.paste(symbol, (sx + 6, sy + 8), symbol)
|
||
# Darken shadow
|
||
shpx = shadow.load()
|
||
for y in range(SIZE):
|
||
for x in range(SIZE):
|
||
r, g, b, a = shpx[x, y]
|
||
if a > 0:
|
||
shpx[x, y] = (0, 0, 0, min(a, 90))
|
||
shadow = shadow.filter(ImageFilter.GaussianBlur(radius=18))
|
||
canvas = Image.alpha_composite(canvas, shadow)
|
||
|
||
# Main symbol
|
||
canvas.paste(symbol, (sx, sy), symbol)
|
||
|
||
# Top highlight (lighter gold on upper part of symbol for 3D)
|
||
hl = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||
hl.paste(symbol, (sx, sy), symbol)
|
||
hlpx = hl.load()
|
||
for y in range(SIZE):
|
||
for x in range(SIZE):
|
||
r, g, b, a = hlpx[x, y]
|
||
if a > 0:
|
||
local_y = y - sy
|
||
progress = local_y / new_h if new_h > 0 else 1
|
||
if progress < 0.45:
|
||
alpha = int(40 * (1 - progress / 0.45))
|
||
hlpx[x, y] = (255, 245, 220, alpha)
|
||
else:
|
||
hlpx[x, y] = (0, 0, 0, 0)
|
||
else:
|
||
hlpx[x, y] = (0, 0, 0, 0)
|
||
canvas = Image.alpha_composite(canvas, hl)
|
||
|
||
# === INNER BEVEL (3D container like Developer icon) ===
|
||
bevel = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||
bd = ImageDraw.Draw(bevel)
|
||
# Top-left light edge
|
||
bd.rounded_rectangle([(2, 2), (SIZE-3, SIZE-3)], radius=CORNER-2,
|
||
outline=(255, 255, 255, 15), width=1)
|
||
canvas = Image.alpha_composite(canvas, bevel)
|
||
|
||
# Bottom-right shadow edge
|
||
bevel2 = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||
bd2 = ImageDraw.Draw(bevel2)
|
||
bd2.rounded_rectangle([(4, 4), (SIZE-1, SIZE-1)], radius=CORNER-2,
|
||
outline=(0, 0, 0, 30), width=1)
|
||
canvas = Image.alpha_composite(canvas, bevel2)
|
||
|
||
# === ROUNDED CORNERS ===
|
||
mask = rounded_rect_mask((SIZE, SIZE), CORNER)
|
||
final = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||
final.paste(canvas, (0, 0), mask)
|
||
|
||
return final
|
||
|
||
|
||
def make_icns(img, output_dir):
|
||
iconset = os.path.join(output_dir, 'Montana.iconset')
|
||
if os.path.exists(iconset):
|
||
shutil.rmtree(iconset)
|
||
os.makedirs(iconset)
|
||
|
||
sizes = [16, 32, 64, 128, 256, 512, 1024]
|
||
for s in sizes:
|
||
resized = img.resize((s, s), Image.LANCZOS)
|
||
resized.save(os.path.join(iconset, f'icon_{s}x{s}.png'))
|
||
if s <= 512:
|
||
resized2x = img.resize((s * 2, s * 2), Image.LANCZOS)
|
||
resized2x.save(os.path.join(iconset, f'icon_{s}x{s}@2x.png'))
|
||
|
||
icns_path = os.path.join(output_dir, 'Montana.icns')
|
||
subprocess.run(['iconutil', '-c', 'icns', iconset, '-o', icns_path], check=True)
|
||
shutil.rmtree(iconset)
|
||
print(f'Created: {icns_path}')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
source = os.path.join(script_dir, '..', '..', 'Русский', 'Генезис', 'СИМВОЛ_ВРЕМЕНИ.PNG')
|
||
source = os.path.normpath(source)
|
||
|
||
print(f'Source: {source}')
|
||
icon = create_icon(source)
|
||
|
||
png_path = os.path.join(script_dir, 'Montana_icon_1024.png')
|
||
icon.save(png_path)
|
||
print(f'Preview: {png_path}')
|
||
|
||
make_icns(icon, script_dir)
|
||
print('Done!')
|