From 3a96977ea85975571141c5c29c1c1bb684a6f7c6 Mon Sep 17 00:00:00 2001 From: Jani Pehkonen Date: Wed, 14 Apr 2021 14:58:43 +0300 Subject: [PATCH] Implement visibility expressions for optional content --- src/core/evaluator.js | 57 ++++++++++++++++++++++--- src/display/optional_content_config.js | 42 ++++++++++++++++-- test/pdfs/.gitignore | 1 + test/pdfs/visibility_expressions.pdf | Bin 0 -> 3921 bytes test/test_manifest.json | 6 +++ 5 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 test/pdfs/visibility_expressions.pdf 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 0000000000000000000000000000000000000000..e4418910d56d80b58081a7c8ecb1b01e64edd27c GIT binary patch literal 3921 zcmbVP30M=?7DffCQx%IVA~J|5h>)4=!Jt55KxGqIR1gfw5C%emlc)$qD9WN00a>Jo z1)mG0f-9BH1*KGMK`XRWK*6PoAT3f9H_&${fCkXl?|YLk$;`Ruo_o%J&i&6F6IZ^# z0<~m7CifrQsE1G#Mqs&r5X9v|6!&eR7{!I>#D^%J%e^VKGBL~s!)`E#{M`U8T?%0` zF{#Z^o8^Uzm4OOxDgyyR^$(RpgUPSD?BDO1G#1RHQ(-2RT>Jn1FxX7hTn3x<5gVkG z!J(^0XlxP!8VlV30FZmgaFJY$!Kf$+_DX#I0_X?UZ9YGQIrW$)@=4ylAyX1n=ukyh$6s=K?NEWJOO)rOd%rh zP^BEqQNgW-vq~^5J;P@VSP(Bu7jpQ3e9kz(N26 zB}R~LC^0@p=8#-Up(HS22o#nbl8oxaAX8$u>b|XP-nqQVZT#MI_XB*a&dl01PT;WT z-Li0xDSMCAo3y?-(RJo9-}lO$`wkVC3Kwhsy-DmdXUuw=^sy(78Z|zsD@M$G?9V=n zdzSp#CdrV_-PMTjJ*U>`oP-ZajEeHRkA&1|n9snS`~&|=X!yG|uj$p&#>RR2Cv+ce zT=Aw?H0cimv;3uv*Sw7UEc(_(%G$;>w_-(Y`|q8Zd@cO>=I!*kYj*l;z8P2EmiCW! zMSV~jx#I?-7~FfR(i{N+mBs#72sWfx2Uj9f;#R*YQ>AAWl2(cPWb;clo^;BZHZd)u zXSY$c?JfGXz>620X_FJ%1*WU2o!t)xY>lmL&wtB~YO(F@m}V6cek=P)M#+YP{-4Iw zPUN^xH|;$C+u2iHUSC(do$vc&^bx%@|FVSO>ectevHXRONx0jiZCVez*B+fP`}w7C z!+6VseY$(IzL8&7K$_+AG_-WMV_%$-j4=&MNViU!alBzmy6;TkXB{G)n|JRnEM?to zPQG_jYr>WdMOQqYdR^`^JQ1-Z+9&&PaIDLCNq63_+Hyn8UjI>ai%xlOeE&z#CHaa( z*VIC2Hbi&i&Kg48g=equR-;9*8^9=FM+zTsC=63v2r)){R09k;;tHh`C{BteuE71J zn5$5NDO9rr0`DP9K#$ytrc&WH43L5uS;-%`mrAZT+ zf^tv}onwyB79z+(Gq71nH927#rn=yPKb75~qEfS|@(Ct)Ctwp*Pv8jwr^qD`g&b13 z@1ek4co1k7u7o@kBa}F%fCu>JgQF-(utScH=q=82KuP2Z>Rp9Ib%ee>GysLIy0hxU zu?iO}yvdxA8Bi@U04aqh-wap?RRa!H?^+}L`OxdUDWq?MsZjThboUe9*{ORsR6y@L zD(MhJal;hy&4dWxM8X1)q z_X}00#1UbUp?HIe9KiZxQKLyf$k!qAM;MHcQXgeN0W8h{tq+)g z-wSHpBOSdxrY&oWEP4=;bWWM77iLj)swS~_`u7u!cYNXh%F$_R@u^vHqUb40i(_a~ z-P)bG#vSt(T$e7QZIabE?$g^Nw)$qmFFp&_)y&FTEL}yvDORNHwtjy53i9^Q+b0)F zyINW!US#(yG3?S98?Cq7Cg%3*hO*utnr0ttkh3w zw##pvEA5r@cH>J91^XlPmQ}HjpZ=lzs=~vnBJ<(p<~mQmye!9npYrrh1{XfTKbu6% z@)Ivs!sFj)?D~>^Icl;`i^(fPOVf@^!9vU9o;E&Qn;4`&YPPN>yCLs4xH_irPa z{8%EyNql^EUq3&QdU36#U5ZVrTXI*`=edOf>)zy;qJ%|v4)v@}xE7S`oBQRW!!x*h za_&v<)ZITlqhS5MqCS)SQ@7?V5E&hmbg#IbRR7ZKqVo+mvydZ30Slok?yWzWmc#x#&UVD|GSwk(UnR~w{?z`1;sVn3GEFfZKrE@9B)rt z)ECrx^xN!-O?r1ciXvA%jF9lITCV-eM89dy%aZcb5ouZF+U4S#F~=e@8)HS; zDPEsBKkxVS=!sA8oGPih@pkP_U#FKFMd@W0txw0Z*80Ua|Gv`Tmfp*Ae--+-+dPj; zss4h)<<925v=!?pXqnAM4awt!OYP<)w`o<5W12s{c+?Rh@VFvtJFlBFefcaW`^!bf zdAa9{zAL#EcR=z8-5(&Usb_7y&M@eBvW(ks`bWq0uDYeZnrCMjm(O?*Uz~4fKKFuD zQTRvRpHI^dKnA;V4kvcH(C1t*ug%|n0=20vO$)KeFgD~>-mja8%gSKm;xKtzV=9~K ze$ec%aeU&Td>D}ra$9_W1I1sqFfyijSUT+rr!p675@a#!0HzF!24&V3g>wvgp z=`*UzISwXsk_rMd-|WL4{?eavL_>3a_b>7tn!dYVYh?buczk|aiw3xDgpADrgg#tGMG-)D!(|A<8l?wB zfP{zZp)?xkHzQsZ<(hW5e|jI&GBC2!}apE=p&q#i1mG;B|;lz0j`Udz&uvTasI%vwq6TyO#cY z*7a1H!J|ds=&(W-Acs{)E6|GF