From a9f00597430ef44be59e388575afd9e65c96f083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jun 2024 12:17:25 +0200 Subject: [PATCH] Add PDF signature zone parsing functionality This update introduces new services into the ChillDocStoreBundle for signature zone parsing within PDFs. The PDFSignatureZoneParser service identifies signature zones within PDF content while the additional classes, PDFPage and PDFSignatureZone, help define these zones and pages. Corresponding tests have also been --- .../Service/Signature/PDFPage.php | 28 +++++++ .../Service/Signature/PDFSignatureZone.php | 33 ++++++++ .../Signature/PDFSignatureZoneParser.php | 57 +++++++++++++ .../Signature/PDFSignatureZoneParserTest.php | 77 ++++++++++++++++++ .../data/signature_2_signature_page_1.pdf | Bin 0 -> 16589 bytes 5 files changed, 195 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php new file mode 100644 index 000000000..138c4e1c8 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php @@ -0,0 +1,28 @@ +index === $this->index + && round($page->width, 2) === round($this->width, 2) + && round($page->height, 2) === round($this->height, 2); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php new file mode 100644 index 000000000..515356e52 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php @@ -0,0 +1,33 @@ +x == $other->x + && $this->y == $other->y + && $this->height == $other->height + && $this->width == $other->width + && $this->PDFPage->equals($other->PDFPage); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php new file mode 100644 index 000000000..3aade2fcc --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php @@ -0,0 +1,57 @@ +parser = new Parser(); + } + + /** + * @return list + */ + public function findSignatureZones(string $fileContent): array + { + $pdf = $this->parser->parseContent($fileContent); + $zones = []; + + $defaults = $pdf->getObjectsByType('Pages'); + $defaultPage = reset($defaults); + $defaultPageDetails = $defaultPage->getDetails(); + + foreach ($pdf->getPages() as $index => $page) { + $pdfPage = new PDFPage( + $index, + $page['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2], + $page['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3], + ); + + foreach ($page->getDataTm() as $dataTm) { + if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) { + $zones[] = new PDFSignatureZone($dataTm[0][4], $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage); + } + } + } + + return $zones; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php new file mode 100644 index 000000000..5a78c8bbf --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php @@ -0,0 +1,77 @@ + $expected + */ + public function testFindSignatureZones(string $filePath, array $expected): void + { + $content = file_get_contents($filePath); + + if (false === $content) { + throw new \LogicException("Unable to read file {$filePath}"); + } + + $actual = self::$parser->findSignatureZones($content); + + self::assertEquals(count($expected), count($actual)); + + foreach ($actual as $index => $signatureZone) { + self::assertObjectEquals($expected[$index], $signatureZone); + } + } + + public static function provideFiles(): iterable + { + yield [ + __DIR__.'/data/signature_2_signature_page_1.pdf', + [ + new PDFSignatureZone( + 127.7, + 95.289, + 180.0, + 180.0, + $page = new PDFPage(0, 595.30393, 841.8897) + ), + new PDFSignatureZone( + 269.5, + 95.289, + 180.0, + 180.0, + $page, + ), + ], + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..89d91e67cffe721456aa7c0c870a49e7c3b4b278 GIT binary patch literal 16589 zcmd73WmsIzvM3zff_n%sKyaJEo!}7M-QC?SxVw7-Aq01KcMtCF?h^Qh_w0T4e$PJl zx%cO{o~LKkR9AOZcggBCQ$;Q(EJ_bzU_&JD{?XmiUDut1$OHfaYz-_Bd3YG5jBQMv z%mB<_i~^&mxs{W#1EZ*wzLT-Av7xPzF&`hIqmzTNzBQs-=8R@*%yR?s$z6FWSBDJU zgX7oUciH8nD-cLO;HP%t89tXa`o3C)krI<@+|tF9Hf-LSdK!KCL`dY=OPic*=pp<4ga+ zIHRk>%k`h}c5l|x!}bqrHPWumy3)RXj@#3}deXqaw+8ntah^cG1h?lq!ywtGjytzI@Mp%s!Rl+~5)~7^Pm9=ZZmt zqvyyzkb9iBlyH=6m8i`3^<}vK>S+siTI7$D%@^ECWSx&#W$s?nWFO*W9fMhK^#v9! z`ROC7=|(CdAH`vt-Pj|kR>?!A4d~C3^JT}HDjkzIW=Tx%k)@ZS_H+GAwkTXdCt6m= znZnUX`i2kmJOBYx7(p+6#5Uv6U5*v!Xtd|em!=3V-`o6%m)FanfZCCihsA!}Y1#m| zIZnEiY!Hus?L`(z@a)1w?Y01+Luc0T3a1WiNMtIf!K6XGW$sk~KJSC|IKK}^eZH@U zOmr!9lK9X^k;0yf&gjjXrl5Wj#I0?W`U&o`1S8`S6MHmh8*ww2qp#7)GimOh9v%V< zB|&n~i6wt{xfncL=w`KFIq8JD_#bY@FiRMS-rw0?=$b1NU2_P3zmK&hDJgCG&APWL z+RH*xU>(ya0>mN;Q~ZV>a2KOD#Kix>A~yrFcKtg9J)(@-=(;`RI)nap8(JR62lTKR zi{9@!na@hrJSg80;1^w9{f;7W-rDq<2@5AIteqC6@^ z$Xbd1nRcoo(r_k1DI zL-u_=O0aG@kHQZ!1BGvKCrGH`wEP`5iJm9 zM$I1)6s;ehershu((N!TlLgPPzxPxaNx+7^eD2$_vE_q6WxnX{Ui zwt^=Xbz(#EeWg=RM6m2nFl=B}F!VEuptL01-_KaRpF&7*{N!b-VF5EH8;XuO7E>Vr zF!qO6U-mBt8AHGFTn)|iD(>Q}NiL3Z4LGTnKbE(<6|19VM_G<6fc(Nf4>F4nrl-!i zd^Ft=W2xPdxmQ2*)mWD*!Loz&6HgGNFz2J^%ztNuC@m9#^GBgerywP7{I-}DvoMF zj6t+ri0HB64ceeKH3%v5v}`#k^YInj-7OW*GKXn==o1n2G6et4S{1KpVH;;$l&DDt zEGbfDI%UU}&|pKR{-5_eb6m5Pat$e`fh*LURM>{S1@Ay;bt<}}p-|3W5A|0&O%O}A zT`0G`gcNR|Oz3MRJ$FjaJcRUL5Re+;JXvQ2+-{#{%=1L#vl5D)df#D`;OtTp>6qIf^@U6G5*EW&#L(|;_t-HAULtE2c=h)?vgC&@gL=8KnKZ* zL-4^j3 zGUN%|#MFLhCM{ej1R=^+?FJu}xx4)5us7k*P?A zMBqfXzsH;`j;@bCdm?GxZjUBL1qhm&GD#Utues)jPVlq5Y6!I9w@SK%r;2D>`(;1# zpQ~s3PQB8^O1WMb?s{w1V*Q~CKVT$3lOIMO(><`Q^^s!PfX4b09gObkaxE}~cLx{= ztPM5xACIq)Q&_b9ILE09e#>ug{VvS({|NO3hcO9+PfGNv#`ET{$DHcxqE_b?$o6rbrmYBke_E z;nQc*3|}1(*?A*|gp$14=O2ecrqneA(DRbkpyR%uk;Tt_oVfSjgFf&dy2fXHu^6_C zBF~Jv->XsJ@oh(fqZIG>Xw|WsBO!8;4 z(?HRkt-XkfAdS z&Zce=UdEyhx3r_Zy3S4I#Y zq%BsBy%0fF4EQo-+`k&8tkV>daY;K5W#Gr!9xn6FO-%@BR%s*=dgU)N)36N8c|`DX zbKfn8I}{tEjgS9438*HfUzpg1C+J_8g5{dFnpxG{Td1j?wHuRhW6uy<43rE-BQ?v2 zu2$Dv`CWz38$=XTFK)Qo<;RAaWv78KPu4w6uV`;`>_#rhQ2u0LhaM}_7kX8^?AS=&z*zy+plQ*1hvPY1Q@uzDN&Y)|7X%C7;m<2j zG>pz#c45yr6Vs$b?4#h{Rx+Zo@=_&Ygvq{Ey|u@{dXGkq;W$UAqj10H^cBH&)_X=R zSEq6VC5LsMxPN}{>ut=7ek)`H#J~y%u+p(h%v4A^G2e9N!G=xgZFy76n z69ZuP&|`;x$63?NVOh@^^?`r6SHE3FYAJ--1GEh zadfKMEO0kDRhF_Sf#twE%;@2WR#!g_(r3muFMrN@_Ve%J>a#(Uc|x2ND)(7LUix98 z)PmQwy}v)@n3cCEWf7cx}>E1ad?F45e1MS8rvBC<7f_s z-cIv~Z-@9dfQge0_}2XwlJ!5%`fUFXXMN@gW+kdU8YQ3{aDo|FB?qi!nPFyPSFQkp zepds*eP4DE=M)CE5S8A&tH*#Z`(LDaWAi2f>%U~-_zzh){|~bK zk4MVPOz_Ggy+J))TConjb1EtH~!PIV}eS5q~v-Ahbd~>$Wt+SCC@npXm z-Mc3rZzyKRv&Owu&r|mm$J2qZww03}VOV&{A9bf@&qx8!2qfEU@DP?u*?|p4S>EW$ zVhH*#tz>O^Pkz3u4$){fsc0+b(OGj&R>q{zXlL0VlWNO1Zvy3Ts*L#Xk(-nvJ#MA%!nm8ONQmSC8CX+XF zfXpeHz&Y22G|Q90ySCwk-B93;*qUqGLeD_*%CD0y)FbnORWdZF=fH8O zmxzqPfu97En5uTLpv7eK>NQ1aU$OjYAkQfEBdKj{sf7>}LL5UC0m;H#%m6LCav9o! z6cJ^BF4Hl{0b@zg4c(!jDqd2D#DS4hz=zE^8SQ{#j)Yz|j$M!FfwSlf;{){dHpL}r z^dmwSL;G(zDF!|+vr00gN1O)=$Ji2sF@qhS{2&A%52pxGpf!gB+K*_<< zCE53w1WBF^DmLjR2zL?fY$3NtTI{zJm}i4?gP>83(LAcse)u>QtxC@2spT5`?#GFy z+LUtjRtw{;@ZiU$o!a3a>G_g4j7=pT8~zW{O^h|P%)MEX-AJ@q( zb7|!HV;<|OKUY&D-U{mprRpXipq?UVbNi#Ow!>JhX;#DKtx;He8J~JdWRr)GN%l;F zqM(9e&r@$FKc-gnYd7jo!CV?)&7RjZs@_l4JO~6DUPC)JpLI}C*zS9SI|uZxw|hEV z+6SKCVlt!xE5c7yL&AP@cRH2~e^V2V6=%!OhnHI3!&-^@$hv+q8=hqoOA}!11h} zVv4mHbc(r`uPbJ+dr#4rn*{RZb$Hfe;jAF6J^CU7^x;c;DqPwlu0b?#21wb9{ID=C zk(9D9Ax)@Ib%Fa$kUtENrESdlsYBdk@1N$cTynbw9OAfnc?^B@l`X)8LyMb?>ZKMj zvyk}WtQMPF)N>e<0#_l%;tzRLl-DH%`+V_ZR5v58Oi;ypt`BEXcFhJp_4K5f?UrvjKk;l$tHL)oI`~Hbjt{$9v zM(QB>E`47~Pc?b&?TJizcI9Z`Ow!Nf8G=8>bGA0>zcAu0cJn8MDHtBIsXS zeCOAwDe~nas@Q^f6NhP-sm7JnS)~h&K}ZY2Uh|n{R3lPsSk2XG8Cn8IX%<^BqqNhb zo8MO3J+{6vHRuw?0(NK!uZtWq8^mh2PW8#J0aS zUzKW*$KTlLUCNY_R1!QRU7Jt7zi1Kjv{84{Pu^g?wVQ0}7KvnQY2h9ZVdDOS&C3h6 zu9k;v^QXY~NJ!hQtXfatrQHO+N|3n$e0u)U3K-zy}>n0|C~QX;_i6(kuxw zpU`178#KwXP)g~+-q|15 z`uWRtb=U`N%jmxc1SjU6*X<4#X0X?10yj>Nf68Y5QfZXa&dj2+T2?viP0gj63*m+% z)9;-Z=L?5@6~cIyl$UzR7Z06H9J#2UqlkLZfYHh8uvD-sg`E2yF36{M2a)Jag%A#_ zHMuF2fQ969O0>)?K*s2`kSSs`oY@Q27!{oDX8 zl>%nGdGUEYLY(zq@5G?z89xhtMS);Lm;iK0L0rO2Alk`%T|=-F{hEV#Uh^&nu_7dw zHGk5Fyn`B|s(%ph8$pibT`1%{Gyqa*OV9K`FBA{+8`DZhBm$78!yG~sIur?jAjd%U z=bp|@;Va4Fhg^8g8B5hiixb6iYk2DwF87o4mPZOk1x32pZ_E=-)|Mdqsc^j@i|Ce5KjTFQq;d6ney0}YH$ zdf<)|jwA3Sb`nhUGoYj5%tE^h$B~0>cnpz((!3@SdJytt;5ol9BeY=lncdW2ie|<- zb;V+$aO-y<*i#B;0ys%#j3>LXuyVgSAnuq8xxA7rGS49AXfkSD*Oh8At|RV1YV(w} zXuu;5ExNB=rzt001zNB}9*=Ba4@Wl$Y#ZJ0Rx@r zXxr%LPFIjrx;ImBsu_oPPoFO|RLu(`u(k7>Y3nBJ(gNi~k;OBzq{u>kj555F^jo#5 z^hDJDgjLtj!qC^#q(xE8%8yldNiq6ZLGAkkEj|dS1RsK={|#+32#q>WjwDu4Eu66s zZ3sz#4!RAkkBW#ie}QlC>V1KP^OpmqY&hk*CF`KCUOIZ?Ft68XkcgkLj=vt?DUU(h z47L$o#U!TqnoeuB-|rwzkgqK*Ah}xmv6lqgg_QIKtyZCazB-_E=!bPXugs%7Ro#}0 zjB7=j<3i0&q<~IvS!5wj<87|_ZE6sJeKjPw;hwzEX6|2D)4+mts}X*}P&%tuVGU1k zub%2wAwtka*G>ta;Sc18XZDXJhPFoIL+)AtWFmCQ2H3H{-$Cnpq5-f1n*fQ5Jep_? z0H^4D^pJp9cz>+GFN=}Zd;aGtXrlhHKMapi1?hgF#C2^`iE;#Vi_`#%TQy6ayx6n9jKI4{X5*NIb)f z~smOgfHPAqV*S3spUK#`)rbAEQ{2!)DA!OK;iglK^Zh3cb%mahgC zIH>7c((WAp@TIc5Y?sPFuO&4~mp+nxVsMFPo|DKYk5^Kl+40hJK_>iaaJr(T{kT~* zqJY1dFjpZ_g9~>N2Q~F5h-6yJmB|^MdEgT=6HHsvIBGy;HFnqFe0rlV(_g*MtQN5y z0>O{Quaxqn{~ML8hUg+7QMv3w$b@%@OR1kA^V;PASzI{)P(~^1Y=`~eLa>#?N4u7w z03$v~v~uaK>X)77X|{77(f-m~hH@gWzBt@tGhgp%eLr+%6;kZf85uwRdPMec1^&IH ziW$wW%2Lc{od6fQm!C!2tD`0SC`-CaAz#*%7MM4j%{+H(NEqEEH-ECOZb(&Pcm=mB z=*1wsD`>_L>bMcsIKhQGI~sKD0nb2Ii*s!~lVC$n7N@Q(0EeNejQ^D>IK1D_M{7m% zV-Vfd;f1foy^Z@p%Z%#l{fpgGa`+k1>cwcsa#7pOXi9mjC-bF@Z@+%b&7X`TqSm^p ztzt{dh8LML$rV32?VCma@T#4R{kAR9wHFl6_pWQU;-x(nyBq;C>vk5GmsfW94>%}7 z1IYT!IkrrJT@+7fE1cjHgZYiw?CO_&lgx#Hgo9RA7=gfJdKT-;=AWS~Zhk!2I?%Mg z1ZhOMB`+=+_2Ad2CVBm#5`h;&qit^2kCL`8n3!0W&3t0H<_rg&ZnYSM7~!K zvh>n*4BbXY#gCm`;2^Vabc<%Z-o5y4N3;m|0GO2ShPh%m?%2xHt6cS7%SJc&9=y6u?niaseQbl;%goQ=-=Yah$$ssm+w~^Fn}Tw~XY3wWhKH2zOgdEFPOi+6}>Xi*i^5l8MgS1_q)Vh^Fd8XP|SCJrai7&xe-v z!=sZ0Mgs8ld*CI=-cQolLK_5vG(=jgYN&n0oVgXfeEJ}$TRsy>HKpnFHoq4T3fNR+ ziBt^hwKg-%UzYxrK>I_Tv$3&>C)t^UGXZ_aXab*}O)&6AKmHY{T=jtx`j?Gy*Ec=U zDB!zX^jKKJ%=Pjjzi+%|cSwmVcuXCO;z!QxHFM98a}h&~OZ#iKM@yZ{+FVDqbLo9| z^u;?VoUKZ}ALM;qD)~um6oi}dKM3e5^@LF*L>RukhGa8)d(WHWLMstRtYZVQ1ZBUE z56;>Dl65ga4MN}tB59cVu^?4cI5T}!!dlZP?PL9+kKkD{I@%SqqegwPtiygy#je4PVNr=n zz9+L%q0@)>kVP>f!o6;R{@)?q~^O!EdhLCi3 zy+?Ip;A)&kD5MLX$7c1)o>srMM8!vi4|3_-P7=u4TECka-F-8l-D~2mJR&^Ve63D^ zK)eEf(f?9&jS*$wmEl#`NAsl~#`u-AT}@~UBQk5vV6CI!N30foY0VDK_JNZW!xlD- zEY*8R-Pi;}CeB2w^htB4?-hOYVL$*R`gg8RU1+7k!k_oWo9M#RQaK5kiZyNfM5MyR z0I4pBNKA1EQDVE(Ujyl__Y+-L5*=si!4bIy<18uSj!^CQnLH%MZPa*H}mpQtMP;G-#Wt5r=Q?n;{1U7MlZ37SV_Y;zk>UM z{ZuF(*dL)p8zjyg6_#@;+&z*K+mhURFWAd z2i9uA%CNlGdxEzug)`Kadl4i*1_a%*O+6d5Di>U#h4jns(|2SR0(32F7p;yTE23n{ zKVV1`0keMb9mA|5<+0?5du-To6*o8q$L0DkA!qXtq{Rc}bEL19A6VKe4t5J1H`1!{ z+}D#u!u)@(1kA+{w-7=u&U=$F{@JaSW-Je?Oz3u*ZV2$rqIFCj8hi+udwB`4e`q)L zDC9F38Ep%(ypA=^w+tQKW!l^QpcO1wUI4(lMI%u8h=-l_@s=le(ku{JADKTMGL{o08`NEZWl<<0^yf3$kvVG{kY;{;+1YLDj}3ibHza~Bi{70XlUUfGn_ zt`Dk!2YBp9h6R1^5~8GKpR$g<kFbIp~U z)OlLGXC&f-&FO}F{o~2@kX0TUddN21WS249pO^zx9*BX2 z)(-d@JDz4q{S!1fvBW9FosmeP4i=YueGN7aq84)>N=*`e0>*bYmu*tAMyKJqszIjd z{bVABQJMX_`X+8Pyw2IFrtN#b_7e9Z_e%P|b(jL=AdtzCQ6puO*u)GS*Zp zHam0r4(Y4ZOVSM$#quy}RtQO{mN1Z+6opYCJ%)>r<-GMyWE`-+CYaMD@l>&aHfWzO ziLX#Pg!GV7HL{5k6?->n?j3KG*p|rLmL^(HyzF_{7rtXnr(?XQehrY%E`7rkt%S}4 z(F7D`QQ7FzCicu$%em<3eIp6y?Z7?vv+1SKZrcH*8|j=>d$PvhIa(`(8gi-W)M zXmm#gvCdDj?%D6N8^TN|y>o_S8^*5YT{Kq53#9mu4KE15Z6hQe{kvT! zh)+m*zus-apwWnzR^8+((U+d%?m<~d8E!pqMiwb8Qk?T$8!eZ9(Ib1^X?Zq%VElpt zXHCYKZ;)|y=EHM)!EHrO-%h^v=NduTWN~f6dG(XJBOwYcT6LK5jYoA7yf20c`}&=ICnwZY&IUht-bz zhLPyf^MJ&F2|**Jc!K+(bDC^YsnbB-=aZhj??w=@n|?cPq|Scd5OGVzB2q+)+%XJ$ zkk2#{T&rCcZrL~KB&4gXpRUVw;pU&JR*qJalory!h}Svc((w)t*EH(ln9PqAu#*Tu}6ym*t zLx;y0tf{qKA8dgY&?+F0jl&?|51aTrz$Acz;P=9}DOhH!uVenzoof-ge^j}Q3>~pd zdtjxeV1?K?tO6hidaTi^{EPw|2twJ`t$|D&*EECy?f9`-Z2*1zxvD|Vk{#auR(pQ()3^&JC1e7`g_B_~5)+^)oZbvtNj`(UAKp!a$+ zGI=2yDuAhlBL6_VO$mHTr^rcYXCzK|edm+d4IJ8d_gM6?N`qzwJ+f3Z4^x`P5C_jGNLw7Q>6I!H&JYa^rBqAH0tcdyX`^Z z?$V?}%&B0r8Q+O3`L2f3_UhMJyJF*uoG|tqcAi%S*T>f@+ma&;hoAPim;aQAo~XG@aFM74 zq0r$T@r0`KUXUhRW5q~CZtx&seZZ!kIO2?5w@lq0>GMT(U_13VjY$vPEm+h#`RbyU zlGiNzs`$tE0|PTp_Ouxt3lW<%^G%venk{qAa4YDpoz8x^+6(EmMULdX`qAaj9p0Qf z+9i!8sJc1okS~KwHDkXSGu$u$nic1mw*~Nu8c5$MyZfc_vkiU*u*!KJh=*vHfJ{zQ zhCgmD|4at0lHC;=)1UgT$q#cm(3gRfXE6y93yq-$gEVS0p?J|0hDDj8-1)9jT#QFD zEEc4S)LJ#h$YG8#f;?m%kiHuptNvy}i!+>I5^~^d*Dx9MdheuQjAHNd%NUt@-9~v> zLt(&Mlm2Lc^Ax7GgtD5I@^_V=)O1UJvSVsJtFQd9XqDV+kp^2FJ#3x^TLLZeL8F62 z{zg($8KHJqndOq90bP1%xOMA=TOL@>A~V^b{95#-JP?umDyUQr)iC?y050EY)Pvu9X4Z6 zLq%I*Yq@TrQsO9Wak^CtT79u@WO!JcSxGDRqHEIp5-16!vfNut)i<{l)lZw(z;U6E z34|qGbN8voOjgQj$VqE%a&l%?#?Si1JB*6-j)E(0iq`ec`KG^VF>&E>mNBMWTpnJ>_H5VlKCY6pR!J z=OU~xvL>OUr&e@l)5Loik)Ur+S2S^yMW%z}sk`H#d`s!=rCYR<#B!Lv+>NnPy%32w zH5Ym%iCAehifZACy|4i6bHn9Gf3iC2EburZ8X2ncOzO_A)mE3(+luLFcYjb2YZ^dl z!F1D=T$G3mbrY*3>Qx{(Ah&lwKFCwcg2x`T3LNWng`uXrI>#AA>^bbc&6?N!G+enH z(wb+m8=Oh#}508|~@xnIxqJEKmQo;9UKIB1qi0cfw=S!)5v)!jS2QC3ktS`;f zH>xBhxKDpreL_R$Wb)@?=uc{J^73d5*~$7u!?AODv1LB(Tk&EQA7C(-b>nka_!1-; zeW;F#E;SRj*w&$QE#hg#cvumm4xRKlsJ+=5!IfN}YO0J=s1F%)=M18~@mW-fxbbn; zrp{)Sr}029IyYcc$Q@ad7?vLz$2%$ASBqJ68ks%>ak0*4y6jQ&wjk@7e#t;{c6kS_>h;EL@?7rc5(7!MUuTXMyANm-T@yr8RCb`gR1BaaLHm( zDT7+k9OXIYiQ1``8m{^zk5NdHeX2pU_9Z)V=47mDt>zP3E~<1F2 zQh1UG&I;0+2OAq9h~Fnyl;W%nSC+L5>mlHOZXt))4ZrDy*9~l&ju`*YGPmJPx%zRH zI?|yBex3W?=ZONi6~;?MUd`+{f9lB?z~G32)F39fd=XNm2Y9;WMA+uZ4{U#)#IM(-}k zHpL&ry^Qp>uLt%A=0knfAG*hx`Mns{YkBs(qc5`6Tn*L`*e>zZh_3kh0^9JxX=(QY z6+HXiz;nP;#~CsizuvsTk&*X!&`PrMznZeEk`8=PuzmID~u8;h97fp=lXE)5@3 zj5`Z(&)%&bouBqT$+Up>1e_RGA=Jp|jWpXK`kI$rvDchewBgRCdTj9yT#46p_{4`XJbo|c(a1xFkO#1qC$tuHqNR}k5f%pS($eDrN{TXaHxaE9>8S@of zg{azbflKis1W5%oK8^~Wn(-{QZMi!f9dli9D1@Z7*)xOTQS^hMlr3EN-TptYU@JO2 zqxTXP?RDFr7Fw^sUSd1#C>Mi~&GIMgd1dV;iS8 z=#AAsKp}lQabt5+GpDy9h>VI(#?~qT4lvEz%;0i8VoZGR!y6sm} zDu*Qd7eHnorVp1V`mXTZnZ6N*u89koAroSaQFrrUoP);jY0f{1o7_1Ms+;Q+lM5EN zw%CSm(8jhrvUb*{o0mQk@0w4?P(CE?=o&KG!{c6EJ9wb2VA%Dg2;h_M@j3?8q%nw^zaEDnph}SWM_m$i|Q6x;f-i>)C zXO69uv;9=cP>ATV&FGP7Tntbhs)h3GleCjq^`kXb{c72#+sRlmb5H9Ivl?4FPH@HR zCaDra=*aRi)&O(nj_Y96$>E{#sCu^E+YURuW}f+?b9JDbSCHQxQl0Os?RTKN(XRQZ z%Bc6~*`WUDdD4DwN&iRkMD`WKvErI>V|`zjbdCP1e}wRl|mQ^1G@z)5C~vqW(Kf;t@#bd#0mtw+4Em`4$i;sZ}1!L zt-aBKX%OG;Z}`9Zz+>3h*#2t_c&>lq-|p;8On)SZmy-jZD&M2e^4k#|>hQkzU|su>;|v+zXf$f{Eux!{MSB$2mC*5BmitU zlIBK^nt-=BssQ*)8E;4-TW7GcnEp)!(7zQ|{3U~sJ~*b^nt~N;3fALadPQWEwKKL6 zFnoJFX#UrXjB4r{02^m3tN;1{g6Edfw=tzMaiA9zq5hlI{IiGxyhs6OCo@|I9#Iid zQ6P{-0BiomFu00Kr=MpLFbCz5EXx zh!gA>f5U-TS=qtr_!|z$%=8vQ{)PjwfKL|AUU>Kj&iw@74dLV+C=5 z!_MFNf))Bd*9Kzc0D|rJpL9+R`rspj!<)+}ntOo5A3LLhtt~i&{pDMTj1o2`wgAwZ z&4Ht&gfKu8z#}Tm!p>^;x5fl|*;S>_#5ESKna{)&ueFvw%%mKs( PWMM@lCl`?wMg0E&^Ql1< literal 0 HcmV?d00001