mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-18 03:04:07 +02:00
Handle outline with Structure Element (SE) destination
This commit is contained in:
parent
2ffd2e65dd
commit
253ce6e323
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -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
540
test/pdfs/outlines_se.pdf
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user