// Builds every brand image of the two frontends from the two source SVGs in
// the repo root (see docs/BRANDING.md):
// logo.svg black mark on transparent — pages, favicons, PWA icons
// logo_bk.svg the same mark on a white rounded card — dark backdrops
//
// node scripts/generate-brand-icons.cjs
//
// sharp is resolved from apps/api, which already depends on it.
const { createRequire } = require('node:module');
const { readFileSync, writeFileSync } = require('node:fs');
const { join } = require('node:path');
const root = join(__dirname, '..');
const sharp = createRequire(join(root, 'apps/api/package.json'))('sharp');
const userApp = join(root, 'apps/user-app-v2');
const admin = join(root, 'apps/admin-panel');
// Illustrator's "
Asset 1" is noise in an .
const clean = (file) => readFileSync(join(root, file), 'utf8').replace(/\s*[^<]*<\/title>/, '');
const logo = clean('logo.svg');
const card = clean('logo_bk.svg');
/** The mark rendered `width` px wide, transparent. */
const mark = (width) => sharp(Buffer.from(logo), { density: 600 }).resize({ width }).png().toBuffer();
/**
* Square icon: the mark centred on white. `fill` is the mark's width as a
* share of the side; `radius` rounds the white square (0 = full bleed, as the
* OS masks PWA icons itself).
*/
async function icon(size, { fill, radius }) {
const r = Math.round(size * radius);
const plate = Buffer.from(
``,
);
return sharp(plate)
.composite([{ input: await mark(Math.round(size * fill)), gravity: 'center' }])
.png()
.toBuffer();
}
/** .ico holding PNG entries (supported by every current browser). */
function ico(pngs) {
const header = Buffer.alloc(6);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(pngs.length, 4);
const entries = [];
let offset = 6 + 16 * pngs.length;
for (const { size, data } of pngs) {
const e = Buffer.alloc(16);
e.writeUInt8(size >= 256 ? 0 : size, 0);
e.writeUInt8(size >= 256 ? 0 : size, 1);
e.writeUInt16LE(1, 4);
e.writeUInt16LE(32, 6);
e.writeUInt32LE(data.length, 8);
e.writeUInt32LE(offset, 12);
offset += data.length;
entries.push(e);
}
return Buffer.concat([header, ...entries, ...pngs.map((p) => p.data)]);
}
function write(path, data) {
writeFileSync(path, data);
console.log('wrote', path.slice(root.length + 1));
}
(async () => {
write(join(userApp, 'public/img/brand-logo.svg'), logo);
write(join(userApp, 'public/img/brand-logo-card.svg'), card);
write(join(userApp, 'src/assets/img/brand-logo.svg'), logo);
write(join(admin, 'src/assets/images/logo/brand-logo.svg'), logo);
// Raster copies at the old public URLs, for anything linking them directly.
const big = await mark(1024);
write(join(userApp, 'public/img/brand-logo.png'), big);
write(join(userApp, 'public/img/brand-logo.webp'), await sharp(big).webp({ quality: 90 }).toBuffer());
// PWA: one file serves "any" and "maskable", so the mark stays inside the
// maskable safe circle (80% of the side): a 1.45:1 mark at 64% width does.
write(join(userApp, 'public/icon-192.png'), await icon(192, { fill: 0.64, radius: 0 }));
write(join(userApp, 'public/icon-512.png'), await icon(512, { fill: 0.64, radius: 0 }));
// Tab icons: white rounded chip, so the black mark shows on dark tab strips too.
const chip = { fill: 0.86, radius: 0.2 };
const favicon = ico(await Promise.all([16, 32, 48, 64].map(async (size) => ({ size, data: await icon(size, chip) }))));
write(join(userApp, 'public/favicon.ico'), favicon);
write(join(admin, 'public/favicon.ico'), favicon);
write(join(admin, 'public/favicon.png'), await icon(192, chip));
})().catch((e) => {
console.error(e);
process.exit(1);
});