Files
butcem/scripts/generate-icons.ts
T
suphicakir 381214c2cb feat(brand): BÜTÇEM logosu ve ikon seti
- public/brand: logo.svg, logo-dark.svg, icon.svg, icon-maskable.svg (yazı Noto Sans Bold yolları, OFL)
- npm run icons artık icon.svg / icon-maskable.svg'den üretir; maskable ve Apple ikonları köşesiz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:44:59 +03:00

126 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 → <link rel="icon">
* 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);
});