Acrobat
Doing an IAmA
Ajax-style PDF part 1: fading highlight setup
If you haven’t already seen it, take a look at Adobe’s walking talking PDF tour of Acrobat 7.0. It’s one of the most creative PDF files I’ve ever seen. (Don’t stop after the first few pages; there are some funny bits near the end.)
Obviously, a lot of work went into making this PDF, but the technical side of it is actually pretty simple. We can add some scripting magic to make it even better.
Take page 11, where our narrator explains the links he’s standing next to, pointing to each one as he describes it:
The links just sit there when he points at them. It would look good, and be a nice usability touch, if we could apply an Ajax-style fading highlight to each link as he points to it:
First we need to find out how the existing page works. If you have Acrobat Professional, you can see it by using the Select Object tool on the Advanced Editing toolbar. That’s a Flash movie on the right with the narrator in it. Right click it and open its Properties to see the Page Enter event and a .swf rendition (Acrobat’s term for a media clip and its associated settings):
Each page is like this, with a Flash movie embedded in the page that runs on the Page Enter event.
The links on the left are (no surprise) PDF pushbutton form fields.
With our narrator in a Flash movie and the links being PDF buttons, is there a way to connect the two? We can write some JavaScript code in the PDF to fade a highlight on and off for a link, but how do we trigger that code at the right time as the movie plays?
Well, one thing at a time. It would be fun to just see the fading highlight in action, so we’ll write that bit of code first and hook it up to a temporary button to test it. The code will use doc.getField(name)
to get a JavaScript Field
object, and then it can set the field’s fillColor
property to change its background color. If we do that on a repeating fast timer we’ll have the fading highlight effect.
For a quick test before we write any code, we can right click one of the buttons and open its Properties to change its fill color manually:
Oops. That worked, but it didn’t do what we want. We got the fill color but the icon and text went away. Let’s Undo it and try something else. (And note that the fill color doesn’t extend all the way to the right end of the field. That’s because the Flash movie overlaps the field. Hopefully this won’t cause any problem.)
We can create a separate field that is a solid rectangle, and if we get the Z-order right it should do what we want. A text field with no text in it will do the trick. Let’s try it without worrying about the exact layout first:
I guess that’s some kind of progress. Maybe changing the Z-order will fix it. The tool to change that is tucked away in Acrobat’s Advanced/Forms/Fields/Set Tab Order menu command:
Now we can click on the fields in order to set their tab order (which is also their Z-order), and if we put the text field in the tab order before the button, we get the transparent background highlight we were looking for:
Finally, we move and resize the text field and we have our field highlight, at least in static form. Here’s the page after a Select All (Ctrl+A) to show all the field rectangles:
Creating a separate text field for the highlight was a minor nuisance, but does have one benefit: we were able to fine tune the highlight position relative to the button icon and text:
The blue outline is the pushbutton field that we tried to work with originally. As you can see, the pushbutton field rectangle doesn’t have consistent margins around the icon and text (and no margin on the left). With a separate text field—the red outline—we can adjust it so the highlight is positioned nicely:
Now that we have a highlighter field, we should be able to write some code to control it. While I was editing the field I changed its name from the default Text1
to Hilite
, so we should be able to use getField
and set its fillColor
. Let’s try it in the JavaScript console first:
Looks good! We got a reference to the field in the hilite
variable and looked at its current fillColor
property. Then we changed the fillColor
and the visible field changed as expected.
The last statement in the JavaScript console (not yet executed in the screen shot) hides the field, so we can save the file and it looks normal. It doesn’t matter that we left the field the wrong color; we’ll take care of that in the code that makes it visible again. For now, it’s time to save the file and take a break. In the next installment we’ll write some code to create the fading highlight effect.
p.s. Here’s an Acrobat editing tip: Open the General tab of Acrobat’s Preferences dialog and turn on the single-key accelerators. Then you can use the H key for “hand” (normal) mode in Acrobat, R for the object selector, and so on. Hover the mouse over a toolbar button to see its shortcut key. It makes this kind of editing a lot easier where you switch tools so often.
Disclaimer: I work for Adobe, but this is my own summer vacation project, not any kind of offical Adobe code.
Invisible JavaScript functions
If you’re writing document-level JavaScript code in a PDF file, any variables you declare or assign to outside a function are created in the global object, which is the PDF document.
Suppose we have a PDF with this code as a document script, which creates three variables in three different ways:
If we load this PDF and examine its variables in the JavaScript console, we see that each of the three variables is defined both as a global variable and as a property of this
, the document object. That seems surprising until you consider that in Acrobat JavaScript, the document object is the global object.
myVar 11 this.myVar 11 myNoVar 22 this.myNoVar 22 myProp 33 this.myProp 33(Keyboard input is in
bold
, and we type Ctrl+Enter at the end of each line to evaluate the expression.)
When you’re writing code, you don’t want to pollute the document object’s namespace needlessly. You can wrap up your code in a function, so that variables you create with a var
statement are local to the function.
Adobe Acrobat encourages this by creating a function for you when you add a document script. If you use Acrobat’s Document JavaScripts dialog to add a script called DocScript
, you’ll get an empty function to fill in:
doc
instead of this
. We didn’t have to do that, but I like to be able to use doc.whatever
instead of this.whatever
for document properties.
If we look at these variables in the console window, the results are different:
myVar1 ReferenceError: myVar1 is not defined 1:Console:Exec undefined this.myVar1 undefined myNoVar1 22 this.myNoVar1 22 myProp1 33 this.myProp1 33As expected,
myVar1
does not show up either in the global object or the document object; it is local to the function. myVar2
does (because we didn’t use var
), as does myProp1
(because we created it as a property of the doc object).
The difference between myVar1
and this.myVar1
is expected too. It’s an error to reference a variable name that does not exist, so myVar1
throws a ReferenceError
exception. But it’s not an error to reference a nonexistent property of an objectthe reference merely returns the undefined
value. So this.myVar1
returns undefined
without error.
But we’ve still put one name in the document object:
DocScript function DocScript(doc) { var myVar1 = 11; myNoVar1 = 22; doc.myProp1 = 33; } this.DocScript function DocScript(doc) { var myVar1 = 11; myNoVar1 = 22; doc.myProp1 = 33; }
To avoid even this bit of namespace clutter, we can define an anonymous function and call it on the spot:
The console results for myVar2
, etc. are the same as the previous example with the named function, so I won’t repeat them. But unlike the named function, we haven’t added any symbols at all to the document or global object.
Why the extra parentheses? function(){}
is the simplest possible anonymous function, but if we try to call it using function(){}()
it’s a syntax error. However, if we wrap the entire function inside a pair of parentheses, then we can follow that with ()
to call it: (function(){})()
is legal JavaScript.
The same trick works when you want to add a nested scope inside a function. The way you would do this in C doesn’t work in JavaScript:
That code prints:
1 2 2
because there is only one variable named i
in the entire function, even though we’ve (incorrectly) tried to add an inner scope with its own variable i
.
In fact, if you load that function into ActiveState Komodo, it puts a green squiggly line under the inner var i = 2;
complaining “strict warning: redeclaration of var i”.
But if we use a nested anonymous function:
it prints:
1 2 1
which indicates that the nested function introduced a new scope.