#!/usr/bin/perl -w #Copyright (C) 2005 Peter Sykora, Andreas Neumann #This program is free software; you can redistribute it and/or #modify it under the terms of the GNU General Public License #as published by the Free Software Foundation; either version 2 #of the License, or (at your option) any later version. #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #requirements: perl >5.8, #module Math::Round (Activestate: Math-Round) #module Image::ExifTool (Activestate: Image-ExifTool, warning: Activestate has usually an old version of this module that won't work well with NEF files) #module Date::Manip (Activestate: DateManip) #module XML::TreeBuilder (Activestate: XML-TreeBuilder) #module HTML::Element - a prerequisite for XML::TreeBuilder (Activestate: automatically installs this with XML-TreeBuilder) #macosx: xcode tools (for gcc compiler) #(exiftags: http://www.sno.phy.queensu.ca/~phil/exiftool/) #version history: #version 0.1 initial version (2005-06-05), A. Neumann #version 0.2 (2005-10-12) a major rewrite: now we read in gpx (xml) files instead of the plain text file, A. Neumann #version 0.3 (2005-10-25) keyhole file output for google earth, P. Sykora #version 0.31 (2005-10-26) added timeoffset parameter, cleanup and documentation at http://www.carto.net/projects/photoTools/gpsPhoto/, A. Neumann #version 0.32 (2005-10-26) corrected a bug that stopped the program execution when a "DateTimeOriginal" exif tag was missing, P. Sykora use strict; use File::Find; use File::Basename; use File::Copy; use File::Spec; use Getopt::Long; use Math::Round; use POSIX qw(floor); use Date::Manip; use Image::ExifTool; use XML::TreeBuilder; my ($version, $program, $date); my $dir; #directory with images my $gpsfile; #text-file containing gps-data my ($maxtimediff, $timeoffset); #maximum time-offset in seconds (integer value) my $writecaption; #indicates that caption should be copied from file-name my $copydate; #indicates that the program should copy the exif date to the iptc tag my $credit; #a string value containing photographer and supporting people my ($city,$state,$country,$copyright,$source,$keywords); #some iptc geo metadata elements my $file; #jpeg file my $line; #holding data from a line from the gpsfile my %gpsData; #array holding GPS data my $time; #temporary timestring my $tagList; my $secs; my $key; my $minKey; my $mintimediff; my $lat; my $lon; my $alt; my ($dateTime,$datum,$month,$day,$year,$fn); my ($hour,$minute,$second); my ($dummy,$result,$success,$errStr,$trkpt,$wpt); my $kml; #path to keyhole file output my $waypoint; #indaicate if we treat the gps file as a waypoint, and tag all picts with these coord my $computeIfTimeDiff; #if this is set we'll compute the coordinate for files with a time difference larger than $computeIfTimeDiff $version = "0.3.2"; $date = "2005-10-26"; $program = $0; $program =~ s/(\.\/)//; my $usage = "$program (version $version, $date)\nUsage: $program --dir myphotos/dir/ [--gpsfile yourgpsfile.gpx] [--computeIfTimeDiff] [--maxtimediff 60] [--timeoffset 0] [--writecaption] [--copydate] [--credit \"your credit text\"] [--city \"your city name\"] [--state \"your state name\"] [--country \"your country name\"] [--copyright \"your copyright info\"] [--source http://www.carto.net/neumann/] [--keywords \"waterfall,mountains,lakes,hotel,cablecar\"] [--kml \"your kml output for google earth\"]\n"; #get parameters GetOptions("dir=s" => \$dir, "gpsfile=s" => \$gpsfile, "credit=s" => \$credit, "city=s" => \$city, "state=s" => \$state, "country=s" => \$country, "copyright=s" => \$copyright, "keywords=s" => \$keywords, "source=s" => \$source, "maxtimediff=i" => \$maxtimediff, "timeoffset=i" => \$timeoffset, "writecaption" => \$writecaption, "copydate" => \$copydate, "kml" => \$kml, "waypoint" => \$waypoint, "computeIfTimeDiff=i" => \$computeIfTimeDiff ); unless ($dir) { die "$usage you have to specify a directory containing the images (dir)!\n"; } if ($kml) { open(KMLFILE,">$kml") or die "Can't open file $kml: $!\n"; my $location = $country." - ".$state." - ".$city; $location =~ s/- -|- $|^-//; print KMLFILE qq ( My images 1 2) { print KMLFILE qq(

Location

$location

);} if ($keywords) { print KMLFILE qq(

Keywords

$keywords

);} if ($source) { print KMLFILE qq(

Source

$source

);} if ($copydate) { print KMLFILE qq(

Copydate

$copydate

);} if ($credit) { print KMLFILE qq(

Credit

$credit

);} if ($copyright) { print KMLFILE qq(

Copyright

$copyright

);} print KMLFILE qq( ]]>
Photos 0); } unless ($maxtimediff) { #set timediff to default of one minute $maxtimediff = 120; } unless ($timeoffset) { #set timediff to default of one minute $timeoffset = 0; } my $tree = XML::TreeBuilder->new(); $tree->parse_file($gpsfile); my $lineCounter=0; if ($waypoint) { print "reading waypoint"; foreach $wpt ($tree->find_by_tag_name('wpt')) { $gpsData{'waypoint'}{"x"} = $wpt->attr_get_i('lon'); $gpsData{'waypoint'}{"y"} = $wpt->attr_get_i('lat'); $gpsData{'waypoint'}{"z"} = nearest(1,$wpt->find_by_tag_name('ele')->as_text); $gpsData{'waypoint'}{"t"} = "N/A"; $gpsData{'waypoint'}{"d"} = "N/A"; $lineCounter++; } if($lineCounter > 1) { print "\nWARNING : more than one waypoint were found, all photos will be tagged with last waypoint coordinates\n\n"; } } else { foreach $trkpt ($tree->find_by_tag_name('trkpt')) { $dateTime = $trkpt->find_by_tag_name('time')->as_text; ($datum,$time,$dummy) = split(/[T|Z]/,$dateTime); ($year,$month,$day) = split(/-/,$datum); ($hour,$minute,$second) = split(/:/,$time); $secs = Date_SecsSince1970($month,$day,$year,$hour,$minute,$second); $gpsData{$lineCounter}{"x"} = $trkpt->attr_get_i('lon'); $gpsData{$lineCounter}{"y"} = $trkpt->attr_get_i('lat'); $gpsData{$lineCounter}{"z"} = nearest(1,$trkpt->find_by_tag_name('ele')->as_text); $gpsData{$lineCounter}{"t"} = $time; $gpsData{$lineCounter}{"d"} = $datum; $gpsData{$lineCounter}{"seconds"} = $secs; $lineCounter++; } } print "\nprocessed $lineCounter coordinates\n\n"; print "done with reading gps file\n\n"; my $pictureCounter = 0; my $pictureCounterCoordinate = 0; #read directory and process files opendir(DIR, $dir) or die "can't open directory $dir: $!"; print "\nprocessing directory \"$dir\"\n\n"; while (defined($file = readdir(DIR))) { $file = File::Spec->rel2abs($dir.$file); #first check if it is a jpeg file my ($base, $dir, $ext) = fileparse($file,'\..*'); if ($ext eq ".jpg" || $ext eq ".JPG" || $ext eq ".nef" || $ext eq ".NEF") { my $writeFile = 0; my $dest_file = File::Spec->rel2abs($dir.$base."_1".$ext); my $exifTool = new Image::ExifTool; my $imgInfo = $exifTool->ImageInfo($file,"DateTimeOriginal"); my @tags = $exifTool->GetRequestedTags(); my $createDate = $exifTool->GetValue($tags[0]); my @dateTime; my $gpsLogTime ; if ($createDate) { @dateTime = split(/\s+|\+/,$createDate); ($year,$month,$day) = split(/:/,$dateTime[0]); ($hour,$minute,$second) = split(/:/,$dateTime[1]); $secs = Date_SecsSince1970($month,$day,$year,$hour,$minute,$second) + $timeoffset; #now compare timestamps $mintimediff = $maxtimediff; $minKey = -99; foreach $key (keys %gpsData) { $gpsLogTime = $gpsData{$key}{"seconds"}; #print "gps log time in seconds = $gpsLogTime\n"; if (not $waypoint and abs($gpsLogTime - $secs) < $mintimediff) { $minKey = $key; $mintimediff = abs($gpsLogTime - $secs); } } print "$file, $createDate, timediff=$mintimediff\n"; } else { print "$file - no date available\n"; $minKey = -99; } #this part only applies if we find a matching GPS data if (int($minKey) != -99 || ($waypoint && $gpsData{'waypoint'})) { $minKey = 'waypoint' if ($waypoint); $lat = $gpsData{$minKey}{"y"}; $lon = $gpsData{$minKey}{"x"}; $alt = $gpsData{$minKey}{"z"}; # Computing the coordinates proportionaly to the time offset if the photo time # is further from the gps time by $computeIfTimeDiff if ($computeIfTimeDiff && $mintimediff > $computeIfTimeDiff) { # try to figure if the photo was taken before or after the gps record my $inBetween = 1; $inBetween = -1 if ($secs - $gpsData{$minKey}{"seconds"} < 0); if ($gpsData{$minKey+$inBetween}{"seconds"}){ # If we are in between 2 gps records... my $ratio = ($secs-$gpsData{$minKey}{"seconds"}) / ($gpsData{$minKey+$inBetween}{"seconds"}-$gpsData{$minKey}{"seconds"}); my $latDiff = $gpsData{$minKey+$inBetween}{"y"}-$gpsData{$minKey}{"y"}; my $lonDiff = $gpsData{$minKey+$inBetween}{"x"}-$gpsData{$minKey}{"x"}; $lonDiff -= 360 if ($lonDiff > 180); $lonDiff += 360 if ($lonDiff < -180); my $altDiff = $gpsData{$minKey+$inBetween}{"z"}-$gpsData{$minKey}{"z"}; $lat += $ratio * $latDiff; $lon += $ratio * $lonDiff; $alt += $ratio * $altDiff; } } my $instructions = "Lat ".$lat.", Lon ".$lon." - Bearing: 0 - Altitude: ".$alt."m"; print $instructions."\n"; #write coordinates to IPTC field "SpecialInstructions" ($success, $errStr) = $exifTool->SetNewValue("SpecialInstructions",$instructions,Group=>'IPTC'); die "problem writing out IPTC data for file '".$file."', Error: ".$errStr."\n" if $success != 1; #write out GPS exif tags #write latitude ($success, $errStr) = $exifTool->SetNewValue("GPSLatitude",dd2dms(abs($lat)),Group=>'GPS'); die "problem writing out GPS latitude data (GPSLatitude) for file '".$file."', lat :'".(dd2dms(abs($lat)))."', Error: ".$errStr."\n" if $success != 1; if ($lat > 0) { ($success, $errStr) = $exifTool->SetNewValue("GPSLatitudeRef","N",Group=>'GPS'); die "problem writing out GPS latitude ref data (GPSLatitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } else { ($success, $errStr) = $exifTool->SetNewValue("GPSLatitudeRef","S",Group=>'GPS'); die "problem writing out GPS latitude ref data (GPSLatitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #write longitude ($success, $errStr) = $exifTool->SetNewValue("GPSLongitude",dd2dms(abs($lon)),Group=>'GPS'); die "problem writing out GPS longitude data (GPSLongitude) for file '".$file."', lon :'".(dd2dms(abs($lon)))."', Error: ".$errStr."\n" if $success != 1; if ($lon > 0) { ($success, $errStr) = $exifTool->SetNewValue("GPSLongitudeRef","E",Group=>'GPS'); die "problem writing out GPS longitude ref data (GPSLongitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } else { ($success, $errStr) = $exifTool->SetNewValue("GPSLongitudeRef","W",Group=>'GPS'); die "problem writing out GPS longitude ref data (GPSLongitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #write altitude ($success, $errStr) = $exifTool->SetNewValue("GPSAltitude",abs($alt),Group=>'GPS'); die "problem writing out GPS altitude data (GPSAltitude) for file '".$file."', Error: ".$errStr."\n" if $success != 1; if ($alt >= 0) { ($success, $errStr) = $exifTool->SetNewValue("GPSAltitudeRef","Above Sea Level",Group=>'GPS'); die "problem writing out GPS altitude ref data (GPSAltitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } else { ($success, $errStr) = $exifTool->SetNewValue("GPSAltitudeRef","Below Sea Level",Group=>'GPS'); die "problem writing out GPS altitude ref data (GPSAltitudeRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #this is only relevant in non-waypoint mode if (! $waypoint) { #write timestamp ($success, $errStr) = $exifTool->SetNewValue("GPSTimeStamp",$gpsData{$minKey}{"t"},Group=>'GPS'); die "problem writing out GPS timestamp data (GPSTimeStamp) for file '".$file."', Error: ".$errStr."\n" if $success != 1; #write datestamp ($success, $errStr) = $exifTool->SetNewValue("GPSDateStamp",$gpsData{$minKey}{"d"},Group=>'GPS'); die "problem writing out GPS timestamp data (GPSDateStamp) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #I personnaly don't want that, uncomment if you want it. #write map datum to WGS84 #($success, $errStr) = $exifTool->SetNewValue("GPSMapDatum","WGS-84",Group=>'GPS'); #die "problem writing out GPS map datum data (GPSMapDatum) for file '".$file."', Error: ".$errStr."\n" if $success != 1; #write destination bearing #($success, $errStr) = $exifTool->SetNewValue("GPSImgDirection",0,Group=>'GPS'); #die "problem writing out GPS image bearing data (GPSImgDirection) for file '".$file."', Error: ".$errStr."\n" if $success != 1; #($success, $errStr) = $exifTool->SetNewValue("GPSImgDirectionRef","T",Group=>'GPS'); #die "problem writing out GPS image bearing reference data (GPSImgDirectionRef) for file '".$file."', Error: ".$errStr."\n" if $success != 1; $writeFile = 1; $pictureCounterCoordinate++; if ($kml) { ($dummy,$dummy,$fn) = File::Spec->splitpath( $file ); print KMLFILE qq( $fn
full size]]>
$gpsData{$minKey}{"y"} $gpsData{$minKey}{"x"} 10000 50 0 #Photo $gpsData{$minKey}{"y"},$gpsData{$minKey}{"x"},0
); } } else { print "Could not find a coordinate\n"; } if ($writecaption) { $writeFile = 1; my $caption = $base; $caption =~ s/^\d+\_//; $caption =~ s/(\_.)/\U$1/g; $caption =~ s/\_/ /g; $caption = ucfirst($caption); ($success, $errStr) = $exifTool->SetNewValue("Caption-Abstract",$caption,Group=>'IPTC'); die "problem writing out caption data (Caption-Abstract) for file '".$file."', Error: ".$errStr."\n" if $success != 1; ($success, $errStr) = $exifTool->SetNewValue("ObjectName",$caption,Group=>'IPTC'); die "problem writing out title data (ObjectName) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #copy exif date to IPTC CreateDate if ($copydate) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("DateCreated",$dateTime[0],Group=>'IPTC'); die "problem writing out IPTC creation date data (DateCreated) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in credits (photographer and supporting people) if ($credit) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("Credit",$credit,Group=>'IPTC'); die "problem writing out IPTC credit data (DateCreated) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in city data if ($city) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("City",$city,Group=>'IPTC'); die "problem writing out IPTC city data (City) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in state/province data if ($state) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("Province-State",$state,Group=>'IPTC'); die "problem writing out IPTC state/province data (Province-State) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in country data if ($country) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("Country-PrimaryLocationName",$country,Group=>'IPTC'); die "problem writing out IPTC country data (Country-PrimaryLocationName) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in copyright data if ($copyright) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("CopyrightNotice",$copyright,Group=>'IPTC'); die "problem writing out IPTC copyright notice data (CopyrightNotice) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in source (URL) data if ($source) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("Source",$source,Group=>'IPTC'); die "problem writing out IPTC source data (Source) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #fill in keyword data if ($keywords) { $writeFile = 1; ($success, $errStr) = $exifTool->SetNewValue("Keywords",$keywords,Group=>'IPTC'); die "problem writing out IPTC keywords data (Keywords) for file '".$file."', Error: ".$errStr."\n" if $success != 1; } #Finally write out the new metadata if ($writeFile == 1) { $success = $exifTool->WriteInfo($file, $dest_file); if ($success != 1) { $errStr = $exifTool->GetValue('Error'); die "\nerror writing $dest_file, error: $errStr !!!!!!!!!!!!!!!!!!!\n\n"; } else { $result = move($dest_file,$file); if (!$result) { die "\nerror replacing $file with $dest_file !!!!!!!!!!!!!!!!!!!!\n\n"; } } } $pictureCounter++; } } closedir(DIR); print "found coordinates for $pictureCounterCoordinate images out of $pictureCounter images ... done\n"; if ($kml) { print KMLFILE qq(
); close(KMLFILE) or die "Can't close file $kml: $!\n"; } sub dd2dms { my $dd = shift; my $minutes = ($dd - floor($dd)) * 60; my $seconds = nearest(.01, (($minutes - floor($minutes)) * 60)); $minutes = floor($minutes); my $degrees = floor($dd); return $degrees.",".$minutes.",".$seconds; }