Random Wok Aperture Export Plugin -- Part 3
You can download the current release of Random Wok from the downloads page.
19. Generating A Random String From A UUID
The final part to implementing the randomness is actually making the real file names. To do that I write a method for generating a string from an NSData object using a character set and a length:

This first uses eight bytes to create a long long (64 bit) value and then uses repeated division to extract character via an index into the character set string.
Then I use that to help create the random string from the UUID and the salt:

First it makes a single string from the UUIDF and salt, then converts that to an NSData object and creates the MD5 digest of that. That is then used to generate the final string that will be used in the file name.

This first uses eight bytes to create a long long (64 bit) value and then uses repeated division to extract character via an index into the character set string.
Then I use that to help create the random string from the UUID and the salt:

First it makes a single string from the UUIDF and salt, then converts that to an NSData object and creates the MD5 digest of that. That is then used to generate the final string that will be used in the file name.
20. Generating The Random Name and Renaming Images
Now I am able to generate the random string I can rename the images after they have been exported. So for each image file I do this:

To get the image UUID, depending on the API version I call either a method that gets the image properties with or without the thumbnail. I don't need the thumbnail, so I prefer to use the one that uses least memory. To determine which API to use, I add this code to the -initiWithAPIManager method:

To generate the random string I either use the salt string or not, depending on whether the checkbox is set. Then the random string is combined with the other parts of the name and used for the rename (achieved with the movePath:toPath:handler method). I add hyphens to the name in the code above for debug purposes. In real life the format string will drop the hyphens.
This code is not very memory-friendly. Each time around the loop will allocate more memory, so I need to refactor it with better memory use.
There. Finally making random file names!

To get the image UUID, depending on the API version I call either a method that gets the image properties with or without the thumbnail. I don't need the thumbnail, so I prefer to use the one that uses least memory. To determine which API to use, I add this code to the -initiWithAPIManager method:

To generate the random string I either use the salt string or not, depending on whether the checkbox is set. Then the random string is combined with the other parts of the name and used for the rename (achieved with the movePath:toPath:handler method). I add hyphens to the name in the code above for debug purposes. In real life the format string will drop the hyphens.
This code is not very memory-friendly. Each time around the loop will allocate more memory, so I need to refactor it with better memory use.
There. Finally making random file names!
21. Dealing With Duplicate Random Names
Now that I am finally exporting with random file names I feel like I am on the home straight. But there is plenty more still to do: looking for problems, for instance.
There are two things that can go wrong with the export. First there could be an existing file in the folder I am exporting to. This is actually quite likely since repeated exports will create the same names for the same files unless the salt is changed. To solve that I add some code to display an alert if the rename fails. I modify the call to movePath by adding a handler. If there is an error with the rename, the handler method will be called:

To support the handler, I added two methods:

I had to incorporate a workaround for a bug: the return value of fileManager:shouldProceedAfterError is actually ignored by movePath:toPath:. NO always comes back. So I had to create an ivar to pass that value.
Although this code handles errors just fine, there is a better way of dealing with the situation of existing files with the same name as new files. I can create all the random file names and make sure that none of them exist in the folder before I even start the export. That will ensure that errors are truly exceptional (cause by hardware or other programs maybe).
Another problem I may run into is that there may be too many files for the randomness. Exporting 1000 files with three random decimal digits in the name is guaranteed to run into trouble.
So my code needs to ensure that all of the random names are unique with respect to themselves and to the destination folder. The solution is to create dictionary of existing filenames with NSNull objects. I use a dictionary for finding duplicates because they are very efficient for large numbers of images:

To that I add the files being renamed, the difference being that the files being renamed are stored with the new random name. The case-insensitive nature of the filing system means that I need another another dictionary to track file names coerced to lower case. Collisions in that dictionary mean that I have a problem.

My renaming code ignores the existing files by looking for the NSNulls and renames the rest:

There are two things that can go wrong with the export. First there could be an existing file in the folder I am exporting to. This is actually quite likely since repeated exports will create the same names for the same files unless the salt is changed. To solve that I add some code to display an alert if the rename fails. I modify the call to movePath by adding a handler. If there is an error with the rename, the handler method will be called:

To support the handler, I added two methods:

I had to incorporate a workaround for a bug: the return value of fileManager:shouldProceedAfterError is actually ignored by movePath:toPath:. NO always comes back. So I had to create an ivar to pass that value.
Although this code handles errors just fine, there is a better way of dealing with the situation of existing files with the same name as new files. I can create all the random file names and make sure that none of them exist in the folder before I even start the export. That will ensure that errors are truly exceptional (cause by hardware or other programs maybe).
Another problem I may run into is that there may be too many files for the randomness. Exporting 1000 files with three random decimal digits in the name is guaranteed to run into trouble.
So my code needs to ensure that all of the random names are unique with respect to themselves and to the destination folder. The solution is to create dictionary of existing filenames with NSNull objects. I use a dictionary for finding duplicates because they are very efficient for large numbers of images:

To that I add the files being renamed, the difference being that the files being renamed are stored with the new random name. The case-insensitive nature of the filing system means that I need another another dictionary to track file names coerced to lower case. Collisions in that dictionary mean that I have a problem.

My renaming code ignores the existing files by looking for the NSNulls and renames the rest:

22. Storing The Settings Between Runs
An inconvenience with the current version is that it always presents blank text fields to the user. I would like to store the prefix, postfix, salt, use salt, random format, random length, and alpha case selection somewhere so that their settings are retained from one run to the next.
Implementing this turned out to be harder than I thought it would be. After reading up on NSUserDefaults and looking at several examples, it all looked straight forward enough. But something very odd happened: the values I were successfully saving and restoring between runs was being stored somewhere, but not in the com.bagelturf.Aperture.Export.Random_Wok file as I had been expecting. Not only were they not in the expected file, the expected file did not exist. It was never created.
The values I was storing were actually getting put into Aperture's own preferences file in ~/Library/Preferences. While under some circumstances this would have been the desired behavior (such as writing my own plugins for my own application), in mine it was not. I could not use most of the NSUserDefaults methods because of this side-effect.
So I created two methods: -getDefaults and -setDefaults and inside them used -persistentDomainForName: and -setPersistentDomain:forName to read and write the complete plist file as a dictionary.
Setting the defaults (that is writing the file) is done like this:

It first accesses the shared instance of user defaults and then uses the bundle identifier to retrieve the settings into a mutable dictionary. That dictionary is then updated with the latest values from the ivars, suitably encoded, and written back to the file. This allows older versions of the plugin to work with newer versions of the plist file: anything not used is simply left alone.
Reading the defaults is much more involved. The extra work comes from the need to set up the file if it does not initially exist and to manage plugin version changes. The first part is much like before, with the addition of reading the version from the plugin bundle:

Then I deal with the first run. This is indicated by an empty dictionary:

This results in a file on the disk, and a mutable variable defaultsDict with the same information. Now I am ready to deal with a change of version number:

I have nothing to do: there is only one version so far. If the version has changed, then the new version number is written to the dictionary and out to the file. Finally I am ready to set up the ivars from the dictionary values:

I call -getDefaults from the -initWithAPIManager method so the ivars are ready to go when the window is created. Then in -willBeActivated I set up the various elements of the view:

Finally in -willBeDeactivated I call -setDefaults to write the current state of the interface to the plist file.
Here is what the final com.bagelturf.Aperture.Export.Random_Wok.plist file looks like:

Implementing this turned out to be harder than I thought it would be. After reading up on NSUserDefaults and looking at several examples, it all looked straight forward enough. But something very odd happened: the values I were successfully saving and restoring between runs was being stored somewhere, but not in the com.bagelturf.Aperture.Export.Random_Wok file as I had been expecting. Not only were they not in the expected file, the expected file did not exist. It was never created.
The values I was storing were actually getting put into Aperture's own preferences file in ~/Library/Preferences. While under some circumstances this would have been the desired behavior (such as writing my own plugins for my own application), in mine it was not. I could not use most of the NSUserDefaults methods because of this side-effect.
So I created two methods: -getDefaults and -setDefaults and inside them used -persistentDomainForName: and -setPersistentDomain:forName to read and write the complete plist file as a dictionary.
Setting the defaults (that is writing the file) is done like this:

It first accesses the shared instance of user defaults and then uses the bundle identifier to retrieve the settings into a mutable dictionary. That dictionary is then updated with the latest values from the ivars, suitably encoded, and written back to the file. This allows older versions of the plugin to work with newer versions of the plist file: anything not used is simply left alone.
Reading the defaults is much more involved. The extra work comes from the need to set up the file if it does not initially exist and to manage plugin version changes. The first part is much like before, with the addition of reading the version from the plugin bundle:

Then I deal with the first run. This is indicated by an empty dictionary:

This results in a file on the disk, and a mutable variable defaultsDict with the same information. Now I am ready to deal with a change of version number:

I have nothing to do: there is only one version so far. If the version has changed, then the new version number is written to the dictionary and out to the file. Finally I am ready to set up the ivars from the dictionary values:

I call -getDefaults from the -initWithAPIManager method so the ivars are ready to go when the window is created. Then in -willBeActivated I set up the various elements of the view:

Finally in -willBeDeactivated I call -setDefaults to write the current state of the interface to the plist file.
Here is what the final com.bagelturf.Aperture.Export.Random_Wok.plist file looks like:

23. Saving The Default Folder
As currently written the plugin defaults to the same folder each time I run it. This is not convenient. There is a very good chance that I will want to go back to the same folder each time, so I added the exported file path to the defaults that are written to a read from the prefs file.
As part of doing that I changed -defaultDirectory to this code:

Here I check to see if the value read from the defaults (_defaultExportPath) is a valid folder using NSFileManager. If it is, then I use it, otherwise I use the built-in default of ~/Documents. This deals with the user moving or renaming the folder between runs.
Another change I have made is to make the Generate button do something. I decided that using the current date and time would make for a good salt value:

And here is the latest interface:

As part of doing that I changed -defaultDirectory to this code:

Here I check to see if the value read from the defaults (_defaultExportPath) is a valid folder using NSFileManager. If it is, then I use it, otherwise I use the built-in default of ~/Documents. This deals with the user moving or renaming the folder between runs.
Another change I have made is to make the Generate button do something. I decided that using the current date and time would make for a good salt value:

And here is the latest interface:

24. Making A Customized Button
The next feature I want to add is a Bagelturf badge that can be clicked to launch a browser to my home page. For that I need a graphic and some way of making a click open a URL. Since the badge won't obviously be a button, or have a button action, I want the cursor to change to a pointing hand to show that it is clickable.

After a lot of messing about with tracking rectangles I discovered that the correct way to implement the pointing hand was by using -resetCursorRects. I have to subclass NSButton and then override -resetCursorRects to define the cursor shape that I want when the cursor is inside my button.
First I drag a button in Interface Builder onto my window and select the Rounded bevel Button type. Then I create a new file and declare it as a subclass of NSButton using this code:

Then I add the implementation code:

To use the custom class in my nib file, I save those files and drag the .h file onto my nib file. This makes the nib file aware of the custom class and selects the class:

Now I can select my button and set its custom class to BTPointingHandButton:

By making a small image in Photoshop that has transparent background and saving it as a TIFF, I can have the image lay on the window background. To add the image to my XCode project I drag it onto the Resources folder and opt to copy it in.

Clicking on the image and using the inspector shows me what is there:

By dragging the image onto the button, the image is automatically set:

And finally I can add an action to File's owner via the attributes pane called bagelturfAction and hook it up to the button with control-drag. To make the action open a URL I add the action code to Random_Wok.m and use NSWorkspace's openURL method:

Done. Now if I hover over the button the cursor changes and a click launches Safari and goes to my home page.
After a lot of messing about with tracking rectangles I discovered that the correct way to implement the pointing hand was by using -resetCursorRects. I have to subclass NSButton and then override -resetCursorRects to define the cursor shape that I want when the cursor is inside my button.
First I drag a button in Interface Builder onto my window and select the Rounded bevel Button type. Then I create a new file and declare it as a subclass of NSButton using this code:

Then I add the implementation code:

To use the custom class in my nib file, I save those files and drag the .h file onto my nib file. This makes the nib file aware of the custom class and selects the class:

Now I can select my button and set its custom class to BTPointingHandButton:

By making a small image in Photoshop that has transparent background and saving it as a TIFF, I can have the image lay on the window background. To add the image to my XCode project I drag it onto the Resources folder and opt to copy it in.

Clicking on the image and using the inspector shows me what is there:

By dragging the image onto the button, the image is automatically set:

And finally I can add an action to File's owner via the attributes pane called bagelturfAction and hook it up to the button with control-drag. To make the action open a URL I add the action code to Random_Wok.m and use NSWorkspace's openURL method:

Done. Now if I hover over the button the cursor changes and a click launches Safari and goes to my home page.
25. Displaying The Image Count With Bindings
To give some feedback to the user I want to include a display of the number of images that will be exported. I can do that by using bindings: by binding text on the window to an ivar in my Random_Wok object.
I create an ivar called imagesToProcess and an accessor to set it:

In -willBeActivated, I add some code to set it up before it is used:

And since changes in the image type (master or version) can change the number of images, I have to set it each time the export type changes:

That is all the code except for this method:

It returns "s" if imagesToProcess is more than one, otherwise an empty string. I need that in order to implement correct pluralization of my display string.
To display the image count on the window I add an NSTextField in the corner like this:

Its value is unimportant because I will be constructing it dynamically with bindings, but it helps to have a descriptive string there. I set up its bindings like this:

The Display Pattern string is what does the magic. Value1 and Value2 are bound to different key paths. The first to the value given by imagesToProcess, and the second to the plural string given by pluralImagesToProcess:

When these are substituted into the Display Pattern string, the result is what the user needs to see how many images are selected:

Because I implemented the accessors and I use them to change the value, bindings take care of doing this display updates automatically. If I select images in Aperture that include some with multiple versions of one master, the image count displayed changes when I click Master and then Version, just as it should.
I create an ivar called imagesToProcess and an accessor to set it:

In -willBeActivated, I add some code to set it up before it is used:
And since changes in the image type (master or version) can change the number of images, I have to set it each time the export type changes:

That is all the code except for this method:

It returns "s" if imagesToProcess is more than one, otherwise an empty string. I need that in order to implement correct pluralization of my display string.
To display the image count on the window I add an NSTextField in the corner like this:

Its value is unimportant because I will be constructing it dynamically with bindings, but it helps to have a descriptive string there. I set up its bindings like this:

The Display Pattern string is what does the magic. Value1 and Value2 are bound to different key paths. The first to the value given by imagesToProcess, and the second to the plural string given by pluralImagesToProcess:

When these are substituted into the Display Pattern string, the result is what the user needs to see how many images are selected:

Because I implemented the accessors and I use them to change the value, bindings take care of doing this display updates automatically. If I select images in Aperture that include some with multiple versions of one master, the image count displayed changes when I click Master and then Version, just as it should.
26. Automating Builds And Using The Debugger
So far in this project all my debugging has been done with NSLog() calls since the code is pretty simple. To run my plugin each time I have been dragging the binary from the Build folder to Aperture's export plugins folder, launching Aperture, and then selecting Random Wok from the File > Export menu.
So how about debugging with the debugger? If I do a debug build, go through the same steps, and the run Aperture, my breakpoints are never hit. What is going on?
This is happening because the application, Aperture, is not being run by the debugger, and so my plugin is not being run by the debugger. To make XCode run Aperture I modified the instructions I found in a technical Q & A on Apple's developer site that shows how to handle this situation with a Web Kit plugin. In my case I create a new custom executable in the Projects folder on the left side of the XCode window and set it up this way:

Then I make sure that my build options for the debug build are set correctly: no optimization, generate all symbols, don't strip:

Now I can set breakpoints and have them hit:

I still have to copy the executable and run Aperture manually. But there is a way to fix that. I add a new run script :

And set it up like this:

The debug version is set up with a symbolic link and the release version with a copy. Here is the full text:
# clean up any previous products/symbolic links in the target folder
if [ -a "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}" ]; then
rm -Rf "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi
# Depending on the build configuration, either copy or link to the most recent product
if [ "${CONFIGURATION}" == "Debug" ]; then
# if we're debugging, add a symbolic link to the plug-in
ln -sf "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
elif [ "${CONFIGURATION}" == "Release" ]; then
# if we're compiling for release, just copy the plugin to the Internet Plug-ins folder
cp -Rfv "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi
Here is how the debug version looks in the Export folder:

Now when I compile and run or compile and debug, the script is run and Aperture is launched. Any breakpoints I have set work.
So how about debugging with the debugger? If I do a debug build, go through the same steps, and the run Aperture, my breakpoints are never hit. What is going on?
This is happening because the application, Aperture, is not being run by the debugger, and so my plugin is not being run by the debugger. To make XCode run Aperture I modified the instructions I found in a technical Q & A on Apple's developer site that shows how to handle this situation with a Web Kit plugin. In my case I create a new custom executable in the Projects folder on the left side of the XCode window and set it up this way:

Then I make sure that my build options for the debug build are set correctly: no optimization, generate all symbols, don't strip:

Now I can set breakpoints and have them hit:

I still have to copy the executable and run Aperture manually. But there is a way to fix that. I add a new run script :

And set it up like this:

The debug version is set up with a symbolic link and the release version with a copy. Here is the full text:
# clean up any previous products/symbolic links in the target folder
if [ -a "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}" ]; then
rm -Rf "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi
# Depending on the build configuration, either copy or link to the most recent product
if [ "${CONFIGURATION}" == "Debug" ]; then
# if we're debugging, add a symbolic link to the plug-in
ln -sf "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
elif [ "${CONFIGURATION}" == "Release" ]; then
# if we're compiling for release, just copy the plugin to the Internet Plug-ins folder
cp -Rfv "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi
Here is how the debug version looks in the Export folder:

Now when I compile and run or compile and debug, the script is run and Aperture is launched. Any breakpoints I have set work.
27. Creating A Universal Binary
Random Wok is only being compiled for Intel right now. I need a universal binary so that it will run on the PowerPC architecture as well. This is easy to change. I select the Random Wok target:

Then click on the Info button, and under the Architectures tab, select what I need:

That's all there is to it. Everything happens behind the scenes.
Jump to Part 4
Then click on the Info button, and under the Architectures tab, select what I need:

That's all there is to it. Everything happens behind the scenes.
Jump to Part 4
The Bagelturf site welcomes Donations of any size