Random Wok Aperture Export Plugin -- Part 2
You can download the current release of Random Wok from the downloads page.
10. Initialization Puzzle
A puzzle I faced when dealing with the interface was was how to handle initialization. What can be done when?
I found that -initWithAPIManager: is called once to instantiate the plug-in and that is matched with a single call to -dealloc, as expected. But that call occurs very early in the set-up and the nib file has not even been loaded. No nib file means no outlets, so although ivars can be set up, the interface cannot. So here I just set up default values for ivars.
-settingsView loads the nib and initializes it. Once that is done, the objects in the nib are instantiated. This method is called next. But it has no balancing method to undo anything I may have set up, so I add nothing to this method.
Next, -willBeActivated is called once and paired with -willBeDeactivated which is also called once. The documentation says that these are called each time the application is made active, but this appears not to be the case. Now that the outlets are ready, this is where I set up delegates and will initialize the state of the interface elements based on the ivar values. Anything I do that needs undoing will be undone in -willBeDeactivated.
I found that -initWithAPIManager: is called once to instantiate the plug-in and that is matched with a single call to -dealloc, as expected. But that call occurs very early in the set-up and the nib file has not even been loaded. No nib file means no outlets, so although ivars can be set up, the interface cannot. So here I just set up default values for ivars.
-settingsView loads the nib and initializes it. Once that is done, the objects in the nib are instantiated. This method is called next. But it has no balancing method to undo anything I may have set up, so I add nothing to this method.
Next, -willBeActivated is called once and paired with -willBeDeactivated which is also called once. The documentation says that these are called each time the application is made active, but this appears not to be the case. Now that the outlets are ready, this is where I set up delegates and will initialize the state of the interface elements based on the ivar values. Anything I do that needs undoing will be undone in -willBeDeactivated.
11. A Better Way To Randomize
Reading the documentation for the Aperture export plugin SDK carefully I think I have found a better way of generating random names. What I really want is a random name per image, not per image name. If the name of the image is changed, the image should still get the same pseudo-random export name. If there is some sort of identifier that is tied to the image, not changing with the name, then I should use that.
And it does exist. By using -propertiesWithoutThumbnailForImageAtIndex: I can get a dictionary of properties for each image. Among the properties available is one keyed with kExportKeyUniqueID. This is a 22 character pseudo-random identifier stored in the library file for each image master and version. Here are some examples: Nku72ZxXT1CkGAF1JoQWRQ, mQ6wSngJQH29z0+o0yldKg, Jo1HuM/bRcu3u81dVYhdqw.
I looked at several thousand UUIDs by using SQLite Database Browser to export a table from one of my Aperture libraries. Then by analyzing the character frequencies I could see roughly how random the strings are and what characters are used. I found 0-9, A-Z, a-z, + and /. That is 64 in all, so I get 6 bits from each character, or 132 bits in all. That's four more than 128, which may explain why several of the characters have abnormally high frequencies (Q especially).
I can take the UUID, feed it and the salt into a hash function to get a 64 bit number, and then generate a string to satisfy the user's needs by repeated division of the resulting long long and conversion of the remainders into characters.
So the diagram now becomes:

And it does exist. By using -propertiesWithoutThumbnailForImageAtIndex: I can get a dictionary of properties for each image. Among the properties available is one keyed with kExportKeyUniqueID. This is a 22 character pseudo-random identifier stored in the library file for each image master and version. Here are some examples: Nku72ZxXT1CkGAF1JoQWRQ, mQ6wSngJQH29z0+o0yldKg, Jo1HuM/bRcu3u81dVYhdqw.
I looked at several thousand UUIDs by using SQLite Database Browser to export a table from one of my Aperture libraries. Then by analyzing the character frequencies I could see roughly how random the strings are and what characters are used. I found 0-9, A-Z, a-z, + and /. That is 64 in all, so I get 6 bits from each character, or 132 bits in all. That's four more than 128, which may explain why several of the characters have abnormally high frequencies (Q especially).
I can take the UUID, feed it and the salt into a hash function to get a 64 bit number, and then generate a string to satisfy the user's needs by repeated division of the resulting long long and conversion of the remainders into characters.
So the diagram now becomes:

12. Using Bindings To Populate The Length Pop-Up
To populate the length pop-up I'm going to use bindings. Bindings allow changes in one object to automatically control another object. In this case I want the contents of the length pop-up to reflect the values I store in an array in my Random_Wok instance. Each time I change the array contents, the pop-up contents change. The advantage to this is that I don't have to write the code that populates the pop-up.
To make this work I need a third object: an array controller instance in my nib. An array controller is a pre-written, general-purpose controller that knows how to communicate with objects that expose bindings. It's like hiring a manager to run a department. Since it knows about arrays it can take a lot of work off my hands by dealing with the individual array elements for me. The array controller will mediate between changes in my data model and updates to the view.
To get an array controller instance into my nib I drag one from the controller palette to the nib file:

And I give it a name by double-clicking it: Length Popup Controller. To make the controller do anything useful I have to hook it up to a data model (an array) and a view (the pop-up). The controller then mediates between these for me.
The first thing I have to do is tell the controller what kind of object is in the array it is dealing with. "Good morning new manager, your staff consists of interns". In this case it is an array of NSMutableStrings that I will be modifying according to which string lengths I want the user to be able to choose:

Then I have to tell the array controller which array it will be dealing with. "Your interns are in that cube farm over there". To do this I bind the array controllers content array to the array of length strings in my Random_Wok object. That's an ivar called _lengthStrings:

I bind to File's Owner because that is a stand-in for my Random_Wok class. What this step does is to make the controller able to observe changes to the _lengthStrings array. The observation is incomplete at this stage -- the manager knows which cube farm to keep an eye on, but none of the interns yet know to tell the manager that anything has happened.
Next I have to tell the pop-up button that its contents will be supplied by an array controller. To do that I select the pop-up button and bind it to the array controller. The pop-up button has a binding called content that is designed for doing just this:

The controller key (arrangedObjects) is what the pop-up will observe to get its content. "Catering department, watch the manager's list of foods on his white board. Any time he changes it update the menu".
The last thing I must do is to add code to Random_Wok so that the object tells the NSArrayController that something has changed (remember that the interns in their cubes don't automatically tell the manager that their choice of foods has been updated). I code manual observer notification for this by using -willChangeValueForKey: and -didChangeValueForKey:

I need this manual method of notifying the controller that something has happened because my Random_Wok class is not Key Value Observing compliant. As a class it lacks the methods that the KVO mechanism requires to have change notifications automatically sent to observers. The array controller is KVO compliant, to the pop-up can automatically get notifications that the content array has changed.
Now the interns are saying to the manager "We will have a new list of foods in a minute -- OK, it's been updated". And the manager knows to update his whiteboard, the caterers see the new list of foods because they are watching the whiteboard, and the menu gets changed. And all along the interns don't know or care why they are creating food lists, and the caterers don't know or care where the food lists are coming from.
I try out the code and it works:

The final code for actually creating the array was more complicated than the little test I initially wrote:

This includes some extra logic to select the current length, or if not possible, use the highest length available. In this way as the user clicks around the available controls the pop-up does not keep resetting to the minimum length setting.
To make this work I need a third object: an array controller instance in my nib. An array controller is a pre-written, general-purpose controller that knows how to communicate with objects that expose bindings. It's like hiring a manager to run a department. Since it knows about arrays it can take a lot of work off my hands by dealing with the individual array elements for me. The array controller will mediate between changes in my data model and updates to the view.
To get an array controller instance into my nib I drag one from the controller palette to the nib file:

And I give it a name by double-clicking it: Length Popup Controller. To make the controller do anything useful I have to hook it up to a data model (an array) and a view (the pop-up). The controller then mediates between these for me.
The first thing I have to do is tell the controller what kind of object is in the array it is dealing with. "Good morning new manager, your staff consists of interns". In this case it is an array of NSMutableStrings that I will be modifying according to which string lengths I want the user to be able to choose:

Then I have to tell the array controller which array it will be dealing with. "Your interns are in that cube farm over there". To do this I bind the array controllers content array to the array of length strings in my Random_Wok object. That's an ivar called _lengthStrings:

I bind to File's Owner because that is a stand-in for my Random_Wok class. What this step does is to make the controller able to observe changes to the _lengthStrings array. The observation is incomplete at this stage -- the manager knows which cube farm to keep an eye on, but none of the interns yet know to tell the manager that anything has happened.
Next I have to tell the pop-up button that its contents will be supplied by an array controller. To do that I select the pop-up button and bind it to the array controller. The pop-up button has a binding called content that is designed for doing just this:

The controller key (arrangedObjects) is what the pop-up will observe to get its content. "Catering department, watch the manager's list of foods on his white board. Any time he changes it update the menu".
The last thing I must do is to add code to Random_Wok so that the object tells the NSArrayController that something has changed (remember that the interns in their cubes don't automatically tell the manager that their choice of foods has been updated). I code manual observer notification for this by using -willChangeValueForKey: and -didChangeValueForKey:

I need this manual method of notifying the controller that something has happened because my Random_Wok class is not Key Value Observing compliant. As a class it lacks the methods that the KVO mechanism requires to have change notifications automatically sent to observers. The array controller is KVO compliant, to the pop-up can automatically get notifications that the content array has changed.
Now the interns are saying to the manager "We will have a new list of foods in a minute -- OK, it's been updated". And the manager knows to update his whiteboard, the caterers see the new list of foods because they are watching the whiteboard, and the menu gets changed. And all along the interns don't know or care why they are creating food lists, and the caterers don't know or care where the food lists are coming from.
I try out the code and it works:

The final code for actually creating the array was more complicated than the little test I initially wrote:

This includes some extra logic to select the current length, or if not possible, use the highest length available. In this way as the user clicks around the available controls the pop-up does not keep resetting to the minimum length setting.
13. Continuous Example File Name Updates
There is a problem with the current interface: the user has to type in the prefix and postfix strings and then tab away from the field to end the editing. Until that happens, the example file name does not get updated. What I want is for the text fields to update the example file name continuously with each key press.
To do this I have to set up my Random_Wok object as the delegate to the text fields. If my Random_Wok class provides a -controlTextDidChange: method, then as a delegate, the NSTextField instances will call it for each change of the text.
I set up Random_Wok as the delegate in -willBeActivated: for each of the text fields:

And provide a way to distinguish between the fields by giving them tags:

I give them numbers 0, 1, and 2. Then I code the delegate to update the appropriate string and remake and display the example string on each call:

And that works. Typing, deleting, pasting, any change to the text fields is reflected in an immediate change in the example filename.
To do this I have to set up my Random_Wok object as the delegate to the text fields. If my Random_Wok class provides a -controlTextDidChange: method, then as a delegate, the NSTextField instances will call it for each change of the text.
I set up Random_Wok as the delegate in -willBeActivated: for each of the text fields:

And provide a way to distinguish between the fields by giving them tags:

I give them numbers 0, 1, and 2. Then I code the delegate to update the appropriate string and remake and display the example string on each call:

And that works. Typing, deleting, pasting, any change to the text fields is reflected in an immediate change in the example filename.
14. Preventing Illegal Path Characters
Since the prefix and postscript strings will be going into a path, I also want to prevent the user from entering characters such as / and : that could cause unintended actions. There is no delegate method I can add to a text field to do this. Instead, a different class is used: a text formatter.
A text formatter can be attached to a text cell. Once attached, it can intervene in the formatting of the text to modify what the user sees. A phone number formatter would automatically add brackets and spaces as a number were entered, for instance. A formatter can also change the color and style of the text, and add and remove characters.
Since none of the supplied formatters do what I want, I will have to write my own. And to do that I have to create a new formatter class as a subclass of NSFormatter, the abstract base class that all formatters descend from.
The new formatter will be called BTValidPathElementFormatter and will prevent forward slash and colon characters from appearing, and optionally disallow leading periods and hyphens. Leading periods in file names make the file invisible in the Finder and that would almost certainly be unintended. Leading hyphens are not valid in a file name.
To create my new class in XCode I use File > New File and select Objective C class. Then I give it a name and make sure it is added to my project:

Then in the header file, I add the prototypes for the necessary methods, and add my own methods and ivars for handling the leading periods and hyphens:

The -isPartialStringValid: method allows me to figure out what was just added to the string, and then act accordingly to adjust the string and the current selection.
I need -init and -dealloc methods for my class. I use the -init method to initialize the superclass and up the default state of my ivars:

And I have to provide methods for changing the period and hyphen options:

These three methods are filled in with what is basically template code:

And finally here are the guts of the class: the code that disallows the characters. The first part sets up a character set that will be used to compare against the string in the text field. Then two easy cases are dealt with quickly:

The code uses NSRange structures to keep track of the current selection. Next, periods and hyphens and disallowed:

I'm not certain if the unichar cast is needed, but I did notice that the type returned by characterAtIndex: is of type unichar, so put it in to be on the safe side. Finally I look for the illegal characters in the text that was inserted. I can figure out what was added by comparing the previous and current selections:

And that is it for the new class. I have to make some changes in Random_Wok so that the new formatter is attached to the fields and set up correctly. I add this code to -willBeActivated:

It creates an instance of the formatter for each of the two fields that need formatting. Additionally the prefix formatter is set to reject hyphens and periods. Finally the formatters are attached to the NSCells associated with the NSTextFields using -setFormatter.
A delegate method to NSControl -control:didFailToValidatePartialString:errorDescription is called whenever the formatter returns NO. Since Random_Wok is already a delegate of the three text fields, this will be called. By coding it to beep, I can give the user audible feedback that the key press is not permitted:

And the code works. The prefix field will not accept leading periods or colons and both the prefix and postfix fields reject forward slashes and colons.
A text formatter can be attached to a text cell. Once attached, it can intervene in the formatting of the text to modify what the user sees. A phone number formatter would automatically add brackets and spaces as a number were entered, for instance. A formatter can also change the color and style of the text, and add and remove characters.
Since none of the supplied formatters do what I want, I will have to write my own. And to do that I have to create a new formatter class as a subclass of NSFormatter, the abstract base class that all formatters descend from.
The new formatter will be called BTValidPathElementFormatter and will prevent forward slash and colon characters from appearing, and optionally disallow leading periods and hyphens. Leading periods in file names make the file invisible in the Finder and that would almost certainly be unintended. Leading hyphens are not valid in a file name.
To create my new class in XCode I use File > New File and select Objective C class. Then I give it a name and make sure it is added to my project:

Then in the header file, I add the prototypes for the necessary methods, and add my own methods and ivars for handling the leading periods and hyphens:

The -isPartialStringValid: method allows me to figure out what was just added to the string, and then act accordingly to adjust the string and the current selection.
I need -init and -dealloc methods for my class. I use the -init method to initialize the superclass and up the default state of my ivars:

And I have to provide methods for changing the period and hyphen options:

These three methods are filled in with what is basically template code:

And finally here are the guts of the class: the code that disallows the characters. The first part sets up a character set that will be used to compare against the string in the text field. Then two easy cases are dealt with quickly:

The code uses NSRange structures to keep track of the current selection. Next, periods and hyphens and disallowed:

I'm not certain if the unichar cast is needed, but I did notice that the type returned by characterAtIndex: is of type unichar, so put it in to be on the safe side. Finally I look for the illegal characters in the text that was inserted. I can figure out what was added by comparing the previous and current selections:

And that is it for the new class. I have to make some changes in Random_Wok so that the new formatter is attached to the fields and set up correctly. I add this code to -willBeActivated:

It creates an instance of the formatter for each of the two fields that need formatting. Additionally the prefix formatter is set to reject hyphens and periods. Finally the formatters are attached to the NSCells associated with the NSTextFields using -setFormatter.
A delegate method to NSControl -control:didFailToValidatePartialString:errorDescription is called whenever the formatter returns NO. Since Random_Wok is already a delegate of the three text fields, this will be called. By coding it to beep, I can give the user audible feedback that the key press is not permitted:

And the code works. The prefix field will not accept leading periods or colons and both the prefix and postfix fields reject forward slashes and colons.
15. Adding a Progress Bar
So far the progress bar supplied by Aperture as my images export does nothing. I'd like it to progress according to the fraction of images that have been exported out of the total number. Since progress for Random Wok includes a final renaming step that is very fast, I can display a progress bar that goes up to 90% for the actual export and then label the last 10% "Randomizing".
To implement the progress bar I have to set up the export progress structure and then update the current values that it contains as the situation progresses. The progress structure looks like this:

To protect it against simultaneous update by multiple threads an NSLock is used. That lock has to be claimed and relinquished by any thread that wants access, so protecting against partial changes caused by context switching. The SDK comes with two methods already included: -lockProgress and -unlockProgress that lock using an NSLock ivar called _progressLock.
I set up the progress bar just before I tell Aperture to start the export. The code I add to -exportManagerShouldBeginExport looks like this:

It does some math to make the progress bar go to about 90% of its full travel when all the images have been exported, then loads up the structure before starting the export. As the export proceeds, -exportmanagerShouldWriteImageData:toRelativePath:forImageAtIndex: will be called for each image. So it is that method that hosts the following code:

In -exportManagerDidFinishExport I have an opportunity to change the progress message and handle the randomizing rename. So I add this code to the beginning of that method to switch the message and use an indeterminate style of bar:

Notice that I do some memory management here with autorelease and retain. The example code does this, and I believe that I need to do it with the way I have handled the strings too. If not, using it in my code is harmless.
And here is the resulting progress bar:

To implement the progress bar I have to set up the export progress structure and then update the current values that it contains as the situation progresses. The progress structure looks like this:

To protect it against simultaneous update by multiple threads an NSLock is used. That lock has to be claimed and relinquished by any thread that wants access, so protecting against partial changes caused by context switching. The SDK comes with two methods already included: -lockProgress and -unlockProgress that lock using an NSLock ivar called _progressLock.
I set up the progress bar just before I tell Aperture to start the export. The code I add to -exportManagerShouldBeginExport looks like this:

It does some math to make the progress bar go to about 90% of its full travel when all the images have been exported, then loads up the structure before starting the export. As the export proceeds, -exportmanagerShouldWriteImageData:toRelativePath:forImageAtIndex: will be called for each image. So it is that method that hosts the following code:

In -exportManagerDidFinishExport I have an opportunity to change the progress message and handle the randomizing rename. So I add this code to the beginning of that method to switch the message and use an indeterminate style of bar:

Notice that I do some memory management here with autorelease and retain. The example code does this, and I believe that I need to do it with the way I have handled the strings too. If not, using it in my code is harmless.
And here is the resulting progress bar:

16. Ending Editing
I discovered a problem with the Use Salt checkbox. If the checkbox is clicked then it disables the salt string text field. When it is clicked again, the field is re-enabled, but the string it contained is gone. Looking into this further I found that if I clicked away from the text field before disabling the field, the string would stick.
What is happening is that the field editor attached to the text field is not being told to end editing. Normally that is done when focus moves to another field or control, but not in the case of disabling the control. The cure was to change the -useSalt: method like this:

That call to -endEditingFor: tells the window manager to end editing for the field and all is well.
I also fixed the name "Random_Wok" in the menu and the window title. I edited the info.plist file by changing the display name string:

What is happening is that the field editor attached to the text field is not being told to end editing. Normally that is done when focus moves to another field or control, but not in the case of disabling the control. The cure was to change the -useSalt: method like this:

That call to -endEditingFor: tells the window manager to end editing for the field and all is well.
I also fixed the name "Random_Wok" in the menu and the window title. I edited the info.plist file by changing the display name string:

17. Adding Randomness
To get my randomness I will use the MD5 message digest. That delivers a 128 bit number based on an input of any number of bytes. Since Mac OS X ships with MD5 functions in a dynamic library, all I have to do is to call that appropriately.
Appropriately in this case means using code that has already been written. Andreas Mayer submitted some code to the Cocoa mailing list that creates a category on NSData for MD5 and SHA1 digests. A Cocoa category is a collection of methods that extend those of an existing class. They cannot add ivars. The interface to the category AMDigest looks like this:

It declares two methods, each returning an NSData object with the digest inside. Since it is a category on NSData, the methods are used just as any other NSData methods are. The implementation code wraps some C function calls and returns a new autoreleased NSData object:

Too add the methods, I create a new Objective C class called AMDigest in my XCode project and paste in the code. Left as is, this does not work, because I have not told XCode to link to the library and the function calls go unresolved. It took some work to figure out that the libcrypto.dylib file that is provided with OS X is a stub and should not be added to the project in the Frameworks and Libraries folder. All that is actually needed is to select the Random_Wok target and add this item to the build configuration:

To use the new method I also have to #import the header file that contains the interface in my Random_Wok.m file.
The plan is to concatenate the salt and the UUID of the Aperture image together and create an MD5 digest of that. I can then use 64 bits of the digest to create a random string based on the settings of the length, format, and alpha case that the user has selected.
Appropriately in this case means using code that has already been written. Andreas Mayer submitted some code to the Cocoa mailing list that creates a category on NSData for MD5 and SHA1 digests. A Cocoa category is a collection of methods that extend those of an existing class. They cannot add ivars. The interface to the category AMDigest looks like this:

It declares two methods, each returning an NSData object with the digest inside. Since it is a category on NSData, the methods are used just as any other NSData methods are. The implementation code wraps some C function calls and returns a new autoreleased NSData object:

Too add the methods, I create a new Objective C class called AMDigest in my XCode project and paste in the code. Left as is, this does not work, because I have not told XCode to link to the library and the function calls go unresolved. It took some work to figure out that the libcrypto.dylib file that is provided with OS X is a stub and should not be added to the project in the Frameworks and Libraries folder. All that is actually needed is to select the Random_Wok target and add this item to the build configuration:

To use the new method I also have to #import the header file that contains the interface in my Random_Wok.m file.
The plan is to concatenate the salt and the UUID of the Aperture image together and create an MD5 digest of that. I can then use 64 bits of the digest to create a random string based on the settings of the length, format, and alpha case that the user has selected.
18. Creating The Example File Name
There are several parameters that depend on the settings for the random string format, the alpha case, and the length. I concentrate all the decisions for these into one big ugly switch statement that starts off like this:

This sets a character set for generating the random string, and also an example string that I will use on the display. Displaying the example random string now consists of this:

I truncate the example random part by the selected string length. Anywhere that the random string parameters change, I add a call to recalculate the parameters, such as in the action code for selecting the length:

Now the dialog looks like this:

The example file name now follows the settings.
Jump to Part 3

This sets a character set for generating the random string, and also an example string that I will use on the display. Displaying the example random string now consists of this:

I truncate the example random part by the selected string length. Anywhere that the random string parameters change, I add a call to recalculate the parameters, such as in the action code for selecting the length:

Now the dialog looks like this:

The example file name now follows the settings.
Jump to Part 3
The Bagelturf site welcomes Donations of any size