import { execFileSync } from "node:child_process"; import { readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { Resvg } from "@resvg/resvg-js"; const root = new URL("../", import.meta.url); const read = (path) => readFile(new URL(path, root), "utf8"); // Use the same official artwork at every size, preserving paths and transforms. const source = await read("assets/brand/lockup.svg"); const lockup = source.trim().replaceAll("fill-rule=", "fillRule=").replace("/)[0]; const bounds = birdBounds(mark); const scale = (512 * birdHeight) / bounds.height; const tx = 256 - scale * (bounds.x + bounds.width / 2); const ty = 256 - scale * (bounds.y + bounds.height / 2); const svg = ` ${bird} `; const png = new Resvg(svg, { fitTo: { mode: "width", value: size } }).render().asPng(); await writeFile(new URL(`public/icons/icon-${size}.png`, root), png); if (size <= 32) await writeFile(new URL(`public/icons/action-${size}.png`, root), png); if (size === 128) await writeFile(new URL("assets/store/store-icon.png", root), png); } // Measures the bird's opaque bounds on the mark's 512 canvas so it can be scaled // to the icon height without touching the official paths. function birdBounds(mark) { const svg = mark.replace('fill="currentColor"', 'color="#000" fill="currentColor"').replace(/ style="[^"]*"/, ""); const pixels = new Resvg(svg, { fitTo: { mode: "width", value: 512 } }).render().pixels; let minX = 512; let minY = 512; let maxX = 0; let maxY = 0; for (let y = 0; y < 512; y++) { for (let x = 0; x < 512; x++) { if (pixels[(y * 512 + x) * 4 + 3] > 40) { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } } } return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }