HOWTO Process Images With AppleScript and jhead


Note: The workflow described here has been rendered completely obsolete as of end of January 2008, when the PHP scaffolding that dealt with server-side watermarking and re-sizing of images was removed. Although I intend to eventually set up something similar again, this is now how I do things right now. Nevertheless, the AppleScript techniques described herein are still mostly valid. Mind you, iPhoto'08 now supports halfway decent IPTC keyword tagging, so the second half of this piece is truly obsolete.

Introduction

Always in search of new ways to actually spend less time doing repetitive tasks, I took the time to figure out how to automate the process of archiving my photos and publishing them on this site.

I archive all my photos according to date and time on a file server, since I want to access them from whatever box I'm using (which rules out iPhoto, as much as I like it), and I downsize the ones I publish here from the megapixel range to 800x600 or 600x800 - the site then deals with tagging them with my copyright notice and generating and caching several views (from standard to two thumbnail sizes) on the fly.

The usual process would be to upload them from my camera, drag them out from iPhoto to a new folder on my desktop, and then fire up a terminal session to use jhead to time-stamp them, generate smaller versions for the site and then upload them to both the archive and the site itself.

As much as I like using jhead from the command-line, I decided to cut down on terminal usage and learn enough AppleScript to automate the job as much as possible. Uploading the files requires SSH and some nifty remote path manipulation - which I'm still figuring out - but renaming them according to time and date and resizing them was easy as pie.

Essentially, I hacked one of the built-in folder actions to do what I wanted:

The Action Folder Script

The AppleScript below is a folder action script - i.e., you place the script (named as, say Photo Processing.scpt) in /Library/Scripts/Folder Action Scripts, create a folder, right-click on the folder, pick Attach Folder Action... and pick Photo Processing.scpt from the list.

Then you just drag photos from iPhoto on to the folder and the script automatically creates two sub-folders: Archive with the renamed photos (at original size and resolution) and Publish (with the scaled-down images).

To use it, you must have jhead installed in /sw/bin (it is not part of Fink, but it compiles trivially), and the Fink port of ImageMagick.

(I did not use Apple's image processing tools because they apparently do not understand EXIF properly, and I tend to stick to stuff I know to work)

property site_upload_foldername : "Publish"
property archive_foldername : "Archive"
-- the list of file types which will be processed
-- eg: {"PICT", "JPEG", "TIFF", "GIFf"}
property type_list : {"JPEG"}
-- since file types are optional in Mac OS X,
-- check the name extension if there is no file type
-- NOTE: do not use periods (.) with the items in the name extensions list
-- eg: {"txt", "text", "jpg", "jpeg"}, NOT: {".txt", ".text", ".jpg", ".jpeg"}
property extension_list : {"jpg", "jpeg"}

on adding folder items to this_folder after receiving these_items
  tell application "Finder"
    if not (exists folder site_upload_foldername of this_folder) then
      make new folder at this_folder with properties {name:site_upload_foldername}
    end if
    set the site_upload_folder to (folder site_upload_foldername of this_folder) as alias
    if not (exists folder archive_foldername of this_folder) then
      make new folder at this_folder with properties {name:archive_foldername}
      set current view of container window of this_folder to list view
    end if
    set the archive_folder to folder archive_foldername of this_folder
  end tell
  try
    repeat with i from 1 to number of items in these_items
      set this_item to item i of these_items
      set the item_info to the info for this_item
      if (alias of the item_info is false and the file type of the item_info is in the type_list) or (the name extension of the item_info is in the extension_list) then
        tell application "Finder"
          set the site_file to (duplicate this_item to the site_upload_folder with replacing) as alias
          set the archive_file to (move this_item to the archive_folder with replacing) as alias
        end tell
        set archive_filename to do shell script ¬
          "perl -e \"print quotemeta('" & POSIX path of archive_file & "');\""
        do shell script "/sw/bin/jhead -exonly -nf%Y%m%d%H%M%S " & archive_filename
        set site_filename to do shell script ¬
          "perl -e \"print quotemeta('" & POSIX path of site_file & "');\""
        do shell script "/sw/bin/jhead -exonly -nf%Y%m%d%H%M%S -cmd \"/sw/bin/mogrify -resize 800x800 -quality 100 &i\" " & site_filename
      end if
    end repeat
  on error error_message number error_number
    if the error_number is not -128 then
      tell application "Finder"
        activate
        display dialog error_message buttons {"Cancel"} default button 1 giving up after 120
      end tell
    end if
  end try
end adding folder items to

The Keyword Tagging Droplet

Later on I felt the need to tag some image files with keywords, in order to enable me to create albums by topic sometime in the future. Since the only way to do this in EXIF is to mangle the Comment: field and jhead 2.2 provides a command-line option to set it directly (although I don't know if its double-byte clean), I put together a droplet that prompts for a tag list (nothing but keywords separated by spaces), get each file's existing tags and adds any new ones.

It needs some optimization - it always saves tags, regardless of changes - and it would be nice to sort tags alphabetically, but it works just fine.

-- the list of file types which will be processed
-- eg: {"PICT", "JPEG", "TIFF", "GIFf"}
property fileTypes : {"JPEG"}
-- since file types are optional in Mac OS X,
-- check the name extension if there is no file type
-- NOTE: do not use periods (.) with the items in the name extensions list
-- eg: {"txt", "text", "jpg", "jpeg"}, NOT: {".txt", ".text", ".jpg", ".jpeg"}
property fileExtensions : {"jpg", "jpeg"}
-- blanks for whitespace trimming
property white_space : {space, tab, return, (ASCII character 10),
        (ASCII character 13)}

on trim_string(the_string, trim_chars, trim_parameter)
        set start_char to 1
        set end_char to length of the_string
        set all_chars to (characters of the_string)

        if trim_parameter is in {"left", "both"} then
                repeat with each_char in all_chars
                        if each_char is not in trim_chars then exit repeat
                        set start_char to (start_char + 1)
                end repeat
        end if
        if trim_parameter is in {"right", "both"} then
                set all_chars to reverse of all_chars
                repeat with each_char in all_chars
                        if each_char is not in trim_chars then exit repeat
                        set end_char to (end_char - 1)
                end repeat
        end if
        try
                return text start_char thru end_char of the_string
        on error
                return ""
        end try
end trim_string

on open of finderObjects
        set input to the text returned of (display dialog
                "Enter tags to be added to files:" default answer ""
                buttons {"Cancel", "OK"} default button "OK")
        if input = "" then
                exit repeat
        end if
        set input to trim_string(input, white_space, "both")
        set text item delimiters to {" "}
        set newTags to text items of the input

        repeat with i in (finderObjects)
                if alias of (info for i) is false and
                        (the file type of (info for i) is in the fileTypes or
                        the name extension of (info for i) is in the fileExtensions) then
                        set filename to quoted form of POSIX path of i
                        set output to do shell script ¬
                        "/sw/bin/jhead " & filename & " | grep Comment | cut -c 16-"
                        set output to trim_string(output, white_space, "both")
                        set oldTags to text items of output
                        repeat with i from 1 to number of items in newTags
                                set tag to item i of newTags
                                if oldTags does not contain tag then
                                        set the end of the oldTags to tag
                                end if
                        end repeat
                        set newTags to (text items of oldTags) as string
                        do shell script ¬
                        "/sw/bin/jhead -cl \"" & newTags & "\" " & filename
                end if
        end repeat
end open

See Also: