// Erzeugt alle Raster-Icons aus den SVG-Vorlagen in public/brand/ (#002). // // Die PNGs und das favicon.ico werden eingecheckt, damit Build und Docker-Abbild // kein sharp zur Laufzeit brauchen. Nach jeder Logo-Aenderung: `pnpm icons`. import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import sharp from "sharp"; const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); const BRAND = join(WURZEL, "public/brand"); const ICONS = join(WURZEL, "public/icons"); const APP = join(WURZEL, "src/app"); // Seitenhintergrund (--bg); App-Icons stehen auf dieser Flaeche statt transparent, // weil iOS und Android transparente Bereiche sonst schwarz oder weiss fuellen. const HINTERGRUND = "#0D1013"; /** Rendert ein SVG scharf in der Zielgroesse (density skaliert die Vektoren statt Pixel). */ async function rendern(svg, groesse) { return sharp(svg, { density: Math.ceil((72 * groesse) / 32) * 2 }) .resize(groesse, groesse) .png() .toBuffer(); } /** Logo mittig auf --bg mit Rand, damit es auf Homescreens nicht an die Kante stoesst. */ async function aufFlaeche(svg, groesse, anteil) { const innen = Math.round(groesse * anteil); const logo = await rendern(svg, innen); return sharp({ create: { width: groesse, height: groesse, channels: 4, background: HINTERGRUND }, }) .composite([{ input: logo, gravity: "center" }]) .png() .toBuffer(); } /** Minimaler ICO-Container mit PNG-Eintraegen (seit Windows Vista von allen Browsern gelesen). */ function icoBauen(bilder) { const kopf = Buffer.alloc(6); kopf.writeUInt16LE(0, 0); kopf.writeUInt16LE(1, 2); kopf.writeUInt16LE(bilder.length, 4); const eintraege = []; let versatz = 6 + 16 * bilder.length; for (const { groesse, daten } of bilder) { const e = Buffer.alloc(16); e.writeUInt8(groesse >= 256 ? 0 : groesse, 0); e.writeUInt8(groesse >= 256 ? 0 : groesse, 1); e.writeUInt8(0, 2); e.writeUInt8(0, 3); e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(daten.length, 8); e.writeUInt32LE(versatz, 12); versatz += daten.length; eintraege.push(e); } return Buffer.concat([kopf, ...eintraege, ...bilder.map((b) => b.daten)]); } async function main() { await mkdir(ICONS, { recursive: true }); const logo = await readFile(join(BRAND, "logo.svg")); // Unter 32 px wird die reduzierte Variante ohne Zahlenring und Bolzen genutzt, // sonst verschwimmen die Details zu grauem Rauschen. const favicon = await readFile(join(BRAND, "favicon.svg")); const icon192 = await aufFlaeche(logo, 192, 0.72); const icon512 = await aufFlaeche(logo, 512, 0.72); const apple = await aufFlaeche(logo, 180, 0.72); const fav32 = await rendern(favicon, 32); const fav16 = await rendern(favicon, 16); const fav48 = await rendern(favicon, 48); await writeFile(join(ICONS, "icon-192.png"), icon192); await writeFile(join(ICONS, "icon-512.png"), icon512); await writeFile(join(ICONS, "apple-touch-icon.png"), apple); await writeFile(join(ICONS, "favicon-32.png"), fav32); await writeFile(join(ICONS, "favicon-16.png"), fav16); // Next-Metadaten-Dateien: favicon.ico, icon.svg und apple-icon.png in src/app // erzeugen die -Tags automatisch. await writeFile( join(APP, "favicon.ico"), icoBauen([ { groesse: 16, daten: fav16 }, { groesse: 32, daten: fav32 }, { groesse: 48, daten: fav48 }, ]), ); await copyFile(join(BRAND, "favicon.svg"), join(APP, "icon.svg")); await writeFile(join(APP, "apple-icon.png"), apple); console.log("Icons erzeugt: public/icons/*, src/app/favicon.ico, src/app/icon.svg, src/app/apple-icon.png"); } await main();