mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-08-02 20:37:21 +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).
|
* 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.
|
* Helper function used to parse the contents of destination dictionaries.
|
||||||
* @param {ParseDestDictionaryParameters} params
|
* @param {ParseDestDictionaryParameters} params
|
||||||
@ -1773,6 +1872,35 @@ class Catalog {
|
|||||||
resultObj.dest = dest;
|
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
|
!sci-notation.pdf
|
||||||
!nested_outline.pdf
|
!nested_outline.pdf
|
||||||
!form_two_pages.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