Thursday, 5 February 2009

Interactive pdfs: using javascript

Recently I have betrayed my flash counterparts, turning to javascript, javascript for PDF's. If that sentence doesn't make you shudder, it should, and perhaps this blog post will reveal why. However my main purpose for writing this is to lighten the learning curve (and horror) for any developers who are striving to create interactive pdfs.

So the sceneario is this: you recive a PDF full of lovely images and designs, but you want the user to interact with elements on the screen; use buttons to navigate and reveal different layers and options. PDF's can contain a number of different media's: images, video and swf. You may also allow the users to submit forms which can be emailed, for example to a sales department who can deal with a request or feedback infromation.

Starting out
In order to start working you'll need Acrobat pro, I use Adobe Acrobat Pro 9. You'll need to specify an external text editor that you will be using to write your javascript, I use TextMate. Select Acrobat-> Preferences-> JavaScript select the radio button "Use external JavaScript editor" and browse for your appliacation.

A word of warning, you can edit code interally using Acrobat JavaScript Editor, however this works less than well and if your planning on writing a lot of code (i.e scripting functionality for a 20-30 page pdf) it will either crash or Acrobat will throw an error.

The documents I work with are created in Adobe's InDesign by a designer, exported in a magical way and sent to me for scripting. There are lots of tutorials describing how to do this and is beyond the scope of this blog post. A quick note, the more consisitant your designer can be with naming layers and fields: the easier your job will be.

A Debugger is available (Advanced -> Document processing -> JavaScript Debugger). You will find that the debugger is sadly lacking and generally uninformative, if you are lucky a reference to the line number of your error will be provided, if not, you're left guessing. Use console.println("this is flash's version of a trace statement!"); to output content to the debugger, this will help in your mystery hunt.

Adding javascript
To start adding code select Advanced -> Documet processing -> Document Javascripts
Select "add" and give your script a name, how about a creative name such as "myScript"?

At this point your external editor will open and your code will be surrounded with Acro Script. Ignore this, this is added by Adobe Acrobat, you won't be able to edit directly since it is created from your own code. For example you code a line which sends the user to page 14 when a button is clicked, you may get something like the following produced in Acro Script:

// //menu_safety 2.Page 8:Annot1:MouseUp:Action1 // /*********** belongs to: AcroForm:menu_safety 2.Page 8:Annot1:MouseUp:Action1 ***********/ pageNum=14; close_menu(pageNum); // //

As far as I can tell the language is horribly inefficient and by the time you have finished coding you will have literally thousands of lines of AcroScript.

Accessing fields and layers using javascript
To access elements (known as fields) within the pdf, use this.getField("my_button_name");
"this" referring to the actual pdf document. You can use this for any fields, including buttons and text input fields. Lets say that at the beginning of your pdf you have a screen sized button that you want to let the users "enter" the pdf and go to the next page, aka page 1 (page numbers start from zero). You may use the following code:
var mybtn=this.getField("enter_btn");
mybtn.setAction("MouseUp", "pageNum=1;");
//on mouseUp go to page number one!

Layers are a little more complex as far as I can tell you cannot access layers by name. You must access layers using their page number and the layers are returned as an array.
var ocgArray = this.getOCGs(pageNum);

One may then cycle through the layers stored within the array and act accordingly. The following function is passed the page number, an array of layers that I want to be visible, and an array of layers which I want invisible. The function cycles through the layers for the particular page, where if finds a match between the layer name and a layer I have specified in my array it acts accordingly setting its state to visible (true) or invisible (false)
Buttons also have states which can be used to control their visiblity, particulary useful if you want to disable an option once a button has been clicked, or maintain a button state upon the click. For example, quite often within my brochure a number of options are displayed as clickable buttons. A rollover state has been created using Indesign; ie the text turns orange and bold. I want to make it obvious which button has been selected by making selected buttons unclickable and maintaining this rolled over state of orange and bold text.

One possible way of achieving this is to store an image of the 'over' state of the button within the content of the corresponding layer. This layer is visible once a button is selected, so all that remains is to render the clicked button invisible upon selection.

Below is such an example (company name blurred out). The top Question is no longer a button but a static image (the button looked like the question below but is now invisible). The bottom question is a button complete with roll over state. The rest of the screen is filled with the layer which contains the image of the top most question and of course the corresponding answer image.

To hide and show a button, the syntax is simple:
var mybtn= this.getField(BUTTON_NAME);

//im here!
var mybtn=this.getField(BUTTON_NAME);
//im gone!
Extra functionaltiy: Printing, Searching and Links
A couple of small bits of functionality are things like printing, searching and hyperlinks.
For Hyperlinks, get the field using the syntax mentioned before and then: (nb: dont forget the quotes around the url or it wont work)
URLbut.setAction("MouseUp", "app.launchURL('', true);" );

To give users the option to search use the syntax below. However it is worth noting that this will also search your hidden layers and make them visible again! This will quite possible mess up the look of your pdf, but it will aslo confuse users.
searchBut.setAction("MouseUp", "app.execMenuItem('FindSearch');" );

Acrobat prompting users to view the invisible layer shown below.

I havent found a way of disabling the option of viewing invisible layers, however there are ways to correct the view after Acrobat has navigated the user to the page. You can set a page action which operates everytime the page opens. This happens after the user has been sent to the page via search and therefore can be used to recorrect the page. The code below will call the function startVis everytime a user navigates to page 12. startVis is a function which restores the default layer visiblities. There is another problem created by this solution, users who have searched content may find that the layer is no longer visible, you risk confusing the user. This solution makes the best of a bad situation, however if you find something better let me know!

this.setPageAction(12, "Open", "startVis(pageDefaults));

Set up some Security!
Generally this is the basics of creating interactive pdfs. I just want to include a word about settings.

Its likely that you wont want users to tamper with or steal your designs, so add some security, its easy!

Advanced-> security -> show security properties
Select password security: here you can add passwords and restrict actions. There are varying security options, prohibiting editing, restricting printing to low res, etc. Be aware that if you want to edit your pdf after this you will need to remove security and enter in your password.

When your finished don't forget to compress and reduce the bulk caused by all those layers!


Sev said...

When I use Textmate as external javascript editor, Acrobat kind of gets stucked: the javascript file opens in Textmate, but Acrobat won't get notified when I close it. It seems to wait for something, and all I can do is force quit Acrobat now.

Do you have any solution for that?

electrobunny said...

did you definitly close textmate, you cant just close the file but completely escape textmate to get back into acrobat. Acrobat should then look like its reading the file back in, shown in the process bar at the bottom.

JennShaggy said...

Not a silly question at all.
It keeps for up to two weeks in the refrigerator. I'm not sure if it freezes or always disappears haha.
Thank you :)