diff --git a/src/core/catalog.js b/src/core/catalog.js index 9dd20e0e73be2..8024c53518abd 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -486,17 +486,17 @@ class Catalog { return shadow(this, "optionalContentConfig", null); } const groups = []; - const groupRefs = new RefSet(); + const groupsMap = new Map(); // Ensure all the optional content groups are valid. for (const groupRef of groupsData) { - if (!(groupRef instanceof Ref) || groupRefs.has(groupRef)) { + if (!(groupRef instanceof Ref) || groupsMap.has(groupRef)) { continue; } - groupRefs.put(groupRef); - - groups.push(this.#readOptionalContentGroup(groupRef)); + const group = this.#readOptionalContentGroup(groupRef); + groups.push(group); + groupsMap.set(groupRef, group); } - config = this.#readOptionalContentConfig(defaultConfig, groupRefs); + config = this.#readOptionalContentConfig(defaultConfig, groupsMap); config.groups = groups; } catch (ex) { if (ex instanceof MissingDataException) { @@ -517,6 +517,7 @@ class Catalog { print: null, view: null, }, + myRbGroups: [], }; const name = group.get("Name"); @@ -565,7 +566,7 @@ class Catalog { return obj; } - #readOptionalContentConfig(config, contentGroupRefs) { + #readOptionalContentConfig(config, contentGroupsMap) { function parseOnOff(refs) { const onParsed = []; if (Array.isArray(refs)) { @@ -573,7 +574,7 @@ class Catalog { if (!(value instanceof Ref)) { continue; } - if (contentGroupRefs.has(value)) { + if (contentGroupsMap.has(value)) { onParsed.push(value.toString()); } } @@ -588,7 +589,7 @@ class Catalog { const order = []; for (const value of refs) { - if (value instanceof Ref && contentGroupRefs.has(value)) { + if (value instanceof Ref && contentGroupsMap.has(value)) { parsedOrderRefs.put(value); // Handle "hidden" groups, see below. order.push(value.toString()); @@ -605,7 +606,7 @@ class Catalog { return order; } const hiddenGroups = []; - for (const groupRef of contentGroupRefs) { + for (const groupRef of contentGroupsMap.keys()) { if (parsedOrderRefs.has(groupRef)) { continue; } @@ -638,10 +639,39 @@ class Catalog { return { name: stringToPDFString(nestedName), order: nestedOrder }; } + function parseRBGroups(rbgrps) { + if (!Array.isArray(rbgrps)) { + return null; + } + + // iterate over RB groups (literal arrays or refs thereof) + for (const value of rbgrps) { + const rbGroup = value instanceof Ref ? xref.fetchIfRef(value) : value; + // ignore wrong (non-array) refs and empty arrays + if (!Array.isArray(rbGroup) || !rbGroup.length) { + continue; + } + + const parsedRbGroup = new Set(); + + for (const ref of rbGroup) { + if (ref instanceof Ref && contentGroupsMap.has(ref)) { + parsedRbGroup.add(ref.toString()); + // keep a record of which RB groups the current ocg belongs to + contentGroupsMap.get(ref).myRbGroups.push(parsedRbGroup); + } + } + } + + return null; + } + const xref = this.xref, parsedOrderRefs = new RefSet(), MAX_NESTED_LEVELS = 10; + parseRBGroups(config.get("RBGroups")); + return { name: typeof config.get("Name") === "string" diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 366da221230e9..26c1703368536 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -33,13 +33,14 @@ class OptionalContentGroup { #visible = true; - constructor(renderingIntent, { name, intent, usage }) { + constructor(renderingIntent, { name, intent, usage, myRbGroups }) { this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY); this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); this.name = name; this.intent = intent; this.usage = usage; + this.myRbGroups = myRbGroups; } /** @@ -229,12 +230,26 @@ class OptionalContentConfig { return true; } - setVisibility(id, visible = true) { + setVisibility(id, visible = true, preserveRB = true) { const group = this.#groups.get(id); if (!group) { warn(`Optional content group not found: ${id}`); return; } + + // if my visibility is about to be set to `true' and if I belong to one or + // more radiobutton groups, hide all other OCGs in these radiobutton groups, + // provided that radio-button state relationships are to be preserved + if (visible && group.myRbGroups.length && preserveRB) { + for (const rbGrp of group.myRbGroups) { + for (const otherId of rbGrp) { + if (otherId !== id) { + this.#groups.get(otherId)._setVisible(INTERNAL, false, true); + } + } + } + } + group._setVisible(INTERNAL, !!visible, /* userSet = */ true); this.#cachedGetHash = null; @@ -258,13 +273,13 @@ class OptionalContentConfig { } switch (operator) { case "ON": - group._setVisible(INTERNAL, true); + this.setVisibility(elem, true, preserveRB); break; case "OFF": - group._setVisible(INTERNAL, false); + this.setVisibility(elem, false, preserveRB); break; case "Toggle": - group._setVisible(INTERNAL, !group.visible); + this.setVisibility(elem, !group.visible, preserveRB); break; } } diff --git a/web/pdf_layer_viewer.js b/web/pdf_layer_viewer.js index 381030f38bf39..8962717a93c44 100644 --- a/web/pdf_layer_viewer.js +++ b/web/pdf_layer_viewer.js @@ -75,6 +75,13 @@ class PDFLayerViewer extends BaseTreeViewer { source: this, promise: Promise.resolve(this._optionalContentConfig), }); + + // update the sidebarView (other groups state may have changed + // if radio button groups are involved) + this.render({ + optionalContentConfig: this._optionalContentConfig, + pdfDocument: this._pdfDocument, + }); }; element.onclick = evt => {