/** * Uygulama ikonlarını logodan üretir: npm run icons * * Kaynak: public/brand/icon.svg (varsa; önce 512×512 icon.png olarak render edilir) * veya doğrudan public/brand/icon.png (kare, en az 512×512). * Çıktılar (commit edilir): * public/brand/icon.png 512 px (icon.svg varsa ondan üretilir) * src/app/favicon.ico 16/32/48 px (PNG gömülü ICO) * src/app/icon.png 192 px → * src/app/apple-icon.png 180 px, saydamsız → iOS ana ekran * public/icons/icon-192.png PWA manifest * public/icons/icon-512.png PWA manifest * public/icons/maskable-512.png PWA manifest (purpose: maskable) * * public/brand/icon-maskable.svg (köşesiz, tam zemin) varsa maskable ve Apple ikonları ondan * üretilir; yoksa icon.png marka mavisi zemine %80 güvenli alanla yerleştirilir. * * Logoyu değiştirdikten sonra bu betiği yeniden çalıştırın. */ import { access, mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import sharp from "sharp"; const ROOT = process.cwd(); const SOURCE = path.join(ROOT, "public/brand/icon.png"); const SOURCE_SVG = path.join(ROOT, "public/brand/icon.svg"); const MASKABLE_SVG = path.join(ROOT, "public/brand/icon-maskable.svg"); /** Saydamlık kabul etmeyen ikonlarda arka plan: marka mavisi (--primary, açık tema). */ const BRAND_BG = "#105bbe"; async function png(size: number, opts: { flatten?: boolean; padding?: number } = {}) { const inner = Math.round(size * (1 - (opts.padding ?? 0) * 2)); const resized = await sharp(SOURCE).resize(inner, inner, { fit: "contain" }).png().toBuffer(); let img = sharp({ create: { width: size, height: size, channels: 4, background: opts.flatten || opts.padding ? BRAND_BG : { r: 0, g: 0, b: 0, alpha: 0 }, }, }).composite([{ input: resized, gravity: "center" }]); if (opts.flatten) img = img.flatten({ background: BRAND_BG }); return img.png({ compressionLevel: 9 }).toBuffer(); } /** PNG görüntülerinden ICO dosyası oluşturur (Vista+ tüm tarayıcılar PNG gömülü ICO destekler). */ function toIco(images: { size: number; data: Buffer }[]): Buffer { const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); // reserved header.writeUInt16LE(1, 2); // type: icon header.writeUInt16LE(images.length, 4); const entries: Buffer[] = []; let offset = 6 + images.length * 16; for (const { size, data } of images) { const e = Buffer.alloc(16); e.writeUInt8(size >= 256 ? 0 : size, 0); e.writeUInt8(size >= 256 ? 0 : size, 1); e.writeUInt8(0, 2); // palette e.writeUInt8(0, 3); // reserved e.writeUInt16LE(1, 4); // color planes e.writeUInt16LE(32, 6); // bpp e.writeUInt32LE(data.length, 8); e.writeUInt32LE(offset, 12); offset += data.length; entries.push(e); } return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]); } async function exists(file: string) { return access(file).then( () => true, () => false, ); } async function main() { if (await exists(SOURCE_SVG)) { // Vektörden yüksek yoğunlukta render: küçük boyutlarda da keskin kenarlar. const png512 = await sharp(SOURCE_SVG, { density: 288 }).resize(512, 512).png().toBuffer(); await writeFile(SOURCE, png512); console.log(`✓ public/brand/icon.png ← icon.svg (${png512.length} bayt)`); } const meta = await sharp(SOURCE).metadata(); if (!meta.width || !meta.height || meta.width !== meta.height || meta.width < 512) { throw new Error(`${SOURCE} kare ve en az 512×512 olmalı (şu an ${meta.width}×${meta.height}).`); } await mkdir(path.join(ROOT, "public/icons"), { recursive: true }); const fullBleed = (await exists(MASKABLE_SVG)) ? (size: number) => sharp(MASKABLE_SVG, { density: 288 }) .resize(size, size) .flatten({ background: BRAND_BG }) .png() .toBuffer() : null; const out: [string, Buffer][] = [ ["src/app/icon.png", await png(192)], [ "src/app/apple-icon.png", fullBleed ? await fullBleed(180) : await png(180, { flatten: true }), ], ["public/icons/icon-192.png", await png(192)], ["public/icons/icon-512.png", await png(512)], [ "public/icons/maskable-512.png", fullBleed ? await fullBleed(512) : await png(512, { padding: 0.1 }), ], [ "src/app/favicon.ico", toIco(await Promise.all([16, 32, 48].map(async (size) => ({ size, data: await png(size) })))), ], ]; for (const [file, data] of out) { await writeFile(path.join(ROOT, file), data); console.log(`✓ ${file} (${data.length} bayt)`); } } main().catch((err: unknown) => { console.error(err instanceof Error ? err.message : err); process.exit(1); });