1#!/usr/bin/env node
2/* Menurunkan ikon raster dari lambang vektor (DESIGN-SYSTEM §8.1):
3 src/app/apple-icon.png dan src/app/favicon.ico dari public/brand/snapme-mark.svg
4 dan src/app/icon.svg. Jalankan `npm run brand:icons` setiap kali salah satu
5 berkas SVG itu berubah, lalu commit hasilnya.
6
7 Diturunkan lewat skrip, bukan digambar dua kali, supaya ukuran kecil tidak
8 pernah menyimpang dari lambang besarnya. Berkas hasilnya tetap ikut di-commit
9 (ukurannya beberapa kilobyte saja dan `next build` membacanya sebagai
10 konvensi berkas `app/`), jadi pemasangan tidak bergantung pada sharp. */
11import { readFileSync, writeFileSync } from "node:fs"
12
13import sharp from "sharp"
14
15const MARK = "public/brand/snapme-mark.svg"
16/* Lambang untuk ukuran kecil: satu kartu saja, bukan kipas empat kartu
17 (docs/DESIGN-SYSTEM.md §8.1). */
18const ICON = "src/app/icon.svg"
19const APPLE = "src/app/apple-icon.png"
20const FAVICON = "src/app/favicon.ico"
21const ICO_SIZES = [16, 32, 48]
22const APPLE_SIZE = 180
23
24/* Digambar pada 600 dpi (64 px x 600/72 = 533 px) lalu diperkecil: garis tepi
25 3 px di dalam lambang tetap tajam setelah penskalaan turun. */
26const DENSITY = 600
27
28async function png(source, size) {
29 return sharp(readFileSync(source), { density: DENSITY })
30 .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
31 .png()
32 .toBuffer()
33}
34
35/* ICO dengan muatan PNG (didukung Windows Vista ke atas dan semua peramban
36 yang memasang favicon). Berkas lama dari `create-next-app` memakai muatan
37 BMP dan jauh lebih besar untuk gambar yang sama. */
38function ico(entries) {
39 const header = Buffer.alloc(6)
40 header.writeUInt16LE(0, 0) // reserved
41 header.writeUInt16LE(1, 2) // 1 = ikon
42 header.writeUInt16LE(entries.length, 4)
43
44 let offset = header.length + entries.length * 16
45 const directory = Buffer.alloc(entries.length * 16)
46 entries.forEach(({ size, data }, index) => {
47 const at = index * 16
48 directory.writeUInt8(size >= 256 ? 0 : size, at)
49 directory.writeUInt8(size >= 256 ? 0 : size, at + 1)
50 directory.writeUInt8(0, at + 2) // jumlah warna palet: 0 untuk truecolor
51 directory.writeUInt8(0, at + 3) // reserved
52 directory.writeUInt16LE(1, at + 4) // bidang
53 directory.writeUInt16LE(32, at + 6) // bit per piksel
54 directory.writeUInt32LE(data.length, at + 8)
55 directory.writeUInt32LE(offset, at + 12)
56 offset += data.length
57 })
58
59 return Buffer.concat([header, directory, ...entries.map((entry) => entry.data)])
60}
61
62const entries = []
63for (const size of ICO_SIZES) {
64 entries.push({ size, data: await png(ICON, size) })
65}
66
67writeFileSync(FAVICON, ico(entries))
68writeFileSync(APPLE, await png(MARK, APPLE_SIZE))
69console.log(`brand: ${FAVICON} (${ICO_SIZES.join(", ")} px), ${APPLE} (${APPLE_SIZE} px)`)
70