Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.");
}
}
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions test/integration/viewer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
})
);
});
});
});
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -879,3 +879,4 @@
!sci-notation.pdf
!nested_outline.pdf
!form_two_pages.pdf
!outlines_se.pdf
Binary file added test/pdfs/outlines_se.pdf
Binary file not shown.