const THUMB_SIZE = 16; const DUPE_THRESH = 0.12; const hashCache = new Map(); function toGray(data: Uint8ClampedArray, pixels: number): Uint8ClampedArray { const gray = new Uint8ClampedArray(pixels); for (let i = 0; i < pixels; i++) { const o = i * 4; gray[i] = (data[o] * 299 + data[o + 1] * 587 + data[o + 2] * 114) / 1000; } return gray; } function loadThumb(url: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = canvas.height = THUMB_SIZE; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, THUMB_SIZE, THUMB_SIZE); resolve(toGray(ctx.getImageData(0, 0, THUMB_SIZE, THUMB_SIZE).data, THUMB_SIZE * THUMB_SIZE)); }; img.onerror = reject; img.src = url; }); } function similarity(a: Uint8ClampedArray, b: Uint8ClampedArray): number { let diff = 0; for (let i = 0; i < a.length; i++) diff += Math.abs(a[i] - b[i]); return diff / (a.length * 255); } export async function getHash(url: string): Promise { if (hashCache.has(url)) return hashCache.get(url)!; try { const thumb = await loadThumb(url); hashCache.set(url, thumb); return thumb; } catch { return null; } } export function areDuplicates(a: Uint8ClampedArray, b: Uint8ClampedArray): boolean { return similarity(a, b) <= DUPE_THRESH; } export function clearHashCache(): void { hashCache.clear(); }