This is a journal of my experience trying to get a filter service working on Cocoa. The goal is to be able to get TextEdit to load a WordPerfect document, roughly the way that it loads a Microsoft Word document. The reason I want to be able to do this is that I have quite a bit of mixed Tibetan and English text in WordPerfect 6 format. Back in the day, the DOS version of WordPerfect, with modifications and some lovely fonts from Tony Duff at the Tibetan Computer Company was the tool of choice for editing Tibetan texts. The Tibetan Machine font has since been open-sourced, although not yet converted to Unicode.
I was able to accomplish this by hacking wpd2text
from libwpd
to act as a unix-style filter, and then use the NSUnixStdio
input mechanism. This causes Cocoa to stream the data
to be translated to the standard input of the program, and then
takes the standard output of the program as the contents of the
file.
I will point out at this juncture that I am a unix geek, not a Mac geek. I'm sure there's a way to do this in Xcode, but I can't find it. There's no template for a Service, so it probably involves some serious Xcode geekery.
I have been unable to find any accurate documentation about setting up filter services online or in the XCode/ADC documentation, although people do make references to such documentation. Maybe I'm just not looking in the right place. If you know where I should have looked, I'd appreciate a pointer. I was able to piece this together by reading what documentation is available (some of which predates xml plist files) and also looking at another handy filter service that I was able to track down.
The ADC documentation that I located for filter services talks about them in reference to converting clipboards from one data format to another. However, the NSTextStorage class documentation refers to the idea of being able to add filter services that convert documents on loading - e.g., when you load a document using NSAttributedString:initWithUrl. It turns out that this is in fact possible.
There is also some documentation about System Services in general. These are a really nifty idea - I didn't realize what they did until reading up on them. These are those puzzling things in the Services-> selection off of the Applications menu. Services do transformations or operations on the selection - for example, speaking the selected text, or converting it from one representation to another.
Like many smart objects in a MacOS X filesystem, system services
are stored in bundles. A bundle is a special kind of directory
that groups a bunch of stuff together so that it looks like a
single object to the Finder. The contents of a bundle are
stored in a subdirectory of the bundle called
Contents. Since filters are just a special
species of system service, they're stored in the same kind of
bundle as a regular system service.
The structure of the bundle for the WordPerfect filter service is as follows:
wpd.services/Contents/Info.Plistversion.plistPkgInfoMacOS/wpd2text
The interesting part of this is the Info.plist
file. Here's what it looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist
PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>wpd2text</string>
<key>CFBundleGetInfoString</key>
<string>1.7 @2004 foo</string>
<key>CFBundleIdentifier</key>
<string>net.sourceforge.projects.libwpd</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Wordperfect Converter Service 1.7</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.7</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.7</string>
<key>NSBGOnly</key>
<string>1</string>
<key>NSServices</key>
<array>
<dict>
<key>NSFilter</key>
<string></string>
<key>NSInputMechanism</key>
<string>NSUnixStdio</string>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSTypedFilenamesPboardType:END</string>
</array>
</dict>
</array>
</dict>
</plist>
Most of this is boilerplate - you can just copy it and edit it. The key things that I needed to tweak to get the service working were:
This is the name of the executable that Cocoa will run to do
the translation. The actual executable is stored in
the MacOS subdirectory of the Contents
directory.
This is the method in the filter's service provider object that's supposed to be invoked to convert the data. This has to be here, and has to be blank.
This determines how Cocoa will use the filter
application. We're using the NSUnixStdio
input mechanism, which means that Cocoa will just stuff the
data into the standard input of the program specified
in CFBundleExecutable, and will expect the
converted text to appear on its standard output.
Right now this is set to NSStringPboardType.
This tells Cocoa that we're just going to feed it a stream
of ASCII or UTF-8 text (it turns out to be important that
we can feed it UTF-8, because Tibetan characters are
obviously _not_ ASCII!). BTW, I said "right now" because
I want to convert to RTF - this is more useful in general.
'WDBN'. You
would express this as:
<string>NSTypedFilenamesPboardType:'WDBN'<string>
This is the least exciting part, because I don't really have the time to document it. It's a unix filter, so it just reads standard input and spits the converted text back out on standard output. Nothing to see here.
The filter service has to be installed in
either /System/Library/Services
or ~/Library/Services, depending on whether you
want it to work for everybody on your computer or just for
you. After you've installed it, and anytime you change
the Info.plist file, you have to log out and log
back in again to get your user environment to recognize that
it's there. When you are debugging the Info.plist, try not to
have too much stuff running, because you're going to log out
and back in again more than once unless you're a lot more
anal-retentive than I was. Which brings us to...
You'd probably like to be able to double-click on a document of the appropriate type and have it come up in TextEdit or something like that. In order to make that happen, find a document of the right type in the Finder and select it. Then select Get Info (command I) from the file menu. The fourth section from the top (as of this writing, of course) is the Open with section. There should be a widget from which you can select the application - change it to TextEdit, or whatever. If you want all documents with the same extension or type to be opened by TextEdit, click on Change All...
Now you're going to be wanting to change the icon. I think to do this you have to hack the Info.plist file in the TextEdit application bundle (or whatever application you're going to have open the file) to add an icon for the END document type. I have (thus far) restrained myself from doing so, simply because I'd have to redo it every time I upgraded TextEdit. Maybe there's a better way to do this - I don't know. Feel free to clue me in!
Nothing is ever easy. I didn't even know if this would work at all until I found AntiWordFilter . Having found that, I ran into quite a few problems, which I will confess here in embarrassing detail, just in case you run into one of the same problems yourself.
There is absolutely no feedback about what's going on with the
service. If you get it wrong, it just doesn't work. What to
do? I was testing my filter with TextEdit, so I used ktrace
to see if it was even referencing my service. First, start
TextEdit. Then use ps or top to find its process ID (PID).
Then ktrace -i -p <pid> to start the trace.
This traces to ktrace.out in the current
directory. Then, in TextEdit, try to load a file that you
expect the filter to operate on. Then once whatever's
happened has happened, quit out of TextEdit.
Then dump the trace into a text file so you can search it
easily: kdump >kdump.txt. Now edit it in
your favorite text editor. Search for the name of your
filter program. If you don't find it, there's something
wrong enough with your Info.plist file that
Cocoa didn't know to run your filter. More on this later.
If you did find the filter program, you can look at what it did. Maybe there was some output to stderr, or an exit code. You need to know how to use ktrace to figure this out, but it's not difficult - read the man page if you don't already know how.
The documentation from Apple says to use NSPortName
for the name of the excutable, but this is no longer
true. Instead, you need to use CFBundleExecutable
. See the sample Info.plist for details.
I accidentally typed in NSTypedFilenamesPBoardType instead of
NSTypedFilenamesPboardType. There's no indication of any
error in the system logs, but Cocoa doesn't recognize this,
and therefore doesn't know to invoke the filter. Your best
bet is to copy a known working file (you're welcome to use
the one included here - there's nothing special about it).
Otherwise, be very careful in checking over all the sTuDLy
cApS in your Info.plist file.
The wpd2text executable in the libwpd build
directory is actually a shell script that invokes an
executable in the .libs subdirectory. If you
copy it somewhere else, it doesn't work. You're probably
smart enough not to make this mistake, but also be aware of
shared libraries that might be required but not accessible,
and stuff like that. You can always debug this sort of
thing with ktrace - you just have to pore over the output to
see what's going on. What clued me in was seeing IO for
#!/bin/sh in the ktrace output right after the NAMI for my
program. Ouch.
Early on I didn't realize that the UI server caches the
information in the service Info.plist files.
This means that whenever you update Info.plist,
you need to log out and in again. I mentioned this earlier,
but just in case you're skimming...
I'm sure there are lots of other ways to screw this up, but these are the ones I was able to find for you. Best of luck!
Copyright (C) 2004 Edward W. Lemon III All Rights Reserved
This page was last updated on January 24, 2004.