diff --git a/src/core/catalog.js b/src/core/catalog.js index 9dd20e0e73be2..88dd44aa2ec75 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 groupRefCache = new RefSetCache(); // Ensure all the optional content groups are valid. for (const groupRef of groupsData) { - if (!(groupRef instanceof Ref) || groupRefs.has(groupRef)) { + if (!(groupRef instanceof Ref) || groupRefCache.has(groupRef)) { continue; } - groupRefs.put(groupRef); - - groups.push(this.#readOptionalContentGroup(groupRef)); + const group = this.#readOptionalContentGroup(groupRef); + groups.push(group); + groupRefCache.put(groupRef, group); } - config = this.#readOptionalContentConfig(defaultConfig, groupRefs); + config = this.#readOptionalContentConfig(defaultConfig, groupRefCache); config.groups = groups; } catch (ex) { if (ex instanceof MissingDataException) { @@ -517,6 +517,7 @@ class Catalog { print: null, view: null, }, + rbGroups: [], }; const name = group.get("Name"); @@ -565,7 +566,7 @@ class Catalog { return obj; } - #readOptionalContentConfig(config, contentGroupRefs) { + #readOptionalContentConfig(config, groupRefCache) { 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 (groupRefCache.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 && groupRefCache.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 groupRefCache.items()) { if (parsedOrderRefs.has(groupRef)) { continue; } @@ -642,6 +643,32 @@ class Catalog { parsedOrderRefs = new RefSet(), MAX_NESTED_LEVELS = 10; + // Parse RBGroups entry. + (rbGroups => { + if (!Array.isArray(rbGroups)) { + return null; + } + + for (const value of rbGroups) { + const rbGroup = xref.fetchIfRef(value); + if (!Array.isArray(rbGroup) || !rbGroup.length) { + continue; + } + + const parsedRbGroup = new Set(); + + for (const ref of rbGroup) { + if (ref instanceof Ref && groupRefCache.has(ref)) { + parsedRbGroup.add(ref.toString()); + // Keep a record of which RB groups the current OCG belongs to. + groupRefCache.get(ref).rbGroups.push(parsedRbGroup); + } + } + } + + return null; + })(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..77c833b8dfa93 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, rbGroups }) { this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY); this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); this.name = name; this.intent = intent; this.usage = usage; + this.rbGroups = rbGroups; } /** @@ -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 radiobutton state relationships are to be preserved. + if (visible && preserveRB && group.rbGroups.length) { + for (const rbGrp of group.rbGroups) { + 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/test/pdfs/.gitignore b/test/pdfs/.gitignore index fa61a789c7d19..3126d12cff6ab 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -589,6 +589,7 @@ !issue15690.pdf !bug1802888.pdf !issue15759.pdf +!issue18823.pdf !issue15753.pdf !issue15789.pdf !fields_order.pdf diff --git a/test/pdfs/issue18823.pdf b/test/pdfs/issue18823.pdf new file mode 100644 index 0000000000000..fcf623bf64930 Binary files /dev/null and b/test/pdfs/issue18823.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index d9f04533552a9..936f8cf6e359c 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6875,6 +6875,23 @@ "7R": false } }, + { + "id": "issue18823-default", + "file": "pdfs/issue18823.pdf", + "md5": "f5246c476516c96df106ced0c5839da3", + "rounds": 1, + "type": "eq" + }, + { + "id": "issue18823-group-three", + "file": "pdfs/issue18823.pdf", + "md5": "f5246c476516c96df106ced0c5839da3", + "rounds": 1, + "type": "eq", + "optionalContent": { + "15R": true + } + }, { "id": "issue2829", "file": "pdfs/issue2829.pdf",