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); return shadow(this, "optionalContentConfig", null);
} }
const groups = []; const groups = [];
const groupRefs = new RefSet(); const groupRefCache = new RefSetCache();
// Ensure all the optional content groups are valid. // Ensure all the optional content groups are valid.
for (const groupRef of groupsData) { for (const groupRef of groupsData) {
if (!(groupRef instanceof Ref) || groupRefs.has(groupRef)) { if (!(groupRef instanceof Ref) || groupRefCache.has(groupRef)) {
continue; continue;
} }
groupRefs.put(groupRef); const group = this.#readOptionalContentGroup(groupRef);
groups.push(group);
groups.push(this.#readOptionalContentGroup(groupRef)); groupRefCache.put(groupRef, group);
} }
config = this.#readOptionalContentConfig(defaultConfig, groupRefs); config = this.#readOptionalContentConfig(defaultConfig, groupRefCache);
config.groups = groups; config.groups = groups;
} catch (ex) { } catch (ex) {
if (ex instanceof MissingDataException) { if (ex instanceof MissingDataException) {
@ -517,6 +517,7 @@ class Catalog {
print: null, print: null,
view: null, view: null,
}, },
rbGroups: [],
}; };
const name = group.get("Name"); const name = group.get("Name");
@ -565,7 +566,7 @@ class Catalog {
return obj; return obj;
} }
#readOptionalContentConfig(config, contentGroupRefs) { #readOptionalContentConfig(config, groupRefCache) {
function parseOnOff(refs) { function parseOnOff(refs) {
const onParsed = []; const onParsed = [];
if (Array.isArray(refs)) { if (Array.isArray(refs)) {
@ -573,7 +574,7 @@ class Catalog {
if (!(value instanceof Ref)) { if (!(value instanceof Ref)) {
continue; continue;
} }
if (contentGroupRefs.has(value)) { if (groupRefCache.has(value)) {
onParsed.push(value.toString()); onParsed.push(value.toString());
} }
} }
@ -588,7 +589,7 @@ class Catalog {
const order = []; const order = [];
for (const value of refs) { 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. parsedOrderRefs.put(value); // Handle "hidden" groups, see below.
order.push(value.toString()); order.push(value.toString());
@ -605,7 +606,7 @@ class Catalog {
return order; return order;
} }
const hiddenGroups = []; const hiddenGroups = [];
for (const groupRef of contentGroupRefs) { for (const [groupRef] of groupRefCache.items()) {
if (parsedOrderRefs.has(groupRef)) { if (parsedOrderRefs.has(groupRef)) {
continue; continue;
} }
@ -638,10 +639,39 @@ class Catalog {
return { name: stringToPDFString(nestedName), order: nestedOrder }; 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, const xref = this.xref,
parsedOrderRefs = new RefSet(), parsedOrderRefs = new RefSet(),
MAX_NESTED_LEVELS = 10; MAX_NESTED_LEVELS = 10;
parseRBGroups(config.get("RBGroups"));
return { return {
name: name:
typeof config.get("Name") === "string" typeof config.get("Name") === "string"

View File

@ -33,13 +33,14 @@ class OptionalContentGroup {
#visible = true; #visible = true;
constructor(renderingIntent, { name, intent, usage }) { constructor(renderingIntent, { name, intent, usage, rbGroups }) {
this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY); this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY);
this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
this.name = name; this.name = name;
this.intent = intent; this.intent = intent;
this.usage = usage; this.usage = usage;
this.rbGroups = rbGroups;
} }
/** /**
@ -229,12 +230,26 @@ class OptionalContentConfig {
return true; return true;
} }
setVisibility(id, visible = true) { setVisibility(id, visible = true, preserveRB = true) {
const group = this.#groups.get(id); const group = this.#groups.get(id);
if (!group) { if (!group) {
warn(`Optional content group not found: ${id}`); warn(`Optional content group not found: ${id}`);
return; 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); group._setVisible(INTERNAL, !!visible, /* userSet = */ true);
this.#cachedGetHash = null; this.#cachedGetHash = null;
@ -258,13 +273,13 @@ class OptionalContentConfig {
} }
switch (operator) { switch (operator) {
case "ON": case "ON":
group._setVisible(INTERNAL, true); this.setVisibility(elem, true, preserveRB);
break; break;
case "OFF": case "OFF":
group._setVisible(INTERNAL, false); this.setVisibility(elem, false, preserveRB);
break; break;
case "Toggle": case "Toggle":
group._setVisible(INTERNAL, !group.visible); this.setVisibility(elem, !group.visible, preserveRB);
break; break;
} }
} }

View File

@ -589,6 +589,7 @@
!issue15690.pdf !issue15690.pdf
!bug1802888.pdf !bug1802888.pdf
!issue15759.pdf !issue15759.pdf
!issue18823.pdf
!issue15753.pdf !issue15753.pdf
!issue15789.pdf !issue15789.pdf
!fields_order.pdf !fields_order.pdf

BIN
test/pdfs/issue18823.pdf Normal file

Binary file not shown.

View File

@ -6875,6 +6875,23 @@
"7R": false "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", "id": "issue2829",
"file": "pdfs/issue2829.pdf", "file": "pdfs/issue2829.pdf",