diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 0be18cd95..afe48de50 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -530,6 +530,14 @@ class PDFEditor { return ref; } + async #resolveStructKids(rawKids, xref) { + if (rawKids instanceof Ref) { + const fetched = await xref.fetchAsync(rawKids); + return Array.isArray(fetched) ? fetched : [rawKids]; + } + return Array.isArray(rawKids) ? rawKids : [rawKids]; + } + async #cloneStructTreeNode( parentStructRef, node, @@ -547,19 +555,11 @@ class PDFEditor { if (pg instanceof Ref && !pagesMap.has(pg)) { return null; } - let kids; - const k = (kids = node.getRaw("K")); - if (k instanceof Ref) { - // We're only interested by ref referencing nodes and not an array. - if (visited.has(k)) { - return null; - } - kids = await xref.fetchAsync(k); - if (!Array.isArray(kids)) { - kids = [k]; - } + const k = node.getRaw("K"); + if (k instanceof Ref && visited.has(k)) { + return null; } - kids = Array.isArray(kids) ? kids : [kids]; + const kids = await this.#resolveStructKids(k, xref); const newKids = []; const structElemIndices = []; for (let kid of kids) { @@ -1523,17 +1523,24 @@ class PDFEditor { } // Get the kids. - let kids = structTreeRoot.dict.get("K"); - if (!kids) { + const rawKids = structTreeRoot.dict.getRaw("K"); + if (!rawKids) { continue; } - kids = Array.isArray(kids) ? kids : [kids]; + const kids = await this.#resolveStructKids(rawKids, xref); for (let kid of kids) { const kidRef = kid instanceof Ref ? kid : null; - if (kidRef && removedStructElements.has(kidRef)) { + kid = await xref.fetchIfRefAsync(kid); + if (!(kid instanceof Dict)) { continue; } - kid = await xref.fetchIfRefAsync(kid); + let setAsSpan = false; + if (kidRef && removedStructElements.has(kidRef)) { + if (!isName(kid.get("S"), "Link")) { + continue; + } + setAsSpan = true; + } const newKidRef = await this.#cloneStructTreeNode( kidRef, kid, @@ -1545,6 +1552,12 @@ class PDFEditor { ); if (newKidRef) { structTreeKids.push(newKidRef); + if (kidRef) { + oldRefMapping.put(kidRef, newKidRef); + } + if (setAsSpan) { + this.xref[newKidRef.num].setIfName("S", "Span"); + } } } diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 5afe735f9..44696e12d 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -6622,6 +6622,102 @@ small scripts as well as for`); }); describe("Struct trees", function () { + it("preserves an indirect StructTreeRoot kid", 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] >>\nendobj\n", + "4 0 obj\n<< /Type /StructTreeRoot /K 5 0 R " + + "/ParentTree 6 0 R /ParentTreeNextKey 1 >>\nendobj\n", + "5 0 obj\n<< /Type /StructElem /S /P /P 4 0 R /Pg 3 0 R " + + "/K 0 >>\nendobj\n", + "6 0 obj\n<< /Nums [0 [5 0 R]] >>\nendobj\n", + ]; + + let loadingTask = getDocument({ data: assemblePdf(objects) }); + let pdfDoc = await loadingTask.promise; + let page = await pdfDoc.getPage(1); + expect((await page.getStructTree()).children[0].role).toEqual("P"); + const data = await pdfDoc.extractPages([{ document: null }]); + await loadingTask.destroy(); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + page = await pdfDoc.getPage(1); + const tree = await page.getStructTree(); + expect(tree.children.length).toEqual(1); + expect(tree.children[0].role).toEqual("P"); + expect(tree.children[0].children[0].type).toEqual("content"); + await loadingTask.destroy(); + }); + + it("extracts pages when the StructTreeRoot /K is dangling", 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 " + + "/MediaBox [0 0 100 100] >>\nendobj\n", + "4 0 obj\n<< /Type /StructTreeRoot /K 99 0 R >>\nendobj\n", + ]; + + let loadingTask = getDocument({ data: assemblePdf(objects) }); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([{ document: null }]); + expect(data).not.toBeNull(); + await loadingTask.destroy(); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(1); + const page = await pdfDoc.getPage(1); + expect((await page.getStructTree())?.children.length ?? 0).toEqual(0); + await loadingTask.destroy(); + }); + + it("preserves a root Link when its annotation is removed", async function () { + const objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R " + + "/StructTreeRoot 6 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Kids [3 0 R 4 0 R] " + + "/Count 2 >>\nendobj\n", + "3 0 obj\n<< /Type /Page /Parent 2 0 R /StructParents 0 " + + "/Annots [5 0 R] /MediaBox [0 0 100 100] >>\nendobj\n", + "4 0 obj\n<< /Type /Page /Parent 2 0 R " + + "/MediaBox [0 0 100 100] >>\nendobj\n", + "5 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " + + "/StructParent 1 /Dest [4 0 R /Fit] >>\nendobj\n", + "6 0 obj\n<< /Type /StructTreeRoot /K 7 0 R " + + "/ParentTree 8 0 R /ParentTreeNextKey 2 >>\nendobj\n", + "7 0 obj\n<< /Type /StructElem /S /Link /P 6 0 R /Pg 3 0 R " + + "/K 0 >>\nendobj\n", + "8 0 obj\n<< /Nums [0 [7 0 R] 1 7 0 R] >>\nendobj\n", + ]; + + let loadingTask = getDocument({ data: assemblePdf(objects) }); + let pdfDoc = await loadingTask.promise; + let page = await pdfDoc.getPage(1); + expect((await page.getStructTree()).children[0].role).toEqual("Link"); + + const data = await pdfDoc.extractPages([ + { document: null, excludePages: [1] }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument({ data }); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(1); + page = await pdfDoc.getPage(1); + expect((await page.getAnnotations()).length).toEqual(0); + const tree = await page.getStructTree(); + expect(tree.children.length).toEqual(1); + expect(tree.children[0].role).toEqual("Span"); + expect(tree.children[0].children[0].type).toEqual("content"); + await loadingTask.destroy(); + }); + it("extract pages and merge struct trees", async function () { let loadingTask = getDocument( buildGetDocumentParams("two_paragraphs.pdf")