import { decode } from "blurhash"; export const DEFAULT_FALLBACK_BLUR_HASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj"; const cache = new Map(); /** * Decodes a BlurHash string into a Base64 PNG Data URL using browser Canvas API. * Uses local Map cache to avoid re-decoding identical hashes. */ export function blurHashToDataURL( blurHash?: string | null, width: number = 32, height: number = 32 ): string { const hash = blurHash && blurHash.trim().length > 5 ? blurHash : DEFAULT_FALLBACK_BLUR_HASH; const cacheKey = `${hash}_${width}x${height}`; if (cache.has(cacheKey)) { return cache.get(cacheKey)!; } if (typeof window === "undefined") { // SSR static fallback SVG placeholder return "data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23e5e7eb'/%3E%3C/svg%3E"; } try { const pixels = decode(hash, width, height); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) { return "data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23e5e7eb'/%3E%3C/svg%3E"; } const imageData = ctx.createImageData(width, height); imageData.data.set(pixels); ctx.putImageData(imageData, 0, 0); const dataUrl = canvas.toDataURL("image/png"); cache.set(cacheKey, dataUrl); return dataUrl; } catch (error) { console.warn("Failed to decode BlurHash, using default fallback:", error); if (hash !== DEFAULT_FALLBACK_BLUR_HASH) { return blurHashToDataURL(DEFAULT_FALLBACK_BLUR_HASH, width, height); } return "data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23e5e7eb'/%3E%3C/svg%3E"; } }