Suggested Contribution: $ ENDFORM; $donate2 = "
\n"; $donate2 .= "

Please help a small developer making useful stuff

\n"; $donate2 .= $paypalForm; $donate2 .= "
\n\n"; if ($_GET['please'] =='contribute') { $donate = "
\n"; $donate .= "

Help me keep making useful stuff

\n"; $donate .= "

Thank you for using
Joe’s iPhoto AppleScripts!

\n"; $donate .= $paypalForm; $donate .= "

I hope you found these scripts helpful. Your support is greatly appreciated.

"; $donate .= "
\n\n"; }elseif ($_GET['paypal']=='thank_you') { $donate = "
\n"; $donate .= "

Thank you for your support!

"; $donate .= "

Your contribution will help me make more useful stuff and continue to share how I did it.

"; $donate .= "

Thank you and enjoy the scripts!

"; $donate .= "
\n\n"; } else { $donate = $donate2; } ?> Scripting Dates in iPhoto

AppleScripting Dates in iPhoto

This page details the set of date-manipulation subroutines I created for my set of iPhoto AppleScripts. Somehow, these seem to be the first scripts to able to change dates in iPhoto. It's my hope that this code will help lead to more iPhoto scripts and an even better application.

These scripts were inspired in part by posts from brian_d_foy, Andy Chase and Geoff Schmit.

There appears to be an intermittent but critical bug in System Events (UI Scripting), please download the test script and report your results. I think this is limited to 10.4.

The most recent subroutines are visible in the unlocked scripts. This page will be updated to include the re-factored routines at some point in the future.

Joe's iPhoto Date-Manipulation Subroutines

Nearly all of my iPhoto date scripting solution was accomplished with the following a handful of functions:

iPhotoGetOriginalDate()

This subroutine extracts the original date from an iPhoto image. If the file is a JPEG, an attempt is made to find the creation date by scraping the EXIF headers with getEXIFdate(). If that fails, an attempt is made to edit the item. If the view switches to edit, the item is a JPEG from which no date could be discovered. If the view does not change to edit, the item is most likely a movie and has opened in QuickTime Player. The subroutine then queries QT Player for the path to the original file of the newly opened movie. The creation date is then gathered from the info for that file.

iPhotoGetOriginalDate returns a record containing a type and a date. If no information could be gathered, both items are null.

This method does not seem to work with CRW (raw) image files. If this ever migrates into an AppleScript Studio application, Phil harvey's open source ExifTool would be a far better solution.

on iPhotoGetOriginalDate(photoID)
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- returns the original date of a selected object in iPhoto
    -- movies are forced to open in Quicktime Player, date is gathered from the movie creation date
    -- JPEG's original date is extracted from the embedded EXIF tags in the JPEG file
    
    -- Movie Date acquisition will likely break if some future upgrade stops opening movies in QTPlayer, hopefully that upgrade will also cause movies to report their Image Path correctly (ie. point at a movie file instead of a JPEG. Alternately, if things still point to the JPEG instead of the movie file, maybe they'll embed the correct information from the video into the JPEGs header.
    
    tell application "iPhoto"
        set theDate to my getEXIFdate(image path of photoID as POSIX file)
        if theDate is not false then
            set theDate to {type:"photo" as string, date:my EXIFDateDecode(theDate)}
        else -- EXIF date failed, check for movieness
            select photoID
            set view to edit
            if view = edit then -- this is a photo
                set view to organize -- reset view
                return {type:null, date:null} --no EXIF data skip this one"
            else
                --    this is a movie
                delay 1
                tell application "QuickTime Player"
                    if movie 1 exists then
                        tell movie 1
                            set thePath to original file
                            if class of thePath is not alias then -- workaround for QT7 in 10.4
                                set thePath to thePath as POSIX file as alias
                            end if
                            close
                        end tell
                    else
                        return {type:null, date:null} -- no movie opened
                    end if
                end tell
                try
                    set theDate to {type:"movie" as string, date:creation date of (info for thePath)}
                on error
                    return {type:null, date:null} -- no movie opened
                end try
            end if
        end if
    end tell
    return theDate
end iPhotoGetOriginalDate

iPhotoSetDate()

This subroutine applies a standard AppleScript date object to a selected iPhoto item by using GUI scripting to access iPhoto's "Photos ->Batch Change…" menu item. Because dates are currently read-only from a script interface, this workaround is necessary.

Date and time components are entered based on the currently active short date string definition. This seems to match what iPhoto displays. Since the date fields are non-standard elements, they couldn't be directly targeted with scripted clicks. I found that the incrementers could be clicked, which brought focus to the first item of the date or time fields. Once focused, scripted tabs worked to move between fields. Component date and time parts are obtained from explodeTime and explodeDate.

I did try to change the time with the small popup info pane of the standard iPhoto window, but had no luck getting changes to apply (I could change the display but nothing would stick), and I wasn't able to successfully target the date/time fields with a scripted click. Additionally, there was no way of determining the current window's state and whether the info pane existed or not. Because of all that, the always-accessible Batch Change menu seemed like a slower but more dependable solution.

Another headache of the non-standard date and time fields is that they don't properly replace selected text when typing new values. When sending the keystrokes for the date of the image, sometimes only the second digit would stick. This stems from a strange behavior of the date fields; if there is already a "1" in hours field, the next text entered won't necessarily replace the selection. So entering a "2", trying to replace a "1" will result in "12" instead. Entering a "12" to replace a "1" will result in "2". So, when the combination of current value and the first character of the new value can combine to be a valid date/time component, the text is not replaced. If the combination is invalid, the field works normally. My workaround is to send a "0" (zero) before the new date/time elements, that clears the field and lets me enter the new time without errors. I restarted too many time before I realized that behavior was just the way iPhoto worked, and not a bug in the scripts.

on iPhotoSetDate(photoID, theDate)
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- 1.1 added extra tabbing to workaround another weirdness in iPhoto's Batch Change date fields
    
    -- sets the date of photoID to theDate using GUI scripting
    -- returns:
    --        0 : failed
    --        1 : date set
    --        2 : dates match, not reset
    
    tell application "iPhoto"
        activate -- bring iPhoto back to front
        select photoID
        if my EXIFDateDecode(date of photoID) is theDate then
            return 2 -- dates match, do nothing
        end if
    end tell
    
    set dateList to my explodeDate(theDate)
    set timeList to my explodeTime(theDate)
    try
        tell application "System Events"
            tell process "iPhoto"
                click menu item "Batch Change…" of menu "Photos" of menu bar 1
                -- keystroke "b" using {shift down, command down} -- alternate way of getting to Batch menu item
                tell sheet 1 of window "iPhoto"
                    if value of pop up button 1 is not "Date" then -- switch to date view
                        click pop up button 1 -- click to reveal the popup menu
                        click menu item "Date" of menu 1 of pop up button 1 -- click the correct item of view menu (menu item 2)
                        delay 1
                    end if
                end tell
                
                tell sheet 1 of window "iPhoto"
                    click button 1 of UI element 13 -- incrementer for date, brings focus to date text field, selecting first item
                end tell
                keystroke tab
                keystroke tab using {shift down} -- workaround for a weirdness where the first field isn't active after clicking incrementer
                
                repeat with i in dateList
                    keystroke "0" & i & tab --(ASCII character 9)    -- fill in date information
                end repeat
                
                tell sheet 1 of window "iPhoto"
                    click button 1 of UI element 12 -- incrementer for time, brings focus to time text field
                end tell
                repeat with i in timeList
                    keystroke "0" & i & tab --(ASCII character 9)    -- fill in time information
                end repeat
                
                click button "OK" of sheet 1 of window "iPhoto"
            end tell
        end tell
    on error
        return 0
    end try
    return 1
end iPhotoSetDate

getEXIFdate()

One thing I'm kind of proud of in these scripts is how I was able to grab the EXIF information from the JPEGs without using any external libraries or scripting additions. The process is somewhat graceless, but it does work. The JPEG files' headers are scraped with a shell script which then uses PERL to look for a date pattern. If there is no date pattern found, the subroutine returns false. This solution was tested against a half-dozen different camera-flavors in my iPhoto library and this entire range of EXIF sample images, all returned the correct dates (as validated against information from GraphicConverter).

on getEXIFdate(imgFile)
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- send in a Macintosh file reference for a JPEG file, returns the EXIF date string without timezone information.
    
    copy (do shell script "head -n15 " & quoted form of POSIX path of imgFile & " | tr -C -d '0-9: ' | perl -p -e 's/.*([0-9]{4}(?::[0-9]{2}){2} [0-9]{2}(?::[0-9]{2}){2}).*$/$1/g'" without altering line endings) to foundDate
    
    if length of foundDate is 19 then return foundDate -- regex failed, probably no EXIF data
    return false -- not JPEG or length is greater than 20
    
end getEXIFdate

EXIFDateDecode

EXIF date strings are always in the same format, so it's very simple to grab the relevant parts and use them to build a standard AppleScript Date object. In the interest of efficiency, the script the relevant parts in deliberate order, resulting in a string which AppleScript could easily coerce into a date object. Sampling data in deliberate order also gets around the strange date field inconsistencies Andy Chase wrote about in January 2004.

I found and fixed a problem where some international short dates were ambiguous. Adding lexical months and building a more standard long date format seems to be a solution.

on EXIFDateDecode(iPhotoDate) -- converts EXIF dates into AppleScript Date objects
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- 1.1 Added a month key to workaround an ambiguity problem with some international date formats
    
    -- EXIF dates are always #### ## ## ##:##:## [-####], extract characters to desired places, translate months to words
    set theMonths to {January, February, March, April, May, June, July, August, September, October, November, December}
    if length of iPhotoDate < 19 then
        return false -- fail on too short a supplied date
    else
        copy (item ((text 6 thru 7 of iPhotoDate) as number) of theMonths) & " " as text to theDate -- month
        copy theDate & text 9 thru 10 of iPhotoDate & ", " to theDate -- day
        copy theDate & text 1 thru 4 of iPhotoDate & " " to theDate -- year
        copy theDate & text 12 thru 19 of iPhotoDate to theDate -- time with seconds
        return date theDate
    end if
end EXIFDateDecode

explodeDate() & explodeTime()

ExplodeDate and explodeTime are simple functions which simply return the current date as list items matching the short date string of the current user. iPhoto seems to use the short date string in the Batch Change dialog, and I needed to be sure the information would be fed to the application in the correct order. Explode time does one additional iPhoto specific thing, which is returning only the first letter of AM or PM, since sending both would cause iPhoto to beep annoyingly.

ExplodeDate() was updated to return a four digit date. This was slightly more complicated than I expected since there is no easy way to discover the current order of the short date string. My solution was to use the date "December 31, 1999 11:59:58 PM" since it's short date string is "12/31/99", all two-digit values with no ambiguity between values. I set that into a list of two dates and noted the position of "99" in the first explode loop. The discovered position is replaced with the four-digit year at the end of the script.

on explodeDate(theDate)
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- 1.1 added a workaround to return a four-digit year to guarantee correct dates beyond near-future and recent-past
    
    set twoDates to {short date string of date "Friday, December 31, 1999 11:59:58 PM"}
    set end of twoDates to (short date string of theDate)
    set yearPosition to null
    
    repeat with shortDate in twoDates
        copy {"", "", ""} to dateList -- blank three item list for holding the date
        copy 1 to i
        repeat with j from 1 to length of shortDate
            try
                copy item i of dateList & ((character j of shortDate) + 0) to item i of dateList
            on error -- character j isn't a number
                copy i + 1 to i
            end try
        end repeat
        if yearPosition is null then
            
            repeat with i from 1 to count dateList
                if item i of dateList is "99" or item i of dateList is "1999" then
                    set yearPosition to i
                end if
            end repeat
            
        end if
    end repeat
    set item yearPosition of dateList to year of theDate
    return dateList
end explodeDate


on explodeTime(theDate)
    -- original subroutine by Joe Maller <http://www.joemaller.com>
    -- authored March 2005
    -- licensed under Creative Commons Attribution Non Commercial ShareAlike 2.0
    -- http://creativecommons.org/licenses/by-nc-sa/2.0/
    
    -- returns 12 hour time string broken into list elements (for iPhoto quirk)
    copy every word of time string of theDate to theTimeList
    if (count items of theTimeList) < 4 then
        if item 1 of theTimeList < 12 then
            copy "A" to item 4 of theTimeList -- use single character for AM/PM to prevent alert in iPhoto
        else
            copy theTimeList & "P" as list to theTimeList -- use single character for AM/PM to prevent alert in iPhoto
            if item 1 of theTimeList > 12 then
                copy (item 1 of theTimeList) - 12 to item 1 of theTimeList
            end if
        end if
    end if
    set last item of theTimeList to character 1 of last item of theTimeList
    return theTimeList
end explodeTime

A note about Quicktime 7 and AppleScript

The behavior of QT's original file command has changed in QT7. In previous versions of QT, the command returned an alias object with AppleScripts colon delimited path formatting. The new behavior returns a Unicode string containing the POSIX path of the object. This will obviously break a script that is expecting to get it's data back as an alias. My solution was to do the following:

     set thePath to original file
     if class of thePath is not alias then
         set thePath to thePath as POSIX file as alias
     end if

I'm planning on adapting these scripts and component functions to Automator modules now that 10.4 is out. Please feel free to beat me to it.