/** * Rasterizes the favicon artwork and writes favicon-png.generated.ts, so that * serving a favicon needs no rasterizer at runtime. Running this when the * output already matches leaves the file alone, which is what lets a test * check that the committed output is current. */ import { Resvg } from "@resvg/resvg-js"; import { faviconSvg } from "./favicon-artwork.ts"; import { FAVICON_FACES, type FaviconFace } from "./favicon-types.ts"; const OUTPUT = new URL("./favicon-png.generated.ts", import.meta.url); const BASE64_LINE_LENGTH = 80; function renderFavicons(): Record { const rendered = {} as Record; for (const face of FAVICON_FACES) { rendered[face] = new Uint8Array( new Resvg(faviconSvg(face), { font: { loadSystemFonts: false }, fitTo: { mode: "width", value: 32 }, }).render().asPng(), ); } return rendered; } async function assetVersion( rendered: Record, ): Promise { const encoded = FAVICON_FACES.map((face) => `${face}\0${rendered[face].toBase64()}` ).join("\0"); const digest = new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(encoded)), ); return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join( "", ); } function pngProperty(face: FaviconFace, png: Uint8Array): string { const encoded = png.toBase64(); const chunks: string[] = []; for (let offset = 0; offset < encoded.length; offset += BASE64_LINE_LENGTH) { chunks.push(encoded.slice(offset, offset + BASE64_LINE_LENGTH)); } const value = chunks.map((chunk, index) => { const indent = index === 0 ? " " : " "; const suffix = index === chunks.length - 1 ? "" : " +"; return `${indent}${JSON.stringify(chunk)}${suffix}`; }).join("\n"); return ` ${JSON.stringify(face)}: decodePng(\n${value},\n ),`; } async function generatedSource( rendered: Record, ): Promise { const version = await assetVersion(rendered); const properties = FAVICON_FACES.map((face) => pngProperty(face, rendered[face]) ).join("\n"); return `/** * Holds every favicon face as a PNG, rendered from the source artwork in * favicon-artwork.ts. Generated by \`deno task regenerate-favicons\`, which * overwrites whatever is here. */ function decodePng(encoded: string): Uint8Array { return Uint8Array.fromBase64(encoded); } export const FAVICON_VERSION = ${JSON.stringify(version)}; export const FAVICON_PNG = { ${properties} } as const; `; } async function currentOutput( output: string | URL, ): Promise { try { return await Deno.readTextFile(output); } catch (error) { if (error instanceof Deno.errors.NotFound) return undefined; throw error; } } export async function regenerateFavicons( output: string | URL = OUTPUT, ): Promise { const source = await generatedSource(renderFavicons()); if (source === await currentOutput(output)) { console.log("Dashboard favicons are already up to date."); return false; } await Deno.writeTextFile(output, source); console.log(`Regenerated ${FAVICON_FACES.length} dashboard favicons.`); return true; } if (import.meta.main) await regenerateFavicons();