How can I programmatically rotate all the pages in a PDF using native macOS tools?

Solution 1:

To my knowledge macOS does not have any one native command line Unix executable that can rotate all pages in a PDF (while keeping text based ones text based). sip can rotate a single page PDF however the resulting PDF is an encapsulated image, not text if it was text base to begin with. Also, not sure if there is a way with just plain AppleScript, other then via UI Scripting the default Preview application, without going to AppleScriptObjC (Cocoa-AppleScript) and or Python, etc.

Using third-party command line utilities is probably the easiest, but you said it has to be done only using what's a default part of macOS. So, I'll offer an AppleScript solution that uses UI Scripting the default Preview application, that can be used in the event there isn't another way with AppleScriptObjC or without third-party utilities, etc.

This solution, as offered (and coded), assumes that Preview is the default application for PDF documents and uses it to rotate all the pages in the PDF document. It is also setup as an Automator workflow. (Although there are other ways to incorporate the AppleScript code shown below.)

First, in Finder, make a copy of the target PDF documents and work with those.

In Automator, create a new workflow document, adding the following actions:

  • Get Specified Finder Items
    • Add the copied target PDF document to this action.
  • Run AppleScript Script
    • Replace the default code with the code below:

AppleScript code:

on run {input}
    set thisLong to 0.25 -- # The value of 'thisLong' is decimal seconds delay between keystrokes, adjust as necessary.
    set theRotation to "r" -- # Valid values are 'l' or 'r' for Rotate Left or Rotate Right.
    set theViewMenuCheckedList to {}
    set theMenuItemChecked to missing value
    repeat with thisItem in input
        tell application "Finder" to open file thisItem -- # By default, in this use case, the PDF file will open in Preview.
        delay 1 --  # Adjust as necessary. This is the only 'delay' not defined by the value of 'thisLong'.
        tell application "System Events"
            perform action "AXRaise" of window 1 of application process "Preview" -- # Just to make sure 'window 1' is front-most.
            delay thisLong
            --  # Ascertain which of the first six 'View' menu items is checked.
            set theViewMenuCheckedList to (value of attribute "AXMenuItemMarkChar" of menu items 1 thru 6 of menu 1 of menu bar item 5 of menu bar 1 of application process "Preview")
            repeat with i from 1 to 6
                if item i in theViewMenuCheckedList is not missing value then
                    set theMenuItemChecked to i as integer
                    exit repeat
                end if
            end repeat
            --  # Process keystrokes based on which 'View' menu item is checked.
            --  # This is being done so the subsequent keystroke ⌘A 'Select All' 
            --  # occurs on the 'Thumbnails', not the body of the document.
            if theMenuItemChecked is not 2 then
                repeat with thisKey in {"2", "1", "2"}
                    keystroke thisKey using {option down, command down}
                    delay thisLong
                end repeat
            else
                repeat with thisKey in {"1", "2"}
                    keystroke thisKey using {option down, command down}
                    delay thisLong
                end repeat
            end if
            repeat with thisKey in {"a", theRotation as text, "s"} -- # {Select All, Rotate Direction, Save}
                keystroke thisKey using {command down}
                delay thisLong
            end repeat
            keystroke theMenuItemChecked as text using {option down, command down} -- # Resets the 'View' menu to the original view.
            delay thisLong
            keystroke "w" using {command down} -- # Close Window.
        end tell
    end repeat
end run

Notes:

  • As this script uses UI Scripting, when run from Automator (or Script Editor), the running app must be added to System Preferences > Security & Privacy > Accessibility in order to run properly. Saved as an application, the saved application would need to be added.
  • Also with UI Scripting, the value of the delay commands may need to be changed for use on your system (and or additional delay commands added as appropriate, although in the case additional delay commands should not be needed). It should go without saying however test this on a set of a few document first to make sure the value set for thisLong works on your system. On my system this worked as coded.
  • When using UI Scripting in this manner, once the task has started, one must leave the system alone and let it finish processing the files. Trying to multi-task will only set focus away from the task at hand and cause it to fail.
  • If you need to rotate more then one time, add additional theRotation as text, to:

      repeat with thisKey in {"a", theRotation as text, "s"} -- # {Select All, Rotate Direction, Save}
    

    Example:

      repeat with thisKey in {"a", theRotation as text, theRotation as text, "s"}
    

Solution 2:

This python script will rotate by 90º clockwise all pages of any PDFs given as arguments, saving the result to a new file with "+90" appended to the filename.

#!/usr/bin/python
# coding=utf-8
# Produces new PDF file with all pages rotated by 90 degrees.

import sys
import os
from Quartz import PDFDocument
from CoreFoundation import NSURL

if __name__ == '__main__':

    for filename in sys.argv[1:]:
        filename = filename.decode('utf-8')
        shortName = os.path.splitext(filename)[0]
        outFilename = shortName + "+90.pdf"
        pdfURL = NSURL.fileURLWithPath_(filename)
        pdfDoc = PDFDocument.alloc().initWithURL_(pdfURL)
        if pdfDoc:
            pages = pdfDoc.pageCount()
            for p in range(0, pages):
                page = pdfDoc.pageAtIndex_(p)
                existingRotation = page.rotation()
                newRotation = existingRotation + 90
                page.setRotation_(newRotation)

            pdfDoc.writeToFile_(outFilename)