From a36e5acb3053f17470063f0d64a69496c2cb19da Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 13 Jul 2026 10:51:08 +0200 Subject: [PATCH] Preserve non-local PDF link destinations --- src/core/editor/pdf_editor.js | 11 +++++++-- test/unit/api_spec.js | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 01d4b7267..9c58301ff 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -1222,6 +1222,13 @@ class PDFEditor { return; } const action = annotationDict.get("A"); + if (action instanceof Dict && !isName(action.get("S"), "GoTo")) { + // Only GoTo actions point to pages in the current document. Other + // actions, such as GoToR, must not be filtered using the current + // document's page map. + newAnnotations[newAnnotationIndex] = annotationRef; + return; + } const dest = action instanceof Dict ? action.get("D") @@ -1233,9 +1240,9 @@ class PDFEditor { ) { // Keep the annotation as is: it isn't linking to a deleted page. newAnnotations[newAnnotationIndex] = annotationRef; - } else if (typeof dest === "string") { + } else if (dest instanceof Name || typeof dest === "string") { const destString = stringToPDFString( - dest, + dest instanceof Name ? dest.name : dest, /* keepEscapeSequence = */ true ); if (destinations.has(destString)) { diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index eb117ab29..ba9c9a8ce 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -6392,6 +6392,49 @@ small scripts as well as for`); }); describe("Named destinations", function () { + it("preserves name destinations and remote actions", async function () { + const objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R " + + "/Dests << /foo [3 0 R /Fit] >> >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n", + "3 0 obj\n<< /Type /Page /Parent 2 0 R " + + "/MediaBox [0 0 100 100] /Annots [4 0 R 5 0 R] >>\nendobj\n", + "4 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " + + "/Dest /foo >>\nendobj\n", + "5 0 obj\n<< /Type /Annot /Subtype /Link /Rect [10 0 20 10] " + + "/A << /S /GoToR /F (other.pdf) /D (target) >> >>\nendobj\n", + ]; + let pdfData = "%PDF-1.7\n"; + const offsets = []; + for (const object of objects) { + offsets.push(pdfData.length); + pdfData += object; + } + const xrefOffset = pdfData.length; + pdfData += `xref\n0 ${objects.length + 1}\n`; + pdfData += "0000000000 65535 f \n"; + for (const offset of offsets) { + pdfData += `${offset.toString().padStart(10, "0")} 00000 n \n`; + } + pdfData += + `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n` + + `startxref\n${xrefOffset}\n%%EOF\n`; + + let loadingTask = getDocument({ data: stringToBytes(pdfData) }); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([{ document: null }]); + await loadingTask.destroy(); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + const annotations = await (await pdfDoc.getPage(1)).getAnnotations(); + expect(annotations.length).toEqual(2); + expect(annotations[0].dest).toEqual("foo"); + expect(annotations[1].unsafeUrl).toEqual("other.pdf#target"); + expect(Object.keys(await pdfDoc.getDestinations())).toEqual(["foo"]); + await loadingTask.destroy(); + }); + it("keeps colliding deduplicated destination names unique", async function () { let loadingTask = getDocument( buildGetDocumentParams("named_dest_collision_for_editor.pdf")