#!/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(" 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()