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

Shadow Blade: Reload guide - How to hack...
Shadow Blade: Reload is the kind of action-platformer that would have happily sucked up hours of your time on a console a few years back.Now, you can take it with you wherever you go, and its mobile conversion is not too shabby at all. To help you... | Read more »
Tomb of the Mask guide - How to increase...
Tomb of the Mask is a great endless arcade game from Happymagenta in which quick reflexes and a persistent attitude can go a long way toward earning a top score. Check out these tips to see if you can give yourself an edge on the leaderboards. [... | Read more »
Smooth Operator! (Games)
Smooth Operator! 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Smooth Operator is a weird, weird two-player kissing game. Squeeze in for 2 player fun on a single iPad, creating awkward... | Read more »
Sinless: Remastered (Games)
Sinless: Remastered 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: | Read more »
_PRISM Guide - How to solve those puzzle...
_PRISM is a rather delightful puzzle game that’s been tailor made for touch screens. While part of the fun is figuring things out as you go along, we thought we’d offer you a helping hand at getting in the right mindset. Don’t worry about messing... | Read more »
Fractal Space (Games)
Fractal Space 1.3.1 Device: iOS Universal Category: Games Price: $.99, Version: 1.3.1 (iTunes) Description: Live the memorable experience of Fractal Space, a unique first person adventure & puzzle game by Haze Games! Will you... | Read more »
Set off on an adventure through the Cand...
Like match three puzzlers? If so, Jelly Blast, the innovative iOS and Android game which launched last year, is worth a look. Jelly Blast sees you head off on an epic adventure through the Candy Kingdom with your friends Lily, Mr. Hare, and Mr.... | Read more »
Ellipsis - Touch. Explore. Survive. (...
Ellipsis - Touch. Explore. Survive. 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Abzorb (Games)
Abzorb 1 Device: iOS Universal Category: Games Price: $2.99, Version: 1 (iTunes) Description: Abzorb is a tilt game about consuming orbs by getting as close as you can to them without touching. By tilting your mobile device, collect... | Read more »
A Short Tale (Games)
A Short Tale 1.0 Device: iOS Universal Category: Games Price: $3.99, Version: 1.0 (iTunes) Description: It’s been years since I lost Ben, so long yet it doesn’t seem long enough. I never thought I’d return here, to where everything... | Read more »

Price Scanner via MacPrices.net

cb Hardcase – Handmade and Premium Protective...
Baden-Baden, Germany based company cb innovations has introduced the new cb Hardcase for iPhone. Featuring fine Italian Premium leather that makes for a unique look and feel, the cb Hardcase... Read more
Sale! B&H Photo offers 12-inch Retina Mac...
B&H Photo has 12″ Retina MacBooks on sale for $300 off MSRP for a limited time. Shipping is free, and B&H charges NY tax only: - 12″ 1.1GHz Gray Retina MacBook: $999 $300 off MSRP - 12″ 1.... Read more
App Annie Reveals Future of the App Economy:...
App Annie, a San Francisco based mobile app data and insights platform, has launched its first comprehensive app economy forecast. This new offering will provide brands, agencies, investors and app... Read more
Apple restocks Certified Refurbished Mac mini...
Apple has restocked Certified Refurbished 2014 Mac minis, with models available starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: - 1.4GHz Mac mini: $419 $... Read more
What iPad Pro Still Needs To Make It Truly Pr...
I love my iPad Air 2. So much that I’m grudgingly willing to put up with its compromises and limitations as a production tool in order to take advantage of its virtues. However, since a computer for... Read more
21-inch 3.1GHz 4K on sale for $1399, $100 off...
B&H Photo has the 21″ 3.1GHz 4K iMac on sale $1399 for a limited time. Shipping is free, and B&H charges NY sales tax only. Their price is $100 off MSRP: - 21″ 3.1GHz 4K iMac (MK452LL/A): $... Read more
Apple price trackers, updated continuously
Scan our Apple Price Trackers for the latest information on sales, bundles, and availability on systems from Apple’s authorized internet/catalog resellers. We update the trackers continuously: - 15″... Read more
Save up to $240 with Apple Certified Refurbis...
Apple is now offering Certified Refurbished 12″ Retina MacBooks for up to $240 off the cost of new models. Apple will include a standard one-year warranty with each MacBook, and shipping is free. The... 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
Apple refurbished Time Capsules available for...
Apple has certified refurbished Time Capsules available for $120 off MSRP. Apple’s one-year warranty is included with each Time Capsule, and shipping is free: - 2TB Time Capsule: $179, $120 off - 3TB... Read more

Jobs Board

Lead Engineer *Apple* OSX & Hardware -...
Lead Engineer Apple OSX & Hardware **Job ID:** 3125919 **Full/Part\-Time:** Full\-time **Regular/Temporary:** Regular **Listed:** 2016\-02\-10 **Location:** Cary, Read more
*Apple* Mobile Master - Best Buy (United Sta...
Job Title Apple Mobile Master **Brand** Best Buy **Job Description** **What does a Best Buy Apple Mobile Master do?** At Best Buy, our mission is to leverage the Read more
Infrastructure Engineer - *Apple* /Mac - Rem...
…part of a team Requires proven problem solving skills Preferred Additional: Apple Certified System Administrator (ACSA) Apple Certified Technical Coordinator (ACTC) Read more
Lead Engineer - *Apple* OSX & Hardware...
Lead Engineer - Apple OSX & Hardware **Job ID:** 3125919 **Full/Part\-Time:** Full\-time **Regular/Temporary:** Regular **Listed:** 2016\-02\-10 **Location:** Cary, Read more
Simply Mac *Apple* Specialist- Service Repa...
Simply Mac is the largest premier retailer of Apple products in the nation. In order to support our growing customer base, we are currently looking for a driven Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.