imagedatadupes

Duplicate image finder
git clone git://lumidify.org/imagedatadupes.git (fast, but not encrypted)
git clone https://lumidify.org/git/imagedatadupes.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/imagedatadupes.git (over tor)
Log | Files | Refs | README | LICENSE

imagedatadupes (29628B)


      1 #!/usr/bin/env perl
      2 
      3 # License: See the bottom of this file.
      4 
      5 # NOTE: There might be various inaccuracies in the image decompression, but that's probably not an
      6 # issue for any realistic scenario that this program is used for:
      7 # https://stackoverflow.com/questions/25119285/weird-results-using-php-imagick-getimagesignature-method
      8 
      9 use strict;
     10 use warnings;
     11 use Image::ExifTool qw(ImageInfo);
     12 use Image::Magick;
     13 use File::MimeInfo::Magic;
     14 use Data::Compare;
     15 use File::Spec::Functions qw(catfile);
     16 use Pod::Usage;
     17 use Getopt::Long;
     18 use Cwd qw(realpath);
     19 
     20 # ignored in comparison
     21 # NOTE: If more common tags are added after a 1.0 release is tagged, they
     22 # should go in a different array that can be enabled with a separate option
     23 # to avoid breaking old commands that might have depended on the old list
     24 # of ignored tags.
     25 my @exif_ignore = (
     26 	"FileInodeChangeDate",
     27 	"Directory",
     28 	"FileModifyDate",
     29 	"FileAccessDate",
     30 	"FileName",
     31 	"FileSize",
     32 	"FilePermissions",
     33 );
     34 my $verbose = 0;
     35 my $skip_mime_check = 0;
     36 
     37 sub load_exif_data {
     38 	my $filename = shift;
     39 	my $info = ImageInfo($filename);
     40 	# Not too important since ImageMagick already should weed out unsupported filetypes
     41 	# ExifTool seems to print at least *something* (e.g. permissions, etc.) even for files
     42 	# that don't have any embedded tags (including directories), so there probably aren't
     43 	# any files that ImageMagick supports but ExifTool doesn't.
     44 	if (exists $info->{"Error"}) {
     45 		warn "WARNING: Ignoring $filename as ExifTool returned an error: $info->{Error}\n";
     46 		return undef;
     47 	} elsif (exists $info->{"Warning"}) {
     48 		warn "WARNING: ExifTool returned a warning for $filename: $info->{Warning}\n";
     49 	}
     50 
     51 	for my $ignore (@exif_ignore) {
     52 		delete $info->{$ignore};
     53 	}
     54 	return $info;
     55 }
     56 
     57 # -1: error
     58 #  0: doesn't matter which image is chosen
     59 #  1: image1 should be chosen (includes more metadata than image2)
     60 #  2: image2 should be chosen (includes more metadata than image1)
     61 sub compare_exif_data {
     62 	# filenames are just for debugging
     63 	my ($info1, $info2, $filename1, $filename2) = @_;
     64 	# Make a new hash containing just the keys so we can delete keys even if
     65 	# the original infos should not be destroyed (makes the logic a bit simpler).
     66 	sub copy_hash_keys {
     67 		my %new_hash;
     68 		my $hash = shift;
     69 		for my $key (keys %$hash) {
     70 			$new_hash{$key} = 1;
     71 		}
     72 		return \%new_hash;
     73 	}
     74 	my $info1_keys = copy_hash_keys($info1);
     75 	my $info2_keys = copy_hash_keys($info2);
     76 
     77 	my $info1_superset = 0;
     78 	for my $key (keys %$info1_keys) {
     79 		# Need Data::Compare because there may be references, etc.
     80 		# (e.g. ThumbnailImage is a reference)
     81 		if (exists $info2_keys->{$key} && Compare($info1->{$key}, $info2->{$key}) == 1) {
     82 			# NOP
     83 		} elsif (!exists $info2_keys->{$key} || !defined $info2->{$key} || $info2->{$key} eq "") {
     84 			if ($key eq "Warning") {
     85 				warn "WARNING: ExifTool gave a warning on $filename1 but not on $filename2\n";
     86 				warn "Treating these files as different to be on the safe side.\n";
     87 				return -1;
     88 			}
     89 			# $info1 contains a key that is empty or nonexistent in $info2
     90 			$info1_superset = 1;
     91 			warn "$filename1 includes $key which is empty/nonexistent in $filename2\n" if ($verbose);
     92 		} else {
     93 			warn "$key is different for $filename1 and $filename2\n" if ($verbose);
     94 			return -1;
     95 		}
     96 		delete $info1_keys->{$key};
     97 		delete $info2_keys->{$key};
     98 	}
     99 
    100 	if ($info1_superset && %$info2_keys) {
    101 		if ($verbose) {
    102 			for my $key (keys %$info2_keys) {
    103 				warn "$filename2 includes $key which is nonexistent in $filename1\n";
    104 			}
    105 			warn "$filename1 and $filename2 both contain EXIF data not included in the other file\n";
    106 		}
    107 		return -1;
    108 	} elsif ($info1_superset) {
    109 		return 1;
    110 	} elsif (!%$info2_keys) {
    111 		return 0;
    112 	} else {
    113 		if (exists $info2_keys->{"Warning"}) {
    114 			warn "WARNING: ExifTool gave a warning on $filename2 but not on $filename1\n";
    115 			warn "Treating these files as different to be on the safe side.\n";
    116 			return -1;
    117 		}
    118 		if ($verbose) {
    119 			for my $key (keys %$info2_keys) {
    120 				warn "$filename2 includes $key which is nonexistent in $filename1\n";
    121 			}
    122 		}
    123 		return 2;
    124 	}
    125 }
    126 
    127 sub build_signature_list {
    128 	my @queue = @_;
    129 	my @signatures;
    130 	while (@queue) {
    131 		my $filename = pop @queue;
    132 		# Just ignore all symlinks. This is the easiest way to
    133 		# prevent issues, and there shouldn't be any symlinks in
    134 		# the directories I use this on anyways.
    135 		if (-l $filename) {
    136 			warn "WARNING: Ignoring symlink $filename\n";
    137 			next;
    138 		}
    139 		if (-d $filename) {
    140 			my $dh;
    141 			if (!opendir $dh, $filename) {
    142 				warn "WARNING: Unable to open directory $filename\n";
    143 				next;
    144 			}
    145 			# FIXME: Maybe test for errors on readdir:
    146 			# https://github.com/Perl/perl5/issues/17907
    147 			my @new_files = map catfile($filename, $_), grep {$_ ne "." && $_ ne ".."} readdir $dh;
    148 			closedir $dh;
    149 			push @queue, @new_files;
    150 		} elsif (-f $filename) {
    151 			if (!-r $filename) {
    152 				warn "WARNING: File $filename is not readable\n";
    153 				next;
    154 			}
    155 			if (!$skip_mime_check) {
    156 				my $mime = mimetype($filename);
    157 				if (!defined($mime)) {
    158 					warn "Unable to determine MIME type for $filename\n" if ($verbose);
    159 					next;
    160 				} elsif ($mime !~ /^image/) {
    161 					warn "Unsupported MIME type for $filename: $mime\n" if ($verbose);
    162 					next;
    163 				}
    164 			}
    165 			my $image = Image::Magick->new;
    166 			my $err = $image->Read($filename);
    167 			if ($err) {
    168 				$err =~ /(\d+)/;
    169 				# 420 is "no decode delegate for this image format", we don't want
    170 				# to spam that for every file that isn't an image format
    171 				if ($1 != 420 || $verbose) {
    172 					warn "WARNING: Unable to open file $filename with ImageMagick: $err\n";
    173 				}
    174 				next;
    175 			}
    176 			my $sig = $image->Get("signature");
    177 			if (!defined($sig)) {
    178 				warn "WARNING: Unable to obtain signature for $filename\n";
    179 				next;
    180 			}
    181 			# This is needed so that it's possible to avoid the same file ending up
    182 			# in the same deletion group (if the same file/directory was added
    183 			# to the command-line arguments twice - not very realistic, but let's
    184 			# just be on the safe side).
    185 			my $fullpath = realpath($filename);
    186 			if (!defined($fullpath)) {
    187 				warn "WARNING: Unable to get absolute path for $filename\n";
    188 				next;
    189 			}
    190 			push @signatures, [$fullpath, $sig];
    191 		} else {
    192 			warn "WARNING: Ignoring non-existent or non-regular file $filename\n";
    193 		}
    194 	}
    195 	return @signatures;
    196 }
    197 
    198 sub handle_group_exif {
    199 	# At this point, there really shouldn't be any errors reading the EXIF data since
    200 	# ImageMagick already successfully read the images (unless there's a file format
    201 	# for which ImageMagick has a loader but ExifTool doesn't).
    202 	my @exif_arr = grep defined($_->[3]), map [@$_, load_exif_data($_->[1])], @_;
    203 	my @all_groups;
    204 	while (@exif_arr) {
    205 		my $first = $exif_arr[0];
    206 		# Strip signature and exif data for elements in final array since we don't need them anymore
    207 		push @all_groups, [[$first->[0], $first->[1]]];
    208 		my @tmp_arr;
    209 		for my $i (1..$#exif_arr) {
    210 			my $ret = compare_exif_data($first->[3], $exif_arr[$i]->[3], $first->[1], $exif_arr[$i]->[1]);
    211 			if ($ret == 0 || $ret == 1) {
    212 				push @{$all_groups[-1]}, [$exif_arr[$i]->[0], $exif_arr[$i]->[1]];
    213 			} elsif ($ret == 2) {
    214 				unshift @{$all_groups[-1]}, [$exif_arr[$i]->[0], $exif_arr[$i]->[1]];
    215 				$first = $exif_arr[$i];
    216 			} else {
    217 				push @tmp_arr, $exif_arr[$i];
    218 			}
    219 		}
    220 		@exif_arr = @tmp_arr;
    221 	}
    222 	# Filter out single-element groups
    223 	return grep {$#$_ > 0} @all_groups;
    224 }
    225 
    226 sub handle_group_image_data {
    227 	my @cur_imgs = @_;
    228 	my @all_groups;
    229 	while (@cur_imgs) {
    230 		my $first = shift @cur_imgs;
    231 		my $image1 = Image::Magick->new;
    232 		my $err = $image1->Read($first->[1]);
    233 		# There really shouldn't be any errors here since all images have been
    234 		# previously read by ImageMagick to calculate the signatures, but let's
    235 		# just be on the safe side.
    236 		# Error 420 isn't ignored here because it doesn't matter if it's shown
    237 		# in the extremely rare case that it occurs here even though all the
    238 		# files were already opened previously.
    239 		if ($err) {
    240 			warn "WARNING: ImageMagick error while reading $first->[1]: $err\n";
    241 			next;
    242 		}
    243 		push @all_groups, [$first];
    244 		my @tmp_arr;
    245 		for my $i (0..$#cur_imgs) {
    246 			my $image2 = Image::Magick->new;
    247 			$err = $image2->Read($cur_imgs[$i]->[1]);
    248 			if ($err) {
    249 				warn "WARNING: ImageMagick error while reading $cur_imgs[$i]->[1]: $err\n";
    250 				next;
    251 			}
    252 			my $diff = $image1->Compare(image=>$image2, metric=>"ae");
    253 			# NOTE: If the two images have different sizes, ImageMagick doesn't seem to throw
    254 			# an error, but the returned error should be non-zero.
    255 			my $img_error = $diff->Get("error");
    256 			if (!defined($img_error)) {
    257 				warn "WARNING: Unknown error during comparison of $first->[1] and $cur_imgs[$i]->[1]\n";
    258 				$img_error = 1;
    259 			}
    260 			if ($img_error == 0) {
    261 				push @{$all_groups[-1]}, $cur_imgs[$i];
    262 			} else {
    263 				push @tmp_arr, $cur_imgs[$i];
    264 			}
    265 		}
    266 		@cur_imgs = @tmp_arr;
    267 	}
    268 	# Filter out single-element groups
    269 	return grep {$#$_ > 0} @all_groups;
    270 }
    271 
    272 my @extra_exif_ignore;
    273 my $no_default_exif_ignore = 0;
    274 my $show_help = 0;
    275 my $isolate = 0; # only find duplicates between different commandline arguments (i.e. not within one directory that is given)
    276 my $print_ignored = 0;
    277 my $skip_exact_equality = 0;
    278 my $delete_files = 0;
    279 my $no_prompt = 0;
    280 my $number_output = 0;
    281 my $default_choice = "";
    282 
    283 Getopt::Long::Configure("bundling");
    284 GetOptions (
    285 	"exif-ignore=s" => \@extra_exif_ignore,
    286 	"no-default-exif-ignore" => \$no_default_exif_ignore,
    287 	"h|help" => \$show_help,
    288 	"v|verbose" => \$verbose,
    289 	"I|isolate" => \$isolate,
    290 	"print-exif-ignore" => \$print_ignored,
    291 	"Q|quick" => \$skip_exact_equality,
    292 	"d|delete" => \$delete_files,
    293 	"N|no-prompt" => \$no_prompt,
    294 	"n|number" => \$number_output,
    295 	"default-choice=s" => \$default_choice,
    296 	"skip-mime-check" => \$skip_mime_check,
    297 ) || pod2usage(-exitval => 1, -verbose => 1);
    298 @extra_exif_ignore = split(/,/, join(',', @extra_exif_ignore));
    299 if ($no_default_exif_ignore) {
    300 	@exif_ignore = @extra_exif_ignore;
    301 } else {
    302 	push @exif_ignore, @extra_exif_ignore;
    303 }
    304 
    305 if ($print_ignored) {
    306 	print join(",", @exif_ignore) . "\n";
    307 	exit 0;
    308 }
    309 
    310 pod2usage(-exitval => 0, -verbose => 2) if $show_help;
    311 pod2usage(-exitval => 1, -verbose => 1) if @ARGV < 1;
    312 if ($default_choice ne "" && $default_choice !~ /^[yYnNp]$/) {
    313 	warn "ERROR: --default-choice must be y, Y, n, N, or p!\n";
    314 	pod2usage(-exitval => 1, -verbose => 1);
    315 }
    316 
    317 my @siglist;
    318 for my $i (0..$#ARGV) {
    319 	# Add index of command-line argument for precedence when outputting which
    320 	# files should be deleted
    321 	# Format is now [original cmd argument index, full path, signature]
    322 	push @siglist, map [$i, $_->[0], $_->[1]], build_signature_list($ARGV[$i]);
    323 }
    324 
    325 # Sort first by signature, then by original command-line argument index, then by file path
    326 # Last sort by file path is so that if, for instance, there are two directories within
    327 # the same cmd arg (and isolate isn't enabled), duplicates are always removed from the
    328 # same directory.
    329 @siglist = sort {
    330 	$a->[2] cmp $b->[2] || $a->[0] <=> $b->[0] || $a->[1] cmp $b->[1];
    331 } @siglist;
    332 
    333 my @group;
    334 my @final_groups;
    335 my $prev_sig = "";
    336 for my $file (@siglist) {
    337 	if ($file->[2] ne $prev_sig) {
    338 		if (scalar(@group) > 1) {
    339 			if ($skip_exact_equality) {
    340 				# Need to make a copy of @group when storing it in @final_groups
    341 				push @final_groups, [@group];
    342 			} else {
    343 				push @final_groups, handle_group_image_data(@group);
    344 			}
    345 		}
    346 		@group = ();
    347 		$prev_sig = $file->[2];
    348 	}
    349 	push @group, $file;
    350 }
    351 if (scalar(@group) > 1) {
    352 	if ($skip_exact_equality) {
    353 		push @final_groups, [@group];
    354 	} else {
    355 		push @final_groups, handle_group_image_data(@group);
    356 	}
    357 }
    358 
    359 @final_groups = map {handle_group_exif(@$_)} @final_groups;
    360 # Sort by length of group, then by filename of first file in group
    361 @final_groups = sort {$#$a <=> $#$b || $a->[0]->[1] cmp $b->[0]->[1]} @final_groups;
    362 
    363 # 0: Do not delete files
    364 # 1: Delete files
    365 # 2: Go to previous duplicate group
    366 sub prompt_delete {
    367 	if ($no_prompt) {
    368 		return $delete_files;
    369 	}
    370 	my $choice = "";
    371 	my $default_printstr = $default_choice eq "" ? "" : "[$default_choice]";
    372 	while ($choice !~ /^[yYnNp]$/) {
    373 		print STDERR "Delete files? (y/Y/n/N/p)$default_printstr ";
    374 		$choice = <STDIN>;
    375 		chomp $choice;
    376 		if ($choice eq "") {
    377 			$choice = $default_choice;
    378 		}
    379 	}
    380 	if ($choice eq "y") {
    381 		return 1;
    382 	} elsif ($choice eq "n") {
    383 		return 0;
    384 	} elsif ($choice eq "Y") {
    385 		$delete_files = 1; # should already be 1 anyways
    386 		$no_prompt = 1;
    387 		return 1;
    388 	} elsif ($choice eq "N") {
    389 		$delete_files = 0;
    390 		$no_prompt = 1;
    391 		return 0;
    392 	} else {
    393 		return 2;
    394 	}
    395 }
    396 
    397 # It doesn't matter that the same reference will be repeated here since the
    398 # array elements are replaced later anyways
    399 my @final_delete_files = ([]) x scalar(@final_groups);
    400 sub output_groups {
    401 	my $cur_group_id = shift;
    402 	while ($cur_group_id <= $#final_groups) {
    403 		# Reset so it is set correctly when going back to correct mistakes
    404 		$final_delete_files[$cur_group_id] = [];
    405 		my $group = $final_groups[$cur_group_id];
    406 		print "\n" if ($cur_group_id > 0);
    407 		if ($number_output) {
    408 			print "[" . ($cur_group_id + 1) . "/" . ($#final_groups + 1) . "]\n";
    409 		}
    410 		my $first = $group->[0];
    411 		print "[+] $first->[1]\n";
    412 		# Note: This may be a bit inefficient, but these hashes make it simple to avoid mistakes
    413 		# and give somewhat decent diagnostic messages.
    414 		my %keep_list;
    415 		my %delete_list;
    416 		$keep_list{$first->[1]} = 1;
    417 		for my $file (@$group[1..$#$group]) {
    418 			if ($isolate && $file->[0] == $first->[0]) {
    419 				print "[+] $file->[1]\n";
    420 				$keep_list{$file->[1]} = 1;
    421 			} else {
    422 				if (exists $keep_list{$file->[1]}) {
    423 					warn "WARNING: Ignoring $file->[1] as it was queued for deletion but refers " .
    424 					     "to the same file as a file in the list of files to keep\n";
    425 				} elsif (exists $delete_list{$file->[1]}) {
    426 					warn "WARNING: $file->[1] already queued for deletion\n";
    427 				} else {
    428 					print "[-] $file->[1]\n";
    429 					$delete_list{$file->[1]} = 1;
    430 				}
    431 			}
    432 		}
    433 		if ($delete_files && (my $choice = prompt_delete())) {
    434 			if ($choice == 1) {
    435 				$final_delete_files[$cur_group_id] = [keys %delete_list];
    436 			} else {
    437 				$cur_group_id--;
    438 				if ($cur_group_id <= 0) {
    439 					$cur_group_id = 0;
    440 					print "\n";
    441 				}
    442 				next;
    443 			}
    444 		}
    445 		$cur_group_id++;
    446 	}
    447 }
    448 
    449 output_groups(0);
    450 
    451 # Loop here so the user can still correct mistakes for the previous prompts when
    452 # we have already reached the final prompt.
    453 while (1) {
    454 	my $any_to_delete = 0;
    455 	for my $delete_group (@final_delete_files) {
    456 		if (@$delete_group) {
    457 			$any_to_delete = 1;
    458 			last;
    459 		}
    460 	}
    461 	if ($any_to_delete) {
    462 		print "\n" if !$no_prompt;
    463 		my $choice = $no_prompt ? "y" : "";
    464 		while ($choice !~ /^[ynp]$/) {
    465 			print "Delete all chosen files? (y/n/p) ";
    466 			$choice = <STDIN>;
    467 			chomp $choice;
    468 		}
    469 		if ($choice eq "y") {
    470 			for my $delete_group (@final_delete_files) {
    471 				for my $filename (@$delete_group) {
    472 					warn "Deleting $filename\n" if ($verbose);
    473 					if (!unlink($filename)) {
    474 						warn "WARNING: Unable to delete $filename\n";
    475 					}
    476 				}
    477 			}
    478 		} elsif ($choice eq "p") {
    479 			print "\n" if $#final_groups == 0;
    480 			# Go back to group output, starting at last group
    481 			output_groups($#final_groups);
    482 			next;
    483 		}
    484 	}
    485 	last;
    486 }
    487 
    488 __END__
    489 
    490 =head1 NAME
    491 
    492 imagedatadupes - find duplicate images
    493 
    494 =head1 SYNOPSIS
    495 
    496 B<imagedatadupes> [option ...] file/directory ...
    497 
    498    Options:
    499       -I, --isolate
    500       -d, --delete
    501       -N, --no-prompt
    502       -Q, --quick
    503       -n, --number
    504       -v, --verbose
    505       -h, --help
    506       --default-choice=CHOICE
    507       --exif-ignore=TAG,...
    508       --no-default-exif-ignore
    509       --print-exif-ignore
    510       --skip-mime-check
    511 
    512 =head1 DESCRIPTION
    513 
    514 B<imagedatadupes> compares image files to check for duplicates and optionally deletes them.
    515 Duplicates are found even if the files are not identical, as long as the image data is the same,
    516 and the metadata is similar enough based on certain criteria.
    517 
    518 The images are first compared using signatures of the image data calculated with ImageMagick(1).
    519 Then, images with matching signatures are compared for exact equality of the image data. Lastly,
    520 the metadata returned by ExifTool(1) is compared (this includes various metadata tags, not just
    521 EXIF data). If the metadata in one file is a superset of the metadata in another file, the
    522 former is preferred over the latter when deciding which files to keep.
    523 
    524 The specific use-case for this is files where the EXIF data was modified to add a comment (e.g. where
    525 a photo was taken). If an old copy of the same image still exists, it won't be found as a duplicate
    526 by programs such as fdupes(1) and jdupes(1) since the EXIF data is different, but the new version
    527 only contains more data than the old one, so there's no point in keeping the old copy. In order for this
    528 to work in practice, some metadata tags that ExifTool returns, such as "FileModifyDate", need to be
    529 ignored (see B<--exif-ignore>, B<--no-default-exif-ignore>, and B<--print-exif-ignore>).
    530 
    531 The output format consists of groups of files separated by empty lines, with each file being preceded
    532 by [+] or [-], depending on whether it was automatically selected for keeping or deletion, respectively.
    533 If B<--delete> is not used, duplicates are only printed and no actual deletion is performed.
    534 
    535 If multiple files are identical (both image data and metadata), files that were included in earlier
    536 command-line parameters are preferred for keeping. If multiple files included in the same command-line
    537 parameter are identical and B<--isolate> is not used, the file whose absolute path comes first
    538 lexicographically is preferred for keeping.
    539 
    540 Before being output, the groups are sorted first based on the number of files in each group, then
    541 based on the lexicographical order of the absolute path of the first file in each group.
    542 This is mainly so that it is easier to spot outliers, such as a group where the file chosen
    543 for keeping (i.e. the first file in the group) is not in the expected directory.
    544 
    545 See B<CAVEATS> for some issues that can arise.
    546 
    547 B<WARNING:> Always make backups of your files before using this program to delete duplicates. There is
    548 a nonzero chance that bugs still exist which could cause the wrong files to be deleted. If you have
    549 directories containing files that should not be modified in any way, and you only want to check if
    550 there are duplicates of those files in other directories, you may want to change the permissions
    551 of those files so they are read-only and a malfunction of this program cannot cause any issues.
    552 At least that's what a paranoid person such as the author of this program would do.
    553 
    554 =head1 OPTIONS
    555 
    556 =over 8
    557 
    558 =item B<-I>, B<--isolate>
    559 
    560 Isolate the command-line parameters from each other. In other words, for each group of
    561 duplicates, all files belonging to the same command-line parameter as the file selected
    562 for keeping are also kept.
    563 
    564 =item B<-d>, B<--delete>
    565 
    566 Prompt user for deletion of duplicates. Note that the actual deletion is not performed until all
    567 duplicate groups have been processed and the user has been prompted once more to confirm that all
    568 the chosen files should really be deleted.
    569 The options are as follows:
    570 
    571 =over 4
    572 
    573 =item B<y>
    574 
    575 Delete the files that were automatically selected for deletion in the
    576 current duplicate group.
    577 
    578 =item B<Y>
    579 
    580 Delete the files that were automatically selected for deletion in the
    581 current duplicate group and in all subsequent duplicate groups.
    582 
    583 =item B<n>
    584 
    585 Do not delete the files that were automatically selected for deletion
    586 in the current duplicate group.
    587 
    588 =item B<N>
    589 
    590 Do not delete the files that were automatically selected for deletion
    591 in the current duplicate group and in all subsequent duplicate groups.
    592 
    593 =item B<p>
    594 
    595 Go back to the previous duplicate group. This is particularly useful
    596 together with B<--default-choice> so that choices can be made very
    597 quickly, but mistakes can still be corrected by going back to previous
    598 duplicate groups. Previously chosen options are not saved, so when
    599 B<p> is used to go back multiple groups, all choices have to be made
    600 again.
    601 
    602 =back
    603 
    604 Note that B<Y> and B<N> also cause the final prompt asking if all chosen
    605 files should be deleted to be skipped.
    606 
    607 =item B<-N>, B<--no-prompt>
    608 
    609 When used in conjunction with B<--delete>, automatically delete duplicates without prompting the user.
    610 This option should be used with care as it can lead to data loss. Make sure to always have backups
    611 of your data in case something goes wrong.
    612 
    613 =item B<-Q>, B<--quick>
    614 
    615 Only determine duplicates based on the data signature and metadata (skip the check for exact
    616 equality of the image data).
    617 
    618 =item B<-n>, B<--number>
    619 
    620 Print the number of the current group and the total number of groups for each duplicate group
    621 that is output. This is useful in interactive mode so there is an indication of how many
    622 groups are still left.
    623 
    624 =item B<-v>, B<--verbose>
    625 
    626 Print informational messages to stdout.
    627 This currently includes some file reading errors, as well as information on the metadata
    628 comparison (e.g. which tags were present in one file but not in the other one).
    629 Additionally, all filenames are printed once more right before they are actually deleted.
    630 
    631 =item B<-h>, B<--help>
    632 
    633 Print usage information, then exit.
    634 
    635 =item B<--default-choice>=CHOICE
    636 
    637 Set a default choice to use in interactive mode. This must be one of the characters that can be
    638 entered at a prompt during interactive mode. When this is set, it is possible to simply press
    639 enter at each prompt in order to use the default choice. Note that this is only used during
    640 interactive mode, it does not have any effect when B<--no-prompt> is in effect.
    641 
    642 =item B<--exif-ignore>=TAG,...
    643 
    644 Add additional metadata tags to the list of tags that should be ignored when comparing two files.
    645 This option can be given multiple times. Alternatively, multiple tags can be given at once
    646 as a comma-separated list. The B<--verbose> option might help to figure out why two images
    647 aren't being considered as duplicates. Additionally, ExifTool can be used to find the list
    648 of all tag names an image contains using the command "exiftool -s image.jpg". It is generally
    649 a good idea to start with the default ignore list and only add more tags later if needed.
    650 Adding too many tags to the ignore list can easily lead to data loss when files are considered
    651 to be duplicates even though the values for some ignored metadata tags were different.
    652 
    653 =item B<--no-default-exif-ignore>
    654 
    655 Do not use the default list of ignored metadata tags.
    656 
    657 WARNING: As this program has not reached version 1.0 yet, the default list of ignored metadata tags
    658 may still change without warning. Always use B<--exif-ignore> together with B<--no-default-exif-ignore>
    659 if you want to be certain that nothing unexpected happens.
    660 
    661 =item B<--print-exif-ignore>
    662 
    663 Print all metadata tags that will be ignored, including the default ones if they haven't been turned off
    664 using B<--no-default-exif-ignore>.
    665 
    666 =item B<--skip-mime-check>
    667 
    668 Do not check the MIME type of files. Instead, simply process anything that is supported by ImageMagick
    669 and ExifTool. By default, only MIME types starting with "image" are processed in order to avoid issues
    670 with ImageMagick trying to open large video files or similar.
    671 
    672 =back
    673 
    674 =head1 CAVEATS
    675 
    676 The ordering of the files within a duplicate group in the output can be a bit weird.
    677 In particular, if a file is chosen for keeping because it contains more metadata, it will be displayed
    678 at the beginning of the file list, regardless of the regular sorting. This can seem strange when
    679 there are multiple duplicates each in multiple command-line parameters since it can happen that the
    680 duplicates from one command-line parameter are not written right after each another if one of them was
    681 moved to the beginning of the output due to its metadata.
    682 
    683 Before printing the output, a final check is performed to make sure that none of the files in
    684 a duplicate group have the same path (this should only happen if the same directory/file was given
    685 multiple times). This can cause only one file to be displayed for a duplicate group.
    686 In a similar vein, when B<--isolate> is used, it is possible for a duplicate group to be printed
    687 even though none of the files are considered for deletion.
    688 
    689 If there are any errors while reading a file, the file is ignored during further processing.
    690 Errors that are only due to a filetype not being supported are normally not printed (except
    691 in rare circumstances) in order to avoid spamming the output with error messages for all
    692 non-image files. These are only printed when B<--verbose> is used.
    693 
    694 Originally, any warnings given by ExifTool also caused the file to be ignored. However, this
    695 caused issues because it meant that identical files which both included the same warning
    696 were not considered as duplicates. Thus, warnings are now treated almost the same as
    697 other metadata. The only difference is that warnings cannot be used to decide that one file
    698 contains a superset of another file's metadata. This is a safety feature as it is theoretically
    699 possible for two files to be identical, except that one is damaged in some way, leading to a
    700 warning. If warnings were treated exactly the same as other metadata, the file without the
    701 warning would be deleted as it contains less metadata, but this would probably not be the
    702 correct choice. In that case, a warning is printed and the files are not considered as
    703 duplicates so they can be examined manually later. If warnings should be disregarded
    704 entirely when comparing files, the "Warning" tag can be ignored using B<--exif-ignore>.
    705 
    706 If there is an unknown error during the exact comparison of the image data, this might be due to
    707 memory restrictions in the ImageMagick configuration. Try opening the ImageMagick configuration
    708 file "policy.xml" and changing the maximum memory. The configuration file is probably located
    709 at /etc/ImageMagick-6/policy.xml or /etc/ImageMagick-7/policy.xml, depending on the version of
    710 ImageMagick. There should be a line similar to
    711 '<policy domain="resource" name="memory" value="256MiB"/>' that can be changed to increase the
    712 maximum memory. There is also a similar line with "disk" instead of "memory" that can be used
    713 to control how much temporary disk space ImageMagick is allowed to use when there is not enough
    714 memory. The "map" and "area" options may also be of interest. See the ImageMagick documentation
    715 for a description of the other options in the policy.xml file. Note that, depending on how much
    716 memory is available, it might be a good idea to set the disk limit to 0 in order to fail more
    717 quickly and avoid a lot of disk writes when large files are opened.
    718 
    719 ImageMagick seems to sometimes open files without any error even though they don't contain any
    720 image data. For instance, it managed to open some JPEG files that had been corrupted and only
    721 contained zeroes, calculating the same signature for all of them. This should not be an issue
    722 normally since a MIME type check is now performed before trying to read any files with ImageMagick.
    723 Another interesting case found in the wild dealt with .exi files, which appeared to contain just
    724 the EXIF data from JPEG files. These passed the MIME type check, and ImageMagick calculated the
    725 same signature for all of them, but returned an error later on during the exact image data
    726 comparison. Such cases should generally not be a problem since the image data or metadata
    727 comparison should still fail, causing the files to be treated as different, but it's probably
    728 still a good idea to keep these edge cases in mind.
    729 
    730 The image data used for the comparisons is whatever ImageMagick decompresses the image
    731 files to, which might include various inaccuracies. However, that probably isn't an issue
    732 in any realistic scenario.
    733 
    734 For simplicity, all comparisons are performed even when B<--isolate> is used and it technically
    735 isn't necessary to compare files from the same command-line parameter.
    736 
    737 This program completely ignores all symbolic links in order to avoid all the issues that
    738 can be caused by them.
    739 
    740 =head1 EXAMPLES
    741 
    742 =over 8
    743 
    744 =item B<imagedatadupes -dnI --default-choice=y directory1 directory2 ...>
    745 
    746 Look for duplicates in the given directories, making sure that duplicate images belonging to the
    747 same command-line paramater are either all kept or all deleted. The user is prompted to confirm
    748 the deletion of each duplicate group, with the default choice being set to B<y> so that it is
    749 easy to always press enter (if a mistake is made, B<p> can be entered at the prompt to go back
    750 to the previous duplicate group). In order to give an indication of progress during the
    751 prompting for deletion, the output groups are numbered.
    752 
    753 =item B<imagedatadupes -dnI --default-choice=y --exif-ignore=ThumbnailOffset directory1 directory2 ...>
    754 
    755 Same as above, but additionally ignore the "ThumbnailOffset" tag when comparing image metadata.
    756 
    757 =back
    758 
    759 =head1 EXIT STATUS
    760 
    761 Always 0, unless the arguments given were invalid.
    762 
    763 =head1 SEE ALSO
    764 
    765 ExifTool(1), fdupes(1), findimagedupes(1p), ImageMagick(1), jdupes(1)
    766 
    767 =head1 LICENSE
    768 
    769 Copyright (c) 2026 lumidify <nobody@lumidify.org>
    770 
    771 Permission to use, copy, modify, and/or distribute this software for any
    772 purpose with or without fee is hereby granted, provided that the above
    773 copyright notice and this permission notice appear in all copies.
    774 
    775 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    776 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    777 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    778 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    779 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    780 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    781 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.