#!/usr/bin/perl use Getopt::Std; use File::Find; getopts('pvr'); @bytes = ("","1","1","2","4","8","1","1","2","4","8","4","8"); %removetag = ("927c" => 1); if (($opt_r) && (@ARGV)) { while ($dir = shift(@ARGV)) { unless (-d $dir) { print "$dir is not a directory.\n"; next; } find(\&wanted, $dir); } if (@r_files) { @ARGV = @r_files; } else { print "No files found in recursive scan.\n"; exit(0); } } unless (@ARGV) { print "\nexifcleaner version 2.1.0\n", "=" x 25, "\n"; print "Removes MakerNote section and blank sections from EXIF compliant JPEG files.\n"; print "This saves up to 56000 bytes depending on camera model.\n"; print "Removal is done in place. Back up important files first.\n\n"; print "Usage:\n exifcleaner [-p] [-v] file [file ...]\n"; print " exifcleaner -r [-p] [-v] directory [directory ...]\n"; print " -r recursively scan directories and subdirectories.\n"; print " -p preserve the filesystem time stamp.\n"; print " -v just view the files without removing anything.\n\n"; exit(0); } while ($file = shift(@ARGV)) { print "$file"; # Examine leading markers and headers &check_file; # Read IFD0 (main image) seek(PIX, $tiff_offset + $ifd0_offset, 0); &read_ifd("ifd0"); $ifd1_offset = $next_offset; # Read Exif SubIFD if ($exif_offset) { seek(PIX, $tiff_offset + $exif_offset, 0); &read_ifd("exif"); } else { print ": No EXIF SubIFD directory.\n"; next; } # Read InteroperabilitySubIFD if ($inop_offset) { seek(PIX, $tiff_offset + $inop_offset, 0); &read_ifd("inop"); } # Read IFD1 if ($ifd1_offset) { seek(PIX, $tiff_offset + $ifd1_offset, 0); &read_ifd("ifd1"); } $new_exif_offset = 8 + $ifd_length{"ifd0"} + $data_length{"ifd0"}; # 8 bytes is the length of the TIFF header $new_inop_offset = $new_exif_offset + $ifd_length{"exif"} + $data_length{"exif"}; $new_ifd1_offset = $new_inop_offset + $ifd_length{"inop"} + $data_length{"inop"}; $new_thum_offset = $new_ifd1_offset + $ifd_length{"ifd1"} + $data_length{"ifd1"}; $new_app1_size = 8 + $new_thum_offset + $thum_length; # 8 bytes is the length of APP1 Data Size + Exif header # Checks to see if we should write a new file or not if ($opt_v) { print ": Exif data is $app1_size bytes"; if ($makn_length) { print " (",($app1_size - $new_app1_size - $makn_length + 12), " bytes unused). "; print "MakerNote is " . ($makn_length + 12) . " bytes.\n"; } else { print " (",($app1_size - $new_app1_size)," bytes unused). No MakerNote.\n"; } next; } if ($app1_size == $new_app1_size) { print ": No unused bytes in Exif data.\n"; next; } if ($app1_size < $new_app1_size) { print ": New file would be larger. Skipping...\n"; next; } # Update offset values $data_value_ifd0{"8769"} = pack("V*", $new_exif_offset) if ($l_end); $data_value_ifd0{"8769"} = pack("N*", $new_exif_offset) if ($b_end); $data_value_exif{"a005"} = pack("V*", $new_inop_offset) if ($l_end); $data_value_exif{"a005"} = pack("N*", $new_inop_offset) if ($b_end); $data_value_ifd1{"0201"} = pack("V*", $new_thum_offset) if ($l_end); $data_value_ifd1{"0201"} = pack("N*", $new_thum_offset) if ($b_end); # Open new file and write all contents before APP1 marker # Often only SOI marker ("FFD8") but can be other markers too $suffix = "." . time . "." . $$; open(OUT,">${file}$suffix"); seek(PIX, 0, 0); read(PIX, $buf, $app1_offset); print OUT $buf; # Write APP1 marker and APP1 data size print OUT pack("H*", "ffe1"); print OUT pack("S*", $new_app1_size); # Write Exif header print OUT pack("H*", "457869660000"); # Write TIFF header print OUT pack("H*", "4d4d002a00000008") if ($b_end); print OUT pack("H*", "49492a0008000000") if ($l_end); # Write IFD0 to new file # data_offset is from start of TIFF header $data_offset = 8 + $ifd_length{"ifd0"}; $next_ifd_offset = $new_ifd1_offset; &write_ifd("ifd0"); # Write Exif to new file $data_offset = $new_exif_offset + $ifd_length{"exif"}; $next_ifd_offset = 0; &write_ifd("exif"); # Write InOp to new file if ($inop_offset) { $data_offset = $new_inop_offset + $ifd_length{"inop"}; $next_ifd_offset = 0; &write_ifd("inop"); } # Write IFD1 to new file if ($ifd1_offset) { $data_offset = $new_ifd1_offset + $ifd_length{"ifd1"}; $next_ifd_offset = 0; &write_ifd("ifd1"); } # Write thum if ($thum_length) { seek(PIX, $app1_offset + 10 + $thum_offset, 0); read(PIX, $buf, $thum_length); print OUT $buf; } # Write rest of new file seek(PIX, $app1_offset + 2 + $app1_size, 0); print OUT $buf while (read PIX, $buf, 16384); close(OUT); close(PIX); @time = stat("$file"); if (rename("${file}$suffix","$file")) { utime($time[9], $time[9], "$file") if ($opt_p); print ": ", ($app1_size - $new_app1_size), " bytes removed.\n"; } else { print ": Not modified ($!)\n"; } } exit(0); ###################################################################### # Read a SubIFD (Dir+Data) into memory sub read_ifd { # Number of directory entries read(PIX, $b0, 2); $dir_entries = unpack("v*", $b0) if ($l_end); $dir_entries = unpack("n*", $b0) if ($b_end); # Loop over the directory entries for ($i = 0; $i < $dir_entries; $i++) { # Read Tag number read(PIX, $b1, 1); read(PIX, $b2, 1); $tag = unpack("H*", $b2.$b1) if ($l_end); $tag = unpack("H*", $b1.$b2) if ($b_end); @{$_[0]} = (@{$_[0]}, $tag) if (! $removetag{$tag}); # Read Data format read(PIX, $b3, 2); $data_format = unpack("v*", $b3) if ($l_end); $data_format = unpack("n*", $b3) if ($b_end); # Read Number of components read(PIX, $b4, 4); $numb_comps = unpack("V*", $b4) if ($l_end); $numb_comps = unpack("N*", $b4) if ($b_end); $data_length = $bytes[$data_format] * $numb_comps; # Read Data read(PIX, $b5, 4); $data = unpack("V*", $b5) if ($l_end); $data = unpack("N*", $b5) if ($b_end); # Save offset values $exif_offset = $data if ($tag eq "8769"); # "ExifOffset" in IFD0 $makn_offset = $data if ($tag eq "927c"); # "MakerNote" in Exif SubIFD $makn_length = $data_length if ($tag eq "927c"); # "MakerNote" in Exif SubIFD $inop_offset = $data if ($tag eq "a005"); # "ExifInteroperabilityOffset" in Exif SubIFD $thum_offset = $data if ($tag eq "0201"); # "JpegIFOffset" in SubIFD1 $thum_length = $data if ($tag eq "0202"); # "JpegIFByteCount" in SubIFD1 # Save the directory entry if (! $removetag{$tag}) { if ($data_length > 4) { $dir_pointer = tell(PIX); seek(PIX, $tiff_offset + $data, 0); read(PIX, $b5, $data_length); seek(PIX, $dir_pointer, 0); } ${"data_format_$_[0]"}{$tag} = $b3; ${"numb_comps_$_[0]"}{$tag} = $b4; ${"data_length_$_[0]"}{$tag} = $data_length; ${"data_value_$_[0]"}{$tag} = $b5; $ifd_length{$_[0]} += 12; if ($data_length > 4) { $data_length{$_[0]} += $data_length; $data_length{$_[0]} += 1 if ($data_length % 2); } } } # Offset to next IFD read(PIX, $bb, 4); $next_offset = unpack("V*", $bb) if ($l_end); $next_offset = unpack("N*", $bb) if ($b_end); $ifd_length{$_[0]} += 6; } ###################################################################### # Write a SubIFD (Dir+Data) to file sub write_ifd { print OUT pack("v*", scalar(@{$_[0]})) if ($l_end); print OUT pack("n*", scalar(@{$_[0]})) if ($b_end); foreach $tag (@{$_[0]}) { print OUT pack("H*", substr($tag,2,2).substr($tag,0,2)) if ($l_end); print OUT pack("H*", $tag) if ($b_end); print OUT ${"data_format_$_[0]"}{$tag}; print OUT ${"numb_comps_$_[0]"}{$tag}; if (${"data_length_$_[0]"}{$tag} > 4) { print OUT pack("V*", $data_offset) if ($l_end); print OUT pack("N*", $data_offset) if ($b_end); $data_offset += ${"data_length_$_[0]"}{$tag}; $data_offset += 1 if (${"data_length_$_[0]"}{$tag} % 2); } else { print OUT ${"data_value_$_[0]"}{$tag}; } } print OUT pack("V*", $next_ifd_offset) if ($l_end); print OUT pack("N*", $next_ifd_offset) if ($b_end); foreach $tag (@{$_[0]}) { if (${"data_length_$_[0]"}{$tag} > 4) { print OUT ${"data_value_$_[0]"}{$tag}; print OUT "\x00" if (${"data_length_$_[0]"}{$tag} % 2); } } } ###################################################################### # Recursivly find all file names down the directory tree sub wanted { @r_files = (@r_files, "$File::Find::dir/$_") if (-f $_); } ###################################################################### # Examine leading markers and headers for offset values sub check_file { if (-d $file) { print " is a directory. Use exifcleaner with the -r option.\n"; next; } unless (-f $file) { print ": No regular file.\n"; next; } open(PIX,$file); # Check for SOI Marker $app1_offset = 0; read(PIX, $bb, 2); $bb = unpack("H*", $bb); $app1_offset = 2; if ($bb ne "ffd8") { print ": No JPEG file.\n"; close(PIX); next; } # Check for APP1 Marker while(read(PIX, $bb, 2)) { $bb = unpack("H*", $bb); last if ($bb eq "ffe1"); $app1_offset += 2; read(PIX, $ss, 2); $app1_offset += 2; $marker_size = unpack("n*", $ss); seek(PIX, $marker_size - 2, 1); $app1_offset += ($marker_size - 2); } if ($bb ne "ffe1") { print ": No EXIF file.\n"; close(PIX); next; } # Determine APP1 Data size read(PIX, $bb, 2); $app1_size = unpack("S*", $bb); # Check for Exif Header read(PIX, $bb, 6); $bb = unpack("H*", $bb); if ($bb ne "457869660000") { print ": No EXIF Header.\n"; close(PIX); next; } # Check the TIFF header for endianness read(PIX, $bb, 4); $bb = unpack("H*", $bb); if ($bb eq "4d4d002a") { $b_end = 1; $l_end = 0; } if ($bb eq "49492a00") { $l_end = 1; $b_end = 0; } # Check offset to first IFD read(PIX, $bb, 4); $ifd0_offset = unpack("V*", $bb) if ($l_end); $ifd0_offset = unpack("N*", $bb) if ($b_end); # TIFF header always starts 10 bytes after APP1 marker if (($b_end) || ($l_end)) { $tiff_offset = $app1_offset + 10; } else { print ": No TIFF Header.\n"; close(PIX); next; } # Reset some values from previous file undef($exif_offset); undef($inop_offset); undef($ifd1_offset); undef($next_offset); undef($thum_offset); undef($thum_length); undef($makn_length); undef($makn_offset); undef(@ifd0); undef(@exif); undef(@inop); undef(@ifd1); undef(%ifd_length); undef(%data_length); undef(%next_subifd); } ###################################################################### print "\n"; exit(0); @formats = ("", "unsigned byte", "ascii strings", "unsigned short", "unsigned long", "unsigned rational", "signed byte", "undefined", "signed short", "signed long", "signed rational", "single float", "double float"); ######################################################################