From 2393443e733df6d7614504b72562f8a0e3857ff3 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 6 Aug 2020 20:59:46 +0200 Subject: [PATCH] Include the `/Order` array, if available, when parsing the Optional Content configuration The `/Order` array is used to improve the display of Optional Content groups in PDF viewers, and it allows a PDF document to e.g. specify that Optional Content groups should be displayed as a (collapsable) tree-structure rather than as just a list. Note that not all available Optional Content groups must be present in the `/Order` array, and PDF viewers will often (by default) hide those toggles in the UI. To allow us to improve the UX around toggling of Optional Content groups, in the default viewer, these hidden-by-default groups are thus appended to the parsed `/Order` array under a *custom* nesting level (with `name == null`). Finally, the patch also slightly tweaks an `OptionalContentConfig` related JSDoc-comment in the API. --- src/core/obj.js | 63 ++++++++++++++++++++++++++ src/display/api.js | 6 +-- src/display/optional_content_config.js | 2 + 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/core/obj.js b/src/core/obj.js index f4b9d2e85..0e1ffbcb6 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -355,6 +355,67 @@ class Catalog { return onParsed; } + function parseOrder(refs, nestedLevels = 0) { + if (!Array.isArray(refs)) { + return null; + } + const order = []; + + for (const value of refs) { + if (isRef(value) && contentGroupRefs.includes(value)) { + parsedOrderRefs.put(value); // Handle "hidden" groups, see below. + + order.push(value.toString()); + continue; + } + // Handle nested /Order arrays (see e.g. issue 9462 and bug 1240641). + const nestedOrder = parseNestedOrder(value, nestedLevels); + if (nestedOrder) { + order.push(nestedOrder); + } + } + + if (nestedLevels > 0) { + return order; + } + const hiddenGroups = []; + for (const groupRef of contentGroupRefs) { + if (parsedOrderRefs.has(groupRef)) { + continue; + } + hiddenGroups.push(groupRef.toString()); + } + if (hiddenGroups.length) { + order.push({ name: null, order: hiddenGroups }); + } + + return order; + } + + function parseNestedOrder(ref, nestedLevels) { + if (++nestedLevels > MAX_NESTED_LEVELS) { + warn("parseNestedOrder - reached MAX_NESTED_LEVELS."); + return null; + } + const value = xref.fetchIfRef(ref); + if (!Array.isArray(value)) { + return null; + } + const nestedName = xref.fetchIfRef(value[0]); + if (typeof nestedName !== "string") { + return null; + } + const nestedOrder = parseOrder(value.slice(1), nestedLevels); + if (!nestedOrder || !nestedOrder.length) { + return null; + } + return { name: stringToPDFString(nestedName), order: nestedOrder }; + } + + const xref = this.xref, + parsedOrderRefs = new RefSet(), + MAX_NESTED_LEVELS = 10; + return { name: isString(config.get("Name")) ? stringToPDFString(config.get("Name")) @@ -367,6 +428,8 @@ class Catalog { : null, on: parseOnOff(config.get("ON")), off: parseOnOff(config.get("OFF")), + order: parseOrder(config.get("Order")), + groups: null, }; } diff --git a/src/display/api.js b/src/display/api.js index f9a295dcc..bb7b184a6 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -778,9 +778,9 @@ class PDFDocumentProxy { } /** - * @returns {Promise} A promise that is resolved - * with an {@link OptionalContentConfig} that has all the optional content - * groups, or `null` if the document does not have any. + * @returns {Promise} A promise that is resolved with + * an {@link OptionalContentConfig} that contains all the optional content + * groups (assuming that the document has any). */ getOptionalContentConfig() { return this._transport.getOptionalContentConfig(); diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 16308623d..5e14c1284 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -26,6 +26,7 @@ class OptionalContentConfig { constructor(data) { this.name = null; this.creator = null; + this._order = null; this.groups = new Map(); if (data === null) { @@ -33,6 +34,7 @@ class OptionalContentConfig { } this.name = data.name; this.creator = data.creator; + this._order = data.order; for (const group of data.groups) { this.groups.set( group.id,