export const faviconSizes = [16, 32, 64, 180, 192, 512] as const; export type GeneratedFavicon = { size: (typeof faviconSizes)[number]; blob: Blob; previewUrl: string; }; function canvasBlob(canvas: HTMLCanvasElement) { return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("favicon_canvas_failed")), "image/png")); } export async function generateFavicons(file: File): Promise { const bitmap = await createImageBitmap(file); try { return await Promise.all(faviconSizes.map(async (size) => { const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const context = canvas.getContext("2d"); if (!context) throw new Error("favicon_context_failed"); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; const scale = Math.min(size / bitmap.width, size / bitmap.height); const width = bitmap.width * scale; const height = bitmap.height * scale; context.drawImage(bitmap, (size - width) / 2, (size - height) / 2, width, height); const blob = await canvasBlob(canvas); return { size, blob, previewUrl: URL.createObjectURL(blob) }; })); } finally { bitmap.close(); } } export async function createIcoBlob(images: GeneratedFavicon[]) { const selected = images.filter((item) => item.size === 16 || item.size === 32 || item.size === 64); const payloads = await Promise.all(selected.map((item) => item.blob.arrayBuffer())); const headerSize = 6 + selected.length * 16; const totalSize = headerSize + payloads.reduce((sum, payload) => sum + payload.byteLength, 0); const buffer = new ArrayBuffer(totalSize); const view = new DataView(buffer); view.setUint16(0, 0, true); view.setUint16(2, 1, true); view.setUint16(4, selected.length, true); let offset = headerSize; selected.forEach((item, index) => { const entry = 6 + index * 16; const payload = payloads[index]; view.setUint8(entry, item.size); view.setUint8(entry + 1, item.size); view.setUint8(entry + 2, 0); view.setUint8(entry + 3, 0); view.setUint16(entry + 4, 1, true); view.setUint16(entry + 6, 32, true); view.setUint32(entry + 8, payload.byteLength, true); view.setUint32(entry + 12, offset, true); new Uint8Array(buffer, offset, payload.byteLength).set(new Uint8Array(payload)); offset += payload.byteLength; }); return new Blob([buffer], { type: "image/x-icon" }); } export type FaviconAssetUrls = { ico?: string; apple?: string; icon192?: string; icon512?: string }; export function applyFaviconAssets(assets?: FaviconAssetUrls) { if (!assets) return; const setLink = (id: string, rel: string, href?: string, sizes?: string) => { if (!href) return; let link = document.querySelector(`link[data-nodedc-favicon="${id}"]`); if (!link) { link = document.createElement("link"); link.dataset.nodedcFavicon = id; link.rel = rel; document.head.append(link); } link.href = href; if (sizes) link.sizes = sizes; }; setLink("ico", "icon", assets.ico); setLink("apple", "apple-touch-icon", assets.apple, "180x180"); setLink("192", "icon", assets.icon192, "192x192"); setLink("512", "icon", assets.icon512, "512x512"); }