# -*- coding: utf-8 -*- """Рисует иконки приложения и пишет их в static/. Свой минимальный PNG-энкодер вместо Pillow: иконки генерируются один раз, и тащить зависимость в проект ради этого не хочется. Запуск: venv\\Scripts\\python.exe tools\\make_icons.py """ import os import zlib import struct from pathlib import Path OUT = Path(__file__).resolve().parent.parent / "static" BG = (0xFF, 0x7A, 0x9C) # розовый акцент интерфейса BOX = (0xFF, 0xFF, 0xFF) # передняя стенка RIM = (0xEC, 0xE2, 0xE6) # борта, видимые сверху — чуть темнее для объёма INSIDE = (0xC9, 0x3B, 0x63) # внутренность: коробка открыта, туда и кладут # Иконку на Android обрезают по маске (круг, скруглённый квадрат), поэтому всё # значимое держим внутри центральных ~80%: 10% с каждой стороны могут срезать. SAFE = 0.80 def png(path, size, pixels): """Пишет RGB-PNG размером size×size из плоского списка (r,g,b).""" raw = bytearray() for y in range(size): raw.append(0) # фильтр строки: none for x in range(size): raw += bytes(pixels[y * size + x]) def chunk(tag, data): body = tag + data return (struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)) ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8 бит, RGB with open(str(path), "wb") as f: f.write(b"\x89PNG\r\n\x1a\n") f.write(chunk(b"IHDR", ihdr)) f.write(chunk(b"IDAT", zlib.compress(bytes(raw), 9))) f.write(chunk(b"IEND", b"")) def draw(size): """Коробка с откинутыми клапанами — узнаётся даже в 32 пикселя.""" px = [BG] * (size * size) def rect(x0, y0, x1, y1, color): # Координаты в долях от размера: одна отрисовка работает для всех иконок. ax, ay = int(x0 * size), int(y0 * size) bx, by = int(x1 * size), int(y1 * size) for y in range(max(0, ay), min(size, by)): row = y * size for x in range(max(0, ax), min(size, bx)): px[row + x] = color def trapezoid(y0, y1, top_l, top_r, bot_l, bot_r, color): """Заливка трапеции: боковые края интерполируются по высоте. Именно она даёт перспективу — без неё коробка выглядит плоским окном. """ ay, by = int(y0 * size), int(y1 * size) if by <= ay: return for y in range(max(0, ay), min(size, by)): t = (y - ay) / float(by - ay) xl = int((top_l + (bot_l - top_l) * t) * size) xr = int((top_r + (bot_r - top_r) * t) * size) row = y * size for x in range(max(0, xl), min(size, xr)): px[row + x] = color m = (1.0 - SAFE) / 2.0 # отступ безопасной зоны s = SAFE def f(v): return m + v * s rim_top, rim_bot = f(0.13), f(0.44) # верхние борта, видимые сверху body_bot = f(0.90) # Борта: сверху уже, снизу шире — взгляд сверху под углом. trapezoid(rim_top, rim_bot, f(0.24), f(0.76), f(0.09), f(0.91), RIM) # Внутренность — вырез в бортах. Виден проём: коробка открыта. trapezoid(rim_top + 0.045 * s, rim_bot - 0.035 * s, f(0.31), f(0.69), f(0.19), f(0.81), INSIDE) # Передняя стенка. rect(f(0.09), rim_bot, f(0.91), body_bot, BOX) # Тонкая тень под кромкой — отделяет стенку от бортов. rect(f(0.09), rim_bot, f(0.91), rim_bot + 0.02 * s, RIM) return px def main(): OUT.mkdir(parents=True, exist_ok=True) for size, name in ((512, "icon-512.png"), (192, "icon-192.png"), (180, "apple-touch-icon.png"), (32, "favicon-32.png")): png(OUT / name, size, draw(size)) print(" %s %d×%d %d байт" % (name, size, size, (OUT / name).stat().st_size)) if __name__ == "__main__": main()