Merge PDF Files

Recent versions of macOS have the native ability to merge PDF files, and a search of this forum will yield a number of scripts that perform this same function. I wrote and use the script included below because:

  • It can be saved as an app and an icon can be placed on the Finder toolbar for frequent use.
  • The script will optionally move the selected files to the trash after they are merged.
  • The script includes a shorthand capability which can be tailored to the user’s preference.

It is important to note that the order in which the selected PDFs are merged will differ, depending on the user’s macOS version. In the last few versions of macOS, the files will be merged in the order they are selected, but, in older versions, the files will be merged in the order they are shown in the Finder (if memory serves).

The script’s shorthand feature is controlled by two arrays at the top of the “getTargetFile” handler. For example,

set searchStrings to current application's NSArray's arrayWithArray:{", ", "dx"}
set replaceStrings to current application's NSArray's arrayWithArray:{" - ", theDate()}

In this simple example, if the user enters the first line below, the actual file name will be the second line:

To test this script, copy it to a script editor, select two or more PDF files in a Finder window, and run the script. I use this script frequently on my Monterey computer, but please test it with file copies to insure it works as expected.

-- Revised 2022.05.14

use framework "AppKit"
use framework "Foundation"
use framework "Quartz"
use scripting additions

on main()
	tell application "Finder" to set theFiles to selection as alias list
	set theFiles to current application's NSArray's arrayWithArray:theFiles
	set fileCount to theFiles's |count|()
	if fileCount < 2 then errorAlert("One or no files selected")
	set thePath to (theFiles's objectAtIndex:0)'s URLByDeletingLastPathComponent()
	set thePredicate to current application's NSPredicate's predicateWithFormat:"self.pathExtension !=[c] 'pdf'"
	set extensionCheck to (theFiles's filteredArrayUsingPredicate:thePredicate)'s |count|()
	if extensionCheck ≠ 0 then errorAlert("A selected file does not have a PDF extension")
	set {targetFile, deleteOption} to getTargetFile(thePath, fileCount)
	mergeFiles(theFiles, fileCount, targetFile)
	
	current application's NSWorkspace's sharedWorkspace()'s activateFileViewerSelectingURLs:{targetFile}
	if deleteOption = "Merge/Delete" then trashFiles(theFiles)
end main

on getTargetFile(thePath, fileCount)
	set searchStrings to current application's NSArray's arrayWithArray:{", ", ",", "dx"}
	set replaceStrings to current application's NSArray's arrayWithArray:{" - ", " - ", theDate()}
	
	display dialog "Enter a file name without extension for " & fileCount & " merged PDF files" default answer "Merged File Name" buttons {"Cancel", "Merge/Delete", "Merge"} cancel button 1 default button 3 with title "PDF Merge"
	set {targetName, deleteOption} to {text returned, button returned} of result
	if targetName = "" then errorAlert("A file name was not entered")
	set targetName to current application's NSString's stringWithString:targetName
	
	repeat with i from 0 to ((searchStrings's |count|()) - 1)
		set targetName to (targetName's stringByReplacingOccurrencesOfString:(searchStrings's objectAtIndex:i) withString:(replaceStrings's objectAtIndex:i))
	end repeat
	
	set targetFile to (thePath's URLByAppendingPathComponent:targetName)'s URLByAppendingPathExtension:"pdf"
	set fileExists to targetFile's checkResourceIsReachableAndReturnError:(missing value)
	if (fileExists as boolean) is true then errorAlert("A file with that name already exists")
	return {targetFile, deleteOption}
end getTargetFile

on theDate()
	set currentDate to current application's NSDate's |date|()
	set dateFormatter to current application's NSDateFormatter's new()
	dateFormatter's setDateFormat:"yyyy.MM.dd"
	return (dateFormatter's stringFromDate:currentDate) as text
end theDate

on mergeFiles(theFiles, fileCount, targetFile)
	set outDoc to current application's PDFDocument's alloc()'s initWithURL:(theFiles's objectAtIndex:0)
	set outDocPageCount to outDoc's pageCount()
	
	repeat with i from 1 to (fileCount - 1)
		set aDoc to (current application's PDFDocument's alloc()'s initWithURL:(theFiles's objectAtIndex:i))
		set aDocPageCount to aDoc's pageCount()
		repeat with j from 0 to (aDocPageCount - 1)
			(outDoc's insertPage:(aDoc's pageAtIndex:j) atIndex:outDocPageCount)
			set outDocPageCount to outDocPageCount + 1
		end repeat
	end repeat
	
	outDoc's writeToURL:targetFile
end mergeFiles

on trashFiles(theFiles)
	set fileManager to current application's NSFileManager's defaultManager()
	repeat with i from 0 to ((theFiles's |count|()) - 1)
		(fileManager's trashItemAtURL:(theFiles's objectAtIndex:i) resultingItemURL:(missing value) |error|:(missing value))
	end repeat
end trashFiles

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()