Skip to content

Commit

Permalink
Optional Content (OC) radiobutton (RB) groups implemented. Resolves #…
Browse files Browse the repository at this point in the history
…18823.

The code parses the /RBGroups entry in the OC configuration dict and adds the property `rbGroups' to instances of the OptionalContentGroup class. rbGroups takes an array of Sets, where each Set instance represents an RB group the OptionalContentGroup instance is a member of. Such a Set instance contains all OCG ids within the corresponding RB group. RB groups an OCG is associated with are processed when its visibility is set to true, as required by the PDF spec.
  • Loading branch information
agrahn committed Oct 2, 2024
1 parent 567df42 commit c0f14a0
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 17 deletions.
51 changes: 39 additions & 12 deletions src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -517,6 +517,7 @@ class Catalog {
print: null,
view: null,
},
rbGroups: [],
};

const name = group.get("Name");
Expand Down Expand Up @@ -565,15 +566,15 @@ class Catalog {
return obj;
}

#readOptionalContentConfig(config, contentGroupRefs) {
#readOptionalContentConfig(config, groupRefCache) {
function parseOnOff(refs) {
const onParsed = [];
if (Array.isArray(refs)) {
for (const value of refs) {
if (!(value instanceof Ref)) {
continue;
}
if (contentGroupRefs.has(value)) {
if (groupRefCache.has(value)) {
onParsed.push(value.toString());
}
}
Expand All @@ -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());
Expand All @@ -605,11 +606,11 @@ class Catalog {
return order;
}
const hiddenGroups = [];
for (const groupRef of contentGroupRefs) {
if (parsedOrderRefs.has(groupRef)) {
for (const item of groupRefCache.items()) {
if (parsedOrderRefs.has(item[0])) {
continue;
}
hiddenGroups.push(groupRef.toString());
hiddenGroups.push(item[0].toString());
}
if (hiddenGroups.length) {
order.push({ name: null, order: hiddenGroups });
Expand Down Expand Up @@ -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"
Expand Down
25 changes: 20 additions & 5 deletions src/display/optional_content_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down

0 comments on commit c0f14a0

Please sign in to comment.