"""@file make_icon.py @brief Генерация иконок программы без внешних библиотек. Иконка рисуется кодом (стопка страниц с корешком и лупой), чтобы в репозитории не заводить бинарный файл неизвестного происхождения. Делаются оба формата: ``ico/catalog.ico`` для Windows и ``ico/catalog.icns`` для macOS. Запуск: ``python scripts/make_icon.py``. """ from __future__ import annotations import struct import zlib from pathlib import Path ROOT = Path(__file__).resolve().parent.parent TARGET_ICO = ROOT / "ico" / "catalog.ico" TARGET_ICNS = ROOT / "ico" / "catalog.icns" #: Размеры изображений внутри .ico. SIZES = (16, 24, 32, 48, 64, 128, 256) #: Типы изображений ICNS и их размеры: macOS берёт подходящий сам. ICNS_TYPES = ((b"ic11", 32), (b"ic12", 64), (b"ic07", 128), (b"ic08", 256), (b"ic09", 512)) #: Служебные байты растровых форматов. TRANSPARENT = bytes(4) # прозрачный пиксель FILTER_NONE = bytes(1) # PNG: строка без фильтра PNG_MAGIC = bytes((137, 80, 78, 71, 13, 10, 26, 10)) BG = (0x1d, 0x28, 0x38) # тёмная плашка в цветах темы PAGE = (0xdc, 0xe6, 0xf2) # страница PAGE_DIM = (0x91, 0xa0, 0xb4) # страницы за первой SPINE = (0x15, 0x70, 0xd8) # синий корешок GLASS = (0x2f, 0x91, 0xff) # лупа def _round_corner(x: float, y: float, size: float, radius: float) -> bool: """@brief Точка внутри скруглённого квадрата 0..size?""" cx = min(max(x, radius), size - radius) cy = min(max(y, radius), size - radius) return (x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2 def _pixel(u: float, v: float) -> tuple[int, int, int] | None: """@brief Цвет точки в относительных координатах 0..1 (None — прозрачно).""" if not _round_corner(u, v, 1.0, 0.22): return None colour = BG # Три страницы уступом: задние приглушены. for shift, page in ((0.10, PAGE_DIM), (0.05, PAGE_DIM), (0.0, PAGE)): left, right = 0.22 + shift, 0.72 + shift top, bottom = 0.16 + shift, 0.80 + shift if left <= u <= right and top <= v <= bottom: colour = page if u <= left + 0.06 and page is PAGE: colour = SPINE # корешок у передней страницы # Лупа: кольцо и ручка. cx, cy, r = 0.66, 0.64, 0.19 d = ((u - cx) ** 2 + (v - cy) ** 2) ** 0.5 if r - 0.055 <= d <= r: colour = GLASS if 0.0 <= u - cx - 0.10 <= 0.055 and 0.0 <= v - cy - 0.10 <= 0.20: if abs((u - cx - 0.135) - (v - cy - 0.135)) < 0.035: colour = GLASS return colour # -------------------------------------------------------------------------- # Windows: .ico # -------------------------------------------------------------------------- def _bgra_bottom_up(size: int) -> bytes: """@brief Растр BGRA снизу вверх — так его хранит формат ICO.""" rows = [] for y in range(size - 1, -1, -1): row = bytearray() for x in range(size): px = _pixel((x + 0.5) / size, (y + 0.5) / size) if px: r, g, b = px row += bytes((b, g, r, 255)) else: row += TRANSPARENT rows.append(bytes(row)) return b"".join(rows) def _ico_entry(size: int) -> bytes: """@brief Одно изображение в формате BMP-в-ICO вместе с пустой маской.""" header = struct.pack(" Path: """@brief Пишет многоразмерный .ico и возвращает путь к нему.""" images = [_ico_entry(size) for size in SIZES] offset = 6 + 16 * len(images) out = bytearray(struct.pack(" bytes: """@brief Растр RGBA сверху вниз, каждая строка с байтом фильтра PNG.""" rows = [] for y in range(size): row = bytearray(FILTER_NONE) for x in range(size): px = _pixel((x + 0.5) / size, (y + 0.5) / size) if px: r, g, b = px row += bytes((r, g, b, 255)) else: row += TRANSPARENT rows.append(bytes(row)) return b"".join(rows) def _png(size: int) -> bytes: """@brief PNG без внешних библиотек: zlib и CRC из стандартной поставки.""" def chunk(tag: bytes, data: bytes) -> bytes: body = tag + data return (struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)) # Ширина, высота, 8 бит на канал, тип 6 (RGBA), без чересстрочности. header = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0) return (PNG_MAGIC + chunk(b"IHDR", header) + chunk(b"IDAT", zlib.compress(_rgba_top_down(size), 9)) + chunk(b"IEND", b"")) def build_icns(target: Path = TARGET_ICNS) -> Path: """@brief Пишет .icns из PNG-изображений нескольких размеров.""" parts = [] for tag, size in ICNS_TYPES: data = _png(size) parts.append(tag + struct.pack(">I", len(data) + 8) + data) body = b"".join(parts) target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(b"icns" + struct.pack(">I", len(body) + 8) + body) return target if __name__ == "__main__": print(build_ico()) print(build_icns())