/** * Erzeugt Favicon/PWA-Icons aus dem Craftvia-Signet (src/components/brand/craftvia-logo.tsx, * Geometrie `SIGNET`). Keine Netzwerkzugriffe; PNGs via `sharp` (liegt über Next.js bei). * * Lauf: npx tsx scripts/generate-brand-icons.ts * Ausgabe: public/favicon/*.svg|png, public/favicon.ico (PNG-in-ICO, 32×32) */ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import sharp from "sharp"; import { BRAND_COLORS } from "../src/lib/brand"; import { SIGNET } from "../src/components/brand/craftvia-logo"; const OUT = join(process.cwd(), "public", "favicon"); mkdirSync(OUT, { recursive: true }); function signetSvg(opts: { tile: boolean; padding?: number; radius?: number }): string { const arc = opts.tile ? BRAND_COLORS.white : BRAND_COLORS.lotsenblau; const check = BRAND_COLORS.signalorange; const pad = opts.padding ?? 9.6; const scale = (64 - 2 * pad) / 64; const bg = opts.tile ? `` : ""; return ` ${bg} `; } async function png(svg: string, size: number, file: string) { await sharp(Buffer.from(svg), { density: 72 * (size / 64) * 2 }) .resize(size, size) .png() .toFile(join(OUT, file)); } /** Minimaler ICO-Container mit einem eingebetteten PNG (von allen Browsern unterstützt). */ function icoFromPng(pngBuf: Buffer, size: number): Buffer { const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(1, 4); const entry = Buffer.alloc(16); entry.writeUInt8(size >= 256 ? 0 : size, 0); entry.writeUInt8(size >= 256 ? 0 : size, 1); entry.writeUInt8(0, 2); entry.writeUInt8(0, 3); entry.writeUInt16LE(1, 4); entry.writeUInt16LE(32, 6); entry.writeUInt32LE(pngBuf.length, 8); entry.writeUInt32LE(6 + 16, 12); return Buffer.concat([header, entry, pngBuf]); } async function main() { // Favicon: freistehendes Signet (transparent) als SVG — skaliert verlustfrei. const plain = signetSvg({ tile: false, padding: 2 }); writeFileSync(join(OUT, "craftvia-signet.svg"), plain); // App-Icon-Kachel (Lotsenblau) für PWA/Apple/OG. const tile = signetSvg({ tile: true }); writeFileSync(join(OUT, "craftvia-app-icon.svg"), tile); // Maskable: volle Fläche ohne Rundung, Signet in der Safe-Zone (80 %). const maskable = signetSvg({ tile: true, padding: 14, radius: 0 }); await png(plain, 16, "icon-16.png"); await png(plain, 32, "icon-32.png"); await png(plain, 48, "icon-48.png"); await png(tile, 180, "apple-touch-icon-180.png"); await png(tile, 192, "pwa-192.png"); await png(tile, 512, "pwa-512.png"); await png(maskable, 512, "maskable-512.png"); const ico32 = await sharp(Buffer.from(plain), { density: 144 }).resize(32, 32).png().toBuffer(); writeFileSync(join(process.cwd(), "public", "favicon.ico"), icoFromPng(ico32, 32)); console.log("✓ Craftvia-Icons erzeugt in public/favicon (+ public/favicon.ico)"); } main().catch((e) => { console.error(e); process.exit(1); });