Photoshop Plug-Ins Part 2
Volume Number: 15
Issue Number: 5
Column Tag: Programming Techniques
Writing a Photoshop Plug-In, Part 2
by Kas Thomas
Learn to harness the awesome power of Adobe's
800-lb graphics gorilla
Plug-ins are among the best things ever to have happened to desktop graphics. They're
small, powerful, versatile additions to the graphic artist's arsenal; and for
programmers, they allow easy entry into the complex world of image processing,
relieving the developer from having to worry about file I/O, event loops, memory
management, blit routines, color modes and gamuts, etc. As a result, more time can be
spent on image-processing issues and less time need be devoted to application
infrastructure issues. It's much easier to test a new idea by implementing it as a
plug-in than by trying to splice new (and possibly buggy) code into a standalone
application.
Last month, in Part 1 of this article, we laid the groundwork for writing plug-ins for
Adobe Photoshop, the bestselling graphics powerhouse. We talked about the Photoshop
plug-in API, the nine types of plug-ins Photoshop currently supports, 'PiPL'
resources, how the plug-in communicates with the host, best ways to "chunk" an
image, and how image data is organized. We also talked about error handling, callback
services, memory issues, and debugging tips. But we still only had time to hit the high
points, without discussing much actual code.
In this installment, we'll actually run through some real, live plug-in code (finally!),
showing how to drive the host through prefetch buffering, how to process large images
in chunks (padded on the edges if need be), and how to get realtime previews to appear
in your user dialog. Along the way, we'll say a word or two about area operators,
gamma and bias, histogram flattening, and sundry other graphics-programming
concepts. We've got a huge amount to cover in a limited space, so let's get right to it.
Rapid-Fire Review
In case you weren't with us last month (or maybe you were, but you've since forgotten
everything), here's a capsule review; see if it makes sense.
Image-filter plug-ins for Photoshop are compiled as shared library modules (filetype
'8BFM', creator '8BIM'), with an entry point of main(), which is typed as pascal void
and must be the first function in the module. Since we're compiling PPC-native (for
Photoshop 5.0), we can use globals and static data with impunity. The resource fork of
the plug-in should contain a 'PiPL' resource (which ResEdit isn't much help with,
incidentally) conforming to Adobe's SDK guidelines <http://www.adobe.com>.
At runtime, the host (which we'll assume is Photoshop, although it could very well be
another program, such as After Effects) calls the plug-in's entry point with a selector
value to indicate the phase of operation that the plug-in is in. There are six possible
selector values, corresponding to About (time to pop the About dialog), Parameters
(allocate globals), Prepare (pop a user dialog if need be; otherwise validate your
global values), Start (initiate processing), Continue (continue processing), and
Finish (cleanup). In the example plug-in for this article, we have an empty Finish
handler, since there are no cleanups to do, and the Continue handler (while
non-empty) isn't really needed. If the host has a callback named AdvanceState(), we
don't need a Continue phase, because we can drive the buffer prefetch process all the
way to completion in Start. Every version of Photoshop since 3.0 has AdvanceState().
But some host programs (After Effects 3.1, for example) do not support this function.
For those programs, you need a Continue-based polling loop in order to retrieve data.
The host communicates with the plug-in by means of a gigantic (200-field) data
structure called the FilterRecord, a pointer to which is included in each call to
main(). Some of the fields of this structure contain pointers to host functions
(callbacks), or suites of functions. The structure is too huge to list here (although key
fields were listed in last month's article). We will discuss important fields, and their
use, as they come up.
The plug-in communicates with the host in two ways: via callback functions, and by
setting parameter values (in the FilterRecord) during the Start phase. The field values
set by the plug-in at Start will be inspected by the host prior to data buffering
operations. This is how the host "knows" what size buffers to allocate for input and
output, which channels of data to fetch for the plug-in, etc.
Two callbacks that are worth noting are the TestAbortProc() and
UpdateProgressProc(). The former looks for user cancellation events and spins the
watch cursor dial. The latter displays a progress bar to the user, automatically
suppressing it for short operations. These should be called frequently during plug-in
execution (i.e., in the main loop, not the user dialog).
The plug-in needn't have any event-loop code (unless you want to write a dialog filter)
and in most cases you won't have any need for Quickdraw, since everything you need to
know is available either from the FilterRecord structure or via host callbacks. Undo
actions are done for you by the host. You don't have to worry about allocating input and
output buffers; the host will point you to them. You also don't have to do masking,
because the host will automatically mask your operation to the user's lasso area.
Image Filter Example
In articles of this sort, example code tends to be rather trivial and cursory, forgoing
sophistication in favor of clarity, which of course is not only boring but unhelpful. So
for this article, rather than follow tradition I tried to produce an example plug-in that
might actually be useful. The filter that resulted implements a subtle edge-detection
method which, combined with histogram flattening and a few other tricks, gives some
visually interesting results (see Figure 1). As a tribute to the pharmacologically
active beverages used in the development of this plug-in, the plug-in was named Latté.
Figure 1. Lena before Latté (left) and after. The version on the right was created by
Latté in Sketch mode with a pixel radius of 2.75 and histogram equalization enabled.
The CodeWarrior project for Latté, available online at <ftp://www.mactech.com>, is
composed of four code modules (written in Metrowerks C): two primary files
containing the handler code and user interface, and two utility files. Latté.c and
LattéUserDialog.c comprise about 1,500 lines of code (total). The two utility modules
comprise another 5,500 lines of rather pedestrian code involving string conversions
and such. The real action is in Latté.c, where our handlers and image-modification code
reside. Listing 1 shows the main() function. Let's quickly go over it.
Listing 1: main()
main()
This is the entrypoint for our plug-in, in Latté.c.
pascal void main (const short selector,
FilterRecord *filterParamBlock,
long *data,
short *result)
// Declare a handler dispatch table
// (an array of function pointers)
static const void (*handlerFunc[])() =
NULL, // filterSelectorAbout (handled in main)
DoParameters, // filterSelectorParameters
DoPrepare, // filterSelectorPrepare
DoStart, // filterSelectorStart
DoContinue, // filterSelectorContinue
DoFinish // filterSelectorFinish
GPtr globals = NULL; // actual globals
// Check for about box request.
if (selector == filterSelectorAbout)
*result = noErr;
return;
}
// Get globals the old-fashioned way.
globals = AllocateOurGlobals (result,
filterParamBlock,
filterParamBlock->handleProcs,
sizeof(Globals),
data,
InitGlobals);
if (globals == NULL)
// Fortunately, everything's already been cleaned up,
// so all we have to do is report an error.
*result = memFullErr;
return;
}
//------------------------------------
// Dispatch to the appropriate handler function.
//------------------------------------
if (selector > filterSelectorAbout &&
selector <= filterSelectorFinish)
(handlerFunc[selector])(globals); // dispatch via jump table
else
gResult = kFilterBadParameters;
// unlock handle pointing to parameter block and data
// so it can move if memory gets shuffled. (Not needed
// if you declare globals normally.)
if ((Handle)*data != NULL)
PIUnlockHandle((Handle)*data);
} // end main
The first thing to note is that we declare a function table to hold pointers to our
handlers; this avoids turning our main() function into one big, long, ugly "case
switch.
We check for the About selector right away and handle it as a trivial case, rather than
dispatching it out, because when the host calls us with the About selector, the
filterParamBlock is not valid and there's no sense going through the rest of main().
Before dispatching to a handler, we should set up our globals, because each handler
expects a pointer to the globals. The AllocateOurGlobals() function (complete code
online) allocates globals if they haven't yet been allocated. The pointer returned by
that function points at a custom record structure that looks like:
short *result; // This is reported to host.
FilterRecord *filterParamBlock; // FilterRecord
Rect proxyRect; // used in our user interface code
} Globals, *GPtr, **GHdl;
Each handler stuffs its return code into the result field of this record, which points to
the address passed in the first argument to main().If a negative number is stuffed, the
host will display an appropriate error message for us. If a positive error code is
passed, the host does nothing, because it expects us to pop an alert. A normal result is
noErr.
The AllocateOurGlobals() function sets up a pointer to our globals the old-fashioned
way (as it had to be done in the pre-PowerMac era) by stuffing a Handle value in data.
This was the mechanism Adobe came up with for globals back in the bad old days when
the MC680x0's A5 register was the key to globals. If you compile PPC-native, there is
no longer any need to set up globals the old way; you can just declare them normally. If
you do it the old way, the data Handle is locked inside AllocateOurGlobals() and has to
be unlocked at the end of main().We call one of our own utilities, PIUnlockHandle(),
in this instance because it unravels the lengthy chain of indirections needed to get at
the host's memory routines. If we didn't do this, the last line of code in main() would
look like:
(*((globals->filterParamBlock)->
handleProcs)->unlockProc)((Handle)*data);
which even Kernighan and Ritchie would find repellant. Even so, such an expression
repays close study, because it shows how the unlockProc (in Photoshop's Handle Suite
of callbacks) is accessed. Throughout our code, we use functions like HostLockHandle()
that eventually call on the host's Handle Suite, which is a suite of callbacks designed to
let the host implement memory routines in a platform-appropriate manner. We avoid
using MacOS calls, not only for device independence but because Photoshop implements
a much more efficient memory management scheme, internally, than the MacOS does.
So far, we've allocated globals but we haven't initialized them. Initialization is done in
the Parameter phase of operation. When main() is called with a selector of
filterSelectorParameters, we vector to our DoParameters() handler function. Listing
2 shows the handler as well as the ValidateParameters() function. The essential thing
to remember is that if this handler is called, it means the plug-in has just been
invoked for the first time. That means the user needs to see a dialog. But first, the
dialog's default parameters have to be set. These constitute additional globals, which
are attached to the parameters field of the FilterRecord. (Remember, a pointer to this
enormous structure was given to us as a parameter to main.) For Latté, we define a
user-prefs struct, the TParameters block, as follows:
// This is our user-dialog params record. Persistent across plug-in
calls.
typedef struct TParameters
{
double userRadius;
Boolean queryForParameters;
short rowSkip;
long userMode;
Boolean useEqualizedHistogram;
Boolean useAdvance;
double histogram[256];
long imagesize;
double blendFactor;
} TParameters, *PParameters, **HParameters;
Ordinarily, to get at the fields of our TParameters record we would need to do a lot of
indirection, such as:
((TParameters *)*(globals->filterParamBlock)->
parameters)->userRadius = 4.0;
which is hard to read and easy to mess up. So to simplify access to these fields, we rely
on certain macros and definitions (given in Latté.h) - see Listing 3 - which lets us
just say gRadius = 4.0.
The macros in Listing 3 are worth scrutinizing since they tend to be used liberally
throughout our plug-in code (as well as Adobe's own example code). Without them, life
would be a lot harder - maybe not worth living. (All right, that might be overstating
it. But you get my point.)
Listing 2: Parameter Handler
DoParameters()
This is our Parameter handler function. If this function is called, it means the plug-in
has been invoked for the first time.
void DoParameters (GPtr globals)
ValidateParameters (globals);
gQueryForParameters = TRUE; // meaning, we need to pop the dialog
}
ValidateParameters()
Here is where the parameters for theuser dialog get initialized. The relevant data
structure gets attached to the parameters field of the FilterRecord, which was passed