Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Using AppleScript with GraphicConverter

Jerry Stratton, August 6, 2005

GraphicConverter has been part of my Internet toolbox almost since I first started putting images on-line. My first QuickTime movies were created with Persistence of Vision creating the stills and GraphicConverter putting them together into a movie.

I still use Persistence of Vision heavily. The banner ad for The Walkerville Weekly Reader at the top of most Mimsy pages is a POV-created image. Whenever I create a new headline article, I paste the headline text into a POV script, and have it create the new image.

However, the new banner ad is not always exactly the same size. Since I have POV resize the headline text to match the size of the banner, the vertical size varies according to the size of the headline. Up until recently, this meant opening GraphicConverter, telling it to “Smart Trim” the image, manually adding or subtracting the right amount of margin to get a 480 by 55 pixel image, setting white to be transparent, and saving as a PNG and a GIF file.

With AppleScript, this process can be completely automated.

This AppleScript project requires GraphicConverter 5.6.3, Cyberduck 2.4.6, and possibly Mac OS X 10.4.2.

GraphicConverter 5.6.3 (or higher, most likely) is the version of GraphicConverter that supports setting up transparency in an image via AppleScript. I wrote Lemkesoft’s support asking if it was possible to set transparency via AppleScript; they replied with a new beta version of GraphicConverter with this feature added. You can’t get much better software support than that!

Properties

The first part of the script sets up some properties. Properties may be used throughout your script. Here, we are going to set out some constant values that I don’t want to embed throughout the script. If I ever need to change any of these values, I will be able to do so at the top of the script; I won't have to search them out throughout the code.

  • --image information
  • property desiredWidth : 480
  • property desiredHeight : 55
  • property imagePath : "Web Site:Banners"
  • property fileName : "headline"
  • --upload information
  • property hostName : "your.host.name"
  • property userName : "yourusername"
  • property passText : "yourpassword"
  • property destinationFolder : "public_html/Banners"
  • --Cyberduck doesn't yet support applescriptable secure ftp
  • property ftpProtocol : "ftp"

As in most programming languages, text values (such as “headline”) must be surrounded by quotes, and numeric values (such as 480) are not surrounded by quotes.

All lines beginning with two dashes are ignored by AppleScript. They are notes so that we can remember what we were doing with this portion of our script. For example, we’ve separated these properties into two groups: one group of image properties, and one group of upload properties. And within the upload properties, we note that we are not using our preferred transfer protocol.

While we’re not using this feature here, properties are also useful because they are retained from one incarnation of the script to another. If you change a property’s value within your script, that property stays at the new value the next time you run the script.

Properties differ from normal variables in that normal variables are lost every time the script quits. Normal variables can also generally only be used inside the “handler” where they were created.

The open handler

All of the rest of our script is going to go between two lines: the “on open” handler. When you add an “on open” handler to your script, you are giving it the ability to accept dropped files. The word that follows “on open” is the name of the array of filenames, or list of filenames, that were dropped onto the script. It is a list variable.

[toggle code]

  • on open theImages
    • --for the moment, we are only going to deal with one image
    • --if more than one image is dropped on this app, we will use only the first one
    • set theImage to the first item of theImages
  • end open

The “on open” handler always assumes we’re dropping a list of items onto it. Even if we only drop one item, it will be in a list of one. For now, we’re going to write this script to only handle one item. If you drop more than one item, only the first item will be handled.

We put the first item in the list “theImages” into a variable called “theImage”. We’ll use that variable later in the script.

While the script won’t do anything yet, you can (and should) save it now if you want. Save it as some name such as “Trim and Upload” (we’ll be both trimming the image and uploading the file to our site). The file format should be “application” and none of the options need be set.

Save Application AppleScript

When you save an AppleScript with an open handler as an application, it gains a special icon: a standard AppleScript icon with a down arrow on it. The down arrow indicates that you can drop files (and folders) onto this script.

Drop Script Icon

The path to the files

Once we trim our image to the correct size and make it transparent, we need to save it to both a PNG and a GIF version. Both GraphicConverter (for saving) and Cyberduck (for uploading) will need to know where our final images are stored. So the first thing we will do is create the full file path for each image in variables for later use.

[toggle code]

  • --set the paths to the PNG file and GIF file that we will eventually save
  • tell application "Finder"
    • set saveFolder to folder imagePath of (path to documents folder) as string
  • end tell
  • set pngFile to saveFolder & fileName & ".png"
  • set gifFile to saveFolder & fileName & ".gif"

When we ask an application in AppleScript to do something for us, we put that request between a “tell” and an “end tell”. Here, we’re asking the Finder to get us the path to the folder where we are going to be storing our images.

The “Finder” is often the place you’ll go in AppleScript to get paths to things. The Finder knows where your documents folder is, for example, and here we are assuming that whatever we put in the imagePath property is inside the documents folder.

By adding “as string” to the end of that line, we get the path as a simple string of text characters. This is how GraphicConverter and Cyberduck are expecting the path.

GraphicConverter

GraphicConverter does the bulk of the work in this script. The first thing we want it to do is trim the image down to just the headline. After POV-Ray creates the image I’m working with, there is a lot of white surrounding the headline. I want to get rid of that unused space.

[toggle code]

  • --graphicconverter handles trimming the image
  • tell application "GraphicConverter"
    • open theImage
    • tell window 1
      • trim
      • --the rest of the image manipulation will go here
    • end tell
  • end tell

Not only are we telling GraphicConverter to do something, we are then telling “window 1” to do something. “Window 1” will always be the top-most image, so immediately after we tell GraphicConverter to open an image, that image will be in window 1. Here, the only thing we do is tell window 1 to trim itself. You can save the script and try it now if you want: when the script completes, you’ll have a trimmed image open in GraphicConverter.

  • copy image dimension to {imageWidth, imageHeight}

Now that we’ve trimmed the image, we need to know what size it was trimmed to. The banner always needs to be the same height and width, and before I alter the size of the image I need to know whether (and how much) to add or subtract from the image’s size.

set vs. copy

We are using “copy” here instead of “set”. Much of the time there is no difference, and this is one of those cases. However, it is always a good idea to use “copy” when you want a new copy, and “set” only when you don’t mind both versions tied together. This will save you a lot of trouble in future scripts. For example, look at the following script:

  • set today to the current date
  • set tomorrow to today
  • set day of tomorrow to (day of tomorrow) + 1
  • get tomorrow

If you paste that into a Script Editor window and run it, you’ll probably not be surprised that “tomorrow” is the same time, next day. Now, replace “get tomorrow” with “get today”. Today is also the same time, next day! That’s because, with certain special values such as dates and lists, “set” does not make a new copy. It is like having multiple windows on the same copy. In the above script, you can change today or tomorrow, and it affects both today and tomorrow. The more appropriate version of the above would be:

  • set today to the current date
  • copy today to tomorrow
  • set day of tomorrow to (day of tomorrow) + 1
  • get today

Those are the kinds of surprises you really want to avoid.

add or subtract margins

Digression over. We now know the width and height of our image; that makes it easy to determine how many pixels we need to add to or subtract from our image.

[toggle code]

  • --ensure that the image is the correct width
  • if imageWidth ≠ desiredWidth then
    • set widthChange to desiredWidth - imageWidth
    • set leftMargin to widthChange / 2
    • copy leftMargin as integer to rightMargin
    • if rightMargin ≠ leftMargin then
      • set leftMargin to leftMargin as integer
      • set leftMargin to leftMargin + 1
    • end if
    • change margins with {leftMargin, 0, rightMargin, 0}
  • end if

The first thing we check is if the image width is or is not the desired width. If the two numbers are equal, we don’t need to make any changes. It is only if they are not equal that we need to add or subtract pixels on the right or left.

If the two numbers are not equal, the difference between the two numbers is what we need to add or subtract. So we take desiredWidth and subtract imageWidth, and the result (widthChange) is the amount we need to change the width by. We don’t want to make all of the changes to one side, however, so we divide widthChange by 2 and put it in leftMargin; then we copy leftMargin as an integer to rightMargin. Remember that, for now, leftMargin and rightMargin are just variables. We aren’t changing the image yet.

We copy it as an integer, because sometimes our change is going to be an odd number. We can’t remove a half pixel off of the right side and a half pixel off of the left side. So now we compare the (integer) rightMargin to the (possibly not integer) leftMargin. If they are the same, then the width change is an even number. For example, if we needed to change the width by 4 pixels, our leftMargin and rightMargin changes are now 2 each.

But if the width change is 3 pixels, our leftMargin is now 1.5 and our rightMargin is now 1. So we change leftMargin to an integer (1) and add one to it (2). The total is three again, so we’ll be changing the left margin by 2 pixels and the right margin by 1 pixel.

The “change margins” feature of GraphicConverter requires a list of four numbers, but I find it easier to do things like this in steps, so I’ve set it to change only the right and left margins; the top and bottom (second and fourth items in the list) are set to zero: no change.

The height change part of the script is pretty much the same thing:

[toggle code]

  • --ensure that the image is the correct height
  • if imageHeight ≠ desiredHeight then
    • set heightChange to desiredHeight - imageHeight
    • set botMargin to heightChange / 2
    • copy botMargin as integer to topMargin
    • if botMargin ≠ topMargin then
      • set botMargin to botMargin as integer
      • set botMargin to botMargin + 1
    • end if
    • change margins with {0, topMargin, 0, botMargin}
  • end if

make transparent

Finally, we want to make the color “white” be transparent.

  • minimize color table
  • change colors to bit depth 16
  • set transparency to true
  • set transparent color to {65535, 65535, 65535}

Before setting the transparency, we change the number of colors from millions to thousands. And before doing that, I usually minimize the color table; sometimes it makes the color reduction more reliable.

We tell the window to turn its transparency on, and then tell it that the transparent color is all white. Colors in GraphicConverter are specified using “RGB” values. An RGB value is a number for red, a number for green, and a number for blue. If all three numbers are zero, we have black. If all three numbers are 65,535, we have white.

save it

Finally, we save the image as both PNG and GIF, and close the window.

  • save in pngFile as PNG
  • save in gifFile as GIF
  • close

Remember that we already created the pngFile and gifFile variables in a previous step. These are the locations of those files.

Test this and make certain that it is working as expected before going on to the next step.

Cyberduck

AppleScript excels at using the various applications you use on the Macintosh to automate repetitive tasks. Not all applications support AppleScript, but they should.

We’ve used the Finder to locate our image files, and GraphicConverter to modify our images. Now, we’ll use Cyberduck to upload our images to our server.

[toggle code]

  • --cyberduck handles uploading the image
  • tell application "Cyberduck"
    • set theBrowser to make new browser
    • tell theBrowser
      • --our upload script will go here
    • end tell
  • end tell

You’ll recognize this as very similar to our initial GraphicConverter script. First, we tell Cyberduck to do something, and within that we create a new item, and tell that new item what we want it to do. In this case, that new item is a “browser”, a window that Cyberduck uses to display the contents of an external ftp site.

  • connect to hostName as user userName with password passText with protocol ftpProtocol with initial folder destinationFolder
  • upload file POSIX path of pngFile
  • upload file POSIX path of gifFile
  • disconnect
  • close

The first line connects to the server using the information we set up in properties at the beginning of the AppleScript. The next two lines upload the PNG version and GIF version of the image. We then disconnect from the server and close the browser.

Mac OS X has two basic ways of identifying the location of a file. At the top of this script, for example, we set up one style, sometimes called HFS, that uses colons to separate the names of folders. On a hard drive named “Genet”, with a user called ”Jean”, the documents folder might be “Genet:Users:jean:Documents:”.

The other style is a Unix style, which uses slashes instead of colons and does not need the startup disk’s name. The same folder in the Unix style would be “/Users/jean/Documents/”.

In AppleScript, the phrase “POSIX path of” converts an HFS style path to a Unix style path. Cyberduck uses the Unix style of path, so we need that conversion.

You may also find “POSIX path of” useful if you use “do shell script” to call Unix command line scripts.

Final code

[toggle code]

  • --image information
  • property desiredWidth : 480
  • property desiredHeight : 55
  • property imagePath : "Web Site:Banners"
  • property fileName : "headline"
  • --upload information
  • property hostName : "your.host.name"
  • property userName : "yourusername"
  • property passText : "yourpassword"
  • property destinationFolder : "public_html/Banners"
  • --Cyberduck doesn't yet support applescriptable secure ftp
  • property ftpProtocol : "ftp"
  • on open theImages
    • --for the moment, we are only going to deal with one image
    • --if more than one image is dropped on this app, we will use only the first one
    • set theImage to the first item of theImages
    • --set the paths to the PNG file and GIF file that we will eventually save
    • tell application "Finder"
      • set saveFolder to folder imagePath of (path to documents folder) as string
    • end tell
    • set pngFile to saveFolder & fileName & ".png"
    • set gifFile to saveFolder & fileName & ".gif"
    • --graphicconverter handles trimming the image
    • tell application "GraphicConverter"
      • open theImage
      • tell window 1
        • trim
        • copy image dimension to {imageWidth, imageHeight}
        • --ensure that the image is the correct width
        • if imageWidth ≠ desiredWidth then
          • set widthChange to desiredWidth - imageWidth
          • set leftMargin to widthChange / 2
          • copy leftMargin as integer to rightMargin
          • if rightMargin ≠ leftMargin then
            • set leftMargin to leftMargin as integer
            • set leftMargin to leftMargin + 1
          • end if
          • change margins with {leftMargin, 0, rightMargin, 0}
        • end if
        • --ensure that the image is the correct height
        • if imageHeight ≠ desiredHeight then
          • set heightChange to desiredHeight - imageHeight
          • set botMargin to heightChange / 2
          • copy botMargin as integer to topMargin
          • if botMargin ≠ topMargin then
            • set botMargin to botMargin as integer
            • set botMargin to botMargin + 1
          • end if
          • change margins with {0, topMargin, 0, botMargin}
        • end if
        • minimize color table
        • change colors to bit depth 16
        • set transparency to true
        • set transparent color to {65535, 65535, 65535}
        • save in pngFile as PNG
        • save in gifFile as GIF
        • close
      • end tell
    • end tell
    • --cyberduck handles uploading the image
    • tell application "Cyberduck"
      • set theBrowser to make new browser
      • tell theBrowser
        • connect to hostName as user userName with password passText with protocol ftpProtocol with initial folder destinationFolder
        • upload file POSIX path of pngFile
        • upload file POSIX path of gifFile
        • disconnect
        • close
      • end tell
    • end tell
  • end open

This might look complex, but if you break it down into its parts--use the Finder, use GraphicConverter, use Cyberduck--it is composed of fairly simple steps. When writing AppleScript scripts, it often helps to write down the steps that you would use within the application to perform the task you wish to automate. Then, look in the application’s dictionary for items that replicate those steps. (You can open application dictionaries in Script Editor under the “File” menu.)

  1. <- SpamAssassin Rules
  2. iTunes Alarm ->