If you haven’t already encountered it, k2pdfopt is a very handy tool for reformatting PDFs to fit on various e-ink devices. I wanted a way to easily reformat the documents in my DEVONthink database, so I wrote a toolbar script.
To use, create a folder ~/.bin and download k2pdfopt to there. Put the following script in the Toolbar subfolder of the scripts folder and add it to the DEVONthink toolbar. Select a file, click the icon, and it will run k2pdfopt for you. You can also use it in smart rules. I have a Kindle Paperwhite, but if you have a different device you may want to adjust the formatting options. They’re in the do shell script call below.
If you have any suggestions for improvement I’m all ears!
-- Convert PDFs for Kindle using k2pdfopt
-- Adapted from the Deadlink script
on run
tell application id "DNtp"
my convertPdf(selected records)
end tell
end run
on performSmartRule(theFiles)
my convertPdf(theFiles)
end performSmartRule
on convertPdf(theFiles)
tell application id "DNtp"
repeat with thisRecord in theFiles
if (type of thisRecord is PDF document) then
set recName to (name of thisRecord as string)
show progress indicator ("Converting " & recName) steps -1
log message recName info "Converting for Kindle"
set {path:recPath, location group:recGroup} to thisRecord
try
set |encrypted| to (do shell script "grep -a -m 1 '/Encrypt' " & (quoted form of recPath))
if |encrypted| ≠ "" then
log message "Encrypted files cannot be converted." info recName
end if
on error -- non-zero status errors --> non-encrypted
with timeout of 3000 seconds
try
set output to do shell script "~/.bin/k2pdfopt -dev kp2 -c -fc- -s- -n " & (quoted form of recPath)
-- log message (name of thisRecord as string) info output
on error errMsg
log message recName info ("Error: " & errMsg)
return
end try
end timeout
set newPath to texts 1 thru ((length of recPath) - 4) of recPath & "_k2opt.pdf"
-- DT3 won't import the file if it's in the database tree, so we have to move it to /tmp
do shell script "mv " & (quoted form of newPath) & " " & "/tmp"
set newPath to "/tmp/" & (do shell script "basename " & (quoted form of newPath))
log message recName info "Importing new file at " & newPath
set newRecord to import newPath name (recName & " - Kindle") to recGroup
hide progress indicator
log message recName info "Converted successfully"
end try
end if
end repeat
end tell
end convertPdf
You should add an error trap in case no documents are selected.
In this case there won’t be any items to iterate over so the script just exits silently. I’m open to suggestions on how else to provide feedback to the user, though.
Why are you using two handlers?
I copied this pattern from the “Deadlink” script which was able to both be run from the toolbar and as a smart rule; I haven’t actually looked into it beyond the naming of the handlers but I assume the “on run” is for the toolbar invocation whereas the “performSmartRule” is for smart rules. Is that correct?
I haven’t actually looked into it beyond the naming of the handlers but I assume the “on run” is for the toolbar invocation whereas the “performSmartRule” is for smart rules. Is that correct?
Yes, but there doesn’t need to be a third handler. And if you’re talking about my old Deadlink script, that would be old code I haven’t looked at in years.
Here’s my suggested improvements though I can’t claim they’re more understandable, just more efficient…
-- Convert PDFs for Kindle using k2pdfopt
-- Adapted from the Deadlink script
-- Cleaned up and optimized by BLUEFROG
on run
tell application id "DNtp"
if selected records is {} then return
my performSmartRule(selected records)
end tell
end run
on performSmartRule(theFiles)
tell application id "DNtp"
repeat with thisRecord in theFiles
if (type of thisRecord is PDF document) then
set {path:recPath, location group:recGroup, name without extension:recName} to thisRecord
show progress indicator ("Converting " & recName) steps -1
log message recName info "Converting for Kindle"
set recName to (quoted form of recName & "-Kindle.pdf") as string
if (encrypted of thisRecord) then
log message "Encrypted files cannot be converted." info recName
return
end if
with timeout of 3000 seconds
try
set output to do shell script "/usr/local/bin/k2pdfopt -dev kp2 -c -fc- -s- -n " & (quoted form of recPath) & " -o $TMPDIR" & recName & " > /dev/null; echo $TMPDIR" & recName
on error errMsg
log message recName info ("Error: " & errMsg)
return
end try
end timeout
log message recName info "Importing new file"
set newRecord to import output to current group
open tab for record newRecord
hide progress indicator
log message recName info "Converted successfully"
end if
end repeat
end tell
end performSmartRule
Bear in mind, this was quickly done so I’m sure it could be further massaged.
Thanks for that – that answers my question if a handler can return – looks a lot cleaner than having the main action happen in a on error branch.
@ndpi: why do you use a hidden folder (~/.bin) to store the script instead of a directory in $PATH like /usr/local/bin? And why not use the k2pdfopt option -o to specify an output filename instead of creating the new file inside DT’s library and then move it out before you import it? Seems a bit contrived to me.
Thanks for the suggestions. I’ve incorporated them and refactored the script significantly to separate concerns, improve readability, and give more granular feedback/error handling:
-- Convert PDFs for Kindle using k2pdfopt
property k2pdfoptPath : "/usr/local/bin/k2pdfopt"
property k2pdfoptOptions : "-dev kp2 -c -fc- -s- -n"
property conversionTimeout : 3000
on run
tell application id "DNtp"
if selected records is {} then return
my performSmartRule(selected records)
end tell
end run
on checkRecordCanBeProcessed(theRecord)
tell application id "DNtp"
set {name without extension:recName} to theRecord
if (not type of theRecord is PDF document) then
log message recName info "Only PDFs can be converted."
return false
end if
if (encrypted of theRecord) then
log message recName info "Encrypted files cannot be converted."
return false
end if
return true
end tell
end checkRecordCanBeProcessed
on convertPdf(theRecord)
tell application id "DNtp"
set {path:recPath, name without extension:recName} to theRecord
log message recName info "Converting for Kindle..."
with timeout of conversionTimeout seconds
set tmpDir to do shell script k2pdfoptPath & " " & k2pdfoptOptions & " " & (quoted form of recPath) & " -o $TMPDIR > /dev/null; echo $TMPDIR"
end timeout
set newRecFile to (tmpDir & recName & ".pdf")
log message newRecFile info "Importing new file..."
set newRecord to import newRecFile to current group
open tab for record newRecord
log message recName info "Converted successfully."
end tell
end convertPdf
on performSmartRule(theFiles)
tell application id "DNtp"
set totalFiles to count of theFiles
log message ("Converting " & totalFiles & " files to Kindle-friendly format")
set currentFile to 0
repeat with thisRecord in theFiles
if not my checkRecordCanBeProcessed(thisRecord) then
log message recName info "Skipping file because it cannot be converted."
else
set {name without extension:recName} to thisRecord
set currentFile to currentFile + 1
show progress indicator ("Converting " & recName & " (" & currentFile & " of " & totalFiles & ")") steps totalFiles
try
my convertPdf(thisRecord)
on error errMsg
log message recName info ("Error: " & errMsg)
display dialog "Error converting " & recName & ": " & errMsg buttons {"Stop", "Continue"} default button "Continue"
if button returned of result is "Stop" then
log message recName info "Exiting, user cancelled processing."
hide progress indicator
error errMsg number -128 -- User cancelled
end if
end try
end if
step progress indicator
end repeat
hide progress indicator
end tell
end performSmartRule
It’s an old habit of mine. I keep personal tools and scripts which aren’t managed by my system package manager (in this case, HomeBrew) in ~/.bin and add it to my $PATH in my shell RC file. I’ve learned the hard way that it’s better not to rely on $PATH in scripts; it often causes problems. (I’ve seen a few topics in this forum where people were bitten by this.)
That said, there’s no major concern with putting k2pdfopt there if you prefer, so it’s really just a matter of taste! De gustibus, etc.
Unless I’m mistaken (I’m no AppleScript expert), it looks as if you were processing the record regardless of the result of checkRecordCanBeProcessed. If that’s the case, why worry about checking in the first place?
Also, in do shell script k2pdfoptPath & k2pdfoptOptions it seems (!) that you are concatenating "/usr/local/bin/k2pdfopt" and "-dev kp2 -c -fc- -s- -n", which looks to me(!) as if you’d have "/usr/local/bin/k2pdfopt-dev kp2 -c -fc- -s- -n" afterward. I may be wrong, of course, but given that you took care to insert space after the options…
In JavaScript, I’d do something like (just for illustrative purposes, not tested):
The main difference being that performsmartrule passes only unencrypted PDFs to convertPDF by using filter first. Which uses a local variable to store the output filename instead of echoing it back to stdout in doShellScript – that approach seems a bit too complicated. What’s missing is any error check. If for some reason the new file can’t be created, the code will not notice at all. And I omitted all the log stuff as well as the progress indicator. Those can be added, of course.
Good catches! Clearly I was being lax with my testing. (In my defense, DEVONthink’s caching of AppleScripts makes iterating on them somewhat annoying.)
Fixed in the post above.
In JavaScript, I’d do something like […]
AppleScript isn’t exactly my cup of tea either. Did you know that it doesn’t even have a continue statement? Wild.
That’s why I don’t touch it with a ten foot pole. No string processing worthy of that name, no array methods, basically nothing that other languages offer nowadays. Not to mention that it’s lacking a formal specification and a grammar. And far too verbose.