1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

Optional Content (OC) radiobutton (RB) groups implemented. Resolves #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.
This commit is contained in:
Alexander Grahn 2024-10-14 14:18:29 +02:00
parent e1f9fa4ea5
commit 441efe456e
5 changed files with 78 additions and 15 deletions

View file

@ -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;
}
@ -638,10 +639,39 @@ class Catalog {
return { name: stringToPDFString(nestedName), order: nestedOrder };
}
function parseRBGroups(rbGroups) {
if (!Array.isArray(rbGroups)) {
return;
}
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.has(ref.toString())
) {
parsedRbGroup.add(ref.toString());
// Keep a record of which RB groups the current OCG belongs to.
groupRefCache.get(ref).rbGroups.push(parsedRbGroup);
}
}
}
}
const xref = this.xref,
parsedOrderRefs = new RefSet(),
MAX_NESTED_LEVELS = 10;
parseRBGroups(config.get("RBGroups"));
return {
name:
typeof config.get("Name") === "string"

View file

@ -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 the visibility is about to be set to `true` and the group belongs to
// any radiobutton groups, hide all other OCGs in these radiobutton groups,
// provided that radiobutton state relationships are to be preserved.
if (preserveRB && visible && group.rbGroups.length) {
for (const rbGroup of group.rbGroups) {
for (const otherId of rbGroup) {
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;
}
}