Last active
December 16, 2024 19:20
-
-
Save nuclearsecrecy/cfd97e868e74ce25fd7fdce403e4b3ff to your computer and use it in GitHub Desktop.
Applescript to batch run OCR on many PDF files using PDF Expert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 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
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
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.