Photo Workflow and Scripting

After about 600 shot images I noticed a couple of things that I had been handling wrongly in my photo workflow until now. The following is a detailed description of my solution to these problems, including code excerpts. I hope that this will be useful to some of my readers, be it for photo editing or other areas.

Instead of transferring the photos with the program Nikon Transfer from the camera to the computer, I had copied them manually in the Finder. Too bad that the files ended up without a creation date. Also, the pictures all had names like _DSC1234 and so on, which is not very expressive and, more importantly, not unique – you quickly end up with three or four files having the same name. My solution to such a problem is always: “I’m going to write myself a little program.” The first attempt lead me to a Cocoa tool with this core method:

- (void)fixCreationDateOfFile:(NSString *)filename
{
    // The file system will not actually change the creation date 
    // to what is passed in here. However, it will update the 
    // creation date to a meaningful value (e.g., the modification date)
    // if no creation date was set -- which is precisely what I need :^)
    NSFileManager* fileManager = [NSFileManager defaultManager];
    NSDate* creationDate = [NSDate date];
    NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:
      creationDate, @"NSFileCreationDate", nil];
    [fileManager changeFileAttributes:attributes atPath:filename];
}

This was nicely embedded into an Automator workflow (actions: Selected Finder objects, Open Finder objects with …). A great thing about Automator is the way lists of files can easily be passed on to programs and AppleScripts.

Later, I thought of an even better solution: Another Automator workflow, but this time with an embedded AppleScript that talks to GraphicConverter and extracts the capture date of an image from its EXIF data:

on run {input, parameters}

    tell application "GraphicConverter"
        repeat with theFile in input
            set date from content (theFile as alias)
        end repeat
    end tell

    return input
end run

That left me with changing the file names to something meaningful. I have been using hierarchical dates for some years, that is, in the format Year-Month-Day. For my pictures, I chose the following principle (which, by the way, is now applied automatically by Nikon Transfer on every transfer) that assigns every picture a unique name and allows it to be located at least in the time domain:

MW-JJJJ-MM-TT-HHMMSS-XX

The following AppleScript (again for Automator) uses the d70reader instead of GraphicConverter to get to the EXIF data. With the Unix command grep the line containing the capture date is extracted and from it the individual date parts:

on run {input, parameters}

    set output to {}
    repeat with theFile in input
        set theExtension to name extension of theFile
        if theExtension = "NEF" or theExtension = "JPG" then

            -- Get line with original capture date from EXIF data.
            set thePath to POSIX path of (theFile as alias)
            set theShellScript to "/Applications/d70reader/bin/d70reader\"" & thePath & "\" | grep DateTimeOriginal_Type6"
            set theEXIF to do shell script theShellScript

            -- Generate date object from EXIF data.
            set theYear to text 50 thru 53 of theEXIF
            set theMonth to text 55 thru 56 of theEXIF
            set theDay to text 58 thru 59 of theEXIF
            set theHour to text 61 thru 62 of theEXIF
            set theMinute to text 64 thru 65 of theEXIF
            set theSecond to text 67 thru 68 of theEXIF

            -- Generate date string with same format as Nikon View.
            set theDateString to theYear & "-" & theMonth & "-" & theDay & "-" & theHour & theMinute & theSecond

            -- Rename file.
            set theName to "MW-" & theDateString & "." & theExtension
            set name of theFile to theName

            set output to output & theName

        end if
    end repeat

    return output
end run

Furthermore, there were problems with the embedded color profiles. These are used to interpret the colors of an image in such a way that the color gamut of each particular output device is used to the maximum extent. The left example image has no assigned profile while the right one has the Adobe RGB profile set in the camera, making the jacket glow more realistically:

Color profile example

Fortunately, I had taken all pictures in the RAW format so that I was able to generate new JPEGs from them. Not manually, of course, but automated, which was imperative at any rate due to the low speed of Nikon Capture, Nikon’s own image editing application. Unfortunately, its AppleScript support leaves much to be desired. So I planned to script not the program itself but to remote-control its user interface elements using UI Scripting (originally intended for impaired people). Nikon Capture seems to have only meager support even for this. For example, the menus for choosing the file format could only be clicked with the mouse, but not activated using keyboard navigation or UI Scripting. What to do?

I remembered that there are remote control application such as Apple Remote Desktop that allow capturing the mouse cursor and access the system on a lower level. After some researching I found the function call I wanted: CGPostMouseEvent() in <CoreGraphics/CGRemoteOperation.h> from the ApplicationServices.framework. I came up with this little command-line utility:

#import <Foundation/Foundation.h>
#import <ApplicationServices/ApplicationServices.h>  

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    if ( argc < 4 ) {
        printf("\nMove mouse to (x, y):            MWMouseSimulator -m x y");
        printf("\nMove mouse to (x, y) and click:  MWMouseSimulator -c x y\n\n");
        exit(0);
    }

    float x = strtof(argv[2], NULL);
    float y = strtof(argv[3], NULL);

    CGPoint mouseCursorPosition = CGPointMake(x, y);
    boolean_t updateMouseCursorPosition = TRUE;
    CGButtonCount buttonCount = 1;
    boolean_t mouseButtonDown = FALSE;
    CGError result = 0;

    if(getopt(argv[1], "c")) {
    // Move mouse with mouse button down.
        mouseButtonDown = TRUE;
        result = CGPostMouseEvent(mouseCursorPosition, 
            updateMouseCursorPosition, buttonCount, mouseButtonDown);
    }

    // Move mouse with mouse button up
    // (also required to end click process!).
    mouseButtonDown = FALSE;
    result = CGPostMouseEvent(mouseCursorPosition, 
        updateMouseCursorPosition, buttonCount, mouseButtonDown);

    [pool release];
    return result;
}

int getopt(char *argument, char *option)
{
    if ( (argument[0] == '-') && (argument[1] == option[0]) ) {
        return YES;
    }
    return NO;
}

The following AppleScript again is intended for use in an Automator workflow. It opens RAW files (Nikon NEF) in Nikon Capture Editor, saves them – which updates the preview and reduces the file size – and exports them as medium quality JPEGs. Should the JPEG already exist, it is overwritten. UI Scripting is utilized wherever possible, complemented by the aforementioned mouse cursor tool where needed. A technique interesting for general AppleScript tasks is the indeterminate delay during display of a modal window with a progress bar for opening and saving (hence the empty window title):

on run {input, parameters}

    tell application "Nikon Capture Editor" to launch
    tell application "System Events" to launch

    tell application "System Events"
        if UI elements enabled then
            tell process "Nikon Capture Editor"
                set frontmost to true

                repeat with theFile in input
                    tell application "Finder"
                        set theExtension to name extension of theFile
                        set theFolder to container of theFile
                    end tell
                    if theExtension = "NEF" then
                        open theFile

                        -- Wait while file is being opened.
                        repeat
                            delay 3
                            try
                                if title of window 1 is not equal to "" then
                                    exit repeat
                                end if
                            end try
                        end repeat

                        -- Nikon Capture Editor > Ablage > Speichern
                        click menu item 4 of menu 3 of menu bar 1

                        -- Wait while file is being saved.
                        repeat
                            delay 3
                            try
                                if title of window 1 is not equal to "" then
                                    exit repeat
                                end if
                            end try
                        end repeat

                        -- Nikon Capture Editor > Ablage > Speichern unter...
                        click menu item 5 of menu 3 of menu bar 1

                        -- Requirements: Save dialog is expanded to show file browser,
                        -- Default Folder X is installed.
                        -- Since usual GUI scripting does not always work with
                        -- Nikon Capture Editor, low-level mouse events must be simulated
                        -- (see <CoreGraphics/CGRemoteOperation.h>).

                        -- Choose folder containing theFile.
                        -- Wait until save dialog is open.
                        tell application "Default Folder X BG"
                            repeat
                                if IsDialogOpen then
                                    SwitchToFolder (theFolder as alias)
                                    exit repeat
                                end if
                            end repeat
                        end tell

                        set theWindow to window 1
                        set theGroup to UI element 1 of UI element 1 of UI element 9 of theWindow

                        -- Choose JPG Format.
                        set theFormatButton to pop up button 2 of theGroup
                        set theButtonPosition to position of theFormatButton
                        -- 5 px offset in order to touch the menu.
                        set x to (item 1 of theButtonPosition) + 5
                        set y to (item 2 of theButtonPosition) + 5
                        set theLineHeight to 19
                        set theToolPath to "/Users/martinwinter/Documents/Developer/MWMouseSimulator/build/Release/MWMouseSimulator"
                        set theShellScript to (theToolPath & " -c " & x & " " & y) as string
                        set theResult to do shell script theShellScript
                        set theShellScript to (theToolPath & " -m " & x & " " & (y + theLineHeight)) as string
                        set theResult to do shell script theShellScript
                        set theShellScript to (theToolPath & " -c " & x & " " & (y + theLineHeight)) as string
                        set theResult to do shell script theShellScript
                        delay 1

                        -- Choose Optimal Quality.
                        set theQualityButton to pop up button 1 of theGroup
                        set theButtonPosition to position of theQualityButton
                        set x to (item 1 of theButtonPosition) + 5
                        set y to (item 2 of theButtonPosition) + 5
                        set theShellScript to (theToolPath & " -c " & x & " " & y) as string
                        set theResult to do shell script theShellScript
                        set theShellScript to (theToolPath & " -m " & x & " " & (y + 2 * theLineHeight)) as string
                        set theResult to do shell script theShellScript
                        set theShellScript to (theToolPath & " -c " & x & " " & (y + 2 * theLineHeight)) as string
                        set theResult to do shell script theShellScript
                        delay 1

                        click button 6 of theWindow -- "Sichern"
                        delay 1

                        set theWindow to window 1
                        set theFirstElement to UI element 1 of theWindow
                        if class of theFirstElement is image then
                            -- This is the file replacing dialog.
                            -- Replace existing JPG file.
                            click button 2 of theWindow -- "Ersetzen"
                            delay 1
                        end if

                        -- Wait while file is being saved.
                        repeat
                            delay 3
                            try
                                if title of window 1 is not equal to "" then
                                    exit repeat
                                end if
                            end try
                        end repeat

                        -- Nikon Capture Editor > Ablage > Schließen
                        click menu item 3 of menu 3 of menu bar 1

                    end if
                end repeat

                beep
            end tell
            else
            display dialog "UI element scripting is not enabled. Please check \"Enable access for assistive devices\" in the Universal Access preference pane."
        end if
    end tell

    return input
end run

This possibility of mixing diverse technologies is marvelous: An Automator workflow that contains an AppleScript that controls an application through UI Scripting, utilitizes a system extension (DefaultFolder) and calls a command-line tool via the Unix shell. Despite all weaknesses of AppleScript – incredible!

Comments are closed.