diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 3c49f9783..f0f2af3a6 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1306,6 +1306,43 @@ class PartialEvaluator { throw new FormatError(`Unknown PatternName: ${patternName}`); } + _parseVisibilityExpression(array, nestingCounter, currentResult) { + const MAX_NESTING = 10; + if (++nestingCounter > MAX_NESTING) { + warn("Visibility expression is too deeply nested"); + return; + } + const length = array.length; + const operator = this.xref.fetchIfRef(array[0]); + if (length < 2 || !isName(operator)) { + warn("Invalid visibility expression"); + return; + } + switch (operator.name) { + case "And": + case "Or": + case "Not": + currentResult.push(operator.name); + break; + default: + warn(`Invalid operator ${operator.name} in visibility expression`); + return; + } + for (let i = 1; i < length; i++) { + const raw = array[i]; + const object = this.xref.fetchIfRef(raw); + if (Array.isArray(object)) { + const nestedResult = []; + currentResult.push(nestedResult); + // Recursively parse a subarray. + this._parseVisibilityExpression(object, nestingCounter, nestedResult); + } else if (isRef(raw)) { + // Reference to an OCG dictionary. + currentResult.push(raw.toString()); + } + } + } + async parseMarkedContentProps(contentProperties, resources) { let optionalContent; if (isName(contentProperties)) { @@ -1324,6 +1361,18 @@ class PartialEvaluator { id: optionalContent.objId, }; } else if (optionalContentType === "OCMD") { + const expression = optionalContent.get("VE"); + if (Array.isArray(expression)) { + const result = []; + this._parseVisibilityExpression(expression, 0, result); + if (result.length > 0) { + return { + type: "OCMD", + expression: result, + }; + } + } + const optionalContentGroups = optionalContent.get("OCGs"); if ( Array.isArray(optionalContentGroups) || @@ -1339,19 +1388,13 @@ class PartialEvaluator { groupIds.push(optionalContentGroups.objId); } - let expression = null; - if (optionalContent.get("VE")) { - // TODO support visibility expression. - expression = true; - } - return { type: optionalContentType, ids: groupIds, policy: isName(optionalContent.get("P")) ? optionalContent.get("P").name : null, - expression, + expression: null, }; } else if (isRef(optionalContentGroups)) { return { diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 7d1f3178e..424c3a5c6 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -57,6 +57,43 @@ class OptionalContentConfig { } } + _evaluateVisibilityExpression(array) { + const length = array.length; + if (length < 2) { + return true; + } + const operator = array[0]; + for (let i = 1; i < length; i++) { + const element = array[i]; + let state; + if (Array.isArray(element)) { + state = this._evaluateVisibilityExpression(element); + } else if (this._groups.has(element)) { + state = this._groups.get(element).visible; + } else { + warn(`Optional content group not found: ${element}`); + return true; + } + switch (operator) { + case "And": + if (!state) { + return false; + } + break; + case "Or": + if (state) { + return true; + } + break; + case "Not": + return !state; + default: + return true; + } + } + return operator === "And"; + } + isVisible(group) { if (group.type === "OCG") { if (!this._groups.has(group.id)) { @@ -65,10 +102,9 @@ class OptionalContentConfig { } return this._groups.get(group.id).visible; } else if (group.type === "OCMD") { - // Per the spec, the expression should be preferred if available. Until - // we implement this, just fallback to using the group policy for now. + // Per the spec, the expression should be preferred if available. if (group.expression) { - warn("Visibility expression not supported yet."); + return this._evaluateVisibilityExpression(group.expression); } if (!group.policy || group.policy === "AnyOn") { // Default diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index f74d3ead8..0de249e8c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -354,6 +354,7 @@ !issue2128r.pdf !issue5540.pdf !issue5549.pdf +!visibility_expressions.pdf !issue5475.pdf !issue10519_reduced.pdf !annotation-border-styles.pdf diff --git a/test/pdfs/visibility_expressions.pdf b/test/pdfs/visibility_expressions.pdf new file mode 100644 index 000000000..e4418910d Binary files /dev/null and b/test/pdfs/visibility_expressions.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index c9a8fe4ba..575ebc8bb 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2635,6 +2635,12 @@ "link": false, "type": "eq" }, + { "id": "visibility_expressions", + "file": "pdfs/visibility_expressions.pdf", + "md5": "bc530d90984ddaa2cc7e0cd53fc2cf34", + "rounds": 1, + "type": "eq" + }, { "id": "issue7580-text", "file": "pdfs/issue7580.pdf", "md5": "44dd5a9b4373fcab9890cf567722a766",