UNPKG

img-duplicates

Version:

Find duplicate images based on visual similarity

165 lines (163 loc) 4.93 kB
// src/lib.ts import path from "path"; import sharp2 from "sharp"; import fs2 from "fs/promises"; import createKDTree from "static-kdtree"; // src/utils.ts import sharp from "sharp"; import fs from "fs/promises"; import assert from "assert"; async function isFolder(path2) { try { return (await fs.stat(path2)).isDirectory(); } catch { return false; } } async function mapLimit(array, limit, iteratee) { const results = []; for (let i = 0; i < array.length; i += limit) { const batch = array.slice(i, i + limit); const batchResults = await Promise.all(batch.map(iteratee)); results.push(...batchResults); } return results; } async function pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } function isImageFile(path2) { return path2.endsWith(".png") || path2.endsWith(".jpg") || path2.endsWith(".jpeg") || path2.endsWith(".webp") || path2.endsWith(".gif") || path2.endsWith(".avif") || path2.endsWith(".tiff") || path2.endsWith(".tif") || path2.endsWith(".svg"); } function px(pixels, width, x, y) { const pixel = width * y + x; assert(pixel < pixels.length); return pixels[pixel]; } function binaryToHex(s) { let output = ""; for (let i = 0; i < s.length; i += 4) { const bytes = s.slice(i, i + 4); const decimal = parseInt(bytes, 2); const hex = decimal.toString(16); output += hex; } return Buffer.from(output, "hex"); } async function dhash(path2, hashSize = 8) { const height = hashSize; const width = height + 1; const pixels = await sharp(path2).grayscale().resize({ width, height, fit: "fill" }).raw().toBuffer(); let difference = ""; for (let row = 0; row < height; row++) { for (let col = 0; col < height; col++) { const left = px(pixels, width, col, row); const right = px(pixels, width, col + 1, row); difference += left < right ? 1 : 0; } } return binaryToHex(difference); } // src/lib.ts var defaultOptions = { hashSize: 8, maxDuplicates: 100, maxDistance: 5 }; async function findDuplicateImages(source, { hashSize = 8, maxDuplicates = 100, maxDistance = 5 } = defaultOptions) { let imageFilePaths; if (Array.isArray(source)) { const allImagePaths = []; for (const item of source) { if (!await pathExists(item)) { continue; } if (await isFolder(item)) { try { const files = await fs2.readdir(item); const imageFiles = files.filter(isImageFile).sort(); const imagePaths = imageFiles.map((f) => path.join(item, f)); allImagePaths.push(...imagePaths); } catch { continue; } } else if (isImageFile(item)) { allImagePaths.push(item); } } imageFilePaths = allImagePaths.sort(); } else { const files = await fs2.readdir(source); const imageFiles = files.filter(isImageFile).sort(); imageFilePaths = imageFiles.map((f) => path.join(source, f)); } const hashList = await mapLimit( imageFilePaths, 1, async (file) => { const hash = await dhash(file, hashSize); return [...hash]; } ); const tree = createKDTree(hashList); const duplicateIdxSet = /* @__PURE__ */ new Set(); const duplicates = []; for (let i = 0; i < hashList.length; i++) { if (duplicateIdxSet.has(i)) { continue; } const hash = hashList[i]; let duplicatesIdx = tree.knn(hash, maxDuplicates + 1, maxDistance); if (duplicatesIdx && duplicatesIdx.length > 1) { duplicatesIdx = duplicatesIdx.filter((idx) => !duplicateIdxSet.has(idx)); if (duplicatesIdx.length > 1) { const limitedDuplicates = duplicatesIdx.slice(0, maxDuplicates); const dups = limitedDuplicates.map((i2) => imageFilePaths[i2]); duplicates.push(dups); for (const idx of limitedDuplicates) { duplicateIdxSet.add(idx); } } } } const duplicatesWithMetadata = []; for (const files of duplicates) { const filesWithMetadata = []; for (const file of files) { if (await pathExists(file)) { try { const metadata = await sharp2(file).metadata(); filesWithMetadata.push({ path: file, width: metadata.width || 0, height: metadata.height || 0 }); } catch (error) { throw new Error(`Could not read metadata for ${file}:`, error); } } } filesWithMetadata.sort((a, b) => { const resolutionDiff = b.width * b.height - a.width * a.height; if (resolutionDiff !== 0) return resolutionDiff; return path.basename(a.path).localeCompare(path.basename(b.path)); }); if (filesWithMetadata.length > 0) { duplicatesWithMetadata.push(filesWithMetadata); } } return duplicatesWithMetadata; } export { findDuplicateImages as default }; //# sourceMappingURL=lib.mjs.map