- assets/: script generate_logo.py + generate_ls_images.py per produrre
loghi (mark/horizontal light+dark/stacked light+dark) e immagini
prodotto Lemon Squeezy (thumbnail 1024x1024, cover 1920x1080,
OG 1200x630). Riproducibili: rilanciare gli script per rigenerare.
- landing/download.html: pagina con form email+chiave o autofill da
query string ?key=&email= (link 1-click dall'email). Bottoni
Mac/Windows + istruzioni installazione per OS.
- landing/index.html: link Download in nav, hero ("Hai gia
acquistato?"), pricing card ("Hai gia una licenza?"), footer.
- landing/js/main.js: rimosso warning placeholder LS (URL ora reale).
- server/src/downloads.js: endpoint GET /api/download/:platform
che verifica (key, email) contro DB e redirect a URL firmato del
binario. Errori chiari (400/401/403/503) leggibili dall'utente.
- server/src/email.js: template Resend con 2 bottoni 1-click
(link con key+email gia compilati).
- server/src/server.js: route aggiunta.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""Genera i loghi brand MusicTools partendo dall'icona master.
|
|
|
|
Produce:
|
|
- logo_mark.png 1024x1024 solo squircle + nota, bg trasparente
|
|
- logo_horizontal_dark.png 2400x720 mark + "MusicTools" testo bianco (per bg scuri)
|
|
- logo_horizontal_light.png 2400x720 mark + "MusicTools" testo nero (per bg chiari)
|
|
- logo_stacked.png 1280x1600 mark sopra + testo sotto, bianco (per bg scuri)
|
|
|
|
Lo squircle riuso quello generato da generate_icon.py (assets/icon.png),
|
|
cosi' resta coerente con l'icona dell'app.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ICON_PATH = HERE / "icon.png"
|
|
|
|
ARIAL_BLACK = "/System/Library/Fonts/Supplemental/Arial Black.ttf"
|
|
ARIAL_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
|
|
|
|
WHITE = (255, 255, 255)
|
|
BLACK = (10, 10, 10)
|
|
|
|
|
|
def font(size: int, *, black: bool = True) -> ImageFont.FreeTypeFont:
|
|
path = ARIAL_BLACK if black else ARIAL_BOLD
|
|
try:
|
|
return ImageFont.truetype(path, size)
|
|
except OSError:
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def load_mark(target_size: int) -> Image.Image:
|
|
"""Carica l'icona master e la riscala alla dimensione richiesta.
|
|
|
|
L'icona originale ha sfondo trasparente fuori dallo squircle
|
|
(vedi generate_icon.py), quindi va bene per overlay su qualunque bg.
|
|
"""
|
|
if not ICON_PATH.exists():
|
|
raise FileNotFoundError(
|
|
f"icon.png non trovata: rilancia prima 'python3 assets/generate_icon.py'"
|
|
)
|
|
icon = Image.open(ICON_PATH).convert("RGBA")
|
|
if icon.size != (target_size, target_size):
|
|
icon = icon.resize((target_size, target_size), Image.LANCZOS)
|
|
return icon
|
|
|
|
|
|
def measure_text(text: str, f: ImageFont.FreeTypeFont) -> tuple[int, int]:
|
|
"""Ritorna (larghezza, altezza) effettiva del testo."""
|
|
bbox = f.getbbox(text)
|
|
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
|
|
def draw_text_top_left(canvas: Image.Image, text: str, top_left: tuple[int, int],
|
|
f: ImageFont.FreeTypeFont, color: tuple) -> None:
|
|
"""Disegna testo con angolo top-left = top_left (compensa font bbox)."""
|
|
draw = ImageDraw.Draw(canvas, "RGBA")
|
|
bbox = f.getbbox(text)
|
|
x = top_left[0] - bbox[0]
|
|
y = top_left[1] - bbox[1]
|
|
draw.text((x, y), text, font=f, fill=(*color, 255))
|
|
|
|
|
|
def make_mark(path: Path) -> None:
|
|
mark = load_mark(1024)
|
|
mark.save(path, "PNG", optimize=True)
|
|
print(f"[ok] mark -> {path}")
|
|
|
|
|
|
def make_horizontal(path: Path, text_color: tuple) -> None:
|
|
"""Layout: mark a sinistra + testo. Canvas si autodimensiona al testo."""
|
|
H = 720
|
|
mark_size = int(H * 0.92)
|
|
pad_left = 60
|
|
gap = 70
|
|
pad_right = 80
|
|
|
|
text = "MusicTools"
|
|
f = font(260, black=True)
|
|
tw, th = measure_text(text, f)
|
|
|
|
W = pad_left + mark_size + gap + tw + pad_right
|
|
|
|
canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
|
|
# Mark a sinistra, centrato verticalmente
|
|
mark = load_mark(mark_size)
|
|
mark_y = (H - mark_size) // 2
|
|
canvas.alpha_composite(mark, (pad_left, mark_y))
|
|
|
|
# Testo allineato top-left calcolato in modo che la sua baseline
|
|
# sia visivamente centrata sull'altezza dello squircle.
|
|
text_x = pad_left + mark_size + gap
|
|
text_y = (H - th) // 2
|
|
draw_text_top_left(canvas, text, (text_x, text_y), f, text_color)
|
|
|
|
canvas.save(path, "PNG", optimize=True)
|
|
name = "dark" if text_color == WHITE else "light"
|
|
print(f"[ok] horizontal ({name}) -> {path} [{W}x{H}]")
|
|
|
|
|
|
def make_stacked(path: Path, text_color: tuple) -> None:
|
|
"""Mark sopra + 'MusicTools' sotto. Canvas autodimensionato al testo."""
|
|
mark_size = 1024
|
|
f = font(180, black=True)
|
|
text = "MusicTools"
|
|
tw, th = measure_text(text, f)
|
|
|
|
pad = 80
|
|
gap = 50
|
|
W = max(mark_size, tw) + 2 * pad
|
|
H = pad + mark_size + gap + th + pad
|
|
|
|
canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
|
|
mark = load_mark(mark_size)
|
|
mark_x = (W - mark_size) // 2
|
|
canvas.alpha_composite(mark, (mark_x, pad))
|
|
|
|
text_x = (W - tw) // 2
|
|
text_y = pad + mark_size + gap
|
|
draw_text_top_left(canvas, text, (text_x, text_y), f, text_color)
|
|
|
|
canvas.save(path, "PNG", optimize=True)
|
|
name = "dark" if text_color == WHITE else "light"
|
|
print(f"[ok] stacked ({name}) -> {path} [{W}x{H}]")
|
|
|
|
|
|
def main() -> None:
|
|
make_mark(HERE / "logo_mark.png")
|
|
make_horizontal(HERE / "logo_horizontal_dark.png", WHITE) # per sfondi scuri
|
|
make_horizontal(HERE / "logo_horizontal_light.png", BLACK) # per sfondi chiari
|
|
make_stacked(HERE / "logo_stacked_dark.png", WHITE) # per sfondi scuri
|
|
make_stacked(HERE / "logo_stacked_light.png", BLACK) # per sfondi chiari
|
|
# rimuovo eventuale vecchio file senza suffisso
|
|
old = HERE / "logo_stacked.png"
|
|
if old.exists():
|
|
old.unlink()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|