From 8070e9dce19b04f059fe8a3f6af47a3aaef3c322 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 13 Jul 2026 11:05:51 +0200 Subject: [PATCH] Preserve OBJR children without restoring deleted pages Skip OBJR entries whose targets were not copied with a retained page. --- src/core/editor/pdf_editor.js | 9 +++- test/unit/api_spec.js | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index afe48de50..181039bd5 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -620,10 +620,15 @@ class PDFEditor { if (!kidRef) { continue; } - const newKidRef = oldRefMapping.get(kidRef); - if (!newKidRef) { + // Only keep the reference when its target was actually copied. A link + // annotation targeting a removed page is dropped, so skip its OBJR. + const oldObjRef = kid.getRaw("Obj"); + if (oldObjRef instanceof Ref && !oldRefMapping.get(oldObjRef)) { continue; } + const newKidRef = + oldRefMapping.get(kidRef) || + (await this.#collectDependencies(kidRef, true, xref)); const newKid = this.xref[newKidRef.num]; // Fix the missing StructParent entry in the referenced object. const objRef = newKid.getRaw("Obj"); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 44696e12d..268001558 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -17,6 +17,7 @@ import { AnnotationEditorType, AnnotationMode, AnnotationType, + bytesToString, DrawOPS, ImageKind, InvalidPDFException, @@ -6718,6 +6719,88 @@ small scripts as well as for`); await loadingTask.destroy(); }); + it("preserves OBJR structure children", async function () { + const objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R " + + "/StructTreeRoot 4 0 R >>\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 /StructParents 0 " + + "/MediaBox [0 0 100 100] /Annots [8 0 R] >>\nendobj\n", + "4 0 obj\n<< /Type /StructTreeRoot /K [5 0 R] " + + "/ParentTree 7 0 R /ParentTreeNextKey 2 >>\nendobj\n", + "5 0 obj\n<< /Type /StructElem /S /Document /P 4 0 R " + + "/K 6 0 R >>\nendobj\n", + "6 0 obj\n<< /Type /StructElem /S /Link /P 5 0 R /Pg 3 0 R " + + "/K 9 0 R >>\nendobj\n", + "7 0 obj\n<< /Nums [0 [6 0 R] 1 6 0 R] >>\nendobj\n", + "8 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " + + "/StructParent 1 >>\nendobj\n", + "9 0 obj\n<< /Type /OBJR /Obj 8 0 R /Pg 3 0 R >>\nendobj\n", + ]; + + let loadingTask = getDocument({ data: assemblePdf(objects) }); + let pdfDoc = await loadingTask.promise; + let page = await pdfDoc.getPage(1); + let tree = await page.getStructTree(); + expect(tree.children[0].children[0].children[0].type).toEqual( + "annotation" + ); + const data = await pdfDoc.extractPages([{ document: null }]); + await loadingTask.destroy(); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + page = await pdfDoc.getPage(1); + tree = await page.getStructTree(); + expect(tree.children.length).toEqual(1); + expect(tree.children[0].role).toEqual("Document"); + expect(tree.children[0].children[0].role).toEqual("Link"); + expect(tree.children[0].children[0].children[0].type).toEqual( + "annotation" + ); + await loadingTask.destroy(); + }); + + it("doesn't resurrect removed pages referenced through an OBJR", async function () { + const objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R " + + "/StructTreeRoot 4 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Kids [3 0 R 10 0 R] " + + "/Count 2 >>\nendobj\n", + "3 0 obj\n<< /Type /Page /Parent 2 0 R /StructParents 0 " + + "/MediaBox [0 0 100 100] /Annots [8 0 R] >>\nendobj\n", + "4 0 obj\n<< /Type /StructTreeRoot /K [5 0 R] " + + "/ParentTree 7 0 R /ParentTreeNextKey 2 >>\nendobj\n", + "5 0 obj\n<< /Type /StructElem /S /Document /P 4 0 R " + + "/K 6 0 R >>\nendobj\n", + "6 0 obj\n<< /Type /StructElem /S /Link /P 5 0 R /Pg 3 0 R " + + "/K 9 0 R >>\nendobj\n", + "7 0 obj\n<< /Nums [0 [6 0 R] 1 6 0 R] >>\nendobj\n", + "8 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " + + "/StructParent 1 /A << /S /GoTo /D [10 0 R /Fit] >> >>\nendobj\n", + "9 0 obj\n<< /Type /OBJR /Obj 8 0 R /Pg 3 0 R >>\nendobj\n", + "10 0 obj\n<< /Type /Page /Parent 2 0 R " + + "/MediaBox [0 0 100 100] >>\nendobj\n", + ]; + + let loadingTask = getDocument({ data: assemblePdf(objects) }); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([ + { document: null, includePages: [0] }, + ]); + await loadingTask.destroy(); + + expect(data).toBeInstanceOf(Uint8Array); + const output = bytesToString(data); + const pageObjects = output.match(/\/Type\s*\/Page(?![A-Za-z])/g) || []; + expect(pageObjects.length).toEqual(1); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(1); + await loadingTask.destroy(); + }); + it("extract pages and merge struct trees", async function () { let loadingTask = getDocument( buildGetDocumentParams("two_paragraphs.pdf")