Handle outline with Structure Element (SE) destination

This commit is contained in:
calixteman 2026-03-08 12:27:51 +01:00
parent 2ffd2e65dd
commit 253ce6e323
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
4 changed files with 710 additions and 0 deletions

View File

@ -1542,6 +1542,105 @@ class Catalog {
* exist in most PDF documents).
*/
/**
* Derive a destination array from a Structure Element reference.
* Walks the SE dict to find its page (Pg) and optional bounding box (A.BBox),
* then returns an XYZ destination array that can be used for navigation.
* @param {XRef} xref
* @param {Ref} seRef
* @returns {Array|null}
*/
static #getDestFromStructElement(xref, seRef) {
const seDict = xref.fetchIfRef(seRef);
if (!(seDict instanceof Dict)) {
return null;
}
// Try to find the page reference for this structure element.
// Search order: the element itself, its descendants down to leaf nodes,
// then ancestor elements via the P entry (up).
let pageRef = null;
// Check the element directly.
const directPg = seDict.getRaw("Pg");
if (directPg instanceof Ref) {
pageRef = directPg;
}
// Walk down into descendants (BFS) until a Pg is found or leaves are
// reached (e.g. integer MCIDs or MCR/OBJR dicts without further K).
if (!pageRef) {
const queue = [seDict];
while (queue.length > 0 && !pageRef) {
const node = queue.shift();
const kids = node.get("K");
let kidsArr;
if (Array.isArray(kids)) {
kidsArr = kids;
} else if (kids) {
kidsArr = [kids];
} else {
kidsArr = [];
}
for (const kid of kidsArr) {
const kidObj = xref.fetchIfRef(kid);
if (!(kidObj instanceof Dict)) {
continue; // integer MCID leaf node, no Pg here
}
const pg = kidObj.getRaw("Pg");
if (pg instanceof Ref) {
pageRef = pg;
break;
}
queue.push(kidObj);
}
}
}
// Walk up the parent chain if still not found.
if (!pageRef) {
const MAX_DEPTH = 40;
let current = seDict;
for (let depth = 0; depth < MAX_DEPTH; depth++) {
const parentRaw = current.getRaw("P");
if (!(parentRaw instanceof Ref)) {
break;
}
const parentDict = xref.fetchIfRef(parentRaw);
if (!(parentDict instanceof Dict)) {
break;
}
if (isName(parentDict.get("Type"), "StructTreeRoot")) {
break;
}
const pg = parentDict.getRaw("Pg");
if (pg instanceof Ref) {
pageRef = pg;
break;
}
current = parentDict;
}
}
if (!pageRef) {
return null;
}
// Try to obtain precise coordinates from the element's attribute BBox.
let x = null,
y = null;
const attrs = seDict.get("A");
if (attrs instanceof Dict) {
const bboxArr = attrs.getArray("BBox");
if (isNumberArray(bboxArr, 4)) {
x = bboxArr[0];
y = bboxArr[3]; // top of the bbox in PDF page coordinates
}
}
return [pageRef, { name: "XYZ" }, x, y, null];
}
/**
* Helper function used to parse the contents of destination dictionaries.
* @param {ParseDestDictionaryParameters} params
@ -1773,6 +1872,35 @@ class Catalog {
resultObj.dest = dest;
}
}
// Handle SE (Structure Element) entry: when no other destination has been
// found, derive one from the structure element's page and optional bbox.
if (
!resultObj.dest &&
!resultObj.url &&
!resultObj.action &&
!resultObj.attachment &&
!resultObj.setOCGState &&
!resultObj.resetForm
) {
const seRef = destDict.getRaw("SE");
if (seRef instanceof Ref) {
try {
const seDest = Catalog.#getDestFromStructElement(
destDict.xref,
seRef
);
if (seDest) {
resultObj.dest = seDest;
}
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
info("SE parsing failed.");
}
}
}
}
}

View File

@ -1646,4 +1646,45 @@ describe("PDF viewer", () => {
);
});
});
describe("Outline with SE (Structure Element) entries", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"outlines_se.pdf",
`.page[data-page-number="1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("should navigate to the correct page when clicking an outline item with an SE entry", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Open the sidebar.
await showViewsManager(page);
// Switch to the outline view.
await page.click("#viewsManagerSelectorButton");
await page.waitForSelector("#outlinesViewMenu", { visible: true });
await page.click("#outlinesViewMenu");
for (let i = 2; i >= 1; i--) {
await waitAndClick(
page,
`#outlinesView .treeItem .treeItem:nth-child(${i}) a`
);
await page.waitForFunction(
pageNum => window.PDFViewerApplication.page === pageNum,
{},
i
);
}
})
);
});
});
});

View File

@ -879,3 +879,4 @@
!sci-notation.pdf
!nested_outline.pdf
!form_two_pages.pdf
!outlines_se.pdf

540
test/pdfs/outlines_se.pdf Normal file

File diff suppressed because one or more lines are too long