Skip to content

Instantly share code, notes, and snippets.

@nuclearsecrecy
Last active December 16, 2024 19:20
Show Gist options
  • Save nuclearsecrecy/cfd97e868e74ce25fd7fdce403e4b3ff to your computer and use it in GitHub Desktop.
Save nuclearsecrecy/cfd97e868e74ce25fd7fdce403e4b3ff to your computer and use it in GitHub Desktop.
Applescript to batch run OCR on many PDF files using PDF Expert
# Applescript to batch OCR PDFs using PDF Expert.
# By Alex Wellerstein. Last updated December 16, 2024. No copyright asserted -- released for public domain use.
# Absolutely no warranties, guarantees, promises, ANYTHING provided. Use at your own risk.
#
# Will automatically save and close each PDF after OCR completes.
# Assumes PDF Expert is the default program to open PDFs!
# Does not have robust error handling. Held together with duct tape.
# Just a temporary solution until Readdle actually supports batch operations.
# Seems to work on OS 14.6.1, with PDF Expert 3.10.10.
# Has not been extensively tested to see what happens if you try to do other work while it is running; could foul up.
# When I use it, I just let it run for awhile by itself, and don't touch anything while it is running.
# Prompt user for PDFs
set theFiles to choose file with prompt "Select all PDFs to run OCR on:" of type {"pdf"} with multiple selections allowed
set totalFiles to length of theFiles
set progress total steps to totalFiles
set progress completed steps to 0
set progress description to "Processing files..."
set progress additional description to "Beginning."
# iterate over all of them
repeat with i from 1 to (count theFiles)
delay 0.5 # these delays are just in case things are taking a moment
set filePath to item i of theFiles
set progress completed steps to i
set progress additional description to "Processing file " & i & " of " & totalFiles
pdfexpert_ocr(POSIX path of filePath)
# will close PDF Expert to clear out RAM every 10 files -- change as necessary
if i mod 10 is 0 then
tell application "PDF Expert"
quit
end tell
delay 1
end if
end repeat
log ("All done")
# main function for OCRing a file with PDFExpert and saving it
on pdfexpert_ocr(filePath)
log ("starting to process " & filePath)
do shell script "open " & quoted form of filePath # assumes PDF Expert is default PDF reader
delay 0.5
activate application "PDF Expert"
delay 0.5
set fileTitle to getFilename(filePath, ".pdf")
tell application "System Events"
tell process "PDF Expert"
# find the right window
set win to false
set allWin to every window
repeat with i from 1 to count allWin
set wTitle to title of window i
if wTitle is fileTitle then
set win to window i
end if
end repeat
#if we couldn't find it... wait a sec and one more time
if (win is false) then
delay 2
set allWin to every window
repeat with i from 1 to count allWin
set wTitle to title of window i
log (wTitle)
log (wTitle is fileTitle)
if wTitle is fileTitle then
log (wTitle)
set win to window i
end if
end repeat
end if
# something went wrong, abort
if (win is false) then
display dialog "File '" & fileTitle & "' apparently failed to open in PDF Expert."
error number -128
end if
# otherwise...
# click "Recognize Text" from "Scan & OCR" menu
# as the AppleScript ocation of this seems to change from version to version,
# we use a lookup function. This adds a little delay, but should be reliable.
click my getElementByClassAndName("Recognize Text", false, entire contents of win)
# click "Recognize..." on side bar.
# ditto re: lookup function.
tell my getElementByClassAndName("Recognize...", false, entire contents of win)
click
# If there is more than 1 page, then it asks how many pages to do.
# We want to select "All" in that case. If there is only 1 page, this doesn't come up, and
# starts automaticaly after the previous step.
if exists radio button "All" of sheet 1 of win then
tell radio button "All" of sheet 1 of win # all pages plz
click
end tell
tell button "Apply" of sheet 1 of win #start OCR
click
end tell
end if
end tell
# Wait a slight delay before we start checking if it is done.
delay 1
# This searches for the sign that the OCR has completed. This is finicky
# and has been the source of most issues with this script. I blame AppleScript.
# Basically, we try to see if the progress indicator, which is kept on a modal "sheet",
# has left us. For some unknown reason, trying to find it will occasionally trigger
# its own error, but only when it is gone? So we just wrap the whole thing in a `try`
# and use any errors as an excuse to move on. Which probably isn't failproof.
repeat
try
# look for the progress indicator - these tiers are to avoid an error if something
# disappears along the hierarchy (which apparently can happen)
if not (exists win) then
# log ("no win")
exit repeat
end if
if ((count sheet of win) = 0) then
# log ("sheet = 0")
exit repeat
end if
if not (exists progress indicator 1 of sheet 1 of win) then
# log ("no indicator")
exit repeat
end if
on error errStr number errorNumber
log (errStr) #one of the weird failure modes
exit repeat
end try
end repeat
log ("OCR finish detected")
# Just a sec to make sure it is done.
delay 1.0
log ("Saving and closing")
set fileMenu to menu "File" of menu bar item "File" of menu bar 1
# Then save file and close window (tries to detect if save is successful)
click my getElementByClassAndName("Save", false, entire contents of fileMenu)
delay 1
if not my isSaved(front window) then
log ("did not save, waiting")
delay 4
if not my isSaved(front window) then
log ("did not save again, waiting")
tell application "PDF Expert" to activate # try to activate it
click my getElementByClassAndName("Save", false, entire contents of fileMenu)
delay 4
end if
end if
if my isSaved(front window) then
tell application "PDF Expert" to activate
click my getElementByClassAndName("Close Window", false, entire contents of fileMenu)
else
log ("did not save -- leaving window open, moving on")
end if
delay 0.5
log ("OCR run on " & filePath)
end tell
end tell
end pdfexpert_ocr
# returns just the filename. if theExtension is set, will trim it.
on getFilename(theFile, theExtension)
tell application "Finder" to set fName to name of (POSIX file theFile as alias)
if (theExtension is not "") then
return trimText(fName, theExtension, "end")
else
return fName
end if
end getFilename
# god awful function for trimming characters in Applescript
on trimText(theText, theCharactersToTrim, theTrimDirection)
set theTrimLength to length of theCharactersToTrim
if theTrimDirection is in {"beginning", "both"} then
repeat while theText begins with theCharactersToTrim
try
set theText to characters (theTrimLength + 1) thru -1 of theText as string
on error
-- text contains nothing but trim characters
return ""
end try
end repeat
end if
if theTrimDirection is in {"end", "both"} then
repeat while theText ends with theCharactersToTrim
try
set theText to characters 1 thru -(theTrimLength + 1) of theText as string
on error
-- text contains nothing but trim characters
return ""
end try
end repeat
end if
return theText
end trimText
# This is a function that will try to search a given set of elements for a
# matching class and name property. See above for example usage, where "Recognize..."
# is the name, `button` is the class (or `false` if you don't care about matching the class),
# and `entire contents` is the set of all UI elements to search. Sometimes elements don't have a
# name or class, which is why the try statement is in there.
# It returns "false" if the element was not found.
#
# This is not super fast, so it is probably best used to get a static reference,
# and not in a dynamic function. On my machine it takes maybe 4 seconds to search through
# 60 elements or so, 20ish seconds to search through 400 elements (not uncommon for a
# full window, if it can't find the element). If it was possible to cull targetElements
# to only the desired classes ahead of time, that would speed it up, but it doesn't
# seem like one can do that...
on getElementByClassAndName(targetName, targetClass, targetElements)
set found to false
repeat with uiElem in targetElements as list
if (targetClass is not false) then
try
if ((name of uiElem is targetName) and (class of uiElem is targetClass)) then
set found to true
end if
end try
else
try
if (name of uiElem is targetName) then
set found to true
end if
end try
end if
if found is true then
return uiElem
exit repeat
end if
end repeat
return false
end getElementByClassAndName
# will try to detect if window saved correctly
on isSaved(win)
set winName to name of win
set char to characters 1 thru 1 of winName as string
if char is "*" then
return false
else
return true
end if
end isSaved
@nuclearsecrecy
Copy link
Author

nuclearsecrecy commented Mar 14, 2024

I like PDF Expert a lot, but its lack of a batch OCR capability was really bugging me, as I occasionally have need to run OCR on many PDFs. This is a very quick and dirty Applescript script for achieving a very minimal version of this. It will ask you to select PDFs, then go over all of them, one by one, opening them in PDF Expert, clicking "Recognize Text" and "Recognize" (make sure you set up the default language ahead of time), and then waits until it detects it is done, saves the file, closes it, and moves on to the next one. I have just been running this in Script Editor.

Shout out to this code by @rpapallas for getting me started on the right path, and to get entire contents of front window which was the magical command that made a lot of this easier (it'll dump a list of every UI component along with the way to reference it in Applescript, so you can find buttons with it -- I was really struggling with it until then). I find Applescript to be an abomination, so this was a little painful. If you actually find this useful for anything, I'd be curious to know!

@lethanh1305
Copy link

It's no longer work with PDF Expert version 3.10. I get this error "System Events got an error: Can’t get button "Recognize..." of group 1 of group 2 of UI element 1 of window "321321" of application process "PDF Expert".".

@nuclearsecrecy
Copy link
Author

Sorry for the slow response — just saw this. It should work now (they apparently put the button inside of another "group" and it just needs it path updated — would be neat if I was comfortable-enough with Applescript to make it find it automatically... if they end up changing it again I will look into it).

@lethanh1305
Copy link

Thank you so much. Love your work. It work perfectly but i suggest small modification . instead of close window on line 139 , replace it wwith Quit PDF expert because when I ran this script with 5000 pdfs file on mac mini M1, the memory it used take up to 95GB ram and cause the computer to crash if you not close the app entirely.

@nuclearsecrecy
Copy link
Author

Huh. That's annoying, that it doesn't clear out the memory on closing a document. (Playing around with it, it seems to clear out SOME of the RAM when you close documents, but still keeps a lot in memory.) OK, what I did is change it so that every X documents (I set it to 10 by default) it will quit the application to clear out any accumulated RAM junk.

@lethanh1305
Copy link

@nuclearsecrecy I am sorry to bother you but now PDF Expert updated to new version 3.10.10 and the script not working again. I tried to figure out the the group UI things using Xcode Inspector but no hope. Can you update the script please. Thank you

@nuclearsecrecy
Copy link
Author

No worries. I have updated it. I used a new method for finding the buttons (one that hopefully will not break between versions). It is a little slower (it looks them up each time), but on my machine at least it is not really noticeable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment