#!/usr/bin/env python3 """ Generate optimized icon PNGs (32/64/128/256) with a tighter composition and rebuild icon.ico. This script avoids external imaging dependencies by parsing and writing PNGs directly. """ 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_TRANSPARENT = ICON_DIR / "logo-raven.png" BACKGROUND_SOURCE = ICON_DIR / "logo-raven-fund-azul.png" TARGET_SIZES = [32, 64, 128, 256, 512] FILL_RATIO = 0.9 # portion of the canvas occupied by the raven figure 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 chunks: list[bytes] = [] 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, ignored (validated by reader) 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": chunks.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(f"{path} missing IHDR") raw = zlib.decompress(b"".join(chunks)) 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: # Sub for i in range(stride): left = row[i - bpp] if i >= bpp else 0 row[i] = (row[i] + left) & 0xFF elif filter_type == 2: # Up for i in range(stride): row[i] = (row[i] + prev[i]) & 0xFF elif filter_type == 3: # Average 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: # Paeth 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: row_pixels = [] if color_type == 6: for i in range(0, len(row), 4): row_pixels.append(tuple(row[i : i + 4])) else: for i in range(0, len(row), 3): r, g, b = row[i : i + 3] row_pixels.append((r, g, b, 255)) pixels.append(row_pixels) 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) # no filter 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 crop_content(pixels: list[list[tuple[int, int, int, int]]]) -> list[list[tuple[int, int, int, int]]]: height = len(pixels) width = len(pixels[0]) min_x, min_y = width, height max_x = max_y = -1 for y, row in enumerate(pixels): for x, (_, _, _, a) in enumerate(row): if a > 0: if x < min_x: min_x = x if y < min_y: min_y = y if x > max_x: max_x = x if y > max_y: max_y = y if max_x < min_x or max_y < min_y: return pixels cropped = [row[min_x : max_x + 1] for row in pixels[min_y : max_y + 1]] return cropped def bilinear_sample(data: list[list[tuple[int, int, int, int]]], x: float, y: float) -> tuple[int, int, int, int]: height = len(data) width = len(data[0]) if x < 0: x = 0 if y < 0: y = 0 if x > width - 1: x = width - 1 if y > height - 1: y = height - 1 x0 = int(math.floor(x)) x1 = min(x0 + 1, width - 1) y0 = int(math.floor(y)) 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 = data[y0][x0][channel] c10 = data[y0][x1][channel] c01 = data[y1][x0][channel] c11 = data[y1][x1][channel] top = lerp(c00, c10, dx) bottom = lerp(c01, c11, dx) value = lerp(top, bottom, dy) result.append(int(round(value))) return tuple(result) def resize_with_background( src: list[list[tuple[int, int, int, int]]], canvas_size: int, fill_ratio: float, background: tuple[int, int, int], ) -> list[list[tuple[int, int, int, int]]]: src_h = len(src) src_w = len(src[0]) scale = fill_ratio * min(canvas_size / src_w, canvas_size / src_h) dest_w = max(1, int(round(src_w * scale))) dest_h = max(1, int(round(src_h * scale))) offset_x = (canvas_size - dest_w) // 2 offset_y = (canvas_size - dest_h) // 2 canvas = [ [(background[0], background[1], background[2], 255) for _ in range(canvas_size)] for _ in range(canvas_size) ] for dy in range(dest_h): src_y = (dy + 0.5) / scale - 0.5 for dx in range(dest_w): src_x = (dx + 0.5) / scale - 0.5 r, g, b, a = bilinear_sample(src, src_x, src_y) canvas[offset_y + dy][offset_x + dx] = (r, g, b, a) 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) entry = { "width": width if width < 256 else 0, "height": height if height < 256 else 0, "colors": 0, "reserved": 0, "planes": 1, "bit_count": 32, "size": len(data), "offset": offset, "data": data, } entries.append(entry) offset += len(data) header = struct.pack(" None: base_w, base_h, base_pixels = read_png(BASE_TRANSPARENT) background_w, background_h, bg_pixels = read_png(BACKGROUND_SOURCE) background_color = bg_pixels[background_h // 2][background_w // 2][:3] cropped = crop_content(base_pixels) generated: list[Path] = [] for size in TARGET_SIZES: dest_pixels = resize_with_background(cropped, size, FILL_RATIO, background_color) out_path = ICON_DIR / f"icon-{size}.png" write_png(out_path, size, size, dest_pixels) generated.append(out_path) print(f"Generated {out_path} ({size}x{size})") # Update base PNG used by some bundlers biggest = max(generated, key=lambda p: int(p.stem.split("-")[-1])) (ICON_DIR / "icon.png").write_bytes(biggest.read_bytes()) # Build ICO with largest size first (common preference) ordered = 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", ordered) print("icon.ico rebuilt with revised artwork.") if __name__ == "__main__": main()