// Renders the raster icons in public/ from their SVG sources with Inkscape. // // The browser tab uses public/favicon.svg directly, but Safari, the iOS home // screen, Android's PWA install and push notifications all want PNGs, and // those can't be transparent (iOS fills a transparent icon with black) or // edge-to-edge (Android crops adaptive icons to a circle or squircle). So // there are two sources: the bare mark for the 32px favicon, and the mark on // the app's stage colour for everything else, sized to sit inside the // maskable safe zone. // // Inkscape is used because it is already a fixture of the design workflow; // rendering with it means no image library in node_modules. Its installer // does not put it on PATH on Windows, so the standard install folders are // tried after PATH and an explicit INKSCAPE environment variable. // // Usage: node scripts/make-icons.mjs // INKSCAPE="C:\path\to\inkscape.com" node scripts/make-icons.mjs import { execFileSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; /** Every PNG the app ships, with the SVG it is rendered from. */ export const ICONS = [ { source: 'public/favicon.svg', out: 'public/favicon-32.png', size: 32 }, { source: 'scripts/icons/app-icon.svg', out: 'public/apple-touch-icon.png', size: 180 }, { source: 'scripts/icons/app-icon.svg', out: 'public/icon-192.png', size: 192 }, { source: 'scripts/icons/app-icon.svg', out: 'public/icon-512.png', size: 512 }, { source: 'scripts/icons/app-icon-maskable.svg', out: 'public/icon-512-maskable.png', size: 512 }, ]; /** Where Inkscape's installer puts the console binary when it is not on PATH. */ export const INKSCAPE_CANDIDATES = [ 'C:\\Program Files\\Inkscape\\bin\\inkscape.com', 'C:\\Program Files (x86)\\Inkscape\\bin\\inkscape.com', '/Applications/Inkscape.app/Contents/MacOS/inkscape', '/usr/bin/inkscape', '/usr/local/bin/inkscape', ]; /** * The Inkscape binary to run: the INKSCAPE variable if set, otherwise the * first standard location that exists, otherwise the bare name and PATH. */ export function findInkscape(env = process.env, exists = existsSync) { if (env.INKSCAPE) return env.INKSCAPE; const found = INKSCAPE_CANDIDATES.find((p) => exists(p)); return found ?? 'inkscape'; } /** The Inkscape 1.x command line for one PNG. */ export function exportArgs({ source, out, size }) { return [ source, '--export-type=png', `--export-filename=${out}`, `--export-width=${size}`, `--export-height=${size}`, ]; } export function makeIcons(root, run = execFileSync) { const inkscape = findInkscape(); for (const icon of ICONS) { const args = exportArgs({ source: resolve(root, icon.source), out: resolve(root, icon.out), size: icon.size, }); run(inkscape, args, { stdio: 'inherit' }); console.log(`[icons] ${icon.out} (${icon.size}px) <- ${icon.source}`); } } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); makeIcons(root); }