If I have a PDF with internal page links and then I merge it with another PDF, the page links no longer work.
Is it possible for the page links to be retained as working?
If I have a PDF with internal page links and then I merge it with another PDF, the page links no longer work.
Is it possible for the page links to be retained as working?
Possible – I believe so. If you need it right now, a script could possibly do that:
Assuming that you append PDF B to PDF A:
noPagesnoPages to its page numberJust brainstorming here, I didn’ test that. And the details are a bit tedious: you have to process B page by page, get the link annotations for each page and adjust them. But it should be feasible.
I guess I should be more clear
Yes I know it is feasible if I manually edit the PDF.
My question is - if I combine them using the DT “Merge” feature, could DT automatically keep the page links intact as a feature.
I suppose that’s currently not possible. The reason why I suggested the scripting approach
Yours is an efficient approach.
Claude suggested this - which is more complex including a Python library/dependency called pikepdf. Interestingly Claude’s approach resulted in a 1MB combined PDF whereas DT gave a 3MB result. According to Claude that is because the macOS PDFKit repackages fonts page by page.
[The script might work generically but has some specific features for my use case so may need to be tweaked for others to use it.]
-- Combine PDFs (keep links)
-- DEVONthink contextual-menu script (installed in
-- ~/Library/Application Scripts/com.devon-technologies.think/Contextual Menu/; also works from
-- the Scripts menu folder ".../Menu/"). Select the main LCP report PDF (Word "Save as PDF"),
-- the Life Expectancy graphs PDF and the LCP spreadsheet PDF, right-click them and choose
-- Combine PDFs (keep links).
--
-- Order: main report first (name contains "LCP" and not "Graph"/"Spreadsheet"/"Sheet"),
-- then the Life Expectancy graphs, then the spreadsheet, then anything else in the
-- order selected. A prompt appears only if the main report cannot be told apart.
--
-- The PDFs are COPIED out of the database (read-only on the package) to
-- ~/Library/Logs/Combine-PDFs-Keep-Links/<timestamp>/, merged there by the Python helper
-- (pikepdf, link-preserving, TOC repair, bookmarks, link check log), and the result is
-- imported into the main report's group as "<main report name> (Combined)", label kLabel,
-- no tags. Originals are untouched. A previous "(Combined)" record of the same name is
-- MOVED to the group's "Temp AI Files" (never trashed). Never opens Word or Preview.
--
-- Also callable headlessly: (load script ...)'s combineUUIDs({"UUID1","UUID2","UUID3"}).
property kLabel : 3 -- 3 = AI work (default); set to 0 for no label
property kHelperDir : "Library/Application Support/Combine-PDFs-Keep-Links/"
property kWorkRoot : "Library/Logs/Combine-PDFs-Keep-Links/"
property kTitle : "Combine PDFs (keep links)"
property gInteractive : false
on run
set gInteractive to true
tell application id "DNtp"
try
set sel to selected records
on error
set sel to selection
end try
end tell
return my combineRecords(sel)
end run
on combineUUIDs(uuidList)
set gInteractive to false
set recs to {}
tell application id "DNtp"
repeat with u in uuidList
set end of recs to (get record with uuid (u as string))
end repeat
end tell
return my combineRecords(recs)
end combineUUIDs
on combineRecords(recs)
set mains to {}
set les to {}
set sheets to {}
set others to {}
set pdfs to {}
tell application id "DNtp"
repeat with i from 1 to count of recs
set r to item i of recs
if (type of r) is PDF document then set end of pdfs to r
end repeat
if (count of pdfs) < 2 then return my fail("Select the main report PDF plus the PDFs to append (Life Expectancy graphs, LCP spreadsheet).")
repeat with i from 1 to count of pdfs
set r to item i of pdfs
set n to name of r
if n contains "LCP" and not (n contains "Graph" or n contains "Spreadsheet" or n contains "Sheet") then
set end of mains to r
else if n contains "Spreadsheet" or n contains "Sheet" or n contains "Future Cost" then
set end of sheets to r
else if n contains "Graph" or n contains "Life Expectancy" then
set end of les to r
else
set end of others to r
end if
end repeat
end tell
-- pick the main report (prompt only when ambiguous)
set mainRec to missing value
if (count of mains) = 1 then
set mainRec to item 1 of mains
else if (count of mains) = 0 and (count of others) = 1 then
set mainRec to item 1 of others
set others to {}
else
if (count of mains) > 1 then
set candidates to mains
else
set candidates to pdfs
end if
if not gInteractive then return my fail("Cannot tell which PDF is the main report; run it from DEVONthink's menu to choose.")
set names to {}
tell application id "DNtp"
repeat with i from 1 to count of candidates
set end of names to name of (item i of candidates)
end repeat
end tell
tell application id "DNtp" to set pick to choose from list names with prompt "Which PDF is the main report (goes first)?" with title kTitle
if pick is false then return "Cancelled"
set pick to item 1 of pick
set newMains to {}
set newLes to {}
set newSheets to {}
set newOthers to {}
tell application id "DNtp"
repeat with i from 1 to count of pdfs
set r to item i of pdfs
if mainRec is missing value and (name of r) is pick then
set mainRec to r
else if les contains {r} then
set end of newLes to r
else if sheets contains {r} then
set end of newSheets to r
else
set end of newOthers to r
end if
end repeat
end tell
set les to newLes
set sheets to newSheets
set others to newOthers
end if
-- work folder + read-only copies
set stamp to do shell script "date +%Y%m%d-%H%M%S"
set workDir to (POSIX path of (path to home folder)) & kWorkRoot & stamp & "/"
set helper to (POSIX path of (path to home folder)) & kHelperDir & "run_combine.sh"
do shell script "mkdir -p " & quoted form of workDir
tell application id "DNtp"
set mainName to name of mainRec
set mainPath to path of mainRec
try
set theGroup to location group of mainRec
on error
set theGroup to parent 1 of mainRec
end try
end tell
set newName to mainName & " (Combined)"
set mainCopy to my copyIn(mainPath, workDir, "0 main")
set args to " --main " & quoted form of mainCopy & " --title " & quoted form of mainName
set partNo to 1
repeat with spec in {{"le", les}, {"sheet", sheets}, {"other", others}}
set k to item 1 of spec
set lst to item 2 of spec
repeat with j from 1 to count of lst
set r to item j of lst
tell application id "DNtp"
set p to path of r
set n to name of r
end tell
set c to my copyIn(p, workDir, (partNo as string) & " " & k)
set args to args & " --part " & k & " " & quoted form of c
if k is "other" then set args to args & " --label-other " & quoted form of n
set partNo to partNo + 1
end repeat
end repeat
set outPath to workDir & my safeName(newName) & ".pdf"
set res to do shell script quoted form of helper & " --out " & quoted form of outPath & args & " 2>&1; echo EXIT=$?"
set status to ""
set summary to ""
set logPath to ""
repeat with ln in paragraphs of res
set ln to ln as string
if ln starts with "STATUS=" then set status to text 8 thru -1 of ln
if ln starts with "SUMMARY=" then set summary to text 9 thru -1 of ln
if ln starts with "MESSAGE=" then set summary to text 9 thru -1 of ln
if ln starts with "LOG=" then set logPath to text 5 thru -1 of ln
end repeat
if status is not "OK" then return my fail("Link check failed - nothing imported. " & summary & " (log: " & workDir & ")")
-- import next to the main report
tell application id "DNtp"
set olds to (children of theGroup whose name is newName)
if (count of olds) > 0 then
set tg to missing value
set kids to children of theGroup
repeat with j from 1 to count of kids
set c to item j of kids
if (name of c) is "Temp AI Files" and (type of c) is group then set tg to c
end repeat
if tg is missing value then set tg to create record with {name:"Temp AI Files", type:group} in theGroup
repeat with j from 1 to count of olds
move record (item j of olds) to tg
end repeat
end if
set newRec to import path outPath to theGroup name newName
set label of newRec to kLabel
set comment of newRec to kTitle & ": " & summary
set newURL to reference URL of newRec
end tell
display notification summary with title kTitle subtitle newName
return "OK" & linefeed & newName & linefeed & newURL & linefeed & summary & linefeed & logPath
end combineRecords
on copyIn(srcPath, workDir, prefix)
set fname to do shell script "basename " & quoted form of srcPath
set dst to workDir & prefix & " - " & fname
do shell script "cp " & quoted form of srcPath & " " & quoted form of dst
return dst
end copyIn
on safeName(s)
return do shell script "printf %s " & quoted form of s & " | tr '/:' '--'"
end safeName
on fail(msg)
display notification msg with title kTitle
if gInteractive then
tell application id "DNtp" to display alert kTitle message msg
end if
return "ERROR: " & msg
end fail
I looked into this trying to write a better script. But I noticed that it’s more complicated than I thought, for a more general case. Let’s assume three PDFs, A, B and C.
B and C shall be merged to A by appending them.
B contains one page link to A and one to C
A contains one page link to C
C contains one page link to A and one to another PDF (D) in DT, that is not merged.
To adjust the page links in B:
To adjust the page link in A:
To adjust the page links in C
All this for just 3 PDFs and a fairly simple situation. I’m not saying that it is not feasible. But I’d not volunteer to write the code for that. It would have to build a data structure first that registered the page count for each PDF in the order the documents are specified (which is, BTW, not something it can deduce from selectedRecords() or selection()). For every PDF in that structure, it would than have to check for every page link in it if it points to the very first one (nothing to do) or to one not in the list (nothing to do). In any other case, it has to add the page counts for all previous PDFs in the list to the page link’s page number and rewrite that annotation.
Now Claude knows what to do, I guess. But why stop at merging – shouldn’t the page links be fixed also when splitting the PDFs?
Those are useful points.
Perhaps this is best left as a personal script rather than a general feature. My use case is consistent but you may be right that it would be too complex to create it as a generic feature applicable to all situations,
The next release will support this.
Thanks