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)
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
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)
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.
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.
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.
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.
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.
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.