[api-minor] Convert getDestinations to return data in a Map

Compared to regular Objects there's a number of advantages to using Maps:
 - They support proper iteration.
 - They have a simple way to check for the existence of data.
 - They have a simple/efficient way to check the number of elements.

If this functionality was added today, I cannot imagine that we'd choose an Object for this data.
This commit is contained in:
Jonas Jenwald 2026-07-19 12:07:08 +02:00
parent dd7e3731d1
commit 6fbd2227db
4 changed files with 77 additions and 63 deletions

View File

@ -763,15 +763,17 @@ class Catalog {
} }
get destinations() { get destinations() {
const rawDests = this.#readDests(), const dests = new Map();
dests = Object.create(null);
for (const obj of rawDests) { for (const obj of this.#readDests()) {
if (obj instanceof NameTree) { if (obj instanceof NameTree) {
for (const [key, value] of obj.getAll()) { for (const [key, value] of obj.getAll()) {
const dest = fetchDest(value); const dest = fetchDest(value);
if (dest) { if (dest) {
dests[stringToPDFString(key, /* keepEscapeSequence = */ true)] = dests.set(
dest; stringToPDFString(key, /* keepEscapeSequence = */ true),
dest
);
} }
} }
} else if (obj instanceof Dict) { } else if (obj instanceof Dict) {
@ -779,8 +781,10 @@ class Catalog {
const dest = fetchDest(value); const dest = fetchDest(value);
if (dest) { if (dest) {
// Always let the NameTree take precedence. // Always let the NameTree take precedence.
dests[stringToPDFString(key, /* keepEscapeSequence = */ true)] ||= dests.getOrInsert(
dest; stringToPDFString(key, /* keepEscapeSequence = */ true),
dest
);
} }
} }
} }
@ -791,11 +795,10 @@ class Catalog {
getDestination(id) { getDestination(id) {
// Avoid extra lookup/parsing when all destinations are already available. // Avoid extra lookup/parsing when all destinations are already available.
if (Object.hasOwn(this, "destinations")) { if (Object.hasOwn(this, "destinations")) {
return this.destinations[id] ?? null; return this.destinations.get(id) ?? null;
} }
const rawDests = this.#readDests(); for (const obj of this.#readDests()) {
for (const obj of rawDests) {
if (obj instanceof NameTree || obj instanceof Dict) { if (obj instanceof NameTree || obj instanceof Dict) {
const dest = fetchDest(obj.get(id)); const dest = fetchDest(obj.get(id));
if (dest) { if (dest) {
@ -807,13 +810,7 @@ class Catalog {
// Always fallback to checking all destinations, in order to support: // Always fallback to checking all destinations, in order to support:
// - PDF documents with out-of-order NameTrees (fixes issue 10272). // - PDF documents with out-of-order NameTrees (fixes issue 10272).
// - Destination keys that use PDFDocEncoding (fixes issue 19835). // - Destination keys that use PDFDocEncoding (fixes issue 19835).
if (rawDests.length) { return this.destinations.get(id) ?? null;
const dest = this.destinations[id];
if (dest) {
return dest;
}
}
return null;
} }
#readDests() { #readDests() {

View File

@ -1615,7 +1615,7 @@ class PDFEditor {
} }
const { destinations, pagesMap } = documentData; const { destinations, pagesMap } = documentData;
const newDestinations = (documentData.destinations = new Map()); const newDestinations = (documentData.destinations = new Map());
for (const [key, dest] of Object.entries(destinations)) { for (const [key, dest] of destinations) {
const pageRef = dest[0]; const pageRef = dest[0];
const pageData = pageRef instanceof Ref && pagesMap.get(pageRef); const pageData = pageRef instanceof Ref && pagesMap.get(pageRef);
if (!pageData) { if (!pageData) {

View File

@ -775,7 +775,7 @@ class PDFDocumentProxy {
} }
/** /**
* @returns {Promise<Object<string, Array<any>>>} A promise that is resolved * @returns {Promise<Map<string, Array<any>>>} A promise that is resolved
* with a mapping from named destinations to references. * with a mapping from named destinations to references.
* *
* This can be slow for large documents. Use `getDestination` instead. * This can be slow for large documents. Use `getDestination` instead.
@ -3085,9 +3085,7 @@ class WorkerTransport {
if (typeof id !== "string") { if (typeof id !== "string") {
return Promise.reject(new Error("Invalid destination request.")); return Promise.reject(new Error("Invalid destination request."));
} }
return this.messageHandler.sendWithPromise("GetDestination", { return this.messageHandler.sendWithPromise("GetDestination", { id });
id,
});
} }
getPageLabels() { getPageLabels() {

View File

@ -1467,9 +1467,11 @@ describe("api", function () {
it("gets destinations, from /Dests dictionary", async function () { it("gets destinations, from /Dests dictionary", async function () {
const destinations = await pdfDocument.getDestinations(); const destinations = await pdfDocument.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
chapter1: [{ gen: 0, num: 17 }, { name: "XYZ" }, 0, 841.89, null], new Map([
}); ["chapter1", [{ gen: 0, num: 17 }, { name: "XYZ" }, 0, 841.89, null]],
])
);
}); });
it("gets a destination, from /Dests dictionary", async function () { it("gets a destination, from /Dests dictionary", async function () {
@ -1494,10 +1496,12 @@ describe("api", function () {
const loadingTask = getDocument(buildGetDocumentParams("issue6204.pdf")); const loadingTask = getDocument(buildGetDocumentParams("issue6204.pdf"));
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const destinations = await pdfDoc.getDestinations(); const destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
"Page.1": [{ num: 1, gen: 0 }, { name: "XYZ" }, 0, 375, null], new Map([
"Page.2": [{ num: 6, gen: 0 }, { name: "XYZ" }, 0, 375, null], ["Page.1", [{ num: 1, gen: 0 }, { name: "XYZ" }, 0, 375, null]],
}); ["Page.2", [{ num: 6, gen: 0 }, { name: "XYZ" }, 0, 375, null]],
])
);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -1506,11 +1510,13 @@ describe("api", function () {
const loadingTask = getDocument(buildGetDocumentParams("issue19474.pdf")); const loadingTask = getDocument(buildGetDocumentParams("issue19474.pdf"));
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const destinations = await pdfDoc.getDestinations(); const destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
A: [{ num: 1, gen: 0 }, { name: "Fit" }], new Map([
B: [{ num: 4, gen: 0 }, { name: "Fit" }], ["A", [{ num: 1, gen: 0 }, { name: "Fit" }]],
C: [{ num: 5, gen: 0 }, { name: "Fit" }], ["B", [{ num: 4, gen: 0 }, { name: "Fit" }]],
}); ["C", [{ num: 5, gen: 0 }, { name: "Fit" }]],
])
);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -6418,7 +6424,10 @@ small scripts as well as for`);
expect(annotations.length).toEqual(2); expect(annotations.length).toEqual(2);
expect(annotations[0].dest).toEqual("foo"); expect(annotations[0].dest).toEqual("foo");
expect(annotations[1].unsafeUrl).toEqual("other.pdf#target"); expect(annotations[1].unsafeUrl).toEqual("other.pdf#target");
expect(Object.keys(await pdfDoc.getDestinations())).toEqual(["foo"]);
const destinations = await pdfDoc.getDestinations();
expect([...destinations.keys()]).toEqual(["foo"]);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -6435,7 +6444,8 @@ small scripts as well as for`);
]; ];
let loadingTask = getDocument({ data: assemblePdf(objects) }); let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise; let pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getDestinations())) let destinations = await pdfDoc.getDestinations();
expect([...destinations.keys()])
.withContext("before extraction") .withContext("before extraction")
.toEqual(["名"]); .toEqual(["名"]);
const data = await pdfDoc.extractPages([{ document: null }]); const data = await pdfDoc.extractPages([{ document: null }]);
@ -6443,7 +6453,8 @@ small scripts as well as for`);
loadingTask = getDocument({ data }); loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
expect(Object.keys(await pdfDoc.getDestinations())) destinations = await pdfDoc.getDestinations();
expect([...destinations.keys()])
.withContext("after extraction") .withContext("after extraction")
.toEqual(["名"]); .toEqual(["名"]);
await loadingTask.destroy(); await loadingTask.destroy();
@ -6456,7 +6467,7 @@ small scripts as well as for`);
let pdfDoc = await loadingTask.promise; let pdfDoc = await loadingTask.promise;
let destinations = await pdfDoc.getDestinations(); let destinations = await pdfDoc.getDestinations();
expect(Object.keys(destinations).sort()).toEqual(["foo", "foo_p2"]); expect([...destinations.keys()].sort()).toEqual(["foo", "foo_p2"]);
const data = await pdfDoc.extractPages([ const data = await pdfDoc.extractPages([
{ document: null }, { document: null },
@ -6468,7 +6479,7 @@ small scripts as well as for`);
pdfDoc = await loadingTask.promise; pdfDoc = await loadingTask.promise;
destinations = await pdfDoc.getDestinations(); destinations = await pdfDoc.getDestinations();
expect(Object.keys(destinations).sort()).toEqual([ expect([...destinations.keys()].sort()).toEqual([
"foo", "foo",
"foo_p2", "foo_p2",
"foo_p2_1", "foo_p2_1",
@ -6489,10 +6500,12 @@ small scripts as well as for`);
let pdfDoc = await loadingTask.promise; let pdfDoc = await loadingTask.promise;
let pagesRef = await getPageRefs(pdfDoc); let pagesRef = await getPageRefs(pdfDoc);
let destinations = await pdfDoc.getDestinations(); let destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
"Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], new Map([
"Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], ["Page.1", [pagesRef[0], { name: "XYZ" }, 0, 375, null]],
}); ["Page.2", [pagesRef[1], { name: "XYZ" }, 0, 375, null]],
])
);
let data = await pdfDoc.extractPages([ let data = await pdfDoc.extractPages([
{ document: null }, { document: null },
@ -6507,12 +6520,14 @@ small scripts as well as for`);
pagesRef = await getPageRefs(pdfDoc); pagesRef = await getPageRefs(pdfDoc);
destinations = await pdfDoc.getDestinations(); destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
"Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], new Map([
"Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], ["Page.1", [pagesRef[0], { name: "XYZ" }, 0, 375, null]],
"Page.1_p3": [pagesRef[2], { name: "XYZ" }, 0, 375, null], ["Page.2", [pagesRef[1], { name: "XYZ" }, 0, 375, null]],
"Page.2_p4": [pagesRef[3], { name: "XYZ" }, 0, 375, null], ["Page.1_p3", [pagesRef[2], { name: "XYZ" }, 0, 375, null]],
}); ["Page.2_p4", [pagesRef[3], { name: "XYZ" }, 0, 375, null]],
])
);
const expectedDests = ["Page.2", "Page.1", "Page.2_p4", "Page.1_p3"]; const expectedDests = ["Page.2", "Page.1", "Page.2_p4", "Page.1_p3"];
for (let i = 1; i <= 4; i++) { for (let i = 1; i <= 4; i++) {
const pdfPage = await pdfDoc.getPage(i); const pdfPage = await pdfDoc.getPage(i);
@ -6534,16 +6549,18 @@ small scripts as well as for`);
pagesRef = await getPageRefs(pdfDoc); pagesRef = await getPageRefs(pdfDoc);
destinations = await pdfDoc.getDestinations(); destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
"Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], new Map([
"Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], ["Page.1", [pagesRef[0], { name: "XYZ" }, 0, 375, null]],
"Page.1_p3": [pagesRef[2], { name: "XYZ" }, 0, 375, null], ["Page.2", [pagesRef[1], { name: "XYZ" }, 0, 375, null]],
"Page.2_p4": [pagesRef[3], { name: "XYZ" }, 0, 375, null], ["Page.1_p3", [pagesRef[2], { name: "XYZ" }, 0, 375, null]],
"Page.1_p5": [pagesRef[4], { name: "XYZ" }, 0, 375, null], ["Page.2_p4", [pagesRef[3], { name: "XYZ" }, 0, 375, null]],
"Page.2_p6": [pagesRef[5], { name: "XYZ" }, 0, 375, null], ["Page.1_p5", [pagesRef[4], { name: "XYZ" }, 0, 375, null]],
"Page.1_p3_p7": [pagesRef[6], { name: "XYZ" }, 0, 375, null], ["Page.2_p6", [pagesRef[5], { name: "XYZ" }, 0, 375, null]],
"Page.2_p4_p8": [pagesRef[7], { name: "XYZ" }, 0, 375, null], ["Page.1_p3_p7", [pagesRef[6], { name: "XYZ" }, 0, 375, null]],
}); ["Page.2_p4_p8", [pagesRef[7], { name: "XYZ" }, 0, 375, null]],
])
);
expectedDests.push( expectedDests.push(
"Page.2_p6", "Page.2_p6",
"Page.1_p5", "Page.1_p5",
@ -6575,10 +6592,12 @@ small scripts as well as for`);
const pagesRef = await getPageRefs(pdfDoc); const pagesRef = await getPageRefs(pdfDoc);
const destinations = await pdfDoc.getDestinations(); const destinations = await pdfDoc.getDestinations();
expect(destinations).toEqual({ expect(destinations).toEqual(
"Page.1": [pagesRef[0], { name: "XYZ" }, 0, 375, null], new Map([
"Page.2": [pagesRef[1], { name: "XYZ" }, 0, 375, null], ["Page.1", [pagesRef[0], { name: "XYZ" }, 0, 375, null]],
}); ["Page.2", [pagesRef[1], { name: "XYZ" }, 0, 375, null]],
])
);
const pdfPage = await pdfDoc.getPage(3); const pdfPage = await pdfDoc.getPage(3);
const annots = await pdfPage.getAnnotations(); const annots = await pdfPage.getAnnotations();
expect(annots.length).toEqual(0); expect(annots.length).toEqual(0);