Files
CloudRecoveryAS/packaging/scripts/make-icons.py
2026-06-30 16:40:01 -06:00

156 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Genera el icono de marca de CloudRestoreAS sin dependencias externas.
Dibuja una "C" (anillo con apertura a la derecha) sobre un cuadro redondeado azul
(#0078D7), con anti-aliasing por supersampling. Codifica los PNG a mano (zlib + CRC)
y ensambla un .ico multi-tamano (entradas PNG, soportadas por Windows Vista+).
Salida:
packaging/assets/tray-icon.png (256x256)
packaging/assets/tray-icon.ico (16,32,48,64,128,256)
Uso: python3 packaging/scripts/make-icons.py
Solo usa la libreria estandar; corre con cualquier Python 3.x.
"""
import math
import struct
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
ASSETS = ROOT / "packaging" / "assets"
ICO_SIZES = (16, 32, 48, 64, 128, 256)
PNG_SIZE = 256
# Colores
BG = (0, 120, 215) # azul #0078D7
FG = (255, 255, 255) # blanco
SS = 4 # factor de supersampling (anti-aliasing)
# Geometria (fracciones del lado)
CORNER = 0.22 # radio de esquina del cuadro
RING_OUTER = 0.34 # radio externo de la "C"
RING_INNER = 0.20 # radio interno de la "C"
GAP_HALF_DEG = 40.0 # medio angulo de la apertura de la "C" (a la derecha)
def _render_rgba(size: int) -> bytes:
"""Renderiza el icono a un buffer RGBA de size*size (4 bytes por pixel)."""
cx = cy = size / 2.0
half = size / 2.0
rc = CORNER * size
inner = RING_INNER * size
outer = RING_OUTER * size
cos_gap = math.cos(math.radians(GAP_HALF_DEG))
inv = 1.0 / (SS * SS)
out = bytearray(size * size * 4)
k = 0
for py in range(size):
for px in range(size):
r_acc = g_acc = b_acc = a_acc = 0.0
for sy in range(SS):
y = py + (sy + 0.5) / SS
dyc = y - cy
for sx in range(SS):
x = px + (sx + 0.5) / SS
# Cuadro redondeado (SDF): dentro si d <= 0
bx = abs(x - cx) - (half - rc)
by = abs(y - cy) - (half - rc)
mx = bx if bx > 0.0 else 0.0
my = by if by > 0.0 else 0.0
d = math.hypot(mx, my) + min(max(bx, by), 0.0) - rc
if d > 0.0:
continue # fuera del cuadro -> transparente
# Dentro del cuadro: base azul
dxc = x - cx
rr = math.hypot(dxc, dyc)
in_ring = inner <= rr <= outer
in_gap = dxc > rr * cos_gap # apertura a la derecha
if in_ring and not in_gap:
r_acc += FG[0]; g_acc += FG[1]; b_acc += FG[2]
else:
r_acc += BG[0]; g_acc += BG[1]; b_acc += BG[2]
a_acc += 255.0
out[k] = int(r_acc * inv + 0.5)
out[k + 1] = int(g_acc * inv + 0.5)
out[k + 2] = int(b_acc * inv + 0.5)
out[k + 3] = int(a_acc * inv + 0.5)
k += 4
return bytes(out)
def _png_chunk(tag: bytes, data: bytes) -> bytes:
return (
struct.pack(">I", len(data))
+ tag
+ data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
)
def _encode_png(size: int, rgba: bytes) -> bytes:
"""Codifica un buffer RGBA a bytes PNG (8 bits, color type 6)."""
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
stride = size * 4
raw = bytearray()
for row in range(size):
raw.append(0) # filtro None por scanline
raw += rgba[row * stride:(row + 1) * stride]
idat = zlib.compress(bytes(raw), 9)
return (
b"\x89PNG\r\n\x1a\n"
+ _png_chunk(b"IHDR", ihdr)
+ _png_chunk(b"IDAT", idat)
+ _png_chunk(b"IEND", b"")
)
def _build_ico(pngs: dict) -> bytes:
"""Ensambla un .ico con entradas PNG. pngs: {size: png_bytes}."""
sizes = sorted(pngs)
count = len(sizes)
header = struct.pack("<HHH", 0, 1, count)
offset = 6 + count * 16
entries = bytearray()
body = bytearray()
for size in sizes:
data = pngs[size]
entries += struct.pack(
"<BBBBHHII",
size if size < 256 else 0, # ancho (0 == 256)
size if size < 256 else 0, # alto
0, # paleta
0, # reservado
1, # planos
32, # bits por pixel
len(data),
offset,
)
body += data
offset += len(data)
return bytes(header + entries + body)
def main() -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
pngs = {}
for size in ICO_SIZES:
rgba = _render_rgba(size)
pngs[size] = _encode_png(size, rgba)
print(f" render {size}x{size} -> {len(pngs[size])} bytes PNG")
png_path = ASSETS / "tray-icon.png"
png_path.write_bytes(pngs[PNG_SIZE] if PNG_SIZE in pngs else _encode_png(PNG_SIZE, _render_rgba(PNG_SIZE)))
print(f"OK {png_path}")
ico_path = ASSETS / "tray-icon.ico"
ico_path.write_bytes(_build_ico(pngs))
print(f"OK {ico_path} ({len(ICO_SIZES)} tamanos)")
if __name__ == "__main__":
main()