Save as PDF – An attempt at a general-purpose GUI method

A few days ago, Peter Bunn offered a couple of GUI Scripting suggestions for a script to save documents from any application in PDF format. It sounds easy enough: Control-p, click the “PDF” button in the Print dialog, select the “Save As PDF.” item in the menu that appears, enter a file name in the following Save dialog, and click the “Save” button. But in fact there are numerous obstacles and uncertainties for such a script to overcome unless it’s tailor-written for a particular use with a particular application.

Knowing what process to address. Invoking a script usually brings the application running it to the front, so the script has to find out which was the frontmost application before it itself was run. That, in turn, depends on the manner in which it was run, which it also has to deduce. If the path to the frontmost application is the same as the path to the script, the script’s either an applet or is being run as a compiled script from the “Scripts” menu of an application that has such a thing. There’s no way to tell these situations apart unless you can guarantee that the script will have a certain name when it’s used as an applet, so you have to tell the user either not to change the name of the script or not to run it from an application’s “Scripts” menu. Since GUI Scripting also comes to grief when using an application’s own “Scripts” menu, that’s what the user’s urged not to do in this case.

If the script’s running from FastScripts, the target application will already be frontmost, so nothing else needs to be done in that respect. If the script’s an applet, we’ll need to set its visible to ‘false’ so that it vacates the ‘frontmost’ role in favour of the application that was frontmost previously. If the applet was invoked by double-clicking its file icon, the Finder may have been made frontmost during the clicking, so that will need to be handled. If the script’s run from Script Menu, the frontmost application will be System Events, which doesn’t remove itself from the frontmost slot. One way round this is to activate another application “ bringing it to the front “ and then immediately to set its ‘visible’ to ‘false’. This brings the target application back to the front rather than System Events “ provided of course that the target application wasn’t used for the activate-and-hide trick. Given the uncertainty about this, the script below creates, opens, and hides its own applet, which it destroys immediately afterwards. This works faster than you might think.

Knowing where the “PDF” button is. With many applications, the Print dialog is a sheet extruded over the frontmost document window. With quite a few, though “ eg. Stickies, RagTime, Finale, and others whose frontmost windows are either floating palettes, tool bars, or browser dialogs “ the Print dialog is a separate dialog window. This means a different UI reference for the “PDF” button. Not only that, but Preview displays a different design of sheet depending on whether it’s printing text or an image. iTunes and GraphicConverter display intermediate dialogs before the Print dialog appears. And to add to all this, while the “PDF” button in a sheet is amenable to GUI scripting, the “PDF” button in a dialog window doesn’t respond to it at all.

Fortunately, it seems to be the case (so far) that the “PDF” button is always a sub-element of the sheet’s first element containing buttons and is the first sub-element of that element with a sub-element of its own, so a ‘whose’ filter is easy to arrange. It’s also easy to arrange for the script to mark time while the user makes any desired adjustments in intermediate windows. The unclickable “PDF” button in a dialog window can be got round “ rather clunkily, 'tis true “ by clicking the “Preview” button instead. This opens the document in Preview, whence it can be saved as a PDF file, with the same settings, using Preview’s Print dialog sheet.

Knowing where to save the file. It’s all very well entering a file name and clicking “Save”, but the location of the save will simply be whatever it was last time the Save dialog was used for that purpose in that application. If we can remember where that was, we’re laughing; otherwise, we have to go looking for the file. The script below performs a Control-d keystroke in the Save dialog to ensure that the files always turn up in the same place “ on the desktop.

While trying to work out how to click automatically through all the various dialogs, it occurred to me that this might not actually be desirable. The script therefore allows the user to interact with the Set-up dialogs but automates the bits in between.

No doubt there’s some one-line Unix script that’ll achieve the same end “ and Adam suggested a simple, third-party solution back in August “ but this has been an interesting exercise. :slight_smile:

-- This script uses GUI Scripting and is only expected to work in Tiger.
-- The GUI aspects mean it won't work in an application's own Scripts menu.

-- Find the process that would be frontmost if it weren't for this script, make it frontmost again, and return its name.
on getProcessName()
	if ((path to me) is (path to frontmost application)) then
		-- The script's running as an applet, from a script editor window, or from an application's Scripts menu.
		-- The user will already been warned not to use the last method.
		-- Set the applet/editor's visible to false to stop it being frontmost.
		tell application "System Events" to set visible of (first application process whose frontmost is true) to false
		-- If the applet was started by double-clicking its icon, the Finder will probably (though not necessarily)
		-- now be the frontmost app. Check with the user before setting its frontmost to false.
		tell application "Finder"
			if (frontmost) then
				display dialog "The frontmost application is the Finder. Is that just because you double-clicked this script or are you seriously expecting to print from the Finder?" buttons {"Cancel", "I'm a dip head", "Double-clicked"} default button 3 cancel button 1 with title "Save As PDF" with icon caution
				if (button returned of the result is "Double-clicked") then set frontmost to false
			end if
		end tell
	else
		-- The script's running from either Script Menu or FastScripts. If FastScripts, the target application's already frontmost.
		tell application "System Events" to set ImInScriptMenu to (name of (first application process whose frontmost is true) is "System Events")
		if (ImInScriptMenu) then
			-- If running from Script Menu, create and open a script applet to nudge System Events from the frontmost slot.
			set nudge to ((path to temporary items from user domain as Unicode text) & "Nudge.app")
			run script "script o
tell app \"System Events\"
repeat until (last application process is not visible)
delay 0.2
end repeat
end tell
end script
store script o in file \"" & nudge & "\" replacing yes"
			tell application "System Events"
				set eoap to (count application processes) + 1
				-- Open the created applet and wait for it to appear at the end of the application processes.
				open file nudge
				repeat until (application process eoap exists)
					delay 0.2
				end repeat
				-- Set its visible to false to vacate the 'frontmost' slot in favour of the target app.
				set visible of application process eoap to false
				-- The applet will finish when it sees it's invisible. Wait for it to quit, then zap its file.
				repeat while (application process eoap exists)
					delay 0.2
				end repeat
				delete file nudge
			end tell
		end if
	end if
	
	-- Return the name of the now frontmost target applicaton process.
	tell application "System Events" to return name of first process whose frontmost is true
end getProcessName

-- Determine whether an application process has anything in its "File" menu that will respond to Control-p.
-- If it does, return the names of the "Print." and "Page Setup." items.
on getPrintInfo(processName)
	tell application "System Events"
		tell application process processName
			tell (menu 1 of menu bar item 3 of menu bar 1) -- The "File" menu.
				set menuItemNames to name of its menu items whose enabled is true
				tell (name of first menu item whose enabled is true and value of attributes contains "P" and value of attributes contains 0)
					if (it exists) then
						-- If there's an enabled item in the "File" menu with an associated Control-p keystroke, get its name.
						set PrintName to it
						-- Work up through the menu item names until this name's reached, then continue to the next
						-- name ending in ".". This is assumed to be the "Page Setup." item. Return both names.
						set PrintNamePassed to false
						repeat with i from (count menuItemNames) to 1 by -1
							set thisName to item i of menuItemNames
							if (thisName is PrintName) then
								set PrintNamePassed to true
							else if (PrintNamePassed) and ((thisName ends with ".") or (thisName ends with "...")) then -- Adobe Reader uses 3 dots!
								return {PrintName, thisName}
							end if
						end repeat
					else
						-- If there's no enabled "Print." item, the app either can't print or has no document ready.
						return {}
					end if
				end tell
			end tell
		end tell
	end tell
end getPrintInfo

on showError(msg)
	tell application (path to frontmost application as Unicode text)
		display dialog msg buttons {"Cancel"} default button 1 with title "Save As PDF" with icon stop
		error number -128
	end tell
end showError

-- Ask for a name for the PDF file and check it over. The user can also choose whether or not to use the "Page Setup." dialog.
on getPDFName(processName, docName)
	set defaultName to docName & ".pdf"
	tell application processName
		display dialog "Enter a name for the PDF file." & return default answer defaultName buttons {"Cancel", "With Page Setup.", "Just do it"} default button 3 cancel button 1 with title "Save As PDF" with icon note
	end tell
	set {text returned:PDF_name, button returned:mode} to the result
	
	if ((count PDF_name) is 0) then
		set PDF_name to defaultName
	else if (PDF_name does not end with ".pdf") then
		set PDF_name to PDF_name & ".pdf"
	end if
	
	return {mode, PDF_name}
end getPDFName

-- Show the "Page Setup" dialog and wait for the user to dismiss it.
on doPageSetup(PageSetup) -- PageSetup is the "Page Setup." menu item name returned by getPrintInfo().
	tell application "System Events"
		tell (first application process whose frontmost is true)
			set frontwindow to a reference to (first window whose subrole is not "AXFloatingWindow")
			set docName to name of frontwindow
			-- Click the "Page Setup." menu item in the "File" menu.
			perform action "AXPress" of menu item PageSetup of menu 1 of menu bar item 3 of menu bar 1
			-- The setup dialog could be either a sheet or another window.
			repeat until (sheet 1 of frontwindow exists) or (name of frontwindow is not docName)
				delay 0.2
			end repeat
			repeat while (sheet 1 of frontwindow exists) or (name of frontwindow is not docName)
				delay 0.2
			end repeat
		end tell
	end tell
end doPageSetup

-- Arrange for the PDF document to be saved by Preview instead of the current process.
-- Called if the process's "Print." dialog is a window instead of a sheet.
on divertToPreview(processName, PrintName) -- The name of the process and the name of the "Print." item in its "File" menu.
	tell application "System Events"
		-- Note if Preview is already open. (Determines whether or not we quit it later.)
		set PreviewWasOpen to (application process "Preview" exists)
		tell application process processName
			-- If the next window isn't the "Print" dialog, wait until it is.
			-- Hopefully, the "Print" dialog's name is just one word meaning "Print".
			set currentWindowName to name of front window
			repeat until ((count currentWindowName each word) is 1) and (currentWindowName is in PrintName)
				delay 0.2
				set currentWindowName to name of front window
			end repeat
			-- Click the dialog's "Preview" button.
			perform action "AXPress" of last button of (first UI element of window 1 whose buttons is not {})
		end tell
		-- Wait for Preview to open the document
		repeat until (name of (first application process whose frontmost is true) is "Preview")
			delay 0.2
		end repeat
		-- Invoke Preview's print dialog, which, happily, is a sheet.
		tell application process "Preview"
			-- We'll need the Preview document's name later to check when it's covered by the "Save" dialog.
			set docName to name of front window
			keystroke "p" using command down
			repeat until (sheet 1 of front window exists)
				delay 0.2
			end repeat
		end tell
	end tell
	
	return {PreviewWasOpen, docName}
end divertToPreview

-- The actual "Save as PDF" process.
on saveAsPDF(processName, docName, PDF_name)
	tell application "System Events"
		tell application process processName
			-- Click the "PDF" button in the current Print dialog sheet. Its position in the GUI structure
			-- varies, but seems always to be in the the sheet's first UI element that contains buttons
			-- and to be the first UI element of that element that contains a UI element of its own.
			click at position of first UI element of (first UI element of sheet 1 of window 1 whose buttons is not {}) whose UI elements is not {}
			delay 0.2
			-- Key the down arrow and space bar to select the first item in the "PDF" menu ("Save as PDF."), and wait for the Save dialog to appear.
			key code {125, 49}
			repeat while (name of front window is docName)
				delay 0.2
			end repeat
			
			-- Enter the PDF name into the Save dialog's text field, use Control-d to set the desktop
			-- as the destination, click the "Save" button, and wait for the Save dialog to disappear.
			set value of text field 1 of window 1 to PDF_name
			keystroke "d" using command down
			perform action "AXPress" of button 1 of front window
			repeat until (name of front window exists) and (name of window 1 is docName)
				delay 0.2
			end repeat
		end tell
	end tell
end saveAsPDF

on main()
	try
		-- Get the name of the target process and check that it has print capability. If not, display a message and finish.
		set processName to getProcessName()
		set printInfo to getPrintInfo(processName)
		if (printInfo is {}) then showError("Can't print from "" & processName & "".")
		set {PrintName, PageSetupName} to printInfo
		
		-- Get a reference to the process's likely front document window and get its name.
		tell application "System Events"
			tell application process processName
				set frontmost to true
				set frontwindow to a reference to (first window whose subrole is not "AXFloatingWindow")
				set docName to name of frontwindow
			end tell
		end tell
		
		-- Ask the use for a name for the PDF file and whether or not to use the "Page Setup." dialog first.
		set {mode, PDF_name} to getPDFName(processName, docName)
		
		-- Invoke the "Page Setup." dialog, if specified.
		if (mode is "With Page Setup.") then doPageSetup(PageSetupName)
		
		tell application "System Events"
			tell application process processName
				-- Invoke the Print dialog. Depending on the app, it'll either be a sheet
				-- in the document window or a new, dialog window.
				keystroke "p" using command down
				repeat until (sheet 1 of frontwindow exists) or (name of frontwindow is not docName)
					delay 0.2
				end repeat
				set dialogIsWindow to (name of frontwindow is not docName)
			end tell
		end tell
		
		-- While the "PDF" button in a sheet responds to GUI Scripting, the one in a dialog window doesn't.
		-- If that's what we have, click the "Preview" button instead, which opens the document in Preview.
		-- Preview uses a sheet, so the document can be saved as PDF from there. Yes, it's very Heath Robinson.
		if (dialogIsWindow) then
			set {PreviewWasOpen, docName} to divertToPreview(processName, PrintName)
			-- From now on, we're dealing with Preview rather than the original process.
			set processName to "Preview"
		end if
		
		-- 'processName' is now either the original app's name or "Preview".
		-- 'docName' is the name of the front document in the relevant app.
		
		-- Perform the actual save.
		saveAsPDF(processName, docName, PDF_name)
		
		-- If Preview was used to help out, tidy up as appropriate.
		if (dialogIsWindow) then
			if (PreviewWasOpen) then
				tell application "System Events"
					tell application process "Preview"
						perform (action of first button of window 1 whose subrole is "AXCloseButton")
					end tell
				end tell
			else
				tell application "Preview" to quit
			end if
		end if
	on error msg number errNum
		if (errNum is not -128) then showError(msg)
	end try
end main

main()

Nigel

Thanks for sharing the script. It works very nicely. I had a bunch of JPG files that needed to convert to PDF and subsequently join together.

Although the script way works (and this is an AppleScript forum), I ended up finding that solution too slow for my needs. As you hinted, using a command line option may be shorter. My solution is posted at http://jcandkimmita.info/jc/?p=61, in case it helps someone else.

Cheers,

Juan C.

Hi, Juan.

Thanks for the feedback. I’m not surprised that my script’s too slow for batches of files. It’s only meant to save the currently open document in an application! Its embarrassingly great length is due to the attempts to make it generic. I don’t use it myself. :wink:

Thanks for posting the link to your solution.

Hi to scripting gods,

First of all:Thanks VERY much for that script.

I just found it as I was looking EXACTLY that thingy :slight_smile:

When trying to use it from Firefox, it just returns an Errormessage saying: “Can’t print from firefox-bin”.

It turns out that it returns the same error message for all other programmes as well.

I’m running on Leopard. Could that be the cause of that tiny issue?

And if yes: Does anyone have an idea how to fix that tiny little problem?

All the best

joh

Hi, joh.

I suppose it’s possible that the “Print” dialog has changed between Tiger and Leopard and that the script’s now “pressing the wrong buttons”. I didn’t bother to get Leopard, so I’m afraid I can’t research the problem. Sorry. :frowning:

I hope someone with Leopard will be able to fix it for you, or that you’ll find some other solution that suits your need.

Hey Nigel

a friend of mine came up with another approach, namely using CUPS-PDF as a printer and create a little script for that.

I suppose that’s a lot easier to write than the script you managed to put together.

Meaning, that even a normal MAc-user with a bit of AS-background should be able to write that one.

And that’s suposed to be me :smiley:

All the best and thanks for the reply!!!

joh