I have a few courses that share materials and ideas. I used to use the age-old practice of cut and paste to share the slides. As handy as it is, cut and paste causes trouble. If I’m teaching TDD in C and make a change to a discussion that applies also to C++ or or some other language, I’ve got a problem. I’ll never remember to put the change in the other places where the change is needed. This means that later I find myself in front of a group of C++ programmers about to make a big point, and find that the dramatic conclusion added to the C version I made is not in the current slide deck.
As with code, duplication in presentations is a liability. So I needed to refactor my training materials, and need a practical way to deliver them and provide a PDF handout of the presentation.
I use Apple Keynote for my presentations. To minimize duplication in my Keynote files, I needed to split the big Keynote files into modules. Then I could build a specific course’s Keynote from the modules through some concatenation process, but no such luck. I could not find a way to concatenate Keynote files. I wish I could say Keynote helped me solve this problem. Help, not really. But it did not prohibit me from finding a less ideal solution.
(I tried the concatenation approach with apple automater and applescript, and could not find a decent solutions. If you know one, let me know).
I settled on an approach using a Keynote homepage and hyperlinks. This is OK, but it left me with the problem of a single PDF for printing the course handout. Manually it is a pain to create the PDF course materials handout, with 10 to 15 separate Keynote files. Not only a pain, but I am sure the quality of my handouts created manually would suffer. So, I need to automate PDF production so that I can rebuild my handouts at anytime.
Thankfully, using a mac, I have the full power of unix under the hood. Concatenating, watermarking, and covering a set of PDF files is no big deal with an open source command line tool called pdftk and some custom bash scripting to glue it all together. But there is one weak link in this process; it is getting a properly formatted PDF for each of the Keynote files.
When I set off on this journey, I thought it would be a breeze to interact with Keynote on the command line or applescript. Unfortunately there is no command line interface that I can use to ask Keynote ’09 to create a PDF. But there is applescript. Thwarted by the incomplete object model exposed by Keynote to applescript, I could not just interact with Keynote on a programmer level, I have to use applescript to pretend to be a user pressing keys and clicking boxes.
I thought google would quickly lead me to the needed how-to; but I must lack the Keynote/applescript vocabulary to find the perfect example with google. As I searched and researched, I discovered that many of the people that posted applescript/Keynote automation examples must have never run them. Many did not work as presented.
After many searches and some stick-to-itiveness, I put together an applescript to generate a single PDF with the settings I needed. Before I show you the code, it is important to know there are a few prerequisites for this script to work:
- In the Keynote print dialog, create a preset named ‘4 per page’, that tells Keynote to print with ‘Layout’ setting that has ‘Pages per sheet’ attribute set to 4.
- In /System Preferences/Keyboard/Keyboard Shortcuts/Application Shortcuts/Keynote define key-press control-command-option-P to mean “Save as PDF…”
- Before running the script, print as PDF some Keynote file into the directory you want the PDFs to go. This established where the PDF is placed on subsequent keynote print operations. (I might try to make this controlled by the script, but directory navigation is not so simple in Keynote driven by applescript. Let me know if you know an easy way.)
The script takes two inputs: the path of the Keynote presentation file and a second string parameter that should be set to “First” on the first call, and something else on subsequent calls. Here is the refactored top level script:
on run {aFile, fileOrder}
tell application "Keynote"
open aFile
try
my bringKeynoteToFront()
my openPrintDialog()
my setPrintOptions(fileOrder)
my saveToPdf()
my closeTheFile()
end try
end tell
end run
This moves the focus from the shell terminal window to Keynote.
on bringKeynoteToFront() tell application "Keynote" activate end tell end bringKeynoteToFront
Pretending to be the user, the script presses command-p, the built-in print command keyboard shortcut.
on openPrintDialog() tell application "System Events" tell process "Keynote" keystroke "p" using {command down} repeat until exists sheet 1 of window 1 end repeat end tell end tell end openPrintDialog
This was a little weird to come up with. Even though I found examples of setting a checkbox to a specific state, it does not work. So you have to query the checkbox and click it to the desired state. (If print dialog presets actually remembered all the settings, I would not have had to fiddle with checkboxes in applescript.) Also notice the delays and the waiting for certain text to appear. It's real easy for the script to get ahead of the GUI.
The quoted text below is the actual text in the dialog box. It makes me think this won't work on a non-English installation.
-- set the print options -- depends on a user defined preset called "4 per page" on setPrintOptions(fileOrder) if fileOrder = "First" then tell application "System Events" tell process "Keynote" repeat with checkboxNumber from 1 to 13 set box to checkbox checkboxNumber of sheet 1 of window 1 if value of box is 1 then click box end repeat set borders to checkbox "Add borders around slides" of sheet 1 of window 1 set margins to checkbox "Use page margins" of sheet 1 of window 1 set individual_slides to radio button "Individual Slides" of radio group 1 of sheet 1 of window 1 set presets_button to pop up button 3 of sheet 1 of window 1 if value of individual_slides is 0 then click individual_slides if value of borders is 0 then click borders if value of margins is 0 then click margins if not (value of presets_button is "4 per page") then keystroke tab delay 0.1 keystroke "4" delay 1 repeat until value of presets_button is "4 per page" end repeat end if end tell end tell end if end setPrintOptions
Here we used the custom applications shortcut key for "Save as PDF...". You'll also find some delays and reading of the GUI text to keep synchronized.
-- save to PDF -- depends on a user defined key for "Save as PDF…" on saveToPdf() tell application "System Events" tell process "Keynote" keystroke "p" using {command down, option down, control down} delay 1 repeat until exists window "Save" end repeat keystroke return delay 2 repeat until not (exists window "Save") end repeat end tell end tell end saveToPdf
Finally, close the file with command-w.
on closeTheFile() tell application "System Events" tell process "Keynote" keystroke "w" using {command down} delay 0.1 end tell end tell end closeTheFile
If you are a real applescript expert look this over and tell me the easy way, please!
The applescript entry function, on run
shown above, gets called from a bash script like this.
printToPdf=${APPLE_SCRIPTS_DIR}/PrintKeynoteToPdfDefault-lion.scpt
file_order="First"
for file in $KEYNOTE_NAMES ; do
echo "Print PDF ${file} ${file_order}"
osascript ${printToPdf} ${KEYNOTE_PATH}/${file} $file_order
if [ ! -f ${PDF_TEMP}/${file}.pdf ] ; then
echo "File not created ${file}.pdf"
exit
fi
file_order="Not first"
done
These names should be self-explanatory if you happen to know bash.
Tools that helped
Applescript Disappointment Summary
I groused a little
set value of borders to 1
true
or false
which does not work, at least in the latest Mac OSX (Lion). That cost a few hours, but I learned a lot. The UIElementInspector helped figure it out. Though figuring out button numbers was strictly trial and error.
borders
instance cannot be passed to a helper function.
if value of borders is 0 then click borders
It appears that a function must have the full hierarchy of tell
statements adding to the duplication. I decided to accept that duplication between my helper functions just to make the code more readable. Originally the code was one long function like this:
on run {aFile, fileOrder}
tell application "Keynote"
open aFile
try
tell application "Keynote"
activate
end tell
tell application "System Events"
tell process "Keynote"
keystroke "p" using {command down}
repeat until exists sheet 1 of window 1
if fileOrder = "First" then
repeat with checkboxNumber from 1 to 13
set box to checkbox checkboxNumber of sheet 1 of window 1
if value of box is 1 then click box
end repeat
set borders to checkbox "Add borders around slides" of
sheet 1 of window 1
set margins to checkbox "Use page margins" of sheet 1 of
window 1
set individual_slides to radio button "Individual Slides" of
radio group 1 of sheet 1 of window 1
set presets_button to pop up button 3 of sheet 1 of window 1
if value of individual_slides is 0 then
click individual_slides
if value of borders is 0 then click borders
if value of margins is 0 then click margins
if not (value of presets_button is "4 per page") then
keystroke tab
delay 0.1
keystroke "4"
delay 1
repeat until value of presets_button is "4 per page"
end repeat
end if
end if
keystroke "p" using {command down, option down, control down}
delay 1
repeat until exists window "Save"
end repeat
keystroke return
delay 2
repeat until not (exists window "Save")
end repeat
keystroke "w" using {command down}
delay 0.1
end tell
end tell
end try
end tell
end run
Applescript seems pretty lacking, unless I am missing something. If you know a better way to do some of these things, please post a comment. I expect this to be fragile and it is likely to only work on MacOSX Lion with an English language installation.
Having to interact with Keynote in this clunky way makes me think of the testers for Keynote have a tough challenge writing automated tests. Keynote should have a command line interface or viable applescript interface to test these important features of Keynote.
Even though its a bit clunky, having this automated was well worth my efforts. Manual tedious repetitive processes lead to inconsistent results and quality.
Wow James. Thanks for this post. I tried this also some time ago, but gave up a lot sooner than you did 🙂 I think I might actually use your script 🙂
This is the sort of thing I use Keyboard Maestro, combined with AppleScript or Automator, to do.
But you did a great job with your script. I’ve never had the patience to do interface stuff through AppleScript, which is why I use Keyboard Maestro. 🙂
I’ll have to check with the Maestro. Thanks.
I don’t have any prior experience with Apple products. I do use pdftk and find it an invaluable utility.
But to come back to your main point, have you considered approaches like reST (http://en.wikipedia.org/wiki/ReStructuredText) ?
And for presentations, if you are willing to take the Latex route, Beamer (http://en.wikipedia.org/wiki/Beamer_%28LaTeX%29) can be very useful.
I’m not sure if this works though, but have you tried this as substitute for closing your keynote? (code below):
—-for closing keynote document
tell application “Keynote”
close document 1
end tell
—-for closing keynote
tell application “Keynote”
quit
end tell
The “document” is somehow universal (works with word, safari, etc). Though you may need to look at the appropriate object (for excel, it’s workbook). To get the right object, in applescript, press command + shift + O to get the dictionary for keynote (hopefully it exists).
—-You’re code—-
on closeTheFile()
tell application “System Events”
tell process “Keynote”
keystroke “w” using {command down}
delay 0.1
end tell
end tell
end closeTheFile
For print settings, if your applescript has the Keynote dictionary, it should have something like this in the objects:
——
print?v : Print the specified object(s)
print specifier : list of objects to print
[with properties print settings] : the print settings
[print dialog boolean] : Should Microsoft Excel show the print dialog?
——
You should be able to set the print settings which i think is your main concern.