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 (29885B)


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