TweetFollow Us on Twitter

July 02 Mac OS X

Volume Number: 18 (2002)
Issue Number: 7
Column Tag: Mac OS X

Writing Plug-Ins for Cocoa: iPhoto to PhotoToWeb Exporter Plug-In

by Andrew Stone

After you've been working with Cocoa for a while, you'll come to appreciate the truth behind this statement: "If it's hard to do, then you're not doing it right”. By this I mean any coding solution that involves convoluted logic, going beneath the API or using undocumented methods, is probably not the right approach.

However, sometimes delving into secret API is the fastest way to get something done. Caveat Emptor: your program may break if the underlying methods change! That's why I usually admonish against using private, hidden methods.

This article will explain how to peek into the guts of an Objective C Cocoa application, iPhoto, and write a PlugIn that allows users to export their photos to the Stone Studio digital photography to web application PhotoToWeb®. iPhoto offers only limited web production facilities, whereas PhotoToWeb offers a complete suite of web design functionality, including the ability to create linked multi-level photo websites.

My design objectives were to keep the PlugIn small and very simple, including using the existing PhotoToWeb AppleScript API. This way, there are no code or file format dependencies in the PlugIn. Instead, we simply build a script that asks PhotoToWeb to build a new album with the exported photos. Also, instead of duplicating the user's photos, we just export the full path names since PhotoToWeb can build albums out of any images anywhere on the user's disk or network.


Figure 1: iPhoto loads the PhotoToWeb export PlugIn via Share - Export

Class-Dump Is Your Friend

If you want to see the inner architecture of an Objective C application, you'll need class-dump, which you can download from:

http://www.omnigroup.com/~nygard/Projects/index.html

class-dump is a utility for examining the Objective-C segment of Mach-O files. It generates the @interface and @protocol declarations for classes, categories and protocols.

Class-dump is open source, and has been an ongoing project since the early NeXT (the grandfather of OS X) days. It has had several contributors, including Eric P. Scott, Steve Nygard, James McIlree, Tom Hageman, and Carl Lindberg. It's a unix executable and has no GUI. Running it without arguments gives you the options:

class-dump 2.1.5

Usage: class-dump [-a] [-A] [-e] [-R] [-C regex] [-r] [-s] [-S] executable-file
        -a  show instance variable offsets
        -A  show implementation addresses
        -e  expand structure (and union) definition whenever possible
        -R  recursively expand @protocol <>
        -C  only display classes matching regular expression
        -r  recursively expand frameworks and fixed VM shared libraries
        -s  convert STR to char *
        -S  sort protocols, classes, and methods

You can dump all the classes of an application, or just those matching some pattern. Since we're interested in the Export methods, we'll run it like this:

class-dump -C Export /Applications/iPhoto.app/Contents/MacOSX/iPhoto
which outputs:
@protocol ExportImageProtocol
- directoryPath;
- (void)cancelExportBeforeBeginning;
- (void)cancelExport;
- (void)startExport;
- (void)clickExport;
- (void)disableControls;
- (void)enableControls;
- window;
- getImageDictionaryAtIndex:(unsigned int)fp16;
- getThumbPathAtIndex:(unsigned int)fp16;
- getImagePathAtIndex:(unsigned int)fp16;
- (char)imageIsPortraitAtIndex:(unsigned int)fp16;
- imageSelectionArray;
- (unsigned int)imageCount;
@end
@interface ExportMgr:NSObject <ExportImageProtocol>
{
    ArchiveDocument *mDocument;
    NSMutableArray *mExporters;
    KeyMgr *mSelection;
    ExportController *mExportController;
}
+ exportMgr;
- (void)releasePlugins;
- (void)setExportController:fp12;
- exportController;
- (void)setDocument:fp12;
- document;
- (void)updateDocumentSelection;
- (unsigned int)count;
- recAtIndex:(unsigned int)fp12;
- (void)scanForExporters;
- (unsigned int)imageCount;
- imageSelectionArray;
- (char)imageIsPortraitAtIndex:(unsigned int)fp12;
- getImagePathAtIndex:(unsigned int)fp12;
- getThumbPathAtIndex:(unsigned int)fp12;
- getImageDictionaryAtIndex:(unsigned int)fp12;
- getImageRecAtIndex:(unsigned int)fp12;
- (void)enableControls;
- (void)disableControls;
- window;
- (void)clickExport;
- (void)startExport;
- (void)cancelExport;
- (void)cancelExportBeforeBeginning;
- directoryPath;
- (void)_copySelection:fp12;
@end

So all we need to do to write a PlugIn is implement the methods defined in the ExportImageProtocol, add a few keys to the Info.plist file, and provide an interface file with any special controls. You can open some of the existing PlugIns inside iPhoto by control-clicking iPhoto in Finder, looking inside the Contents/PlugIns folder, and control-clicking one of the Exporter bundles.

Of course, without documentation or source, we'll have to guess at what certain methods do. ExportMgr has a class method "+exportMgr” which provides us with the ExportMgr instance. Reading over the ExportMgr class methods, we see methods for retrieving a set of photos one at a time, and getting information about that photo.

Making PlugIns

To make a loadable bundle (which is exactly what a PlugIn is), launch Project Builder and choose New Project..., select "Bundle”, provide a path, and save. Create a new empty nib in InterfaceBuilder, add a CustomView (we really don't need a window, just a view containing our interface elements), and save it into your bundle project.

We only need one class, "PhotoToWebExport”, but we also need to include the protocol as defined in ExportPluginProtocol, and declare that our object conforms to this protocol:

/* PhotoToWebExport.h */
#import <Cocoa/Cocoa.h>
@protocol ExportPluginProtocol
- (void)cancelExport;
- (void)unlockProgress;
- (void)lockProgress;
- (void *)progress;
- (void)performExport:fp16;
- (void)startExport:fp16;
- defaultDirectory;
- defaultFileName;
- requiredFileType;
- (void)viewWillBeDeactivated;
- (void)viewWillBeActivated;
- lastView;
- firstView;
- settingsView;
- initWithExportImageObj:fp16;
@end
#define IS_NULL(s) (!s || [s isEqualToString:@””])
#define NOT_NULL(s) (s && ![s isEqualToString:@””])
@interface PhotoToWebExport : NSObject <ExportPluginProtocol>
{
    IBOutlet id settingsView;
    IBOutlet id titleField;
    IBOutlet id imageView;
    IBOutlet id actionMatrix;
    IBOutlet id finishView;
    IBOutlet id firstView;
    id exportManager;
    ComponentInstance myComponent;
    NSString *_lastDirectoryPath;
}
- (void)downloadLatest:(id)sender;
@end
#import "PhotoToWebExport.h”
#import <Carbon/Carbon.h>
@implementation PhotoToWebExport
// Designated initializer 
// we'll grab the ExportMgr instance - although you could
// just use the class method + [ExportMgr exportMgr]
- initWithExportImageObj:fp16 {    
    self = [super init];
    exportManager = fp16;
   // we'll use AppleScript to talk to PhotoToWeb:
    myComponent = OpenDefaultComponent(kOSAComponentType, kOSAGenericScriptingComponentSubtype);
    return self;
}
// if there are photos to export, create and run an AppleScript
// to load the photos into a new PhotoToWeb album:
- (void)doExport:fp16 {
    BOOL good = YES;
    if ([exportManager imageCount] > 0) 
        [self runScript:[self scriptCommand]];
    else {
        NSRunAlertPanel(@”No images selected”,@”No images to export”,@”OK”, nil, nil);
        good = NO;
    }
    
    // make panel disappear - might be other way:
    [[exportManager exportController] cancel:self];
   
   // Bring the album to the front so user can make web site or thumbnails etc...
    if (good) [[NSWorkspace sharedWorkspace] openFile:[exportManager directoryPath] 
    withApplication:@”PhotoToWeb”];
}
// this is the guts of the plugin 
// We'll create the AppleScript script necessary to export
// the selected photos, and attempt to grab as much information
// as the users have provided in iPhoto - such as comments, rotation
// crop information, title, etc.
- (NSString *)scriptCommand {
    NSMutableString *s = [NSMutableString stringWithCapacity:2000];
    unsigned i, c = [exportManager imageCount];
    
    // for next time the user runs it, we'll have their preferred directory:
    [_lastDirectoryPath release];
    _lastDirectoryPath = [[[exportManager directoryPath] stringByDeletingLastPathComponent] 
    copyWithZone:[self zone]];
    [s appendString:@”tell application \”PhotoToWeb\”\r”];
    [s appendString:@”\tmake new album at before front album\r”];
    [s appendString:@”\ttell the front album\r”];
// this will prevent the AppleScript from timing out if there is a huge number of photos:
    [s appendString:@”\t\tignoring application responses\r”];
    
    for (i = 0; i < c; i++) {
        NSString *path = [exportManager getImagePathAtIndex:i];
        NSDictionary *dict = [exportManager getImageDictionaryAtIndex:i];
        int rotation = [[dict objectForKey:@”Rotation”] intValue];
        NSString *title = [dict objectForKey:@”Caption”];
        NSString *story = [dict objectForKey:@”Annotation”]; // could be nil
        NSDate *archiveDate = [dict objectForKey:@”ArchiveDate”];
        NSString *date = [[NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
        [archiveDate timeIntervalSinceReferenceDate]] descriptionWithCalendarFormat:@”Archived: 
        %e %B %Y”];
        [s appendString:[NSString stringWithFormat:@”\t\t\tadd photos in \”%@\”\r”, path]];
        if (rotation != 0) {
                [s appendString:[NSString stringWithFormat:@”\t\t\tset the photorotation of last photo 
                to %d\r”, rotation]];
        }
        if (NOT_NULL(title)) 
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the title of last photo to 
            \”%@\”\r”, title]];
        if (NOT_NULL(story)) {
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the text contents of last photo to 
            \”%@\”\r”, NOT_NULL(date)? [NSString stringWithFormat: @”%@ %@”,story,date]:story]];
        } else if (NOT_NULL(date))
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the text contents of last photo to 
            \”%@\”\r”, date]];
    
    }
    [s appendString:@”\t\tend ignoring\r”];
    [s appendString:[NSString stringWithFormat:@”\t\tsave in \”%@\”\r”,[exportManager directoryPath]]];
    [s appendString:@”\tend tell\r”];
    [s appendString:@”end tell\r”];
    return s;
}
- (void)performExport:fp16  {
    // not sure how this differs from startExport but doesn't seem to be needed
}
- (void)startExport:fp16 {
    [self doExport:fp16];
}
// if the plugin has been run before, use the last saved path
// Otherwise choose a standard location:
- (id) defaultDirectory {
    if (NOT_NULL(_lastDirectoryPath)) return _lastDirectoryPath;
   return [NSHomeDirectory() stringByAppendingPathComponent:@”Documents”];
}
// here's a nice way to guarantee uniqueness of the album name
// concatenate the year-month-day-seconds:
- defaultFileName {
    return [NSString stringWithFormat:@”%@-%d”,[[NSCalendarDate calendarDate] 
    descriptionWithCalendarFormat:@”%Y-%m-%d”], [NSDate timeIntervalSinceReferenceDate]];
}
// this is PhotoToWeb's single Album file type:
- requiredFileType {
    return @”album”;
}
- lastView {
   return finishView;
}
- firstView {
   return firstView;
}
- settingsView {
   return settingsView;
}
// An action method to provide user with latest version of PhotoToWeb:
- (void)downloadLatest:(id)sender {
            [[NSWorkspace sharedWorkspace] openURL:[NSURL
            URLWithString:@”http://www.stone.com/NewDownload.html#PHOTO”]];
}
// I can't think of anything to do in these two methods
// In a more complex plugin, you might set default values:
- (void)viewWillBeDeactivated {
}
- (void)viewWillBeActivated {
}
// these next 4 methods are not yet flushed out because I haven't figured
// out exactly what to do with them - if you do, email me!
- (void)cancelExport {
}
- (void)unlockProgress {
}
- (void)lockProgress {
}
- (void *)progress {
    return NULL; //??
}
// The AppleScript compilation methods:
#define CHECK  
//fprintf(stderr,”result code = %d”, ok);
// This converts an AEDesc into a corresponding NSValue.
static id aedesc_to_id(AEDesc *desc)
{
    OSErr ok;
    
    if (desc->descriptorType == typeChar)
    {
        NSMutableData *outBytes;
        NSString *txt;
        
        outBytes = [[NSMutableData alloc] initWithLength:AEGetDescDataSize(desc)];
        ok = AEGetDescData(desc, [outBytes mutableBytes], [outBytes length]);
        CHECK;
    
        txt = [[NSString alloc] initWithData:outBytes encoding:[NSString defaultCStringEncoding]];
        [outBytes release];
        [txt autorelease];
        
        return txt;
    }
    
    if (desc->descriptorType == typeSInt16)
    {
        SInt16 buf;
        
        AEGetDescData(desc, &buf, sizeof(buf));
        
        return [NSNumber numberWithShort:buf];
    }
    
    return [NSString stringWithFormat:@”[unconverted AEDesc, type=\”%c%c%c%c\”]”, 
    ((char *)&(desc->descriptorType))[0], ((char *)&(desc->descriptorType))[1], 
    ((char *)&(desc->descriptorType))[2], ((char *)&(desc->descriptorType))[3]];
}
- (void)runScript:(NSString *)txt
{
    NSData *scriptChars = [txt dataUsingEncoding:[NSString defaultCStringEncoding]];
    AEDesc source, resultText;
    OSAID scriptId, resultId;
    OSErr ok;
    // Convert the source string into an AEDesc of string type.
    ok = AECreateDesc(typeChar, [scriptChars bytes], [scriptChars length], &source);
    CHECK;
    
    // Compile the source into a script.
    scriptId = kOSANullScript;
    ok = OSACompile(myComponent, &source, kOSAModeNull, &scriptId);
    AEDisposeDesc(&source);
    CHECK;
            
    // Execute the script, using defaults for everything.
    resultId = 0;
    ok = OSAExecute(myComponent, scriptId, kOSANullScript, kOSAModeNull, &resultId);
    CHECK;
    
    if (ok == errOSAScriptError) {
        AEDesc ernum, erstr;
        id ernumobj, erstrobj;
        
        // Extract the error number and error message from our scripting component.
        ok = OSAScriptError(myComponent, kOSAErrorNumber, typeShortInteger, &ernum);
        CHECK;
        ok = OSAScriptError(myComponent, kOSAErrorMessage, typeChar, &erstr);
        CHECK;
        
        // Convert them to ObjC types.
        ernumobj = aedesc_to_id(&ernum);
        AEDisposeDesc(&ernum);
        erstrobj = aedesc_to_id(&erstr);
        AEDisposeDesc(&erstr);
        
        txt = [NSString stringWithFormat:@”Error, number=%@, message=%@”, ernumobj, erstrobj];
    } else {
        // If no error, extract the result, and convert it to a string for display
        
        if (resultId != 0) { // apple doesn't mention that this can be 0?
            ok = OSADisplay(myComponent, resultId, typeChar, kOSAModeNull, &resultText);
            CHECK;
    
            txt = aedesc_to_id(&resultText);
            AEDisposeDesc(&resultText);
        } else {
            txt = @”[no value returned]”;
        }
        OSADispose(myComponent, resultId);
    }
    
    ok = OSADispose(myComponent, scriptId);
    CHECK;
}
@end

After you compile your PlugIn, drop it into the PlugIns folder in iPhoto, relaunch iPhoto, choose "Share” and "Export”, and if all is well, you'll see your PlugIn as a tab in the Export TabView window.

Then Apple Released iPhoto 1.1

Before this article went to print, Apple released an update to iPhoto—and indeed, my Exporter bundle broke! First, I looked through the console error messages and saw that my PlugIn wasn't responding to certain messages. Then, I used class-dump to confirm that some protocols had changed.

The first problem was that my PlugIn's main settingsView didn't respond to the ExportPluginBoxProtocol protocol:

@protocol ExportPluginBoxProtocol
- (char)performKeyEquivalent:fp16;
@end

So, by looking at the nib files in the Apple-provided PlugIns, I deduced this simple NSBox subclass:

@interface P2WExportPluginBox:NSBox <ExportPluginBoxProtocol>
{
    id mPlugin;
}
@end
@implementation P2WExportPluginBox
- (char)performKeyEquivalent:fp12 {
}
@end

Drop this source file into the Interface Builder class window to have it read the file. Assign the class "P2WExportPluginBox” to the NSBox containing your PlugIn controls in Interface Builder. Connect the "mPlugin” instance variable to the file's owner. Save, build, and drop into the PlugIns folder. (An interesting sidenote: variable names can reveal a lot. The coder here was probably an old Mac toolbox programmer, as indicated by the lower-case "m”, as in "my”, for the ivar. Objective-C coders conventionally use an underscore to prefix ivars, so the ivar name would have been "_plugin”.)

Second, a few more methods were needed in the main PlugIn class to get it to work again:

@implementation PhotoToWebExport
- (NSString *)name {
    return @”Stone Design's iPhoto to PhotoToWeb Exporter”;
}
- (BOOL)treatSingleSelectionDifferently { return NO; }
...
 

Conclusion

Cocoa, especially the Objective C flavor, is the future of Mac OS X, as evidenced by the new apps coming out of Apple written almost entirely in Cocoa. Dynamic runtime loading comes free, and you can examine the classes and methods in an application using class-dump. Knowledge of Carbon is still very useful. For example, in this PlugIn we use it to compile and execute our AppleScript. It's a good idea to keep an agnostic attitude towards the various Apple API's.

Using the techniques outlined in this article, you should be able to add all sorts of functionality to the ever-increasing number of applications available for Mac OS X.


Andrew Stone

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

How to get a high score in every level o...
Sky Charms is an adorable match three puzzler that provides a decent challenge thanks to its creative level design. It regularly presents something new, forcing you to think on your feet. [Read more] | Read more »
Apestorm: Full Bananas (Games)
Apestorm: Full Bananas 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: ***Launch sale – limited time only!*** Fugitive Apes have taken to the skies in search of revenge after humans have... | Read more »
How to create bigger words in Spellspire
Words have power. At least they do in Spellspire,a game about blasting out magical attacks by making words out of a jumble of letters. And it's a lot of fun. But if you want to be the best, you're going to have to think tactically when you start... | Read more »
Steel Media and DeePoon have partnered f...
Virtual reality is the next big thing, and 148Apps's publisher,Steel Media, wants to know what the hottest upcoming games are. [Read more] | Read more »
Airline Director 2 - Tycoon Game (Games...
Airline Director 2 - Tycoon Game 1.2.1 Device: iOS Universal Category: Games Price: $2.99, Version: 1.2.1 (iTunes) Description: Airline Director 2 is a management game set in the challenging field of commercial aviation. As the... | Read more »
Dog Mendonca (Games)
Dog Mendonca 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: [ Solve a criminal case beyond believe in this supernatural adventure game based on the popular graphic novel trilogy published... | Read more »
Amidakuji Knight (Games)
Amidakuji Knight 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Ghost leg rules meets RPG!Select the best path and save a princess! A long long time ago, there was a beautiful and peaceful... | Read more »
The 5 best mobile games like Game of Thr...
Everyone's favourite weekly dosage of medieval depression, Game of Thrones, is back for its sixth season. An excellent time for the bloodthirsty! [Read more] | Read more »
How to approach quests in Fallen London
Sitting at over 1.5 million words, Fallen London is filled to the brim with intriguing tales and mysterious characters. From the start, you’ll find a slew of quests ripe for the taking. [Read more] | Read more »
How to survive in LOUD on Planet X
LOUD on Planet X is a hybrid of a tower defense and rhythm game that pits famous indie acts against invading aliens. You need timing and strategy in this game in order to succeed, things can get pretty overwhelming pretty quickly. Here are some... | Read more »

Price Scanner via MacPrices.net

Aleratec Releases Mac Software Upgrade for 1...
California based Aleratec Inc., designer, developer and manufacturer of Portable Device Management (PDM) charge/sync products for mobile devices and professional-grade duplicators for hard disk... Read more
Sale! Amazon offers 27-inch iMac, 13-inch 2.9...
Amazon has the 27″ 3.2GHz 5K iMac and the 13″ 3.9GHz Retina MacBook Pro on sale for $300 off MSRP, each including free shipping, for a limited time: - 27″ 3.2GHz/1TB HD 5K iMac (model MK462LL/A): $... Read more
Apple refurbished 13-inch Retina MacBook Pros...
Apple has Certified Refurbished 13″ Retina MacBook Pros available for up to $270 off the cost of new models. An Apple one-year warranty is included with each model, and shipping is free: - 13″ 2.7GHz... Read more
13-inch 2.7GHz/128GB Retina MacBook Pro on sa...
Take $200 off MSRP on the price of a new 13″ 2.7GHz/128GB Retina MacBook Pro (model MF839LL/A) at Amazon. Shipping is free: - 13″ 2.7GHz/128GB Retina MacBook Pro: $1099.99 $200 off MSRP Act now if... Read more
Apple refurbished clearance 15-inch Retina Ma...
Apple has Certified Refurbished 2014 15″ 2.2GHz Retina MacBook Pros available for $1609, $390 off original MSRP. Apple’s one-year warranty is included, and shipping is free. They have refurbished 15... Read more
27-inch 5K iMacs on sale for up to $150 off M...
B&H Photo has 27″ 5K iMacs on sale for up to $150 off MSRP including free shipping plus NY sales tax only: - 27″ 3.3GHz iMac 5K: $2199 $100 off MSRP - 27″ 3.2GHz/1TB Fusion iMac 5K: $1849.99 $150... Read more
What Does The Refreshed 12-Inch MacBook Tell...
A lot of commentators are complaining that Apple’s update of the 12-Inch MacBook last week is a bit of a damp squib. I don’t know what they were expecting, since it would be very unlike Apple to do a... Read more
Free Wittify Keyboard Now Available On The Ap...
A team of Harvard Business School students have announced that the Wittify Keyboard, a new app utility for iOS devices, is now available on the Apple App Store. The Wittify keyboard and application... Read more
Apple Reports First Year-Over-Year Quarterly...
Apple on TUesday announced financial results for its fiscal 2016 second quarter ending March 26, 2016. The Company posted quarterly revenue of $50.6 billion and quarterly net income of $10.5 billion... Read more
13-inch 2.7GHz Retina MacBook Pros on sale fo...
Take $130-$150 off MSRP on the price of a new 13″ 2.7GHz Retina MacBook Pro at Amazon. Shipping is free: - 13″ 2.7GHz/128GB Retina MacBook Pro: $1169 $130 off MSRP - 13″ 2.7GHz/256GB Retina MacBook... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Restaurant Manager (Neighborhood Captain) - A...
…in every aspect of daily operation. WHY YOU'LL LIKE IT: You'll be the Big Apple . You'll solve problems. You'll get to show your ability to handle the stress and Read more
*Apple* Solutions Consultant - … (United Sta...
Job Summary As an Apple Solutions Consultant, you'll be the link between our future customers and our products. You'll showcase your entrepreneurial spirit as you Read more
Restaurant Manager (Neighborhood Captain) - A...
…in every aspect of daily operation. WHY YOU'LL LIKE IT: You'll be the Big Apple . You'll solve problems. You'll get to show your ability to handle the stress and Read more
Restaurant Manager (Neighborhood Captain) - A...
…in every aspect of daily operation. WHY YOU'LL LIKE IT: You'll be the Big Apple . You'll solve problems. You'll get to show your ability to handle the stress and Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.