#!/usr/bin/env python3 """ Generate icon PNGs/ICO for the desktop installer using the high-resolution Raven artwork. The script reads the square logo (`logo-raven-fund-azul.png`) and resizes it to the target sizes with a simple bilinear filter implemented with the Python standard library, avoiding additional dependencies. """ from __future__ import annotations import math import struct import zlib from binascii import crc32 from pathlib import Path ICON_DIR = Path(__file__).resolve().parents[1] / "src-tauri" / "icons" BASE_IMAGE = ICON_DIR / "logo-raven-fund-azul.png" TARGET_SIZES = [32, 64, 128, 256, 512] def read_png(path: Path) -> tuple[int, int, list[list[tuple[int, int, int, int]]]]: data = path.read_bytes() if not data.startswith(b"\x89PNG\r\n\x1a\n"): raise ValueError(f"{path} is not a PNG") pos = 8 width = height = bit_depth = color_type = None compressed_parts = [] while pos < len(data): length = struct.unpack(">I", data[pos : pos + 4])[0] pos += 4 ctype = data[pos : pos + 4] pos += 4 chunk = data[pos : pos + length] pos += length pos += 4 # CRC if ctype == b"IHDR": width, height, bit_depth, color_type, _, _, _ = struct.unpack(">IIBBBBB", chunk) if bit_depth != 8 or color_type not in (2, 6): raise ValueError("Only 8-bit RGB/RGBA PNGs are supported") elif ctype == b"IDAT": compressed_parts.append(chunk) elif ctype == b"IEND": break if width is None or height is None or bit_depth is None or color_type is None: raise ValueError("PNG missing IHDR chunk") raw = zlib.decompress(b"".join(compressed_parts)) bpp = 4 if color_type == 6 else 3 stride = width * bpp rows = [] idx = 0 prev = bytearray(stride) for _ in range(height): filter_type = raw[idx] idx += 1 row = bytearray(raw[idx : idx + stride]) idx += stride if filter_type == 1: for i in range(stride): left = row[i - bpp] if i >= bpp else 0 row[i] = (row[i] + left) & 0xFF elif filter_type == 2: for i in range(stride): row[i] = (row[i] + prev[i]) & 0xFF elif filter_type == 3: for i in range(stride): left = row[i - bpp] if i >= bpp else 0 up = prev[i] row[i] = (row[i] + ((left + up) // 2)) & 0xFF elif filter_type == 4: for i in range(stride): left = row[i - bpp] if i >= bpp else 0 up = prev[i] up_left = prev[i - bpp] if i >= bpp else 0 p = left + up - up_left pa = abs(p - left) pb = abs(p - up) pc = abs(p - up_left) if pa <= pb and pa <= pc: pr = left elif pb <= pc: pr = up else: pr = up_left row[i] = (row[i] + pr) & 0xFF elif filter_type not in (0,): raise ValueError(f"Unsupported PNG filter type {filter_type}") rows.append(bytes(row)) prev[:] = row pixels: list[list[tuple[int, int, int, int]]] = [] for row in rows: if color_type == 6: pixels.append([tuple(row[i : i + 4]) for i in range(0, len(row), 4)]) else: pixels.append([tuple(row[i : i + 3] + b"\xff") for i in range(0, len(row), 3)]) return width, height, pixels def write_png(path: Path, width: int, height: int, pixels: list[list[tuple[int, int, int, int]]]) -> None: raw = bytearray() for row in pixels: raw.append(0) # filter type 0 for r, g, b, a in row: raw.extend((r & 0xFF, g & 0xFF, b & 0xFF, a & 0xFF)) compressed = zlib.compress(raw, level=9) def chunk(name: bytes, payload: bytes) -> bytes: return ( struct.pack(">I", len(payload)) + name + payload + struct.pack(">I", crc32(name + payload) & 0xFFFFFFFF) ) ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) out = bytearray(b"\x89PNG\r\n\x1a\n") out += chunk(b"IHDR", ihdr) out += chunk(b"IDAT", compressed) out += chunk(b"IEND", b"") path.write_bytes(out) def bilinear_sample(pixels: list[list[tuple[int, int, int, int]]], x: float, y: float) -> tuple[int, int, int, int]: height = len(pixels) width = len(pixels[0]) x = min(max(x, 0.0), width - 1.0) y = min(max(y, 0.0), height - 1.0) x0 = int(math.floor(x)) y0 = int(math.floor(y)) x1 = min(x0 + 1, width - 1) y1 = min(y0 + 1, height - 1) dx = x - x0 dy = y - y0 def lerp(a: float, b: float, t: float) -> float: return a + (b - a) * t result = [] for channel in range(4): c00 = pixels[y0][x0][channel] c10 = pixels[y0][x1][channel] c01 = pixels[y1][x0][channel] c11 = pixels[y1][x1][channel] top = lerp(c00, c10, dx) bottom = lerp(c01, c11, dx) result.append(int(round(lerp(top, bottom, dy)))) return tuple(result) def resize_image(pixels: list[list[tuple[int, int, int, int]]], target: int) -> list[list[tuple[int, int, int, int]]]: src_height = len(pixels) src_width = len(pixels[0]) scale = min(target / src_width, target / src_height) dest_width = max(1, int(round(src_width * scale))) dest_height = max(1, int(round(src_height * scale))) offset_x = (target - dest_width) // 2 offset_y = (target - dest_height) // 2 background = (0, 0, 0, 0) canvas = [[background for _ in range(target)] for _ in range(target)] for dy in range(dest_height): src_y = (dy + 0.5) / scale - 0.5 for dx in range(dest_width): src_x = (dx + 0.5) / scale - 0.5 canvas[offset_y + dy][offset_x + dx] = bilinear_sample(pixels, src_x, src_y) return canvas def build_ico(output: Path, png_paths: list[Path]) -> None: entries = [] offset = 6 + 16 * len(png_paths) for path in png_paths: data = path.read_bytes() width, height, _ = read_png(path) entries.append( { "width": width if width < 256 else 0, "height": height if height < 256 else 0, "size": len(data), "offset": offset, "payload": data, } ) offset += len(data) header = struct.pack(" None: width, height, pixels = read_png(BASE_IMAGE) if width != height: raise ValueError("Base icon must be square") generated: list[Path] = [] for size in TARGET_SIZES: resized = resize_image(pixels, size) out_path = ICON_DIR / f"icon-{size}.png" write_png(out_path, size, size, resized) generated.append(out_path) print(f"Generated {out_path} ({size}x{size})") largest = max(generated, key=lambda p: int(p.stem.split("-")[-1])) (ICON_DIR / "icon.png").write_bytes(largest.read_bytes()) ico_sources = sorted( [p for p in generated if int(p.stem.split("-")[-1]) <= 256], key=lambda p: int(p.stem.split("-")[-1]), ) build_ico(ICON_DIR / "icon.ico", ico_sources) print("icon.ico rebuilt.") if __name__ == "__main__": main()