Files
NODEDC_DESIGN_GUIDELINE/apps/catalog/src/favicon.ts
T

85 lines
3.3 KiB
TypeScript

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<Blob>((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("favicon_canvas_failed")), "image/png"));
}
export async function generateFavicons(file: File): Promise<GeneratedFavicon[]> {
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<HTMLLinkElement>(`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");
}