TweetFollow Us on Twitter

Acrobat Plugins

Volume Number: 16 (2000)
Issue Number: 6
Column Tag: Programming Techniques

How to Write Plug-Ins for Adobe Acrobat

by Kas Thomas

The Acrobat SDK provides a bewilderingly diverse assortment of tools for manipulating PDF files

Adobe's Portable Document Format (PDF) is the premier electronic document format for "rich content" on the web, not just because of its ability to convey the appearance attributes of a paper document (which it is very good at, since the underlying technology is Postscript-based) but also because of its annotation features, hyperlink embedding, support for form widgets (and JavaScript), Unicode compatibility, security (encryption) handlers, and accessibility features. (PDF is also evolving to become more adept at encapsulating document metastructure and private data types.) Access to all these types of functionality is available to creators of PDF documents via Adobe Acrobat, the commercial version of Reader. Unlike Reader, the full Acrobat product (street price: $200 or so) lets you mark up a PDF document in various ways, add links and form fields, attach JavaScript to documents, etc. — and save the modified PDF file to a storage device.

Most of the kinds of functionality you'd ever want to see in a PDF file can be added using Acrobat or created in the source file at the time of the document's creation (via MS Word, FrameMaker, or whatever). But in the PDF world, as in the world of Photoshop, Quark, After Effects, etc., there are times when the functionality you need just isn't available from the host application. In such a situation, it makes sense to consider a plug-in.

As with its other flagship products, Adobe has incorporated a plug-in architecture into Acrobat to enable third-party developers to extend the functionality of the core product. Quite a few of the key features of Acrobat (features most users think of as being "core functionality") are, in fact, implemented in the form of plug-ins. For example, form-editing and form-fillout functions are done as plug-ins. Acrobat also uses plug-ins to implement weblinks, annotations, text touchup, Quicktime movie attachments, scanning ("paper capture"), image-import, and quite a few other functions. (Check the plug-ins folder of your Acrobat installation.)

Extending Acrobat's capabilities via custom plug-ins is a powerful (and popular) way to give users extra control over PDF documents. But this capability comes at a cost. Learning to use the Acrobat plug-in API (one of Adobe's most complex APIs) can be a formidable challenge. There are well over 1,000 functions in the Acrobat API; just finding the ones you need to carry out a specific task can be daunting. Suffice it to say, this is one API you won't learn in a weekend.

Because of the sheer enormity of the API, there is no way I can give you more than a very abbreviated introduction to Acrobat plug-ins in the limited space available here. What I can do is briefly outline the available tools, talk about a few of the most common idioms of Acrobat programming, and point you toward resources for further study. In addition, just so you won't think writing an Acrobat plug-in is totally beyond reach, we'll walk through the code for a small plug-in designed to do something at least vaguely useful: namely, add a text-based "watermark" to a page, in response to a menu command.

Getting Started with the SDK

You can download a copy of the Acrobat software developer's kit (SDK) from http://partners.adobe.com/asn/developer/acrosdk/acrobat.html. The SDK is also available on CD-ROM to members of the Adobe Solutions Network Developer Program. (See http://partners.adobe.com/asn/developer for details.) The complete SDK requires over 30 megabytes of disk space, including 27 megs for documentation alone. Example code comes in the form of Code Warrior projects for two dozen sample plug-ins and a handful of AppleScripts showing how to control Acrobat via IAC (Inter-Application Communication). The code is well-packaged, thoroughly commented, and well-summarized in a separate docfile, Samples.pdf, which gives a brief overview of what each example is meant to demonstrate.

The documentation that accompanies the SDK is so overwhelming that Adobe thoughtfully includes a "Start Here" document as well as a flowchart-style roadmap diagram in which every box is a clickable link taking you to the appropriate docfile. The roadmap occurs at the top of nearly every docfile in the package, which is a nice touch. Just by clicking on various portions of the roadmap, you can jump from doc to doc. Of course, all the docs are PDF files with navigation bookmarks.

The 213-page Acrobat Core API Overview (Tech Note No. 5190) is a good document to start with, since it serves as a kind of giant FAQ for developers, explaining how the various components of the API work together. But the two most important docfiles in the package — the two you'll spend the most time with — are The PDF Reference Manual and the Acrobat Core API Reference. The 513-page PDF Reference Manual, also known as the PDF Specification, contains the formal language specification for PDF. It spells out all the keywords, data types, architectural schemas, and syntax constructs that make a PDF file a PDF file. Just as it would be hard to write a Netscape browser plug-in without understanding how HTTP and HTML work, it's inconceivable that you would write an Acrobat plug-in without knowing a fair amount about the low-level organization of PDF files. The PDF Reference Manual gives you this information. Plan on spending many hours reading it.

Your primary reference manual for the plug-in API itself will be the 2,795-page Acrobat Core API Reference (Tech Note No. 5191). Yes, I said 2,795 pages. But don't worry, that's sure to get bigger as Acrobat evolves. (Version 5.0 is set to appear sometime within the next nine months.)

The Acrobat Core API Reference is not a book that you sit down and read cover to cover; rather, it's a reference you consult as the need arises. Inside it you'll find descriptions (calling syntax, etc.) of the 1,100-or-so core routines that make up the Acrobat API. You should understand that one reason this API is so voluminous is that it comprises the bulk of the library routines from which Acrobat itself was (and is) written. Adobe uses these same routines internally for Acrobat development.

What You Can and Can't Do

It's possible to write a plug-in for Reader as well as for Acrobat. (There are also mini-APIs for other products in the Acrobat suite: Capture, Distiller, Catalog, etc.) By default, any plug-ins you write using Adobe's SDK will run under Acrobat (the commercial product) with no problem. Writing plug-ins for Reader is another matter. To get Reader to load your plug-in, you will need to obtain an Acrobat Reader Integration Key. Getting a key is a matter of filling out the Acrobat Reader Integration Key License Agreement (available on Adobe's developer web site), signing it, and submitting it — along with a $100 payment (in the form of credit card only) — to Adobe's Klamath Falls, Oregon group (which administers the developer programs). When you sign the agreement, you are agreeing to some fairly serious limitations as to what you will and won't try to do in your plug-in. Specifically, you are agreeing never to include any of the following kinds of functionality in your Reader plug-in:

  • Anything that would allow Reader to permanently modify, save, or write files, including PDF files, annotations for PDF files, form data, etc. Included in this is any functionality that would enable another process (on a server, say) to save such data.
  • Anything that opens encrypted documents by bypassing normal security measures.
  • Anything that would display a PDF file in the window of another application.
  • Accepting navigational commands from an application other than Reader itself.
  • Making use of any function call in the Forms HFT (host function table).
  • Implementing a replacement file system for Reader.
  • Anything that would remove the menu item that calls up the "About" screen for Reader.

In other words, forget about making permanent changes to files (or doing disk writes) in a Reader plug-in. The whole idea is that Reader must remain read-only. (Otherwise it wouldn't be called Reader!) If your plug-in does any of the things listed above, it has to be written as an Acrobat plug-in, targeting the commercial product only (which does modify and save files). This cuts the audience for your plug-in down substantially (probably to one percent or less of the audience of Reader). But there's still a sizable market left. In fact, the market for Acrobat plug-ins is surprisingly brisk, reflecting the product's widespread use in prepress and enterprise settings.

Other Limitations

Not everything you might want to do to a PDF file can be done using the Acrobat API anyway, it turns out. It's important to realize up-front that some things just can't be done. For example, let's say you wanted to implement a new image-compression codec. (PDF already implements JPEG, LZW, CCITT, and Flate compression filters for image streams.) Unfortunately, the Acrobat plug-in API does not expose this kind of functionality — purposely, to ensure maximum interoperability of PDF files. On the other hand, you can write an entirely new security handler, should you want to implement a different kind of encryption or passwording than is available by default in Acrobat. With security, interoperability is not a consideration.

Another kind of functionality you'll have trouble modifying is changing the way Acrobat draws things to the screen. For example, if you want to change the default order in which various objects are drawn, apply a different kind of antialiasing, or improve the blitting performance — you're basically out of luck. These kinds of functionality aren't exposed in the API. That's not to say you can't try going outside the API; but in that case, maybe what you're trying to do would best be implemented as a helper app, system extension, WDEF, or something other than an Acrobat plug-in per se.

Layers in the API

Figure 1 shows how the Acrobat plug-in API is organized into a number of layers, designed to provide functionality at high and low levels. At the highest level is the Acrobat Viewer (AV) Layer. In this layer are hundreds of routines for manipulating the Viewer's windows, menus, toolbars, etc. as well as for querying the runtime environment. This is where, for example, you can call AVAppRegisterForPageViewClicks() if you want to track mouse hits. Most menu commands can be programmatically driven at this level. That means you can control the page view, open and close windows, control the cursor's appearance, and drive just about any GUI-related action. But you cannot do file I/O, inspect or modify stream contents, or perform low-level operations on document internals from this layer.


Figure 1. Layers in the Acrobat API.

The Portable Document Layer implements the PDModel, which is an assortment of objects allowing access to individual components of the PDF file, such as annotations, bookmarks, links, or text selections. The objects in this model are generally opaque, which is to say they are neither pointers to data structures nor pointers to pointers. Access to the contents of objects that live at this level occurs via special accessor functions. For example, to get an annotation you would use PDPageGetAnnot(); then you might inspect its flags with PDAnnotGetFlags()and change the flags with PDAnnotSetFlags().

At the very lowest level, the Cos Layer allows direct access to stream contents and page resources. (Cos is a recursive acronym for "Cos Object System.") Cos objects come in seven main varieties, mirroring the data types upon which PDF is built: booleans, numbers, names, dictionaries, streams, strings, and arrays. Cos methods go by names like CosArrayGet() and CosArrayInsert(). They allow "subatomic particle" access to the PDF file. Of course, as with other low-level programming tools, operating at the Cos level brings risk as well as rewards. As Adobe says in the Core API Overview: "Unlike the AcroView and PDModel layers, it is very easy to produce an invalid file using Cos methods. For this reason, they should not be used unless necessary, for example to add private data to portions of a PDF file that cannot be accessed in other ways." In other words, don't go here unless you know darn well what you are doing. And even then, look out for land mines.

Cutting across all three major layers of API support is one additional layer, known as the Acrobat Support Layer. You can think of this as a "utility" layer where you can find platform-independent functions for doing file I/O, memory allocation, etc.

PDFEdit Layer

One "mini-layer" we didn't talk about (which is shown in Fig. 1 as a small layer residing midway between the PD Layer and the Cos Object level) is the PDFEdit Layer. In a nutshell, this layer was created in order to provide easier (and safer) access to page contents than would otherwise be obtainable. At the PD Layer level, you have access to page content objects, but the objects are opaque and not always easy to modify using the available accessor functions. At the Cos level, on the other hand, you're dealing with raw streams consisting of arbitrary mixtures of page operators, numbers, and text. To manipulate those items, you have to "parse them out" yourself, which is not always easy. The PD Layer offers some parsing and enumeration methods, but in general the objects returned are not easily modified. The problem is even worse than that, however. Resource handling is difficult because stream contents and resources are treated as disjoint entities. (A given piece of text is not easily associated with its font resource, for example.) Also, stream data are often encoded via LZW or Flate compression. Merely retrieving the raw stream data usually isn't helpful.

The PDFEdit methods were invented to overcome these obstacles by providing access to low-level page components in such a way that related items are grouped together, so that objects encapsulate any relevant information about themselves. (A text object contains font information, for instance.) PDFEdit methods thus allow you not only to access but also create page contents, including whole pages, from scratch. Most of the relevant data structures are public (rather than opaque), so that you can manipulate attribute values directly.

You can think of the PDFEdit Layer as offering much of the power of the Cos Layer, but with the safety and convenience of the PD Layer.

PDSEdit Methods

There is actually one more mini-layer in the Acrobat API, not shown in Fig. 1, called PDSEdit. The 'S' in PDSEdit refers to Structure. Prior to version 1.3 of the PDF Specification (which became current with the introduction of Acrobat 4.0), there was really no convenient way to store metastructural information — information about structural relationships of document components — in a PDF file. As a result, PDF gave you a very accurate representation of document appearance, but no way to correlate text strings with paragraphs, say, or paragraphs with topics. (There is no easy way, for example, to reflow text inside a PDF file.) The PDSEdit Layer, building on new features in PDF 1.3, allows authoring processes to build a structure tree into PDF files, such that document content can be mapped to structure. The various methods of the PDSEdit Layer let you access, create, insert, remove, inspect, reorder, and otherwise manipulate nodes and leaves in the structure tree and/or traverse contents in a structured fashion. (A high-level Adobe official gave a talk not long ago in which he touted PDF's ability to act as a container for XML. Adobe is known to be devoting a good deal of in-house R&D to finding ways to make PDF and XML play together.)

The PDSEdit Layer offers some interesting, cutting-edge functionality for developers interested in beefing up the metastructural capabilities of PDF. Most plug-in developers will never use this layer, however, which is why it's not included in Fig. 1 and won't be discussed further here.

Cutting Across Layers

It may be possible for some plug-ins to implement all of their functionality by relying entirely on methods from just a single API layer; but in general, this is rarely the case. It's more often the case that a plug-in needs to draw on many types of functionality, using methods from more than one layer. It's not unusual, accordingly, to have to call on conversion routines that derive one type of "doc object" from another. Figure 2 shows some of the more commonly accessed doc object types and their associated conversion routines.


Figure 2. How to get from one type of "doc object" to another.

In the example plug-in for this article (discussed in further detail below), we make use of methods from the AV Layer, the PD Layer, and PDFEdit. For example, we get the AVDoc object with AVAppGetActiveDoc(), obtain an AVPageView using AVDocGetPageView(), and get a PDPage from the page view using AVPageViewGetPage(). Not all of these methods are shown in Figure 2; it would take many additional flow charts to show all methods.

As I said at the outset, this is one of Adobe's most complex APIs. Just finding your way around can be a challenge — with or without flow chart diagrams.

Notifications

The Acrobat API implements a notification mechanism whereby plug-ins can register to receive notification whenever an event of interest occurs. For example, say your plug-in has a need to know when a document has been opened, so it can traverse the document looking for resources of a particular kind. In that case, you'd probably want to register with AVAppRegisterNotification() to receive AVDocDidOpen() notifications (which are broadcast whenever a document first opens). Over 80 different kinds of notifications are available, corresponding to an amazing variety of user actions and updates. In many cases, the notification will provide pointers to data which may have changed as part of the update action. In all cases, you can register a callback routine of your own to be executed whenever an event of interest occurs.

For more information, see the section on Notifications in the Core API Overview.

Enumeration

It is often desirable to be able to enumerate through all the components of a page or all the objects of a given type in a document, etc. Fortunately, the API provides many enumeration methods that do this while also calling a custom callback procedure for each target object encountered. For example, when PDDocEnumFonts() is called, Acrobat enumerates all fonts used in the document and calls a user-specified callback for each font. (In Acrobat-API argot, a user callback in this situation is often called a monitor.)

Exception Handling

In general, API methods do not return error codes but instead raise exceptions when an error occurs. Developers can write their own exception handlers, since the default handler provided by the viewer app cannot always back out of unfinished operations gracefully. Depending on the severity of the condition, your exception handler can either "fix" the problem and go on (thus absorbing the exception) or do whatever damage control is appropriate and then re-raise the exception so as to pass it up to the next exception handler in the stack. There is no definitive list of which of Acrobat's 1,100 API methods raise exceptions and which do not. The safest policy is: When in doubt, assume that code can raise exceptions and surround it with handler calls. The idiom for doing this is:

DURING

	avd = AVDocOpenFromFile(asp, NULL,(char *)NULL);

HANDLER

	avd = NULL;
	errorCode = ERRORCODE;
	AVAlertNote("Error opening file");

END_HANDLER

The DURING/HANDLER/END_HANDLER macros (defined in CorCalls.h) expand to:

#define DURING {					\
	ACROjmp_buf ASException; \	ACPushExceptionFrame(&ASException, \
(ACRestoreEnvironProc)&RestorePlugInFrame);	 \
	if (!ACROsetjmp(ASException)) {

#define HANDLER _E_RESTORE; } else { _E_RESTORE;

#define END_HANDLER }}

If you want the host app to handle the exception, use the RERAISE() macro. However, don't exit the current function while you're still inside an exception-handled block of code. Instead, use E_RETURN(x) to return gracefully (with 'x' as the return value) from inside a handler block. It also goes without saying that you shouldn't execute code that might (itself) raise an exception, inside a handler, and you should avoid nesting handlers.

For more information, be sure to read the section on exception handling in the Core API Overview.

Handshaking and Initialization

Acrobat performs a special handshake with each plug-in at load time, to give each plug-in a chance to register its name with the host, perform initializations, request notifications, install callbacks, and install an optional unload procedure. In order for handshaking to occur, the plug-in must implement the following method:

ACCB1 boolean ACCB2 PIHandshake(Uns32
handshakeVersion, void *hsData)

The ACCB1 macro expands to the pascal keyword (while the ACCB2 macro currently expands to nothing). The hsData pointer will point to a data structure that currently looks like:

typedef struct {
	ASUns32		handshakeVersion;	 // IN value: HANDSHAKE_V0200 
	ASAtom			appName;		// IN value: Name of host app
	ASAtom			extensionName;	// OUT: Name of plug-in 
	ASCallback	exportHFTsCallback;		// OUT: User routine to register any
						// HFTs this plug-in is providing 
	ASCallback	importReplaceAndRegisterCallback; // OUT: User routine
     					// to import other plug-in's HFTs, replace HFT functions, 
						// and register notifications 
	ASCallback	initCallback;	// OUT: User callback for doing inits
	ASCallback	unloadCallback;		
} PIHandshakeData_V0200;

The extensionName field must be filled out with an ASAtom corresponding to the name of the plug-in, formatted according to ABCD_Name, where ABCD is a four-character "vendor" prefix while Name is the name of the plug-in. The AS Layer utility routine ASAtomFromString() can be used to convert a string literal to an ASAtom, which is a hashed representation of the string that Acrobat uses internally.

The exportHFTsCallback field can contain NULL but can optionally be filled out with a user callback (which has been converted to an ASCallback) to a routine to call if the current plug-in is exporting any HFTs, or Host Function Tables, of its own. The HFT is an abstraction layer through which plug-ins and other processes can expose and/or access each other's functions. Several of the Adobe plug-ins that ship with Acrobat export their own HFTs for other plug-ins to use. For example, the Forms plug-in exports an HFT containing pointers to functions that other plug-ins can use to traverse and/or manipulate form elements.

After all plug-ins have loaded, the viewer (normally Acrobat) will call the callback, if any, stored in the importReplaceAndRegisterCallback field. This callback is where you can import other plug-ins' HFTs or register for notifications. (There is no need to import Acrobat's own HFT, since that's done automatically for you.)

The initCallback should point to a user routine that performs any initializations that the plug-in needs done before it can function. For example, if the plug-in is going to install its own menu commands or toolbar buttons, this is where it shoudl be done.

The unloadCallback should point to a routine that handles any memory-freeing or other unload-related operations your plug-in may have.

Compilation

Acrobat is a PPC-native product, so there is no need to compile for a 68K target unless you are compiling a Reader plug-in and want to include owners of older machines. (As mentioned earlier, there are significant limitations on what your plug-in can do if it is to use Reader as a host; and you must obtain a special key from Adobe.) You should plan on using a creator type of CARO and a filetype of XTND for your plug-in. You can place it, or an alias to it, in Acrobat's Plug-Ins folder after linking.

Access to Resources

Beginning with version 2.1, Acrobat opens a plug-in's resource fork with read-only permissions. So don't plan on using your resource fork for persistent storage. What's more, to use resources from your resource fork safely, you should plan on using the functions described in SafeResources.h. That's because plug-ins cannot assume that their resources are located at the top of the resource chain. Calls like GetResource() can fail unexpectedly unless plug-in resources are explicitly moved to the top of the chain. The resource calls in SafeResources.h accomplish this. The functions described there are safe replacements for all Toolbox routines that call, or can call, GetResource() because they move plug-in resources to the top of the chain before each call, then restore the resource chain to its original configuration after the call is finished.

See Acrobat API Development (Tech Note No. 5167) for more information.

Example Plug-In: AddWatermark

The example-code CW Pro 5 project for this article (available at ftp://www.mactech.com) illustrates many of the programming issues described in this article. The compiled plug-in, named AddWatermark, adds a light-gray text string (in 72-pt. Times-Roman) at a 45-degree angle across the current page, in response to an Acrobat menu command. For example purposes, I decided to use a text string of "Rough Draft." An example of the plug-in's output is shown in Figure 3.


Figure 3. AddWatermark applied to a page. (Note the words "Rough Draft" in gray.)

The code for the plug-in comprises 260 lines of straight procedural C (in just five functions) in a file named AddWatermark.c. To compile the plug-in, you will need the Adobe SDK files PIMain.c and SafeResources.c in addition to AddWatermark.c. The project is compiled as a Shared Library. There are no resource (.rsrc) files in the project.

Code for the plug-in's handshake routine is shown in Listing 1. This is where we fill out various fields of the PIHandshakeData_V0200 structure so that the host will know who we are and what services we need. Note that we use a fictitious "vendor prefix" of PLUG on the plug-in's name. (This will not show up in the menu bar.) We do assign callbacks to two fields, but note that we don't simply stuff function pointers into the relevant fields. Rather, we use the API method ASCallbackCreateProto() to create properly formatted function pointers to our MyInit() and MyUnload() procedures.

Listing 1: PIHandshake()

PIHandshake()
This is the plug-in's handshake routine. See article for details.

extern ACCB1 ASBool ACCB2 PIHandshake(Uns32 handshakeVersion, 		void *handshakeData)
{
	if (handshakeVersion == HANDSHAKE_V0200) {

			// Cast handshakeData to the appropriate type 
		PIHandshakeData_V0200 *hsData = 
			(PIHandshakeData_V0200 *)handshakeData;

			// Set the name you want to go by 
		hsData->extensionName = 
			ASAtomFromString("PLUG_AddWatermark"); 

			// If you export your own HFT, do so here 
		hsData->exportHFTsCallback = NULL;
		
			// If you import HFTs, replace functionality, 
			// or want to register for notifications before
			// the user has a chance to do anything, do so here.
		hsData->importReplaceAndRegisterCallback = NULL;
		
			// Perform your plug-in's initialization here 
		hsData->initCallback = 
			ASCallbackCreateProto(PIInitProcType, MyInit);
	
			// Free memory or save state on "quit" here 
		hsData->unloadCallback = 
			ASCallbackCreateProto(PIUnloadProcType, MyUnload);

			// Done 
		return true;
	
	}	 // Each time the handshake version changes, add new "else if" branch here
	
	return false;
}



Code for MyInit() and MyUnload() can be seen in Listing 2. The real purpose of MyInit() is to set up a custom menu command for the plug-in. Notice that to do this we call a method named AVMenubarAcquireMenuByName(). Methods that have 'Acquire' in their names increment a reference count for the object returned. It's important that we later decrement the reference count by calling the appropriate Release method on the object. We do this at the end of MyInit(). Since we created a new menu item (storing it in the global variable AddPluginMenuItem) in this routine, we later have to release it as well. We do that in MyUnload().

Listing 2: MyInit() and MyUnload()

MyInit() and MyUnload()

// This is our initialization callback routine. (It is optional.)
// If the plug-in can't initialize for one reason or another, return
// false. Otherwise return true.

// Here, we set up our plug-in's own menu item in the Extensions menu.

static ACCB1 ASBool ACCB2 MyInit(void)
{	
	AVMenubar bar = AVAppGetMenubar();
	AVMenu extensionsMenu = 
		AVMenubarAcquireMenuByName(bar, "Extensions");
	
	AddPluginMenuItem = 
		AVMenuItemNew("AddWatermark", 
			"PLUG:AddWatermark", NULL, true,
			NO_SHORTCUT, 0, NULL, gExtensionID);
		
	AVMenuItemSetExecuteProc(AddPluginMenuItem, 
	  ASCallbackCreateProto(AVExecuteProc, 
				AddWatermarkCommand), 
	  		NULL);
	  
	AVMenuItemSetComputeEnabledProc(AddPluginMenuItem, 
	 	ASCallbackCreateProto( AVComputeEnabledProc, 
			AddWatermarkEnabled), 
	 		(void *)pdPermEdit);
	
	AVMenuAddMenuItem(extensionsMenu, AddPluginMenuItem, 0);
	
		// We did an Acquire (above), so now Release:
	AVMenuRelease(extensionsMenu);

	return true;
}

static ACCB1 ASBool ACCB2 MyUnload(void)
{
		// Remove and release any menus or menu items we created
	if (AddPluginMenuItem) {
			AVMenuItemRemove(AddPluginMenuItem);
			AVMenuItemRelease(AddPluginMenuItem);
		}
	return true;
} 



The API routine AVMenuItemSetComputeEnabledProc() lets us specify a callback for determining whether our plug-in's menu command should be "greyed out" under conditions when it would not make sense to let the user invoke the plug-in. We use this routine to install our custom function AddWatermarkEnabled(), shown in Listing 3. The AddWatermarkEnabled() function checks to see if a document is currently being viewed and if so, whether the document is edit-enabled. (Some PDF documents are set up so that text cannot be selected or altered.) If there is no doc open, or the current doc is not edit-enabled, then it doesn't make sense to let the user try to use the plug-in for anything. Hence, we tell Acrobat to disable (grey out) the menu item.

Listing 3: AddWatermarkEnabled()

AddWatermarkEnabled()
This routine just determines whether or not our menu command should be enabled 
at a given instant.

static ACCB1 ASBool ACCB2 AddWatermarkEnabled(void *permRequired)
{
	AVDoc avDoc = AVAppGetActiveDoc();
	PDPerms docPerms = 
		PDDocGetPermissions(AVDocGetPDDoc(avDoc));
	
	if (!avDoc)
		return false;
	
	return ((PDPerms)permRequired & docPerms);
}

The main action (creating the watermark) takes place in AddWatermarkCommand(), shown in Listing 4. You'll notice that the entire body of the function is wrapped in exception-handler macros. We begin by fetching various doc and page objects, but since we use "get" (rather than "acquire") methods to obtain them, no reference counts are incremented and we don't need to Release those objects later. We do create or acquire other kinds of objects in this function, however, that do need to be released later. See if you can pick out what they are.

The font we choose for our text (Times-Roman) happens to be one of the base-14 printer fonts that is guaranteed to be available on every machine that has Acrobat. We create the relevant font object for it so we can add that to the PDEText object that will comprise our watermark. That text object also needs some graphics state info and a default transform matrix. (See my articles in the Sept. 1999 and March 2000 MacTech for more information on transform matrices in PDF.) The graphics-state information has to include a color-space designation and an ink color, as well as a miter limit, etc. (In truth, some of these values will be defaulted to reasonable values if we leave them out.) We're using a greyscale color space and an ink color of 0.875 (12.5% grey), specified as a Fixed number.

Note that all of the numbers in the transform matrix must be supplied as Fixed numbers. Since we want the text in our watermark to be rotated 45 degrees counterclockwise, we have to factor the sine and cosine of 45 degrees into the first four positions of the matrix. (The first and fourth numbers have to be premultiplied by the cosine of the angle of rotation; the second number has to incorporate the sine of the rotation angle; and the third number has to incorporate the negated sine of the angle.) The text will begin drawing at a point one quarter of the way across the page horizontally and 50% of the way up the page vertically. The fifth and sixth coefficients of the transform matrix accomplish this translation for us.

Listing 4: AddWatermarkCommand()

AddWatermarkCommand()
static ACCB1 void ACCB2 AddWatermarkCommand(void *data)
{
	ASInt32 num;
	ASBool b;
	char buf[256];
	AVDoc avDoc;					// Viewer's doc object
	PDPage pdPage;				// reference to an editable page object
	ASFixedRect cropBox;		// doc's crop box
	AVPageView pgView;			// current page in viewer
	PDEContent pdeContent;	// container for page content 
	PDEFont pdeFont;				// reference to a font used on a page 
	PDEFontAttrs pdeFontAttrs;	// font attributes  
	PDEText pdeText;				// container for text 
	FixedMatrix textMatrix;			// transform matrix for text 
	PDEGraphicState graphicsState;	// graphic state to apply to operation 
	PDEElement pdeElement;			// element of page content  
	FixedMatrix elemMatrix;			// element matrix   
	PDEColorSpace pdeColorSpace;	// ColorSpace 
	ASInt32 i,j, numElems, numRuns; // for enumeration of elements
	

DURING
  		// Get the currently displayed page and its cropbox 
	avDoc = AVAppGetActiveDoc();
	pgView = AVDocGetPageView (avDoc);
	pdPage = AVPageViewGetPage(pgView);
	PDPageGetCropBox (pdPage, &cropBox);
		
		// Create Font object 
	memset(&pdeFontAttrs, 0, sizeof(pdeFontAttrs));
	pdeFontAttrs.name = ASAtomFromString("Times-Roman");
	pdeFontAttrs.type = ASAtomFromString("Type1");

	pdeFont = PDEFontCreate(&pdeFontAttrs, 
					sizeof(pdeFontAttrs), 
					0, 
					255, 
					0, 
					0, ASAtomNull,
					0, 0, 0, 0); 

	// The following code sets up the default Graphics State. 	 
	pdeColorSpace = PDEColorSpaceCreateFromName(ASAtomFromString("DeviceGray"));

	memset(&graphicsState, 0, sizeof(PDEGraphicState));
	graphicsState.strokeColorSpec.space = 
		graphicsState.fillColorSpec.space = pdeColorSpace;
	graphicsState.fillColorSpec.value.color[0] = fixedSevenEighths;
	graphicsState.miterLimit = fixedTen;
	graphicsState.flatness = fixedOne;
	graphicsState.lineWidth = fixedOne;	

	// Fill out the text matrix, which determines the point size of the font	 
	memset(&textMatrix, 0, sizeof(textMatrix)); // clear it 
	textMatrix.a = FloatToFixed(.707 * WATERMARK_PTSIZE);
	textMatrix.b = textMatrix.a; // set font width and height 
	textMatrix.c = FloatToFixed(-.707 * WATERMARK_PTSIZE);
	textMatrix.d = textMatrix.a; 	    
	textMatrix.h = cropBox.right / 4;  // x coord of start point
	textMatrix.v = cropBox.top / 2;   // y coord of start point
	
	// create new text run 
	pdeText = PDETextCreate();       
	PDETextAdd(pdeText,      // text container to add to 
		kPDETextRun,       // kPDETextRun, kPDETextChar 
		0,            // index 
		(Uns8 *)WATERMARK_STR,  // text to add  
		strlen(WATERMARK_STR),  // length of text 
		pdeFont,         // font to apply to text 
		&graphicsState, sizeof(graphicsState), // graphic state
		NULL, 0,         // text state and size of structure
		&textMatrix,       // transformation matrix for text 
		NULL);          // stroke matrix 

	// Acquire the page contents as a PDEContent object
 pdeContent = PDPageAcquirePDEContent(pdPage, gExtensionID);
  
	// insert text into page content 
	PDEContentAddElem(pdeContent, kPDEBeforeFirst, 
																	(PDEElement) pdeText);

	// Set the PDEContent for the page 
	b = PDPageSetPDEContent(pdPage, gExtensionID);
	
	// remember to release all objects that were created 	
	num = PDPageReleasePDEContent(pdPage, gExtensionID);

	// Remember to release the objects we acquired or created 
	PDERelease((PDEObject) pdeText);
	PDERelease((PDEObject) pdeColorSpace);
	PDERelease((PDEObject) pdeFont);

	// Announce to the world that we changed the contents 
	// so the viewer will refresh the page
	PDPageNotifyContentsDidChange (pdPage);

// Now some exception-handler stuff, in case of failure
HANDLER

	AVAlertNote(ASGetErrorString(ERRORCODE, buf, 255));
	
	if (pdeText)
		PDERelease((PDEObject) pdeText);
	if (pdeColorSpace)
		PDERelease((PDEObject) pdeColorSpace);
	if (pdeFont)
		PDERelease((PDEObject) pdeFont);

	num = 
		PDPageReleasePDEContent(pdPage, gExtensionID);
END_HANDLER

}

The rest is pretty straightforward. We acquire the page contents as a PDEContent object, add our text element to the contents list (specifying kPDEBeforeFirst, to be sure the watermark text is the first thing drawn on the page, making it draw "in back of" all other page elements), set the page's content to the new content list, then release the page contents object and notify the host app that the page has changed (so it will be redrawn).

Because PDF's graphics model uses opaque ink (at least in version 4.0 of Acrobat), objects draw on top of each other in the order they're laid down. Fortunately, the API lets us add new objects to either end of the "draw list." Thus, we can draw our watermark text either on top of all other items (partially obscuring other page elements) or underneath all other items (in which case the watermark itself may be partially obscured). The disadvantage of drawing the watermark first is that if the page is particularly "busy" or has some very large graphics, the watermark may be mostly obscured. One workaround for this is to use a watermark that forms a tiled pattern across the page, with small individual elements. (PDF does allow tiled vector graphics, incidentally.) Another tactic is simply to draw the watermark on top of other page elements, but make it less conspicuous by (for example) drawing outlined text rather than solid-filled text.

There are many ways in which the AddWatermark plug-in could be improved. Now that you have the basic code for adding text to a page, maybe you can implement some of those enhancements yourself. (If you do, please send me a copy.)

Conclusion

PDF is a complex and sophisticated document format. Not surprisingly, the Acrobat plug-in API is correspondingly complex and sophisticated. In fact, with 1,100 methods (covered in a reference document that's almost 3,000 pages long), it is arguably Adobe's most elaborate API — outdoing even the Photoshop plug-in API for sheer intricacy. One well-known Acrobat plug-in developer routinely tells clients that to accomplish any non-trivial task using the Acrobat API can take an experienced programmer (i.e., one who is experienced at C/C++ development, but new to the Acrobat API) a minimum of six months. That may be overstating it a bit. But you can probably appreciate the fact that Acrobat plug-in development is not something you become adept at in a weekend. The API is rich to the point of being unwieldy at times (and suffers from subtle architectural shortcomings, which only become obvious after you've spent many hours working with it). But although the Acrobat API lacks the elegance of, say, the After Effects API and fails, for the most part, to make a complex document format (PDF) easy to understand and work with, it does expose an amazingly diverse set of capabilities to programmers who might need to extend Acrobat's already impressive arsenal of PDF editing and annotation tools. And that, in and of itself, is saying quite a lot.


Kas Thomas <kt@acroforms.com> has been programming in C and assembly on the Mac for more than ten years and consults for Adobe Systems and others on PDF-related development issues. You can read his work at Acroforms.com, PlanetPDF.com, and MightyWords.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more

Jobs Board

Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
Liquor Stock Clerk - S. *Apple* St. - Idaho...
Liquor Stock Clerk - S. Apple St. Boise Posting Begin Date: 2023/10/10 Posting End Date: 2024/10/14 Category: Retail Sub Category: Customer Service Work Type: Part Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.