An Obsession with Everything Else

http://www.derrickschneider.com/atom.xml

Friday, October 28, 2005

I Hate AppleScript

Many of this blog's readers know that I wrote the first book about Apple's popular AppleScript language, so you might have been surprised to see me practically screaming at my Mac as I worked on a script the other day. AppleScript may be popular with users, but Apple seems to periodically ignore it, only to trumpet exciting improvements every few years. It had been a long while since I worked with the language, but it seemed like a good fit for my goal, and I thought it'd be interesting to delve into it again.

I wanted to blog photos from iPhoto, Apple's image management software. I wanted something that would export large and small versions of selected photos, FTP them to my site, and put the URLs somewhere so I could paste them into a blog post. It's a simple process spanning multiple applications that I currently do manually. It's what AppleScript should do really well, since it provides a friendly face for Apple Events, complex messages one program can send to another to control it or retrieve data. But it took me anywhere between seven to eight hours over a couple days to get it working.

The problem wasn't the language (though, amusingly, I had to rummage through sample scripts to remind myself of its syntax). It's the fact that it's still unusual to find a program that provides good support for this popular, fifteen-year-old technology. The application defines what you can do via AppleScript, and few developers bother to implement AppleScript in a thoughtful way. I was surprised to find that the state of the "scriptoverse" seems to be only slightly better than when the language came out.

I tackled the iPhoto portion first. I looked up the AppleScript commands the program supports, and saw that while I could get lots of information about the selected photos, I couldn't export them. Silly me: I expected Apple's software to provide good support for their own technology. Eventually, I thought of a workaround. I could get the location of the photo on disk, and use another program to resize the photo to a new location. After a few false starts ( The "resize image" sample script? Don't follow its example), I discovered that Apple recently released the unintuitively named Image Events. It's the kind of program I envisioned and encouraged developers to write as I evangelized AppleScript in its early days. Image Events is a faceless application whose raison d'être is to provide services to scripters. It's one of the great AppleScript improvements Apple trumpets in the release notes for an operating system upgrade they released a year ago, and it is great. I just wish I had discovered it six hours earlier.

It took some time to figure out the program's scripting quirks. It always does. Apple's done an admirable job of getting developers to follow some basic guidelines for the graphical user interface, but they've never managed to provide an analogous user experience for scripters.

Before I discovered Image Events, I took a break from the frustrating image manipulation and focused on generating the text I'd later post into a blog entry. Ideally, it would have all the information I normally type out manually. I write blog posts in TextEdit, a simple editor that comes with the operating system (I don't know why, to be honest; I normally use the much more powerful emacs as a text editor). So naturally, I decided to put the generated text into TextEdit. I was heartened when I saw that TextEdit claimed to provide good AppleScript support. But looks are deceiving. I continually ran up against inscrutable error messages and odd behavior. Eventually I gave up: I just threw all the text on the clipboard. This works well, but I don't like the clipboard's ephemeral nature. An absentminded "Copy" in another program would overwrite my script's hard work.

Compare the hours of work struggling with inadequate AppleScript support to my experience with Fetch, a popular Mac FTP client. Fetch provided great AppleScript support even when the language was in its infancy. It's even recordable, so it will write a script mimicking your actions. I hit "Record" in Script Editor, uploaded a sample file and within a couple minutes I had the FTP portion of my script finished. I immediately registered the program. The author deserves the money for its AppleScript support alone.

This isn't a new experience. Every time I use AppleScript, I struggle with quirks and poor support. It's frustrating, and each time it drives me bonkers. Sure, I'm out of practice with the language, but it's supposed to be easy to use. Maybe I should listen to my friend and former co-worker Phil, learn Python, and never write another AppleScript.

For those who are morbidly curious, here's the script. It's probably more verbose than it needs to be. My AppleScript performance tuning skills are very out-of-date. Suggestions are welcome.

Technorati tags:  |  | 


-- some constants
set mySmallPhotoWidth to 200
set myBigTallPhotoWidth to 480
set myBigWidePhotoWidth to 640

set myHome to (path to home folder)
set finalText to ""

-- set up Fetch. We just want to make one transfer window here at the beginning and reference it in the loop (rather than making a new transfer window each time
--note: I, of course, changed the connection parameters
tell application "Fetch"
set ftpLoc to make new transfer window at beginning with properties {hostname:"HOSTNAME", username:"USERNAME", password:"PASSWORD"}
open remote folder "images"
end tell
tell application "iPhoto"
-- get the selected photos
set photoList to the selection

repeat with i from 1 to number of items in photoList
-- cache all the values in local variables
set curPhoto to item i of photoList
set photoWidth to the width of curPhoto
set photoHeight to the height of curPhoto
set photoPath to the image path of curPhoto
set photoName to the name of curPhoto

-- check image name root. This allows me to override the photo's name as it exists in iPhoto
display dialog "Enter a root for the image name" default answer photoName buttons {"OK"}
set photoName to the text returned of the result

-- new file names
set smallName to photoName & "_small.jpg"
set largeName to photoName & "_large.jpg"

-- new dimensions. just set them to the current values
set newSmallWidth to photoWidth
set newSmallHeight to photoHeight
set newBigWidth to photoWidth
set newBigHeight to photoHeight

set newSmallWidth to mySmallPhotoWidth -- my pics are 200 wide
set newSmallHeight to round (photoHeight * (mySmallPhotoWidth / photoWidth)) -- set the new height to the current height times the proportion of the new width to the original width
-- for the big photo, the width is either 480 or 640 depending on orientation
if photoWidth >= photoHeight then
-- wider than tall
set newBigWidth to myBigWidePhotoWidth
set newBigHeight to round (photoHeight * (newBigWidth / photoWidth))
else
set newBigWidth to myBigTallPhotoWidth
set newBigHeight to round (photoHeight * (newBigWidth / photoWidth))
end if
-- do the actual resizing
-- scale takes the longest dimension and scales it to the number you want, so the process involves a little caution
-- make sure to check tall pics vs. wide pics
tell application "Image Events"
-- small pics
set ieImage to (open photoPath)
if photoWidth >= photoHeight then
scale ieImage to size newSmallWidth
else
scale ieImage to size newSmallHeight
end if
save ieImage as JPEG in ((myHome & smallName) as string)
close ieImage

-- large. re-open because I don't want to scale the in-memory image; I want to scale the original, and I don't know how Image Events behaves. Maybe there's a performance hit, but better safe than sorry:This doesn't have to scale to support thousands of photos
set ieImage to open photoPath
if photoWidth >= photoHeight then
scale ieImage to size newBigWidth
else
scale ieImage to size newBigHeight
end if
save ieImage as JPEG in ((myHome & largeName) as string)
close ieImage

tell application "Fetch"
put into ftpLoc item alias ((myHome & smallName) as string)
put into ftpLoc item alias ((myHome & largeName) as string)
end tell
end tell

--ftp pics
-- I typically alternate left and right when I have multiple photos
set floatSide to "left"
if i mod 2 = 0 then
set floatSide to "right"
end if
set finalText to finalText & return & "<a href=\"images/" & largeName & "\"><img src=\"images/" & smallName & "\" margins width=\"" & newSmallWidth & "\" height=\"" & newSmallHeight & "\" " & floatSide & " /></a>"
end repeat
-- quit Fetch
set the clipboard to finalText
-- this just puts a giant "Done" on my screen so I know when the script has finished.
tell application "Quicksilver"
show large type "Done"
end tell
end tell