From 142fc90cc56207fa0cff4fe3d83d1d925a92b3da Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Sat, 19 Aug 2023 09:29:21 -0600 Subject: [PATCH] Support for marked content section IDs (#961) --- README.md | 9 ++++++ pdfplumber/page.py | 56 ++++++++++++++++++++++++++++++++++-- tests/pdfs/mcid_example.pdf | Bin 0 -> 24694 bytes tests/test_convert.py | 7 +++-- tests/test_mcids.py | 55 +++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/pdfs/mcid_example.pdf create mode 100644 tests/test_mcids.py diff --git a/README.md b/README.md index 19134d6..9bd2a0d 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ Each object is represented as a simple Python `dict`, with the following propert |`bottom`| Distance of bottom of the character from top of page.| |`doctop`| Distance of top of character from top of document.| |`matrix`| The "current transformation matrix" for this character. (See below for details.)| +|`mcid`| The marked content section ID for this character if any (otherwise None)| +|`tag`| The marked content section tag for this character if any (otherwise None)| |`ncs`|TKTK| |`stroking_pattern`|TKTK| |`non_stroking_pattern`|TKTK| @@ -191,6 +193,8 @@ my_char_rotation = my_char_ctm.skew_x |`linewidth`| Thickness of line.| |`stroking_color`|The color of the line. See [docs/colors.md](docs/colors.md) for details.| |`non_stroking_color`|The non-stroking color specified for the line’s path. See [docs/colors.md](docs/colors.md) for details.| +|`mcid`| The marked content section ID for this line if any (otherwise None)| +|`tag`| The marked content section tag for this line if any (otherwise None)| |`object_type`| "line"| #### `rect` properties @@ -210,6 +214,8 @@ my_char_rotation = my_char_ctm.skew_x |`linewidth`| Thickness of line.| |`stroking_color`|The color of the rectangle's outline. See [docs/colors.md](docs/colors.md) for details.| |`non_stroking_color`|The rectangle’s fill color. See [docs/colors.md](docs/colors.md) for details.| +|`mcid`| The marked content section ID for this rect if any (otherwise None)| +|`tag`| The marked content section tag for this rect if any (otherwise None)| |`object_type`| "rect"| #### `curve` properties @@ -231,6 +237,8 @@ my_char_rotation = my_char_ctm.skew_x |`fill`| Whether the shape defined by the curve's path is filled.| |`stroking_color`|The color of the curve's outline. See [docs/colors.md](docs/colors.md) for details.| |`non_stroking_color`|The curve’s fill color. See [docs/colors.md](docs/colors.md) for details.| +|`mcid`| The marked content section ID for this curve if any (otherwise None)| +|`tag`| The marked content section tag for this curve if any (otherwise None)| |`object_type`| "curve"| #### Derived properties @@ -531,6 +539,7 @@ Many thanks to the following users who've contributed ideas, features, and fixes - [Shannon Shen](https://github.com/lolipopshock) - [Matsumoto Toshi](https://github.com/toshi1127) - [John West](https://github.com/jwestwsj) +- [David Huggins-Daines](https://github.com/dhdaines) - [Jeremy B. Merrill](https://github.com/jeremybmerrill) ## Contributing diff --git a/pdfplumber/page.py b/pdfplumber/page.py index 521002f..c86a363 100644 --- a/pdfplumber/page.py +++ b/pdfplumber/page.py @@ -22,7 +22,7 @@ from pdfminer.layout import ( LTPage, LTTextContainer, ) -from pdfminer.pdfinterp import PDFPageInterpreter +from pdfminer.pdfinterp import PDFPageInterpreter, PDFStackT from pdfminer.pdfpage import PDFPage from pdfminer.psparser import PSLiteral @@ -62,6 +62,8 @@ ALL_ATTRS = set( "stream", "stroke", "stroking_color", + "mcid", + "tag", ] ) @@ -115,6 +117,56 @@ def normalize_color( return separate_pattern(tuplefied) +class PDFPageAggregatorWithMarkedContent(PDFPageAggregator): + """Extract layout from a specific page, adding marked-content IDs to + objects where found.""" + + cur_mcid: Optional[int] = None + cur_tag: Optional[str] = None + + def begin_tag(self, tag: PSLiteral, props: Optional[PDFStackT] = None) -> None: + """Handle beginning of tag, setting current MCID if any.""" + self.cur_tag = decode_text(tag.name) + if isinstance(props, dict) and "MCID" in props: + self.cur_mcid = props["MCID"] + else: + self.cur_mcid = None + + def end_tag(self) -> None: + """Handle beginning of tag, clearing current MCID.""" + self.cur_tag = None + self.cur_mcid = None + + def tag_cur_item(self) -> None: + """Add current MCID to what we hope to be the most recent object created + by pdfminer.six.""" + # This is somewhat hacky and would not be necessary if + # pdfminer.six supported MCIDs. In reading the code it's + # clear that the `render_*` methods methods will only ever + # create one object, but that is far from being guaranteed. + # Even if pdfminer.six's API would just return the objects it + # creates, we wouldn't have to do this. + cur_obj = self.cur_item._objs[-1] + cur_obj.mcid = self.cur_mcid # type: ignore + cur_obj.tag = self.cur_tag # type: ignore + + def render_char(self, *args, **kwargs) -> float: # type: ignore + """Hook for rendering characters, adding the `mcid` attribute.""" + adv = super().render_char(*args, **kwargs) + self.tag_cur_item() + return adv + + def render_image(self, *args, **kwargs) -> None: # type: ignore + """Hook for rendering images, adding the `mcid` attribute.""" + super().render_image(*args, **kwargs) + self.tag_cur_item() + + def paint_path(self, *args, **kwargs) -> None: # type: ignore + """Hook for rendering lines and curves, adding the `mcid` attribute.""" + super().paint_path(*args, **kwargs) + self.tag_cur_item() + + class Page(Container): cached_properties: List[str] = Container.cached_properties + ["_layout"] is_original: bool = True @@ -174,7 +226,7 @@ class Page(Container): def layout(self) -> LTPage: if hasattr(self, "_layout"): return self._layout - device = PDFPageAggregator( + device = PDFPageAggregatorWithMarkedContent( self.pdf.rsrcmgr, pageno=self.page_number, laparams=self.pdf.laparams, diff --git a/tests/pdfs/mcid_example.pdf b/tests/pdfs/mcid_example.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a90e125c8490a11d9df4655d968b4e3d56ce8b9 GIT binary patch literal 24694 zcmeIa1z42Z7B_AYA}t^wF@S*d%rL~zozfxQLw7d{3eqVZ(p?H7EhQ!0(p}Oi`3-pP zIqEt0-tV6ObD!t`e9s*nXTN*)T6^ua*1PuW`t3E8vO*#ZAVyXc%BGB_nx^+nDJWn7 z5MZTeio(Uk^aN&UWN!>$hWE%biI|w%!)%#E%ysQyLNI+R0~ikvik-bJOxFU%IkE8R zxakBC)8pvrN+qT+;ba5;+sY_B6shW7zka`72wOj2Ow@&^=mR%OVb(VBs{x|(0>t?p@4R?{=)_t2|<}n^;#&_;%w!Xt1 zdE4`GR$PNjQ-NPdQpu3Y>gca5Jn!f`6$Y=17AbY)l zzAu#@xMjLI)m@<4#oF``YUs!D)t3H%cD`CQ1lj#EimJXcXFESszxsn+M&;w zA>Fx-5;P-PzE#1<((xe6=3|^g;-{B~1IJi=v8`R~p%~AO z9FB(+?;i_MswqAbyhM%jK#MaEzsuKk2>5XQ{@$-*C!h}^ThPAy zd@l92iLRN`s^JTxyR)7=iTDK7N2E-r&oGA+UM%pifL)D`IeEEHo(hy<37{y8Ha7Hb zR#735WvTal@sQ(RWVX)FF?U*{Qp9}kYaXH3J!AhepB;vHoa%?y zKonqCBasxD-`>dtxMadOkdv3-7*zKUc{J)U*1#*>(FPL0o5~!mTz4O76upd*GX__< zwNWh4*hbaxGd{&9nWK~gheTzzVzlRrW%|W@r3?*k?*W~%LtmZ+X*`<{VXmB+rF4pn zT+oho*@*zzqJ7%c+(jG=PnpSSZa;lP#uoOVckNVc+<{DiNScnoaAq5OEn$IY6M3w? z)~5~BF{_c7%q0`j^AZ{!PWW<$HF|ebwz}N z7H_w!znLuwu1&6qmZ*nflb3IeuOA&89UOk$aon038iMl|tiNZoqF%kxg;g^ zkZGz{<>`8!jdHQ3f@-On2CO1Z64g~xPM6;^nTk!ca1II>@on*3&z3XwB$ zNFV{)zgA#*pZlgNG75M~@wJ{P5`{m%Dl)Py<&?K%+f7x0_SwBhnL!z4kMFwb-WwCp zeC2&6pZ_?~yVlJenbZRJ+}l06p&Gv+&p?cf+>z6?B*(Z+AmJ+%GtT^;P7qKtY?}Q2 zi@l^F1CII|4Yyt_l@>IDP|6% z?{9q40cEgdWTp`^Sy^}m)Z}J%S*PJ9KxHo@1L4$srrzTpOj;PHqOPZXNM!qZl=i9S zc?bbBM(2>8S;E&h%A9U^=l02Vci&bCFrh?xk;R5MA5Jbty%A{7?Y(>{E4!p-u(C6@ zs=K38NKMV1HWP>X1W|_J32K11R$}6?DfN8|70bDURDXqgJw&B#$?E5Cc`%<#c~Uya z$Yj^(_3)&8R&stJhDmcgLjQ$L?W?aa!Pau=-DreBAKdrjCIlgg5p4kS-DVPP#0PC} z7RAXci9F-($#y@9R`?2P1ygDX2^;mr<;9MoMzpCadJ~LwVB6=1 zSIw?!jv24vNkyll$SEMlul!UmT-Gf2RyUeb+#qwtO4H|!0t8}3!hn}lSY5K57JBM@ z-#9Tr={IlQo1AS!6-beZR>KPOmT3KwWu}>fg{sq(R#*`CR`N+3Wtv-QHKgFZadBww zV^dr4z$do^1ERm0>aR-}C!m%Y3cPvW%w3l)-qiSY-?hQ^?ehZa3uRh!^9GuH^Mw7E zzA*KV*o_Ws^vCp$s=NAr=V?q|v-SOO^Ww`(X1Cv|uI@P;1|+#1p0LtBH^3iE%uCzg zVovLc0Qua0Y(@+54t=-ni8DySzYPFXRm>=L7xvENwt6$u5^EgcH6YNZ6m^PaOWr8Pa^>dwS9vFsLcyNGh;P^MTU zdCTM`DPd5D94)21&>Q6PL7!K??dr+V$Z{!L*zJR@K(Saz{ZO^LF zS6J#kUI3pDXycD8oA_+YZ0qnHI}y?n`V(ksp@KcB`x-=Yz*~7TI$O2i-n*mA@8%*>H`Abe}<$_!FY-gC${E|9>_F1uXGZ6P&IY~%Zs~N-vktY=6c<)#* z#QL_U{Dk?PBrL;V>o3R^1hcLbsQ5SiZ>4KPjb6kATYK+di>$DE2ZO-&W5^gl<-}ZZ zksR2qW@Ibo%{%EM;EpAPCF*p?hx+!+Q$*>Vs-y%6&sRT#1erf~-I>=jE-DD^d=Oe} z5Sn58l{fT)0DUps(C;}Uc5J9u#}VyoeNBTbY8I8Cpk`t1j-Jm438b_JOU#kMXSY$a z@$2Spc4B1#jwYI zd{F1k=TS|pHR6l1>gWQ{l}_D|_EcHc(v||o)(m8++*n9y5T-ZPXJqCK6MhqMC}hy; z!-q2DW~JA@2M-DF2+|aE(3BS!-x(TS7iFDLRX;71oO0hW$|mFRa(aYbp^a#TBSW%> z_rSbtsOH`p#&inl=JWYb9`{}RbE?sbZN{T}pB)%>B-b}8De3Ns-_2EfYP>9!Wm`P0 z*T09L<;7@~sDkA0fT@BsrePqEvsKK3GHdAz*4syjcKa|uIR`EnyS{8VF|Fw*VB zX`@cx>K$^tdUioeeSPUcc>aj`MxkJ`nbo^m$kOh6)(tIH?E%}nso$b>c%+BWn#I0k zvV0R3uB3CGGEg?TSEl8qW%<151!fZoFNjA6aSXM~&^pcprTr^}1ybVDxE+|Vwrdd9cQLSqkk-s*-F3E`0$!$goh%f4d7f7q z8G4<1oTMWjW)cMM#Ga7pJxA+bZxNxeDvecM$S-(6zyQN5t}lp+D)?x+O=n-J67Q{1 zqQVb*iBYgEqx_*A#f0uf!AuG5KHYR_BU9ma4Q}$=#-t~=#*!rMFP`@ufs(LAyg-to z>K@9*UHxq5;RLRuC<>Aqg%ypaRQv@WyHs_DQHoV&U*J*|gq3(3AT*%3(zT%uF;~J8 zhWU0UO1(@xVwAnU`9^E<2ac;5a_SSSkg;mjm=CEu4@u9(si^YuT1mC?(2uF^uFQMy zaxO|*%U;aUc)B+HK|4-!r=KO$-!1gWV>fkjwQQb?qXeU6jZywt^*CFmZraciJ}MPO z%E&r3*NRJ>A}zz=iKK&?PxrF~O%wmSid4oYAMYqo&0td@$9^=s)qaPWDoo1SJJtK{ zJ{5o}Rv!mPleUt}CI0Y6r@PHPYPznI6j%i()%!H_rD;s3V#GV!1P2OLbrmsM z;qsQMw~wr<6Rh@6q|im{zR1(S|5MJR4Ma|;_2jq8IXXdmz`f{u-}1u%+I>s0YO)1i z9=5V7-P(uB)v3*Bv}~WNW@9X4ZEmCRetu!4#+VrX$ny9j+7@tjIp&@pkN!K8+?6KP z)XX}i6f6~c8W2GPT4Hi#lAUC}aa3{1V_0-QjwH>j@3V)daofAWIy1LIr+X~%B}x02 z?=reX&o+IA>8bk*#;tx`*2o`Om`m@{vZ80u?O`=PYhmeiE(PDr3Y4l8A*mp7!!kG| z$(inroeVghU9KCMH(BCjeYTU#`RQ`LAuh%KPQ2r^*JWsOuGo0@D^ayPu8lpXBC{ID zxvhFmFI)5J3o(1($I0VfimHcbi)@+ct*014EFoou9}0HbldL9Aa~OFyW7p0M%EZ-( zi)%CX)GW`c)~zlicC!+S(%j+@j`o?)=0Bu+2@(QYUeYRQ*(A3YXz6M^50yxlepy`2 zeIZaQm#}4)@yBjArpUS6u(^LZQILZIq#{xbha=BHX4bidOYiE zbI?naZ=Lc1DMg*qAant;u;i_*Zu%L7=`-~aHy4fTY1+sG*K=3RB34q;!f%sCX>iXZa1+-n7~ zoEWndD~cFKlm-{xOcT5K?Tyol3~yeEnwr=9Hc}Zc^74_#y0}H+v77*3n+Cj{aR*${ zSsU*j-#&Z(aMA3WpAzrWiae{Q1!nDz6M*RFHqu{Vw=E>glpDi<-;gJvUIoNrJv z$?1eFMCc+c`M(UJDM5ju)0M>IM7ZIw`>nK9;IPMNNF~ebpn0a`$jxxc-?2pK_QQ9^ z{qRhq`CMm*_cqH!G46*j$SxrQBRSo-LWnEwx$vINg**Zxd5$x}l32KeqE528?9dfw z5gZjM<-EGQkI)Q_gEzO9BQO-Raa9C|^JHkPNh-z8vu-WhS>;~leMB%wUwVL2KpC@$ z``DVSlALKh|5ZVcTR95{wv=cwy59>~ATf^boWEfEir6ED0^&UEHM4comxs>o1`ztP z=16~MaBRpYeU@(w%Yj4pxQ){&VvFz`n|)2I(tKuv3VI((i}<-UQp~3}EF2EXvg4sp zBV=3GHGey%oHYm_KVWfS30{s}?i5iFkQJl$N!?Zfx@2OKYLWPbp@s@6tXgCH_r>;V z52#Z(`>a~iGu+W9OSO_Kb#rFtz%gu2wFVOl%{p^!8D>3$_J{kwyN%_PJ@SrjYI^oiJx zei$g+JEb~bOXHtBR!Pt;$i*a_%8dEL8X%;ibh zWyGG`%qO_|%1(TtX-9=nXJ&qukhOZynjy@K9bCLY94^;M@!u1M>6aN#Gu zK~}ss)?SdGtDvDnknV4vM}t+vNj{k+PSmL8L zvIqlttPGT6VLN0t>F0xyYVv;Bd|%{fL(b#|ABf#+6dz0*0la%i4U+fKexp`5sKzb8 zErCP)q?$05IEVKIUX`pBW~Jl?N=A%a+J`RrE-Y|}&7JVKNg1+qL0~c}GEC}ZZNvG_ z;~3!M^0wU84{O`zHE)(;#)8u*pDS^6u|xVnc;~Oh0@J4+3<>g80@t-rpt`;rFM6=o-y zy4>S9)A+8hGmZ13`=*|KKsJ6)u(L!vox(dFJYk~TGCU*@_8ME`3?Sd=mB|1>AWggv zyF=rH6D*v`H~lgAdp3Oj5o?Nc2PB3gIC~ETNpA%*4JiYp9%<{|c+9vO90_IA>GV^B*yA8P3u930lk(iboH?4voyX&$MH*jwfC&{(gatpx z@7bC1d^U|#uKA=YM0RGtksMdZz83P%(_mP^Fn%BRSj|f_Zo;SDO4h3Sfn(#YsEC=dVvT2)5$< znRf{e2xd;F=YmuIlH*Z{YgTlBl6uRJd2;yl}r zS4hgaJ6yKzO|(i8BTd?j+l<&;OzE?{*WXxtU%jQKpHOSI$+$V#__@6r1ufYq^49oZtKJ(gVa&QreBp3D3;50e>JB3O5CPe1b>XQ<$`tY12$ zsK~!x@niz*aF5q!Z?J3PK9V%)jQkX>I@eN3%a@U{4#Lmpm1yo>=vDk%{UHqj<;fqP z89d}m^>8z@a2wAzry~qF9f~bML2#%EwpyzG#_sjdE52uF-?`JmZ9DOx+XdR5kbSUH zWieS~c`?{nfU{YaUY_Kibf5DZaWbvC=&49T(d6)^saNIs#^KQyJH#)uH_!ICW}zFi zlrpb7DO@@>k^?#D&p-M0d){6-IY6uOL@((>%b3no@3rBWe75OITujmRA_3@4DBUn< zuU*{K=R)h0nyoPGeAI5OQnZr!)TBoXx?J+)b85A*Ir+Gv&BZ#x|@gX$F;2} zL1ZzUSUx_hBv$MS-q#YGqc_o!8t0uULu`<+kFvpvZ`-Ita5Ag|B|wzcvd>Y`ogHLJ zuyaV>9A4n0KIpJFh{|!40JTOjKR}E>#1Q2Q5ka?;G|E@!d~*@NPHjEB2ezw%#O#9j#Fv!)g`!f zcP;W+PU>f8P!ucgkr0t&V&mb}s-ZhS6Q_+@R1w=eq@8j%M8w+KsAy@J$dwymA-Afl zV6UESQd+sWiRn^$t3dHSkNbqH_EA1a8|Y&jo0ipZON4jXKhB8twtta6wYz%N#tMU% z_fgl4ZvCW8r4M)|_@5fUEI0FLEUYKrqurvsICwh=4myieW$LPl>Ytl%?xu(lo>5p> z!#Pf5Hw|cmqG?chJp3S`fmkQwe%D=G1EDSpYI7kt$HiD_b$=Mm<)Zo7H`BsUfG^23 zi53Iy*+pO<)rYxnh^#Iz=KsQ3n1O$&^>?>mQgG0-zw!}9TL;*6uYj%{?8-X-+&fHc z?d%1Ob#1R~{Y3Y-HVAIsrzQsW#&+s#a1UVx0Ra%W&oHw?0aqRaW&r|NfIt8;n%~{w4-W-^uRK^Dg-Ov$$ibyUX#F zDTi~A+sTQrb71JoI^N7&VOOs0UU`hvo!AFm?v6|4m?uiY(v@9Kg>ajBjWN8mIi8<5 zot|8+cvn{r|1<6C9?C~PO#A|M%I8~Iy_aGYf_=bqN-n*5^|;m&ZE1<@-H*ZJRwOZf`i2?yVF<}%bn1*0V06!xn_f7ljgS@2!_x0 zhx)|0N+0@)+{_Y%xj^uOcxv`n00*Zxk6W4rPugSKOB?}W6z_uR$@dbB zpb1&G2-ttReaM#3h_P*PjK?KOQIF8Ndj6;|sQSabZ3au|i%RWXbsah`F`msP%&!?c z4BMI-XlAbxT!;BE$)DnH_;?+vuf-Sl zU)^3c2}}y~F`;Y$hDA#oaLBGIZ7KIVtaQJ;vqCH_IR&Go9Ssaam)O@{h>toxwS+yJOqxayZZl^?aGm2ywp) zrm+d6)R_7jMw;NKH#xFw6OddN&2u?p+{2M=Y<-AkV6ZQ2_IP5(g6h6cIuo}+Bp1f% zBSA@|?jxEax9Ab^F|_;y%A#Hh$76p%FD2<#jf5t4o=>Nt&3wA4GqNv;(V?gqXjQtz z4jIY5ON!br9NtsRFw*UcYXs605JsxR8;JMB8fetEh*uVxnQunEXw1B6geH7ORzLVI zB+sli5kH&1{n)j78e)~tMVg7$=hUd@>1!5@L?f@yoaipJs&w#y`(*o%KPpN z1X0-}M}BT|G)-h-=$Q6Jrz60m=`9z|6;SD;ugv$iK7AcDgc=nS?pyy78!ykWJ*p#7 zuKlY9Bw3R{St_SA4vq{(xRl;uC=7^JRBjr6tuinopX3b~w`GN-+!$He{%GFkIEe7B z;VA<~x9Srm*21h}XfM^6y7^5tCkwrIU1XIWQMxJbpevD`A>9R8VF(%fQ0)fW<|hx^Y@vbWp7G+5hxt}JmyS_2Ei2E$DGwDdo~r3B`ev*chg9t z()s}dWK0Lx*P zKVCb=>%elK2&qF6X#y!8}Udgn^KQt}3n<83hdyg1qML1gD~WFCXIg|d=o z5}iA_Hag~EzlVa%`~Ej=7IQ+H3BY3F? zpCf0M^blJwtl2w0Ptk?!I@Z+`@TDXec7q?ddKd~ShmXX(zD-!%L;gnXqy0_uOh0Pq2OmlastRoxI!IoRGH&- zrnvHhcw=6Nosm&nca*J;NQ=Z67uXx7+MyUhVl{A~l|^HAMGm&2=+L#938XQwy<3k<~+Ga zOg5N?wqcC=je4v9Gt_yfYeD0J`ly!B-RzjH7)i^p{EK8gHSc{(Z};u$2eI~5kT#jl z3fx#*fav1EB0l)QLq<>g4YaMzhGWI%%g zctx^p4(&$_7)Q!6+jRrOQ=GaO>I$nvb20k2R6?|mKW9-)&nMSgG<~Iz3I00sP`gq@ ziL02$b^T*C1uSa*b$SYla-+F7*XYWj!NZ|=t`o|{RM75w_j1BxuI&@>UN8N^;_Chh zQL&6w?~Yaq<8m3ApZSMr-*eWlrMiQ@46OTKN+x_OLbcR8r(7wLXvG z%tH7`fd}td_bfL(#zvWuL5^dSfyxTrJ%D6!`IEOR{3U^3yy+upxHp+iFj3ykxDWU1l24|jc3PQ{y1ZP@YeUx@&&_I_ zz)psCM}4AOb=5~pLOt9S4>4*E4*eHpcs;DUD!6IP+2m^WkjGJt9eM_GED)k;2yQ5# zpdpwZd^SS~MocH0WmbDvF(t-SBWES#6M&c4Wm)mb!x61G@DvgiN&A$x1iYYV5ctV- zVYe+WX}7V>M)5P5Xh2DS%PW79T$bqe#|!5NM_Zw4+VKG=Q`AVFz3(kYn8zzD-09~L zY&RZwojG1AU|V|4pO0vPJRL4Qb{O0$nk}atjUKI6j!Z|9ume?rB)V=Y1TE)k@1G{< zgm6yhfNc`-lwvIq8y+`%sHI~K44G!cKE>Ju#{_BTu7N%(c85qj`r?gaG??yFe?B9I zRB`?~<FVK%&l931g2*OXCo^UapqPZDGI*%#5lPP#igzfL;k~vS7_+ z=0j7ao%|q^J_`QnsO<+?EBw-7t%a^ymGvGy5+H(vNc(CPbOKM%7&P+6?jO+TS6+~ z8OBxby76FZ>)VCCdNR}_cCO7Q)V+lF@@(2Fokpp|xFAXcY}} z*5{SO)oh*jt{pg2I*(Tl2_Wv`N6q*$8pqQbNw3QUpGK2=Xwr&$zSsX8_ccl%F;9o= zvY6D1{bW#cXT!^Nc|T=ShO6Y1t-YAl%4<8hYdp!#iR<`Iw|fC8sr&q8&86XV=x6VZ z(JU{|b+1dEfl>{!moFaZ%pI%g3LynRN?KIUjy24`)?aoS##dKTjc_(@2^38 z-XPr*_TE&xj~6P*<7G?#^|-yf#DIWQj&u-3xbJ=LON9qQY}@zrJ$znblxKP{ZItU% zY`t}mpr)FVvFv0RZYXRDwXmsDB{O$9_tt&nFnO`3v=w^%5bErkfiRw9a;JFrMFdxUIEXiQr@* zHPlYAJNbOmOWD{)+XzG`r4}q686#7=EJb)PW%XdkciMek%`Hdn&nXt+OGO=1<6HBIC%4$ zioyH+<2L1?>UOu{>G-UPhjWS)(vST_C)fg+jZ7iY^d2-l>kQunS_;I65vMbVu!7>_ z(0GDBAuFlo^aL2#thy^l_r-1=$UVw-^$25De11w-Qh&>h7O3`|0e7YFqwS}fgATVB z5mDp8_O3ysrL3 zZEuA*p>dU$w&ir{JjSf3(@Bx(Zh>;XSxB*|N8{1bl0|a)l3;ZTcNShzHPd_d*I3zg zTw(MFtj1}+viqBdNXj1Bebm;A&wZDo6?rz&m!WD(I=+NQgHPT;HH$~?3WQKfW@f%j zDH_p5EVeT{NNiu9w{^8u`4%4fjk>*l$0s}~5cjRZ%NI5MD+TtN>u*2McSmtfu+*tv zo+qEuZaJ?rJRKS|Ej*CiTpnlC7-j9ZS5Q($L0a^`Bv_=u_%gu~X40)}1{sShV@0jh zN|~$~s~K?rL?7FQ=#vwY=Qfx<)MSdAgSLh@Oib=8BFbCQ-@noy6RltGo;0DgNLyXL zvdfaDu|%cS$WqV^tUhXpW;l7k(B$Xowt2RF*mu93X1-IFrJsVAM}M)kO-g`@f{Ik@Tt zE9H8XxIHSZ`OBm!N4qw>I}?ffLq?^;z2ni{#oeg6@L-qw%&!q;)>Bsn zCf}vRom%x~D(}MIFrF&)O<5!Tmr{i|P;c!F9(i47Uw2nVB#3LXv%{tybAM1!Sn{?7 zO4mT#XlC!NU>CkrrLeozxN2ys+d%?mGt&67#tgBgVyO;cFO^V|e93C$QU_9>x!Ig~ z9ZFQD(AiTd?&^=e1*8-|dh~cD=ZH(YxEDF{8`jGufMf{`1moSyI24AD2D8d9mHF$e zV+8V3`J6RUS+-vS-}I`*rk*4VtiR!NkWU@>kdn_Mw_+cg@1dH?JcGBfjFNPB-2u*8 z=l!OTLk@Qo@+1N3tD9bpOz%a~67*6JLte0_J7lTI@#Jf0VEdOjA-1%Ht3a^#s0G5U zb7S8hci7<0*}j!qm%#~?Y%0v`6j=ApZO}{SCDJA_-`d=7wR}~9*z{cI2|ZRZzP0et zk`6uN0B+_x2|A|sB7A~?Ku-|X9+J1_#i)f|#)d7b9{ngu0JS$r&$m%e!9+iI<$&6I z_QeVYQZw#+z=4HD>M%h6J)-_yUbK)$9LP&2n<|ezbq+2c#Am)4zS*K<8ia$-4pAHIH#T@{&XF?%TfE&~NHADmbOGW{C(sdH|90 z*@*Z}l}Ch+!sue?t07@JnYRc+?Jbc%jjdP%@n3cNBZmbhWeDAO5+Sc>FDet(yTcT= z@;L5U1p~Hpn5ob{(yaw%1ls;Dla$%7$P@8zFIxo*i{WkTQ@2Sxttq1jf7Q)IH~zs! zKUce5f{rpXiathDDtZ(LjW+jHyK)srjNDR_?QoTe?;$NipO5R>%$HJ_`sp06 z=sI|3bg4X^_97VLv0z2_<{-Coifdr*kH`Rx4Q(i4mTNS~b?yiH zt4ZLgwSNNltBdygM++NgXyMO2DJHJE>z5)j!I!8!_o6+zfK1?SdA|CGs(nK$|Ij@; zjDpnx2E~O}Abo zDJ<2B>{VN808P@hC@!Ri$5MQHG<9{iAjDS;Y@Ivkr&2vI!wB0py+Hei=)5f<} zWw8On%>hx&eeK(ys7KM6%Wi%O3=&l)8Or?}wfiA*gSR5sC^J||mDcs9Vc`O4b473q zRqAI#-B-8v#TTJUa#;NvhN_ZA()1seKWl!cMgt`tnRMvb4$9tfk^OHEohfHZ++Hz$B6D3d|;vy}}u zIliF>?Y+7CfOa~tSE`<8!}+$y`06Q#^{o3`Po`Cb&qJ2oEycqrKkrijdhpCF)(fmB z`Im$(#XFK)sHX@$sHZ7hBcnZT0bk8}EO=U1dxlcf-7J?Yo0b(!2%em%kH zJ5QxA?TH&NK&{Yr#pG`2dxyXY^2jpXWKTx2f{%lV_rfWjq9O0m-immK0A3Jo6oD} zus0LBE$Kcso7XgPv7E8|A$3`rxAiq~9!K-WDH%F3mq%DWZ^_<`r;Y&I znW5Y1NH1z-q|4n>n%gYh9oRMxpS9I<;+7CJI- z`8aVFy4aFQ(x)XE9V5&6wShU=j-;s72Z|d_MX{EEihDwZPILLpvARy8Ub$YCY2^TR z;_4w(uiz!Zn>V6}wUwA0dSm$Z;ldjfq*5;(VSPEd$v1}fE=d_XMbRZsY0aaiH+_{N zs-AjhDdi_1VUq4xOqbrqlgf*vB}~YS7O&%=;d`M@<@yTvgs|hFqB0tIf+k?=>sBTeZ#RGuqQE@0;qu`*)0(OPofb_;_O1-W1ACUF(2bsdR`OQ zQYF5qeRSdR|_3P1F9n*y>3S--+KR7P~AVuFj?8yp}*szaOmv!;>_OxygyL0 zKX6^aD_Hj@Tvyl9?%%+=%y2*s$_D+9VO=)pU&FfB@Z8UU9pn%3e+TSX{vE8_FKOum z#6+B_m6?TAy()SBLP$W2M*I<;M@|{i4L7myfKpUd>r+>*ve#8&`_MyFFD@aNL(vmn z(PB$Qmd`co!H&RSr|f4H*6V1Ippn}65v5~gL4DN)vvpX!Zq5_7C1~K(*%YMurIoa+ z`RsFHCTXAbu3#&kjxWXQ)#cj2XK|G}!VN6;bv+9DDG4{zWXlW7rZ<;i>yj7fHB5NU zM8gqjIdV+~4-tw$Y&w;g2tCCk-p~6k59eM;e3ABG?IwMFk%Q?^Uo$_W@+NY|!(X~x zbU!39^=RQ92gOz|(i%Z}=)xTj^6V8+TW4kQsq~(O+b7tGzSG4G1cx!+LFbF{moKu; zXm})xzLkb*?9F>cMe#PF+IEq47k^vrA0BU5Z~Ar-^fJHN&hybMCGxwEH2a92)Au>m z3Q$t4ga+QU|CN8S{9%gUJ?URS!T-#m{t^iWGyj1EU!lG%tltq}FdXZLzpt+U-TMkX zhL1t{2U^Vb1Kkyxe2pH%`TQ91*}M>KehAWOZ$? zPDOyPCt;F@*;zT*>ci{+%-^dVrCsi{u3&mr5E#G)1#7}jOxQZ;+kYpuy8_%r zZLJ)v;nFC;5qtPuZGFX5--Ss~0eo1us?U+mqu7!fL{B1l3T-)|MATFk#uCL}~x|-yNN&f>ueiy>hj`3O}jQUm< zS2Hky7@2<%fCKJw1`hf#TORm%9b1@;p&{Jb05(SEYjOVg_Wz4Ga1;HtIL^A(aB+bD z5C=YvLl9p34?j($Xk}&2^VHS^{_6l-OuzQSWdU&g-U$PqAPZ@m7ukpI8g@qZTipGE#l8u*`U z{g)K`x7q&BB7a+T|0TlzS>(T@*uTy8e-`=Ms{1by{?8)+CB^=2w*Rxp-&Wm!iSU0G z`7bH$0C1Z#o$#;+~iJIIj_G+yh47c!I6O%&$z(TQt)J=xvoCU0-mU25-_*Y zH~TS@_-mr}dp=Ou9A*JeyTh}Cx|RmIwgzw(fbVHeCRroERrV2{qT~l$&jxQv!gGp0 z+I)`T7{>Ve^2ZF z1wYvfoQX2>L;b{{a`~U%33Bi~oQN zELVy6AFK=d*G~J-WMTV-%fI%yf5wIV7cT$WbN(3@=r3G;O_%@QX&3HhKP%>k!p8qV zy>LJKi4FMgc`qDN_?gY$b6?Oe{tJg@{?5Gwcaoo_`Fjow`o;Iaf6s$Kzc?Sfa`E>? z{qq3zz2Ff5WdyRJF#T5N%EAZ&f>D^Rt7h4tjBvCE!pzRd4q#yhGqSM&SfR{}(4Q9m zuYE$q#K^%Gb`_6)D~bFmzF$ds9pA5h3G=EAxQJfZOW2^LlRpT;G@Q@2(*% z0DQ^U-(B+sfUd(G;M%q10pRP14FEylh~4!l@b{UnwJZO#;UGU~{GY4phw$*P@1=jY zrCl%OYD@br@K37zL6X0?y@agv9j-RnD`hfYQ^Q-o@0OD4*SH+uyK=tk=DTWsn&O)9 zH$d=*ITUSSFnKGhE8f>A-_T zLE6E>P8|gZPzQV$?)#emrm0at|H9P2ng2&?1Ae!*<`34phA)3qz!kO$KV0xDo{7Tr zJD>^opx+TqxbHAYni$xre}783x)ii>fWxAoALfAk8u|s{$fUWI5nQ?Oxqd@XQJ7?` zVV3;*S9R;^-~E+IMO6)8>0oaD{U0El`S(Em5A*zP_b0lRMvo0`83g%h;DYMfnu%K) zTEQPiu9`4|-{8}$C45Ik;bqKMx51Ah{;;q7hJ%A@EN) zSj`58Tl*jQ-EZR{g5Srm!pB~ZhmV86=~-ESd&B8j;iodL=;1?v%y7P7VfZIAoY(b~ z@P%I?YLA6Dt}rzqI}i$jfFRd!8Z{6|P4k1e&bBZ^xF12-QQ(|@djVKkSm21aA>ex- z2rf9>%dTDk%isFo?W@4>a~}wFb<6+M2Zq<@!)^9+ACMjTD=ioVyn^F@9tQ%k!rkuY zJ}~Q*5B$;x`ICHL5cuj*_UCaR=Bpt1Yajcc`yecT?qj(MeZSDMK>ysw%KGO%wyQHQ zztFM+|DuoWPwNFiuXdcDX~95v{QXlO`>!?vgF*0<5WkLtTjbY1W+3#}d4X`<|H==@ z2LD;+UufZdf8qy(FZWmb0@+yqWJ@3$JL|9GKoGX8F!$4X;eD*X$_#=)S>Six&*R`u z`>U?t8UX%@mIZ$2{E3#G_0PIuzdHBv(;Q$R`=4mRKqnVIDW`Rr|V;Xj*Udvzd1!Ne85$*{xs7b|