#!/usr/bin/perl use Data::Dumper; use Image::ExifTool; require 'libs/iphoto.pl'; require 'libs/flickr_cache.pl'; # NOTE: This code is for reference purposes only. I highly recommend NOT # running this code until you understand every single solitary line of it. ########################################################################## # I WILL NOT BE HELD RESPONSIBLE FOR LOSS OF DATA WHICH COULD EASILY OCCUR # IF YOU RUN THIS. ########################################################################## # This file should point to your iPhoto Library XML file. $iPhoto::dbfile = '/Users/gid/Pictures/iPhoto Library/AlbumData.xml'; # This file is a cache of data retrieved from Flickr. $Flickr::dbfile = 'flickr_data.txt'; # On first run, you need to extract all metadata from Flickr. This is # then saved for subsequent usage in $Flickr::dbfile. ### Flickr::createdb(); die "You need to set a whole bunch of things before running this script, and then remove this message."; # Load the Flickr data cache, previously saved using Flickr::createdb(); print STDERR "Loading Flickr cache...\n"; Flickr::loaddb(); # Load the iPhoto dbfile, and get the master image list. This is a list # of all images stored by iPhoto. A separate set of lists is used to # position those photos relationally within Rolls and Albums. print STDERR "Loading iPhoto database...\n"; $mil = iPhoto::master_image_list(); # Keywords are referred to relationally for each photo. In other words, # each photo has a list of integer keyword IDs. The master list of # keywords is retrieved here. print STDERR "Loading iPhoto keywords...\n"; $lok = iPhoto::list_of_keywords(); # Get an enumerator (an iterator) for the master image list. $enum = $mil->objectEnumerator(); # Now, go through all iPhoto image files and read off the EXIF data. This # is to index the photos by creation date. print STDERR "Reading iPhoto image data...\n"; my $img; while ($img = $enum->nextObject() and $$img) { # Note: testing ($img and $$img) is necessary when interacting with # Cocoa. $img being undef is the result of something screwing up # Perl-side, whereas $$img being undef is the result of something # screwing up Cocoa-side. # Get the path of the image file. my $path = $img->objectForKey_("ImagePath")->cString; # Extract the EXIF data from the image file. my $exifTool = new Image::ExifTool; $exifTool->ExtractInfo($path); # Record the image in the %bypath index. $bypath{$path} = $img; # Record the image in the %bycaption index (ie. usually by file leaf name) my $caption = $img->objectForKey_('Caption')->description()->cString(); $bycaption{$caption} = $img; # Retrieve the creation date (and failing that the file modification date) my $taken = $exifTool->GetValue('CreateDate'); unless($taken) { $taken = $exifTool->GetValue('FileModifyDate'); } # Flickr and iPhoto/EXIF write dates in slightly different ways: # Flickr uses hyphens between YYYY-MM-DD, whereas EXIF uses colons. # (Note: I think that's the right way round). Anyway, convert to # hyphens for the purpose of indexing. $taken =~ s/(\d{4}):(\d{2}):/$1\-$2\-/g; # Record the image in the %bytaken index. push @{$bytaken{$taken}}, $img; } # Now, index the Flickr images by date taken. print STDERR "Indexing Flickr data by date...\n"; Flickr::index_by_taken(); # Iterate all pictures in Flickr by date taken... print STDERR "Matching Flickr to iPhoto...\n"; foreach my $fl_taken (keys %{$$Flickr::flickr{bytaken}}) { # Get the Flickr photo ID my $flid = $$Flickr::flickr{bytaken}{$fl_taken}[0]; # ... and the metadata for that photo my $flimg = $$Flickr::flickr{photos}{$flid}; # Get all iPhoto images with that specific date. my @ipimgs = @{$bytaken{$fl_taken}}; # For each image (as there may be more than one, due to modifications # and duplications)... foreach my $ipimg (@{$bytaken{$fl_taken}}) { # Get the image file path my $path = $ipimg->objectForKey_('ImagePath')->cString(); # Retrieve the EXIF image data for the iPhoto image. We've # already done this before, but it's not worth caching at this # time. Too damn complex. my $exifTool = new Image::ExifTool; $exifTool->ExtractInfo($path); # Get the Flickr URL for this image using the metadata block. We # *assume* that there's only one URL for this image. That's true # for my current Flickr collection. Otherwise, this needs filling # out a bit. my $urls = Flickr::child($$flimg{info}, 'urls'); my $urlobj = Flickr::child($urls, 'url'); my $url = $$urlobj{children}[0]{content}; # Add the URL to the Comment field of the EXIF data. I don't know # any better place to put this, but it's good enough for my # purposes: to establish a cross-reference between Flickr and # iPhoto for future reference. $exifTool->SetNewValue('Comment', $url); # %kwhash will be a hash of keywords. This is done (instead of an # %array) to remove duplicates. my %kwhash; # Get the tags object from the Flickr metadata my $tags = Flickr::child($$flimg{info}, 'tags'); # For each child of the tags object foreach my $tagobj (@{$$tags{children}}) { # Skip whitespace garbage, and only act on tag objects next unless ($$tagobj{name} eq 'tag'); # Add this tag's "raw" value to %kwhash, lowercasing the key. $kwhash{lc($$tagobj{attributes}{raw})} = $$tagobj{attributes}{raw}; } # Get any existing keywords from the EXIF data my @exifkws = $exifTool->GetValue('Keywords', 'ValueConv'); foreach my $kw (@exifkws) { # And add them to the hash $kwhash{lc($kw)} = $kw; } # Get any keywords from iPhoto my $kwarr = $ipimg->objectForKey_("Keywords"); # If there are some... if($kwarr and $$kwarr) { # Enumerate to set up loop. These will be Keyword IDs that # need to be looked up. my $kwenum = $kwarr->objectEnumerator(); my $kwid; # Get the text for each keyword while ($kwid = $kwenum->nextObject() and $$kwid) { # Use the master list of keywords my $kw = $iPhoto::lok->objectForKey_($kwid)->cString; # And add it to the hash. $kwhash{lc($kw)} = $kw; } } # Alphabetically sort the keywords, for fun. my @kws = sort values %kwhash; # Add them to the EXIF data $exifTool->SetNewValue('Keywords', \@kws); # Get Geotagging data from the Flickr metadata my $location = Flickr::child($$flimg{info}, 'location'); # If there's any location data present... if($location and $$location{'attributes'}{'latitude'}) { # Get the lat/long my $lat = $$location{'attributes'}{'latitude'}; my $long = $$location{'attributes'}{'longitude'}; # Add it to the EXIF data. $exifTool->SetNewValue('GPSLatitude', abs $lat); $exifTool->SetNewValue('GPSLatitudeRef', $lat<0 ? 'S' : 'N'); $exifTool->SetNewValue('GPSLongitude', abs $long); $exifTool->SetNewValue('GPSLongitudeRef', $long<0 ? 'W' : 'E'); # I don't have a GPS, so the altitude is set to zero. $exifTool->SetNewValue('GPSAltitude', 0); $exifTool->SetNewValue('GPSAltitudeRef', 'Above Sea Level'); } # Work out a new name for the photo file, based on the path in the # iPhoto library... remove everything up to and including "iPhoto # Library". (my $fn = $path) =~ s/.+\/iPhoto Library\///; # Remove (but remember in $1) the "Originals" or "Modified" # directory prefix $fn =~ s/^(Originals|Modified)\///; # If "Modified", then add this to the filename, before the extension $fn =~ s/(\..+?)$/_modified$1/ if($1 eq 'Modified'); # Get the directory base (ie. without the filename) (my $dir = $fn) =~ s/\/[^\/]+?$//; # Create the directory tree mkdir_all('pics/'.$dir, 0); # Write out the new file, including the new EXIF data $exifTool->WriteInfo($path, "pics/$fn") or die "Can't write: $!"; printf "%s => %s\t%s\n", $flid, $path, $url; } } exit; sub mkdir_all { # The equivalent of "mkdir -p" my ($dir) = @_; @d = split('/', $dir); $buf = ''; foreach $dir (@d) { $buf .= $dir.'/'; mkdir $buf, 0775 unless(-d $buf); } }