85 lines
3.3 KiB
TypeScript
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");
|
|
}
|