Random Wok Aperture Export Plugin -- Part 1

You can download the current release of Random Wok from the downloads page.

1. Tools For Writing An Aperture Export Plug-in

I'm going to write an Aperture export plugin. Or rather I'm going to try to write an Aperture export plug-in -- we'll see how far I get before either getting totally stuck or running out of time and patience.

The purpose of my plugin is to provide randomness to exported images. Initially it will write images to a folder that the user selects and give them random file names. It will be called Random Wok.

Since I have XCode 2.4.1 already installed, the first thing I have to do is to get the Software Developer Kit for Aperture 1.5.1. That is downloaded from Apple's developer web site. Once logged in it is found under Applications in the Downloads area. Since I'm an ADC member (a free, cheap Online member) I have a log-in and can access the disk image that I need. Sign up to be a member if you are not already. Doing so will subject you to a NDA, so beware. I certainly won't be giving any secrets away in writing about my attempts. Note that the documentation for the Aperture Export SDK is publicly available.

Once I have the SDK disk image I open it and install it and copy the documentation to a folder on my hard drive. The Read Me contains instructions on how to integrate the documentation into the XCode documentation viewer. It is worth following those instructions because it makes accessing it very easy.

The SDK installs a template that can be used as a starting point for writing a plugin. Creating a new project in XCode allows me to select the plug-in template:
rwok1
I give it the name Random Wok and my project is created:
rwok2
Random_wok.m is where most of my code will live.

I have limited tools to work with. The template contains some starter code, and the installation of the SDK installed an example plugin called SampleFTPExportPlugin. And there is the documentation, and that is it.

My understanding of the way this plugin works and what I need to do is also limited. I know that I have to set up some data in the info.plist file to describe my plugin, create and edit any interface elements I need with Interface Builder, and manage the plugin interface. As far as I can tell, the way the plugin works is that my interface is presented to the user and then everything is event-driven based on what the user clicks and what Aperture gives me. The plugin can either have Aperture write images to files or can handle the image data itself. I have to at least partially understand the dance that is required between Aperture and the plugin.

I also have to set up a number of methods so that Aperture knows what to do with my plugin. For instance -(BOOL)allowsMasterExport returns either YES or NO depending on what my plugin is capable of. The first thing to do is to fill in enough to make it compile and do roughly what I want and sprinkle some NSLog() function calls around so I can see what is happening.

2. Setting Up The info.plist File

Why write a plugin that assigns random file names? I can think of several reasons:

• It has not been done yet (always a good reason)
• There is no random file naming feature in Aperture, so it is creating new functionality
• Random file names for images posted to the web can be changed periodically. This prevents people from deep linking to them and using your bandwidth
• Random file names for images that are subsequently sorted in name order will have a random order. This is useful for creating a random image order for imports into other applications
• It removes all meaning from the file names and so makes them neutral
• The order of sequences can be hidden

On with the task. An Aperture plugin is a bundle: a folder that looks to the user like a single file. Inside that bundle goes the executable code, plus all the resources: interface elements, menus, images, strings, etc. A necessary part of the bundle is the info.plist file that contains basic information about the bundle itself.

The info.plist file in my project has already been partially set up by the XCode template. I have to edit some of what is provided: the CFBundleIdentifier string for instance.
rwok3
There is a help URL that I can set that I will need to ensure is active on my site:
rwok4
And there is a location that requires a UUID. That's a globally unique identifier. So I run that utility and it gives me a string:
rwok5
That is it for the plist. I don't believe I have to do anything to the rest of the bundle at this stage, but next I will have to write some code to get some basic functionality out of my plugin.

3. Adding Basic Code

The template code in Random_Wok.m comes with some placeholders and empty methods that need to be modified slightly. By pressing control / (forward slash) I can skip to the next placeholder. The first code it finds looks like this:
rwok7
I have to replace that with YES or NO depending on what behavior I want. In this case I want NO because I will not be supplying a list of presets:
rwok8
I return YES for -allowsMasterExport and -allowsVersionExport because initially I don't care about the file format or contents, just the file name. For -wantsFileNamingControls I return NO. I'm going to set the name myself. For -wantsDestinationPathPrompt I return YES, since I want the user to supply that. And because the user supplies that, -destinationPath can return nil.

I do need to return a value for -defaultDirectory. I'm going to return the user's Documents folder by putting this code in:
rwok9
This method is called when the user clicks the Export button. So I just tell the export manager to start the export:
rwok11
When the export manager is ready to start the export it calls the -exportManagerWillBeginExportToPath: method and provides the folder path. That folder path will need the file name appended later. So I make a copy of that path in an ivar that I create:
rwok12
Since I will be exporting all the images, this method always returns YES:
rwok13
Confirmation comes back that the image will be exported. I don't need to know this, so I ignore it:
rwok14
This method returns NO because I want to pretend that the plug-in wrote the image data somewhere. Instead I will just log what it did:
rwok15
I have to tell the export manager that the export is done if the user cancels or if the export completes:
rwok16
And that is it for now. What this should do is to give me a dialog that lets me select the export preset and whether I want the master or the version exported. Then I should get a dialog to select a destination folder that defaults to me Documents folder. When I click Export it will do nothing except log the image indexes and file names.

I compile it and there are no errors. A miracle! Here is the bundle in the Random Wok/build/Release folder:
rwok21

4. What Happens When I Run It??

So I compiled the plugin, copied it from the Random Wok/build/Release folder to the ~/Library/Application Support/Aperture/Plug-Ins/Export folder, and fired up Aperture (with a small test library!). The plug-in appears in the Export menu along with another plug-in:
rwok19
But when I select some images and run the plug-in:
rwok20
That's me. I'm the vendor for this plugin, so I better fix it. After some poking around and some help from another plugin author, it turned out that the info.plist contained these lines:
rwok18
and should have contained these instead:
rwok17
The difference is the underscore in the name. That's a bug in the template code: most but not all uses of the plug-in name have their spaces changed to underscores.

So I fix that (and tell Apple) and run again, this time getting this dialog:
rwok22
The help button works, taking me to my help page. And I can select a version preset and whether versions or masters are exported. Canceling works. Clicking Export brings up a folder dialog and lets me select a destination. Clicking Export on that dialog makes Aperture process for a little while, and then it is done. The progress bar is not functional yet, and my only output is in the console log file:
rwok23
So far so good. Next I need to make the code actually save the files.

5. Saving The Image Files

Since my code just logs file names so far, the next thing I am going to do is change it so that it actually saves the data to disk. Initially I am going to save the files without using any randomization. Later I will implement the random naming.

The full path name to use comprises the name that I was given by -exportManagerWillbeginExportToPath: (and saved in _exportBasePath) with the file name given in the path variable in -exportManagerShouldWriteImageData:relativeToPath:forImageAtIndex:
rwok25
And this works. I'm using the file names given to me by Aperture which means either the master file name or the version name depending on the user's selection on the dialog.

After some experimentation I discovered that Aperture always gives the plug-in a filename that can be written successfully. If a file already exists with the name of the image to be written, Aperture appends (1) or (2), etc. to the end of the name and gives that to the plug-in. So instead of trying to handle all the cases that Aperture is already handling for me regarding the naming, the strategy for generating random names should be to let Aperture do all the file writing with its own names and then rename the files myself when it is done. This actually makes randomizing the names easier because I will be in possession of all the information about all the files at the same time and won't have to maintain any state between naming image files.

To rename all the files at once I will have to make a copy of all the paths as I am given them. Once the exports are all done, I will generate a list of random names, and then use NSFilemanager -movePath:toPath:handler: to do the rename.

To store the names I create an NSMutableArray called _origFileNames and initialize it in the initWithAPIManager: method. Then in -exportManagerShouldWiteImageData: I add each file name to the array:
rwok26
And in -exportManagerDidFinishExport I iterate through the stored filenames and do the rename:
rwok27
For now I am just using a sequence of numbers 0001, 0002, etc. And I am being careful to keep the same file extension. My code is a little sloppy here with memory usage (what if this code is run with a huge number of files?) and I am not checking a lot of possible error conditions, such as existing files with the same numbered names. I will have to tackle problems like that when I write my final randomizing code.

6. Random Thoughts

"The generation of random numbers is too important to be left to chance" -Robert R. Coveyou

At some point in this project I will have to implement the random file names. But how to generate them? I want to give the user the ability to repeatedly create the same random names for the same images if the export is run over and over. And I want the user to be able to add more images and have the original set keep the same names. This means that I will need to use a hashing technique that transforms each image name into a random name rather than simply using a random string generator. Since complete randomness is also desired (if chosen by the user) I will need "salt" that is added to the hash that is fixed for each export but changeable between exports.

Another feature I want is to be able to control the format of the random name: its length, what characters it can contain, what it must start and end with. So I am now getting close to the point of needing to design and implement a user interface. Here is a quick diagram that shows how I think it will work. Red boxes are user input. Green boxes are code:
Plan
For testing purposes I can make a really simple hasher: one that does nothing but convert all of its settings to a readable string. That will let me examine the output file names to test the user interface. Once that is all working I can implement the hash routine for real.

I can offer a variety of random string formats. Depending on the format I will be able to generate a string of a certain length. There is an upper limit on the number of random characters I can generate for a given number of random input bits. Assuming that I have 64 bits of randomness to work with, a purely numeric random string can have up to 19 characters, an alphanumeric string with both cases only 10. I plan to offer numeric, hex, alpha, and alphanumeric, all with either uppercase, lowercase, or both cases as an option.

So far I have found two likely ways of generating the hash. One is a C routine called lookup3 that would need adapting for Objective C. That will get me up to 64 bits. The other is to use the built-in openSSL library that comes with Cocoa that will give me 128 bits.

7. Designing The Interface

The user interface for the plugin is a single window. It's a combination of elements put there by Aperture and elements put there by the plugin. So far I have not defined an interface or hooked it up, so there has been nothing to see when the plugin is used except the Aperture elements.

In XCode the resources folder holds the user interface files:
rwok28
And it is the nib file that describes the interface and contains instances of the objects it uses. Double-clicking on the nib file opens Interface Builder and shows the current Settings View:
rwok29
I can remove that message, resize the window and add whatever I need to that view to implement the user controls. After some dragging and dropping, and more dragging, and more dragging, I came up with this:
rwok31
It's not 100% there, but is good enough to get me to the next stage. The string format pop-up is populated with the four ways of creating the random string, but the length pop-up that follows is not. That will need to be filled dynamically by code depending on which choice of format is made.

It took me a little while to figure out how to add new items to a pop-up menu. Either drag a new one (called "Item") from the menu palette, or much more easily, just option drag an existing one.

Some of the interface elements will be disabled when they are not in use. For instance there is no letter case option if Numeric is chosen, so that will go gray. And if Salt is not selected then the Generate button and the text field will be gray and disabled.

The Example file name at the bottom will change dynamically as the user selects and types so they can see an example of the format. It will include the prefix and postfix strings in their proper positions and the random string in the middle.

I added the None strings in the text fields as placeholders in Interface Builder via the Attributes pane of the Inspector. They will appear whenever nothing is present in the field.
rwok90
The next step is to join the user interface elements to the code and make them do something useful.

8. Hooking Up The Interface

Hooking up the interface to an application is confusing for beginners. I've done it enough times to think that I know how it is done, but not enough times to be sure about it. So here goes.

For the Random Wok plugin, here is the nib file that contains the user interface:
rwok41
The first area of confusion is File's Owner. What the heck is that? It's a stand-in for an instance of my Random_Wok object instance. It's called File's Owner because it is an instance of the Random_Wok class that "owns" the nib file (the "file" part).

The first thing I have to do is to tell Interface Builder that File's Owner is an instance of Random_Wok. I click on File's Owner, open the Inspector (shift command I), go to the Custom Class pane, and select Random_Wok from the long list of possibilities:
rwok40
It would be so nice if Interface Builder would change the name of File's owner to Random_Wok after I had done this, but alas it does not. I have heard that many changes are coming to IB, so hopefully this is one of the improvements.

The next task is to tell IB what outlets and actions File's Owner (Random_Wok) has. More confusion because File's owner is an instance in the nib file and the outlets and actions are part of the class description. So with With File's owner selected, I click the Classes button:
rwok42
and in the Inspector bring up the Attributes pane:
rwok44
Those outlets represent pointers from the Random_Wok object instance to the interface elements in the nib file. That's how my plug-in will control the user interface. The SDK has already created these three for me, but has not connected them to the nib file (how would it know how?).

To make the outlet connections from File's owner to the views I control drag from File's Owner to the prefix NSTextField and click connect and then repeat for the postfix NSTextField, and to the Settings View NSView. This makes the prefix text field the first view for tabbing, and the postfix text field the last view for tabbing.
rwok45
Actions are methods in the Random_Wok object that are sent messages when the user interacts with the interface. Clicking on the actions button shows that there are no actions for the File's Owner (Random_Wok) class yet:
rwok43
To create the actions for the Random_Wok class I click on the Add button and fill in the names for my actions:
rwok46
Then hook all of them up in turn by control-dragging from each of the controls to the File's owner instance and clicking Connect:
rwok47
These actions coincide with the pars of the user interface that can send messages to my code: pop-up buttons, text fields, etc. Each action I create has to be added to the Random_Wok class files as a method and declared as in this example:
rwok51
Having dealt with the actions I now need more outlets. The three that are provided for me are not enough: I have to be able to control many more aspects of the interface. So one at a time I add these to Random_Wok class, selecting the correct class for each one as I go:
rwok49
Once that is done, I click back on the File's Owner instance, and use the Connections pane of the Inspector to connect up each interface element with File's owner, this time control clicking on File's Owner, dragging to the interface element, and clicking connect:
rwok100
Finally I add each of these as outlet declarations to Random_Wok.h by adding this code:
rwok50
Now when I compile and run I get this window instead of the generic controls:
rwok52
I clearly need to adjust this a bit and clean up some problems, but now it is hooked up, I will be able to start writing the code that makes it come alive.

9. Making The Interface Work

Now I need to add code to the Random_Wok class so that the interface does something. The alphaSet action is connected to the Lower/Upper/Both alpha case selection radio buttons. When the user changes the selection I get the selectedTag of the NSMatrix that holds the buttons and then update the example filename string:
rwok55
The tags are set up to be 0, 1, and 2 for the three buttons via the Attributes pane of the Inspector:
rwok60
And I have a typedefed enum called BTStringAlpha that encodes the values that the buttons can take.

The code for the length selection changing is a little different and took me a while to figure out. To get a number from the pop-up containing a list of number strings I need to get the integer value of the string that is the title of the selected item in the pop-up menu:

rwok56
Changes to the postfix string just store the string and update the example string:
rwok57
For that code to work, there must be an accessor _setPostfixString that stores the new string and releases any existing string:
rwok58
If the Salt checkbox is selected I want to have the Generate button and the salt string available to the user. If not I want them grayed out. Here is the code that does that:
rwok59
I use tags again for the four possible values of the string format pop-up. This time I have to update the state of the alpha buttons as well:
rwok61
If Numeric is selected then the alpha buttons get grayed out, since they do not apply:
rwok62
And for the purposes of debugging all of that, my example string display just shows the values that have been obtained from the interface:
rwok63
This gives a display that looks like this:
rwok115
Here the <1> comes from the selection of Hex, <14> is the length, and <2> shows Both.

Jump to Part 2
The Bagelturf site welcomes Donations of any size