Run Automator .workflow file from AppleScript

I’m having a little trouble with this. I’ve set up a workflow that converts image files to a PDF. I’ve tested it as both a Service and a Workflow. When it’s a service I just have to select the images in the Finder, right click and choose the workflow to convert them all into a single PDF. It works perfectly.

I want to put this into an AppleScript so I tried the following by adapting the last code from mark hunte above:


set InputFolder to alias "Macintosh HD:Users:david.morgan:Desktop:Temp:Test JPEGs:"
tell application "Finder" to set FileList to every file in folder InputFolder as alias list
repeat with i from 1 to count FileList
		set item i of FileList to (quoted form of (POSIX path of item i of FileList)) & space
end repeat
--
set workflowpath to "Macintosh HD:Users:david.morgan:Desktop:Make_PDFs.workflow"
set qtdworkflowpath to quoted form of (POSIX path of workflowpath)
set command to "/usr/bin/automator -i " & (items of FileList as string) & qtdworkflowpath
set output to do shell script command

but it only ever makes the first image into a single page PDF, seemingly ignoring the other images in the folder. I looked up the manual for automator in Terminal and it says to use the newline character (\n) as the delimiter for multiple strings. I tried this too but with even less success.

Any suggestions?

Model: iMac
Browser: Chrome
Operating System: Mac OS X (10.10)

You could skip Automator altogether:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use framework "AppKit" -- for NSImage
use framework "Quartz" -- required for PDF stuff

set inFiles to (choose file of type {"public.image", "com.adobe.pdf"} with prompt "Choose your  files:" with multiple selections allowed)
set destPosixPath to POSIX path of (choose file name default name "Combined.pdf" with prompt "Save new PDF to:")
my combineFiles:inFiles savingToPDF:destPosixPath

on combineFiles:inFiles savingToPDF:destPosixPath
	--  make URL of the first file
	set inNSURL to current application's |NSURL|'s fileURLWithPath:(POSIX path of item 1 of inFiles)
	-- make PDF document from the URL
	if (inNSURL's pathExtension()'s isEqualToString:"pdf") as boolean then
		set theDoc to current application's PDFDocument's alloc()'s initWithURL:inNSURL
	else
		set theDoc to my pdfDocFromImageURL:inNSURL
	end if
	-- loop through the rest
	set oldDocCount to theDoc's pageCount()
	set inFiles to rest of inFiles
	repeat with aFile in inFiles
		--  make URL of the next PDF
		set inNSURL to (current application's |NSURL|'s fileURLWithPath:(POSIX path of aFile))
		-- make PDF document from the URL
		if (inNSURL's pathExtension()'s isEqualToString:"pdf") as boolean then
			set newDoc to (current application's PDFDocument's alloc()'s initWithURL:inNSURL)
		else
			set newDoc to (my pdfDocFromImageURL:inNSURL)
		end if
		-- loop through, moving pages
		set newDocCount to newDoc's pageCount()
		repeat with i from 1 to newDocCount
			-- get page of  PDF
			set thePDFPage to (newDoc's pageAtIndex:(i - 1)) -- zero-based indexes
			-- insert the page into main PDF
			(theDoc's insertPage:thePDFPage atIndex:oldDocCount)
			set oldDocCount to oldDocCount + 1
		end repeat
	end repeat
	set outNSURL to current application's |NSURL|'s fileURLWithPath:destPosixPath
	-- save the main PDF
	(theDoc's writeToURL:outNSURL)
end combineFiles:savingToPDF:

on pdfDocFromImageURL:inNSURL
	set theImage to current application's NSImage's alloc()'s initWithContentsOfURL:inNSURL
	set theSize to theImage's |size|()
	set theRect to {{0, 0}, theSize}
	set theImageView to current application's NSImageView's alloc()'s initWithFrame:theRect
	theImageView's setImage:theImage
	set theData to theImageView's dataWithPDFInsideRect:theRect
	return current application's PDFDocument's alloc()'s initWithData:theData
end pdfDocFromImageURL:

Rescued me again Shane. Let me guess, that’s something you prepared earlier?
I knew there must be a way to do this using Obj C but would never have worked it out. There sure is a lot going on in that code. Works blazingly fast though, thanks very much!

Perhaps academic now, but the trick with the automator shell script appears to be to use the quoted form of the entire linefeed-delimited input rather than quoting the paths individually:


set InputFolder to ((path to desktop as text) & "Temp:Test JPEGs:")
tell application "Finder" to set FileList to every file in folder InputFolder as alias list

repeat with i from 1 to count FileList
	set item i of FileList to POSIX path of item i of FileList -- Not quoted forms here.
end repeat
-- Coerce the list to a single, linefeed-delimited text.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to linefeed
set input to FileList as text
set AppleScript's text item delimiters to astid
-- Get the quoted form of the result.
set qtinput to quoted form of input
--
set workflowpath to (path to desktop as text) & "Make_PDFs.workflow"
set qtdworkflowpath to quoted form of (POSIX path of workflowpath)
set command to "/usr/bin/automator -i " & qtinput & space & qtdworkflowpath
set output to do shell script command

That works a treat too. Thanks Nigel, I was messing around with Terminal but didn’t think of putting the whole lot in single quotes.

You can also run a workflow like this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "Automator"
use scripting additions

set InputFolder to ((path to desktop as text) & "Temp:Test JPEGs:")
tell application "Finder" to set FileList to every file in folder InputFolder as alias list

repeat with i from 1 to count FileList
	set item i of FileList to POSIX path of item i of FileList 
end repeat
--
set workflowPath to POSIX path of ((path to desktop as text) & "Make_PDFs.workflow")
set workflowURL to current application's |NSURL|'s fileURLWithPath:workflowPath
set {theResult, theError} to current application's AMWorkflow's runWorkflowAtURL:workflowURL withInput:FileList |error|:(reference)
if theError is not missing value then error (theError's localizedDescription() as text)

Hi Shane.

  1. This script’s problematic on my El Capitan machine for some reason. The PDF is invariably correctly created, but the runWorkflowAtURL: method usually doesn’t return. Script Debugger goes into an eternal beachball state; Script Editor reports after a quarter of a minute or so that the operation couldn’t be completed. Occasionally, though, the method does complete, usually (but not always) when I feed FileList in directly as a list of aliases instead of POSIX paths or NSURLs. :confused:

  2. By my reading of the method’s documentation, the result will be nil either if an error occurs or if there’s no output from the workflow. If I’ve got that right, the last line of the script (assuming it gets that far!) should be:

if theError ≠ missing value then error (theError's localizedDescription() as text)

Do you have Show this action. turned off and Replace Existing Files on?

Yes, you’re right: “The error argument must be examined to determine which scenario occurred.” Which flies in the face of Apple’s advice on how to deal with NSErrors.

Yep. And it actually does what it’s supposed to. It just doesn’t hand back to the script afterwards.

Prompted by the fact that the effect’s slightly different depending on whether I run the script in Script Debugger (beachball) or Script Editor (eventual “operation couldn’t be completed” message), I’ve just tried saving the script as an application with an extra line to say that it’s done and running it under its own steam. It works perfectly well in this form and announces “Finished” almost immediately. There must be something about running it in an editor which my system doesn’t like.

Does it make any difference if you only have a couple of small files to process?

I’ve tried fewer JPEGs, smaller JPEGs, different JPEGs, and JPEGs all with the same orientation. I’ve also tried making all the script variables ” and even the script itself ” non-persistent. The results are always the same:

Script as application ” no problem.
Script in Script Debugger ” workflow works, SD beachballs.
Script in Script Editor ” workflow works, SE displays error message after several seconds: error “The operation couldn’t be completed. (com.apple.Automator error 0.)” number -2700 from «script» to item. In this case, if the error reporting line at the end of the script is left out, SE’s result pane eventually shows {missing value, «class ocid» id «data optr0000000000F1D1338D7F0000»}, so SE does get a reply from the workflow, even if it’s just the error.

I’ve also added a Run AppleScript action to the end of the workflow to announce when the New PDF from Images action has finished and how many items it’s passed on. The announcement always happens, however the script’s run. SD’s timer stops at the end of the announcement, but its status bar still shows “Running” and eventually the beachball appears.

PS. In ASObjC Explorer 4, the behaviour’s the same as in Script Editor. However, with the “Run in foreground” box checked, everything works perfectly! Could be a thread problem.

You’ve covered all possibilities, by the look of it. I know some of the PDF stuff was reworked in 10.12, but I’d be surprised if the action is doing much different from my script earlier, so that probably doesn’t explain it. It may well just be improved handling of ASObjC methods that are slow to return. I guess I should hunt down some others to test the theory.

Ahah! – sounds like it. So does it run fine in Script Editor if you hold down the Control key to force it to the foreground?

Strange that it’s changed in 10.12, though.

Yes! :slight_smile:

So presumably it’s also happy with this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "Automator"
use scripting additions

set InputFolder to ((path to desktop as text) & "Temp:Test JPEGs:")
tell application "Finder" to set FileList to every file in folder InputFolder as alias list

repeat with i from 1 to count FileList
	set item i of FileList to POSIX path of item i of FileList
end repeat
--
set workflowPath to POSIX path of ((path to desktop as text) & "Make_PDFs.workflow")
set workflowURL to current application's |NSURL|'s fileURLWithPath:workflowPath
if current application's NSThread's isMainThread() as boolean then
	its doMainThreadStuff:{workflowURL, FileList}
else
	my performSelectorOnMainThread:"doMainThreadStuff:" withObject:{workflowURL, FileList} waitUntilDone:true
end if

on doMainThreadStuff:theArgs
	set {workflowURL, FileList} to theArgs
	set {theResult, theError} to current application's AMWorkflow's runWorkflowAtURL:workflowURL withInput:FileList |error|:(reference)
	if theError is not missing value then error (theError's localizedDescription() as text)
end doMainThreadStuff:

Even Script Debugger likes it ” from the point of view of not hanging. If theResult’s needed for anything, it’s not available this way. But given that the thread problem only occurs when the script’s run in an editor, and not at all in Sierra, your original script should be fine and one only needs to downgrade to Script Editor or ASObjC Explorer to test it. :wink:

It turns out that although the handler doesn’t return a result when it’s executed on a different thread ” and in fact the AS error generated in it is invisible to the main script too ” the script can access the information through properties or globals:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "Automator"
use scripting additions

--property theResult : missing value
--property theError : missing value
global theResult, theError -- For results from a handler possibly run on a different thread.

set InputFolder to ((path to desktop as text) & "Temp:Test JPEGs:")
tell application "Finder" to set FileList to every file in folder InputFolder as alias list

repeat with i from 1 to count FileList
	set item i of FileList to POSIX path of item i of FileList
end repeat
--
set workflowPath to POSIX path of ((path to desktop as text) & "Make_PDFs.workflow")
set workflowURL to current application's |NSURL|'s fileURLWithPath:workflowPath
if current application's NSThread's isMainThread() as boolean then
	its doMainThreadStuff:{workflowURL, FileList}
else
	my performSelectorOnMainThread:"doMainThreadStuff:" withObject:{workflowURL, FileList} waitUntilDone:true
end if

-- Check the results _after_ performing the handler.
if theError is not missing value then error (theError's localizedDescription() as text)
return {theResult, theError} -- Both values returned here just as proof of concept.

on doMainThreadStuff:theArgs
	set {workflowURL, FileList} to theArgs
	-- When this handler's run on a different thread from the main script, no errors or results it generates get back to the script.
	-- So instead set globals or properties to the relevant information.
	set {theResult, theError} to current application's AMWorkflow's runWorkflowAtURL:workflowURL withInput:FileList |error|:(reference)
	--if theError is not missing value then error (theError's localizedDescription() as text)
	--return theResult
end doMainThreadStuff:

theResult can be coerced to alias in El Capitan, but I don’t know if that’s also true in Yosemite.

What the result is, and it’s class, is going to depend a bit on the workflow, obviously. In my example here, it returns a path as as an NSString. But the whole thing with what actions return is a bit murky, because Automator often adds invisible coercion actions (.cactions) between them to make stuff work.

Ah yes. Of course. :rolleyes:

In fact the return in my case was being passed through (but not altered during) the Run AppleScript action I’d added to the workflow to indicate progress. I was getting an NSAppleEventDescriptor. Having disabled the Run AppleScript action, I’m now getting an NSArray containing a single NSString, which can’t be coerced to alias on my machine.

Oops, my path string was in an array, too.