Skip to content

Commit 4c55b7d

Browse files
committed
fix: bookmarks.js silently destroyed a PDF's existing bookmarks when adding a new one
Add Bookmarks always rebuilt /Outlines from scratch containing only what the user typed in the current session, never reading the PDF's own pre-existing outline first. A user adding one bookmark to an already-bookmarked PDF (e.g. a Word/InDesign export with a heading-based TOC) silently lost every existing bookmark in the output, with zero warning. Fixed by reading the source PDF's top-level /Outlines linked list at load time (same resolution logic as split-by-bookmarks.js's readTopLevelOutline) and pre-populating the bookmarks queue with it, so existing entries are preserved unless the user explicitly removes them via the same Remove button used for newly-added ones. Verified against the real vendored pdf-lib.js: reproduced the original bug (2 existing bookmarks -> 1 after adding one new one), then confirmed the fix (3 bookmarks survive), plus a no-existing-outline PDF still loads with an empty queue and no throw. Also reworded the success message from "N bookmark(s) added" to "N bookmark(s) saved", since the count can now include preserved pre-existing entries, not just ones typed this session.
1 parent 8933fb2 commit 4c55b7d

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

bookmarks.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,59 @@ function updateActions() {
102102
saveBtn.disabled = bookmarks.length === 0;
103103
}
104104

105+
/**
106+
* Reads the PDF's own top-level /Outlines linked list, if any, so pre-existing
107+
* bookmarks aren't silently destroyed when the user adds a new one — addOutline()
108+
* below always rebuilds /Outlines from scratch, so whatever isn't in `bookmarks`
109+
* at save time is gone. Only direct [pageRef, ...] /Dest arrays are resolved
110+
* (not named destinations via /Names, and not /A GoTo actions) — an entry that
111+
* can't be resolved to a page is dropped rather than guessed at.
112+
*/
113+
async function readExistingBookmarks(doc) {
114+
const { PDFName } = await getPdfLib();
115+
const context = doc.context;
116+
const outlinesRef = doc.catalog.get(PDFName.of("Outlines"));
117+
if (!outlinesRef) return [];
118+
const outlines = context.lookup(outlinesRef);
119+
if (!outlines) return [];
120+
121+
const pageRefs = doc.getPages().map((p) => p.ref);
122+
function resolvePage(dest) {
123+
if (!dest || typeof dest.get !== "function") return -1;
124+
try {
125+
const pageRef = dest.get(0);
126+
if (!pageRef) return -1;
127+
return pageRefs.findIndex(
128+
(r) => r.tag === pageRef.tag && r.objectNumber === pageRef.objectNumber
129+
);
130+
} catch (e) {
131+
return -1;
132+
}
133+
}
134+
135+
const items = [];
136+
let cur = outlines.get(PDFName.of("First"));
137+
let guard = 0;
138+
while (cur && guard++ < 10000) {
139+
const item = context.lookup(cur);
140+
if (!item) break;
141+
const titleObj = item.get(PDFName.of("Title"));
142+
const title = titleObj && titleObj.decodeText ? titleObj.decodeText() : "";
143+
const dest = item.get(PDFName.of("Dest"));
144+
const pageIndex = resolvePage(dest);
145+
if (title && pageIndex >= 0) items.push({ title, page: pageIndex + 1 });
146+
cur = item.get(PDFName.of("Next"));
147+
}
148+
return items;
149+
}
150+
105151
async function loadFile(file) {
106152
try {
107153
const { PDFDocument } = await getPdfLib();
108154
const bytes = await file.arrayBuffer();
109155
const doc = await PDFDocument.load(bytes, { ignoreEncryption: true });
110156
loaded = { file, pageCount: doc.getPageCount() };
111-
bookmarks = [];
157+
bookmarks = await readExistingBookmarks(doc).catch(() => []);
112158
renderFile();
113159
renderBookmarks();
114160
updateActions();
@@ -291,7 +337,7 @@ saveBtn.addEventListener("click", async () => {
291337
resultEl.setAttribute("role", "status");
292338
resultEl.setAttribute("aria-live", "polite");
293339
resultEl.innerHTML = `
294-
<span><strong>Done.</strong> ${count} bookmark${count === 1 ? "" : "s"} added
340+
<span><strong>Done.</strong> ${count} bookmark${count === 1 ? "" : "s"} saved
295341
${elapsedMs}ms, ${requestsDuring} network requests, entirely on this device.</span>
296342
<a class="btn btn-primary" href="${url}" download="${escapeHtml(fileName)}">Download ${escapeHtml(fileName)}</a>
297343
`;

0 commit comments

Comments
 (0)