img-duplicates
Version:
Find duplicate images based on visual similarity
1 lines • 27.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/cli.ts","../src/lib.ts","../src/utils.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { resolve } from \"node:path\";\nimport { access, unlink } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport findDuplicateImages from \"./lib\";\n\ninterface CLIOptions {\n source: string[];\n hashSize: number;\n maxDuplicates: number;\n maxDistance: number;\n delete: boolean;\n forceDelete: boolean;\n help: boolean;\n version: boolean;\n}\n\nconst HELP_TEXT = `\nDuplicate Image Finder\n\nUsage: img-duplicates [options] <source...>\n\nArguments:\n source One or more directories or image files to search for duplicates\n\nOptions:\n -h, --hash-size <size> Hash size for perceptual hashing (default: 8)\n -d, --max-duplicates <num> Maximum number of duplicates to find per image (default: 100)\n -m, --max-distance <dist> Maximum distance for similarity matching (default: 5)\n --delete Delete duplicate images (keeps highest resolution, asks for confirmation)\n --force-delete Delete duplicate images without confirmation\n --help Show this help message\n --version Show version number\n\nExamples:\n img-duplicates /path/to/images\n img-duplicates /path/to/dir1 /path/to/dir2\n img-duplicates --hash-size 16 --max-distance 3 /path/to/images\n img-duplicates /path/to/image1.jpg /path/to/image2.png /path/to/dir\n img-duplicates --delete /path/to/images\n img-duplicates --force-delete /path/to/images\n\nNote: When using --delete or --force-delete, the image with the highest resolution\n in each duplicate group will be kept, and all others will be deleted.\n`;\n\nasync function parseArguments(): Promise<CLIOptions> {\n try {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n \"hash-size\": {\n type: \"string\",\n short: \"h\",\n },\n \"max-duplicates\": {\n type: \"string\",\n short: \"d\",\n },\n \"max-distance\": {\n type: \"string\",\n short: \"m\",\n },\n delete: {\n type: \"boolean\",\n },\n \"force-delete\": {\n type: \"boolean\",\n },\n help: {\n type: \"boolean\",\n },\n version: {\n type: \"boolean\",\n },\n },\n allowPositionals: true,\n });\n\n return {\n source: positionals,\n hashSize: values[\"hash-size\"] ? parseInt(values[\"hash-size\"]) : 8,\n maxDuplicates: values[\"max-duplicates\"]\n ? parseInt(values[\"max-duplicates\"])\n : 100,\n maxDistance: values[\"max-distance\"]\n ? parseInt(values[\"max-distance\"])\n : 5,\n delete: values.delete || false,\n forceDelete: values[\"force-delete\"] || false,\n help: values.help || false,\n version: values.version || false,\n };\n } catch (error) {\n console.error(\"Error parsing arguments:\", error.message);\n process.exit(1);\n }\n}\n\nasync function validatePaths(paths: string[]): Promise<string[]> {\n const resolvedPaths: string[] = [];\n\n for (const path of paths) {\n const resolvedPath = resolve(path);\n try {\n await access(resolvedPath);\n resolvedPaths.push(resolvedPath);\n } catch (error) {\n console.error(`Error: Path does not exist: ${path}`);\n process.exit(1);\n }\n }\n\n return resolvedPaths;\n}\n\nfunction validateOptions(options: CLIOptions): void {\n if (options.hashSize < 1 || options.hashSize > 32) {\n console.error(\"Error: Hash size must be between 1 and 32\");\n process.exit(1);\n }\n\n if (options.maxDuplicates < 1) {\n console.error(\"Error: Max duplicates must be at least 1\");\n process.exit(1);\n }\n\n if (options.maxDistance < 0) {\n console.error(\"Error: Max distance must be non-negative\");\n process.exit(1);\n }\n\n if (options.delete && options.forceDelete) {\n console.error(\n \"Error: Cannot use both --delete and --force-delete at the same time\"\n );\n process.exit(1);\n }\n}\n\nfunction formatResults(\n duplicates: Array<Array<{ path: string; width: number; height: number }>>,\n showDelete: boolean = false\n): void {\n if (duplicates.length === 0) {\n console.log(\"No duplicate images found.\");\n return;\n }\n\n console.log(`Found ${duplicates.length} group(s) of duplicate images:\\n`);\n\n duplicates.forEach((group, index) => {\n console.log(`Group ${index + 1}:`);\n group.forEach((image, imageIndex) => {\n const resolution = `${image.width}x${image.height}`;\n let marker = \"\";\n if (imageIndex === 0) {\n marker = showDelete\n ? \"(highest resolution - will be kept)\"\n : \"(highest resolution)\";\n } else if (showDelete) {\n marker = \"(will be deleted)\";\n }\n console.log(` ${image.path} [${resolution}] ${marker}`);\n });\n console.log(\"\");\n });\n}\n\nasync function askForConfirmation(message: string): Promise<boolean> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n return new Promise((resolve) => {\n rl.question(`${message} (y/N): `, (answer) => {\n rl.close();\n resolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n });\n });\n}\n\nasync function deleteDuplicates(\n duplicates: Array<Array<{ path: string; width: number; height: number }>>,\n forceDelete: boolean = false\n): Promise<void> {\n if (duplicates.length === 0) {\n return;\n }\n\n const filesToDelete: string[] = [];\n\n // Collect all files to delete (skip the first one in each group as it has the highest resolution)\n duplicates.forEach((group) => {\n for (let i = 1; i < group.length; i++) {\n filesToDelete.push(group[i].path);\n }\n });\n\n if (filesToDelete.length === 0) {\n console.log(\"No files to delete.\");\n return;\n }\n\n console.log(`\\nAbout to delete ${filesToDelete.length} duplicate image(s):`);\n filesToDelete.forEach((file) => console.log(` ${file}`));\n console.log(\"\");\n\n let shouldDelete = forceDelete;\n if (!forceDelete) {\n shouldDelete = await askForConfirmation(\n \"Do you want to proceed with deletion?\"\n );\n }\n\n if (!shouldDelete) {\n console.log(\"Deletion cancelled.\");\n return;\n }\n\n let deletedCount = 0;\n let errorCount = 0;\n\n for (const file of filesToDelete) {\n try {\n await unlink(file);\n console.log(`Deleted: ${file}`);\n deletedCount++;\n } catch (error) {\n console.error(`Failed to delete ${file}: ${error.message}`);\n errorCount++;\n }\n }\n\n console.log(\n `\\nDeletion completed: ${deletedCount} files deleted, ${errorCount} errors.`\n );\n}\n\nasync function main(): Promise<void> {\n const options = await parseArguments();\n\n if (options.help) {\n console.log(HELP_TEXT);\n process.exit(0);\n }\n\n if (options.version) {\n console.log(\"1.0.2\");\n process.exit(0);\n }\n\n if (options.source.length === 0) {\n console.error(\"Error: No source paths provided\");\n console.log(HELP_TEXT);\n process.exit(1);\n }\n\n validateOptions(options);\n const validatedPaths = await validatePaths(options.source);\n\n try {\n console.log(\"Searching for duplicate images...\");\n\n const duplicates = await findDuplicateImages(validatedPaths, {\n hashSize: options.hashSize,\n maxDuplicates: options.maxDuplicates,\n maxDistance: options.maxDistance,\n });\n\n const shouldDelete = options.delete || options.forceDelete;\n formatResults(duplicates, shouldDelete);\n\n if (shouldDelete) {\n await deleteDuplicates(duplicates, options.forceDelete);\n }\n } catch (error) {\n console.error(\"Error finding duplicates:\", error.message);\n process.exit(1);\n }\n}\n\n// Run the CLI\nmain().catch((error) => {\n console.error(\"Unexpected error:\", error);\n process.exit(1);\n});\n","import path from \"node:path\";\nimport sharp from \"sharp\";\nimport fs from \"node:fs/promises\";\nimport createKDTree from \"static-kdtree\";\nimport { dhash, isFolder, isImageFile, mapLimit, pathExists } from \"./utils\";\n\n/**\n * Configuration options for the duplicate image detection algorithm.\n */\ninterface Options {\n /**\n * Size of the hash grid used for perceptual hashing.\n * Larger values provide more precision but require more processing time.\n * @default 8\n */\n hashSize?: number;\n\n /**\n * Maximum number of duplicate images to find for each source image.\n * @default 100\n */\n maxDuplicates?: number;\n\n /**\n * Maximum Hamming distance between image hashes to consider them as duplicates.\n * Lower values require higher visual similarity.\n * @default 5\n */\n maxDistance?: number;\n}\n\nconst defaultOptions = {\n hashSize: 8,\n maxDuplicates: 100,\n maxDistance: 5,\n};\n\n/**\n * Finds duplicate images based on visual similarity using perceptual hashing.\n *\n * This function analyzes images using difference hashing (dHash) to create perceptual fingerprints\n * and then uses a k-d tree for efficient similarity matching. Images are considered duplicates\n * if their hash distance is within the specified threshold.\n *\n * @param source - Path to a directory containing images, or an array of paths to directories and/or image files\n * @param options - Configuration options for the duplicate detection algorithm\n * @param options.hashSize - Size of the hash grid (default: 8). Larger values provide more precision but slower processing\n * @param options.maxDuplicates - Maximum number of duplicates to find per image (default: 100)\n * @param options.maxDistance - Maximum Hamming distance between hashes to consider images as duplicates (default: 5). Lower values require higher similarity\n *\n * @returns Promise that resolves to an array of duplicate groups. Each group is an array of image objects\n * containing path, width, and height. Images within each group are sorted by resolution (highest first).\n *\n * @throws {Error} When image metadata cannot be read or when source paths don't exist\n *\n * @example\n * ```typescript\n * // Find duplicates in a single directory\n * const duplicates = await findDuplicateImages('/path/to/images');\n *\n * // Find duplicates across multiple sources with custom settings\n * const duplicates = await findDuplicateImages(\n * ['/path/to/dir1', '/path/to/dir2', '/path/to/image.jpg'],\n * {\n * hashSize: 16, // Higher precision\n * maxDistance: 3, // Stricter similarity\n * maxDuplicates: 5 // Limit results\n * }\n * );\n *\n * // Process results\n * duplicates.forEach((group, index) => {\n * console.log(`Duplicate group ${index + 1}:`);\n * group.forEach((image, i) => {\n * const isHighest = i === 0 ? '(highest resolution)' : '';\n * console.log(` ${image.path} [${image.width}x${image.height}] ${isHighest}`);\n * });\n * });\n * ```\n *\n * @since 1.0.0\n */\nexport default async function findDuplicateImages(\n source: string | string[],\n {\n hashSize = 8,\n maxDuplicates = 100,\n maxDistance = 5,\n }: Options = defaultOptions\n): Promise<Array<Array<{ path: string; width: number; height: number }>>> {\n let imageFilePaths: string[];\n\n if (Array.isArray(source)) {\n // If source is an array of file and directory paths, process each item\n const allImagePaths: string[] = [];\n\n for (const item of source) {\n // Check if the path exists first\n if (!(await pathExists(item))) {\n continue;\n }\n\n if (await isFolder(item)) {\n // If it's a directory, read files and add image paths\n try {\n const files = await fs.readdir(item);\n const imageFiles = files.filter(isImageFile).sort(); // Sort for consistent ordering\n const imagePaths = imageFiles.map((f) => path.join(item, f));\n allImagePaths.push(...imagePaths);\n } catch {\n // Skip directories that can't be read\n continue;\n }\n } else if (isImageFile(item)) {\n // If it's an image file, add it directly\n allImagePaths.push(item);\n }\n }\n\n imageFilePaths = allImagePaths.sort(); // Sort for consistent ordering\n } else {\n // If source is a directory path, read files and create full paths\n const files = await fs.readdir(source);\n const imageFiles = files.filter(isImageFile).sort(); // Sort for consistent ordering\n imageFilePaths = imageFiles.map((f) => path.join(source, f));\n }\n const hashList: Array<Array<number>> = await mapLimit(\n imageFilePaths,\n 1,\n async (file: string): Promise<Array<number>> => {\n const hash = await dhash(file, hashSize);\n return [...hash];\n }\n );\n\n const tree = createKDTree(hashList);\n\n const duplicateIdxSet = new Set();\n const duplicates: Array<Array<string>> = [];\n\n for (let i = 0; i < hashList.length; i++) {\n if (duplicateIdxSet.has(i)) {\n continue;\n }\n\n const hash = hashList[i];\n let duplicatesIdx = tree.knn(hash, maxDuplicates + 1, maxDistance); // +1 to account for the original image\n\n if (duplicatesIdx && duplicatesIdx.length > 1) {\n // Filter out any indices that have already been processed\n duplicatesIdx = duplicatesIdx.filter((idx) => !duplicateIdxSet.has(idx));\n\n // Only add if we still have more than one duplicate after filtering\n if (duplicatesIdx.length > 1) {\n // Ensure we don't exceed the maxDuplicates limit\n const limitedDuplicates = duplicatesIdx.slice(0, maxDuplicates);\n const dups = limitedDuplicates.map((i) => imageFilePaths[i]);\n duplicates.push(dups);\n\n // Mark all found duplicates as processed\n for (const idx of limitedDuplicates) {\n duplicateIdxSet.add(idx);\n }\n }\n }\n }\n\n // Get metadata for each duplicate group and sort by resolution\n const duplicatesWithMetadata: Array<\n Array<{ path: string; width: number; height: number }>\n > = [];\n\n for (const files of duplicates) {\n const filesWithMetadata: Array<{\n path: string;\n width: number;\n height: number;\n }> = [];\n\n for (const file of files) {\n if (await pathExists(file)) {\n try {\n const metadata = await sharp(file).metadata();\n filesWithMetadata.push({\n path: file,\n width: metadata.width || 0,\n height: metadata.height || 0,\n });\n } catch (error) {\n throw new Error(`Could not read metadata for ${file}:`, error);\n }\n }\n }\n\n // Sort by resolution (width * height) in descending order (highest resolution first)\n filesWithMetadata.sort((a, b) => {\n // First sort by resolution (highest first)\n const resolutionDiff = b.width * b.height - a.width * a.height;\n if (resolutionDiff !== 0) return resolutionDiff;\n\n // If resolution is the same, sort by filename\n return path.basename(a.path).localeCompare(path.basename(b.path));\n });\n\n if (filesWithMetadata.length > 0) {\n duplicatesWithMetadata.push(filesWithMetadata);\n }\n }\n\n return duplicatesWithMetadata;\n}\n","import sharp from \"sharp\";\nimport fs from \"node:fs/promises\";\nimport assert from \"assert\";\n\n/**\n * Checks if a given path points to a directory.\n * @param path - The file system path to check\n * @returns Promise that resolves to true if the path is a directory\n */\nexport async function isFolder(path: string) {\n try {\n return (await fs.stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Processes an array with a concurrency limit to avoid overwhelming the system.\n * @param array - Array of items to process\n * @param limit - Maximum number of concurrent operations\n * @param iteratee - Async function to apply to each item\n * @returns Promise that resolves to an array of results\n */\nexport async function mapLimit<T, R>(\n array: T[],\n limit: number,\n iteratee: (item: T) => Promise<R>\n): Promise<R[]> {\n const results: R[] = [];\n for (let i = 0; i < array.length; i += limit) {\n const batch = array.slice(i, i + limit);\n const batchResults = await Promise.all(batch.map(iteratee));\n results.push(...batchResults);\n }\n return results;\n}\n\n/**\n * Checks if a file exists at the given path.\n * @param filePath - The path to check\n * @returns Promise that resolves to true if the file exists\n */\nexport async function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Determines if a file is a supported image format based on its extension.\n * @param path - The file path to check\n * @returns True if the file extension indicates a supported image format\n */\nexport function isImageFile(path: string) {\n return (\n path.endsWith(\".png\") ||\n path.endsWith(\".jpg\") ||\n path.endsWith(\".jpeg\") ||\n path.endsWith(\".webp\") ||\n path.endsWith(\".gif\") ||\n path.endsWith(\".avif\") ||\n path.endsWith(\".tiff\") ||\n path.endsWith(\".tif\") ||\n path.endsWith(\".svg\")\n );\n}\n\nexport function px(pixels: Buffer, width: number, x: number, y: number) {\n const pixel = width * y + x;\n assert(pixel < pixels.length);\n return pixels[pixel];\n}\n\nexport function binaryToHex(s: string) {\n let output = \"\";\n for (let i = 0; i < s.length; i += 4) {\n const bytes = s.slice(i, i + 4);\n const decimal = parseInt(bytes, 2);\n const hex = decimal.toString(16);\n output += hex;\n }\n return Buffer.from(output, \"hex\");\n}\n\n/**\n * Computes a difference hash (dHash) for an image, creating a perceptual fingerprint.\n *\n * The dHash algorithm works by:\n * 1. Converting the image to grayscale\n * 2. Resizing to a small grid (hashSize x hashSize+1)\n * 3. Comparing adjacent pixels to create a binary hash\n * 4. Converting the binary string to a hexadecimal buffer\n *\n * This creates a hash that is resilient to minor changes like compression,\n * resizing, and slight color adjustments while being sensitive to structural changes.\n *\n * @param path - Path to the image file\n * @param hashSize - Size of the hash grid (default: 8, creates 64-bit hash)\n * @returns Promise that resolves to a Buffer containing the image hash\n * @throws {Error} When the image cannot be processed by Sharp\n */\nexport async function dhash(path: string, hashSize = 8) {\n const height = hashSize;\n const width = height + 1;\n\n // Covert to small gray image\n const pixels = await sharp(path)\n .grayscale()\n .resize({ width, height, fit: \"fill\" })\n .raw()\n .toBuffer();\n\n let difference = \"\";\n for (let row = 0; row < height; row++) {\n for (let col = 0; col < height; col++) {\n // height is not a mistake here...\n const left = px(pixels, width, col, row);\n const right = px(pixels, width, col + 1, row);\n difference += left < right ? 1 : 0;\n }\n }\n return binaryToHex(difference);\n}"],"mappings":"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","names":["sharp","fs","path","fs","i","sharp","path","resolve"]}