Files
OmniRoute/scripts/ad-hoc/generate-brand-icons.mjs
gogo 10e5dd81e0 fix(branding): regenerate raster favicons — white mark was shipped without its gradient tile (#7390)
* fix(branding): regenerate raster favicons — white mark was shipped without its gradient tile

favicon.ico, icon-512.png and apple-touch-icon.png contained only the
white network mark on a transparent background: every opaque pixel was
pure #FFFFFF, with no trace of the red gradient tile that favicon.svg
and apple-touch-icon.svg define. On light browser tab strips and
bookmark bars the icon therefore rendered as a blank white square.

Regenerate all three assets from the canonical SVG sources:

- favicon.ico: same 7 frames as before (16–256), each rasterized from
  the vector at native size, stored as PNG frames — 141 KB → 16 KB
- icon-512.png: rendered from favicon.svg with the gradient tile
- apple-touch-icon.png: rendered from apple-touch-icon.svg at the
  180×180 size layout.tsx declares (the shipped file was 512×512)

Add scripts/ad-hoc/generate-brand-icons.mjs so the raster assets can be
reproduced from the SVGs (byte-identical) instead of drifting again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(branding): keep standalone PNGs truecolor; palette-quantize only ICO frames

Review feedback (gemini-code-assist): 8-bit palette quantization
measurably clips the antialiased gradient on large assets — 1242 → 253
distinct colors at 512px. Make palette opt-in per render: ICO frames
keep it (16 KB container), icon-512.png and apple-touch-icon.png are
now truecolor (+9 KB total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:18:45 -03:00

78 lines
3.1 KiB
JavaScript
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.
// Regenerates the raster brand icons in public/ from their SVG sources, so the
// PNG/ICO assets can never drift from the canonical vector artwork again.
//
// public/favicon.svg → public/favicon.ico (16/32/48/64/96/128/256, PNG frames)
// public/favicon.svg → public/icon-512.png
// public/apple-touch-icon.svg → public/apple-touch-icon.png (180×180, as declared in layout.tsx)
//
// Usage:
// npm i --no-save sharp
// node scripts/ad-hoc/generate-brand-icons.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
let sharp;
try {
sharp = (await import("sharp")).default;
} catch {
console.error("sharp is required: npm i --no-save sharp");
process.exit(1);
}
const publicDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "public");
const faviconSvg = readFileSync(join(publicDir, "favicon.svg"));
const appleSvg = readFileSync(join(publicDir, "apple-touch-icon.svg"));
// favicon.svg is authored on a 32×32 viewBox; scale density so each target
// size is rendered from the vector instead of upscaling a small raster.
// Palette quantization (8-bit, ≤256 colors) is only applied where asked:
// it keeps the ICO frames tiny, but measurably clips the antialiased
// gradient on large standalone PNGs (1242 → 253 distinct colors at 512px),
// so those stay truecolor.
const renderPng = (svg, viewBox, size, usePalette = false) =>
sharp(svg, { density: 72 * (size / viewBox) })
.resize(size, size)
.png({ compressionLevel: 9, palette: usePalette, effort: 10 })
.toBuffer();
// ICO container with PNG-compressed frames (identical layout to the icon the
// package previously shipped, but ~16 KB instead of ~141 KB).
function buildIco(frames) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserved
header.writeUInt16LE(1, 2); // type: icon
header.writeUInt16LE(frames.length, 4);
let offset = 6 + 16 * frames.length;
const entries = frames.map(({ size, png }) => {
const e = Buffer.alloc(16);
e.writeUInt8(size === 256 ? 0 : size, 0); // width (0 means 256)
e.writeUInt8(size === 256 ? 0 : size, 1); // height
e.writeUInt16LE(1, 4); // color planes
e.writeUInt16LE(32, 6); // bits per pixel
e.writeUInt32LE(png.length, 8);
e.writeUInt32LE(offset, 12);
offset += png.length;
return e;
});
return Buffer.concat([header, ...entries, ...frames.map((f) => f.png)]);
}
const icoSizes = [16, 32, 48, 64, 96, 128, 256];
const frames = [];
for (const size of icoSizes) {
frames.push({ size, png: await renderPng(faviconSvg, 32, size, true) });
}
writeFileSync(join(publicDir, "favicon.ico"), buildIco(frames));
writeFileSync(join(publicDir, "icon-512.png"), await renderPng(faviconSvg, 32, 512));
// layout.tsx declares apple-touch-icon.png as 180×180; its SVG source is
// authored on a 180×180 viewBox.
writeFileSync(join(publicDir, "apple-touch-icon.png"), await renderPng(appleSvg, 180, 180));
console.log(`favicon.ico (${icoSizes.join("/")}), icon-512.png, apple-touch-icon.png regenerated`);