feature/build-y-ui-contraible
This commit is contained in:
155
packaging/scripts/make-icons.py
Normal file
155
packaging/scripts/make-icons.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user