Skip to content

Commit ed98b14

Browse files
committed
Implement visibility expressions for optional content
1 parent 3ff7627 commit ed98b14

File tree

5 files changed

+98
-10
lines changed

5 files changed

+98
-10
lines changed

src/core/evaluator.js

+52-7
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,47 @@ class PartialEvaluator {
13061306
throw new FormatError(`Unknown PatternName: ${patternName}`);
13071307
}
13081308

1309+
_parseVisibilityExpression(array, nestingCounter, currentResult) {
1310+
const MAX_NESTING = 10;
1311+
const length = array.length;
1312+
const operator = this.xref.fetchIfRef(array[0]);
1313+
if (length < 2 || !isName(operator)) {
1314+
warn("Invalid visibility expression");
1315+
return;
1316+
}
1317+
switch (operator.name) {
1318+
case "And":
1319+
case "Or":
1320+
case "Not":
1321+
currentResult.push(operator.name);
1322+
break;
1323+
default:
1324+
warn(`Invalid operator ${operator.name} in visibility expression`);
1325+
return;
1326+
}
1327+
for (let i = 1; i < length; i++) {
1328+
const raw = array[i];
1329+
const object = this.xref.fetchIfRef(raw);
1330+
if (Array.isArray(object)) {
1331+
if (nestingCounter + 1 >= MAX_NESTING) {
1332+
warn("Visibility expression is too deeply nested");
1333+
return;
1334+
}
1335+
const nestedResult = [];
1336+
currentResult.push(nestedResult);
1337+
// Recursively parse a subarray.
1338+
this._parseVisibilityExpression(
1339+
object,
1340+
nestingCounter + 1,
1341+
nestedResult
1342+
);
1343+
} else if (isRef(raw)) {
1344+
// Reference to an OCG dictionary.
1345+
currentResult.push(raw.toString());
1346+
}
1347+
}
1348+
}
1349+
13091350
async parseMarkedContentProps(contentProperties, resources) {
13101351
let optionalContent;
13111352
if (isName(contentProperties)) {
@@ -1324,6 +1365,16 @@ class PartialEvaluator {
13241365
id: optionalContent.objId,
13251366
};
13261367
} else if (optionalContentType === "OCMD") {
1368+
const expression = optionalContent.get("VE");
1369+
if (Array.isArray(expression)) {
1370+
const result = [];
1371+
this._parseVisibilityExpression(expression, 0, result);
1372+
return {
1373+
type: "OCMD",
1374+
expression: result,
1375+
};
1376+
}
1377+
13271378
const optionalContentGroups = optionalContent.get("OCGs");
13281379
if (
13291380
Array.isArray(optionalContentGroups) ||
@@ -1339,19 +1390,13 @@ class PartialEvaluator {
13391390
groupIds.push(optionalContentGroups.objId);
13401391
}
13411392

1342-
let expression = null;
1343-
if (optionalContent.get("VE")) {
1344-
// TODO support visibility expression.
1345-
expression = true;
1346-
}
1347-
13481393
return {
13491394
type: optionalContentType,
13501395
ids: groupIds,
13511396
policy: isName(optionalContent.get("P"))
13521397
? optionalContent.get("P").name
13531398
: null,
1354-
expression,
1399+
expression: null,
13551400
};
13561401
} else if (isRef(optionalContentGroups)) {
13571402
return {

src/display/optional_content_config.js

+39-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,43 @@ class OptionalContentConfig {
5757
}
5858
}
5959

60+
_evaluateVisibilityExpression(array) {
61+
const length = array.length;
62+
if (length < 2) {
63+
return true;
64+
}
65+
const operator = array[0];
66+
for (let i = 1; i < length; i++) {
67+
const element = array[i];
68+
let state;
69+
if (Array.isArray(element)) {
70+
state = this._evaluateVisibilityExpression(element);
71+
} else if (this._groups.has(element)) {
72+
state = this._groups.get(element).visible;
73+
} else {
74+
warn(`Optional content group not found: ${element}`);
75+
return true;
76+
}
77+
switch (operator) {
78+
case "And":
79+
if (!state) {
80+
return false;
81+
}
82+
break;
83+
case "Or":
84+
if (state) {
85+
return true;
86+
}
87+
break;
88+
case "Not":
89+
return !state;
90+
default:
91+
return true;
92+
}
93+
}
94+
return operator === "And";
95+
}
96+
6097
isVisible(group) {
6198
if (group.type === "OCG") {
6299
if (!this._groups.has(group.id)) {
@@ -65,10 +102,9 @@ class OptionalContentConfig {
65102
}
66103
return this._groups.get(group.id).visible;
67104
} else if (group.type === "OCMD") {
68-
// Per the spec, the expression should be preferred if available. Until
69-
// we implement this, just fallback to using the group policy for now.
105+
// Per the spec, the expression should be preferred if available.
70106
if (group.expression) {
71-
warn("Visibility expression not supported yet.");
107+
return this._evaluateVisibilityExpression(group.expression);
72108
}
73109
if (!group.policy || group.policy === "AnyOn") {
74110
// Default

test/pdfs/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@
354354
!issue2128r.pdf
355355
!issue5540.pdf
356356
!issue5549.pdf
357+
!visibility_expressions.pdf
357358
!issue5475.pdf
358359
!issue10519_reduced.pdf
359360
!annotation-border-styles.pdf

test/pdfs/visibility_expressions.pdf

3.83 KB
Binary file not shown.

test/test_manifest.json

+6
Original file line numberDiff line numberDiff line change
@@ -2635,6 +2635,12 @@
26352635
"link": false,
26362636
"type": "eq"
26372637
},
2638+
{ "id": "visibility_expressions",
2639+
"file": "pdfs/visibility_expressions.pdf",
2640+
"md5": "bc530d90984ddaa2cc7e0cd53fc2cf34",
2641+
"rounds": 1,
2642+
"type": "eq"
2643+
},
26382644
{ "id": "issue7580-text",
26392645
"file": "pdfs/issue7580.pdf",
26402646
"md5": "44dd5a9b4373fcab9890cf567722a766",

0 commit comments

Comments
 (0)