From 9fbeda1dc776c602ce14d3874368d4620c079b60 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 12 Aug 2008 04:02:02 +0000 Subject: [PATCH] add image handling and prevent leaking temporary files (ourselves, Archive::Zip might be) --- MANIFEST | 5 +- Makefile.PL | 7 +-- TODO | 15 ++++-- lib/HTML/AutoConvert.pm | 39 +++++++++++++-- lib/HTML/AutoConvert/OpenOffice.pm | 77 +++++++++++++++++++++++------ lib/HTML/AutoConvert/poppler.pm | 69 ++++++++++++++++---------- t/01-doc.t | 2 + t/34-doc_images-OpenOffice.t | 35 +++++++++++++ t/46-pdf_images-poppler.t | 38 ++++++++++++++ t/HeatherElko.doc | Bin 0 -> 36864 bytes 10 files changed, 237 insertions(+), 50 deletions(-) create mode 100644 t/34-doc_images-OpenOffice.t create mode 100644 t/46-pdf_images-poppler.t create mode 100644 t/HeatherElko.doc diff --git a/MANIFEST b/MANIFEST index 173ec7a..36a7492 100644 --- a/MANIFEST +++ b/MANIFEST @@ -20,6 +20,9 @@ t/04-doc-OpenOffice.t t/14-rtf-OpenOffice.t t/15-rtf-unrtf.t t/26-pdf-poppler.t +t/34-doc_images-OpenOffice.t +t/46-pdf_images-poppler.t +t/attitude.pdf t/DiaryofaKillerCat.doc +t/HeatherElko.doc t/VEGAN_RECIPES.rtf -t/attitude.pdf diff --git a/Makefile.PL b/Makefile.PL index cd7c009..18af759 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -12,9 +12,10 @@ WriteMakefile( 'INSTALLSCRIPT' => '/usr/local/bin', 'INSTALLSITEBIN' => '/usr/local/bin', PREREQ_PM => { - 'Test::More' => 0, - 'IPC::Run' => 0, - 'File::Slurp' => 0, + 'Test::More' => 0, + 'IPC::Run' => 0, + 'File::Slurp' => 0, + 'Archive::Zip' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'HTML-AutoConvert-*' }, diff --git a/TODO b/TODO index 0058ab2..2969e53 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,13 @@ -- DOC: images -- PDF: images -- RTF: images +- add the ability to supress starting our own OO and connect to one running + elsewhere + +- OpenOffice.pm: image converter seems to be leaving behind images in /tmp... + Archive::Zip? + +- auto-convert non-web images to jpg/gif/png? + +- wvWare/other backends besides OO and poppler: handle images? + +- OpenOffice.pm: poll via UNO to determine readiness rather than sleep (or not) -- OpenOffice.pm: poll via UNO to determine readiness rather than sleep - OpenOffice.pm: convert DocumentConverter.py to Perl using OpenOffice::UNO diff --git a/lib/HTML/AutoConvert.pm b/lib/HTML/AutoConvert.pm index 7df3b82..bdbc5fd 100644 --- a/lib/HTML/AutoConvert.pm +++ b/lib/HTML/AutoConvert.pm @@ -23,6 +23,8 @@ our $VERSION = '0.01'; #or to turn on debugging my $converter = HTML::AutoConvert->new('debug'=>1); + my $html = $converter->html_convert( $file ); + # OR my( $html, @images ) = $converter->html_convert( $file ); #turn on or off debugging later @@ -58,7 +60,22 @@ sub new { =head2 html_convert FILENAME -Convert the given filename to HTML. The HTML output is returned as a scalar. +Convert the given filename to HTML. + +In a scalar context, simply returns the HTML output as a scalar. + + my $html = $converter->html_convert( $file ); + +In a list context, returns a list consisting of the HTML output as a scalar, +followed by references for each image extracted, if any. Each image reference +is a list reference consisting of two elements: the first is the filename and +the second is the image itself. + + my( $html, @images ) = $converter->html_convert( $file ); + foreach my $image ( @images ) { + my( $filename, $data ) = @$image; + #... + } =cut @@ -72,10 +89,21 @@ sub html_convert { or die "no registered handlers for filetype ". $self->filetype( $file ); my( $converted, $html, $errors ) = ( 0, '', '' ); + my @imgs = (); foreach my $handler ( @handlers ) { my $module = 'HTML::AutoConvert::'. $handler->{'module'}; - my $tmp_html = eval { $module->html_convert( $self->{'file'} ) }; + + my $tmp_html = ''; + my @tmp_imgs = (); + if ( $handler->{'returns_images'} && wantarray ) { + ( $tmp_html, @tmp_imgs ) = + eval { $module->html_convert( $self->{'file'} ) }; + } else { + $tmp_html = + eval { $module->html_convert( $self->{'file'} ) }; + } + if ( $@ ) { my $tmp_err = "conversion with $module failed: $@\n"; warn $tmp_err if $self->{'debug'}; @@ -85,12 +113,17 @@ sub html_convert { $converted = 1; $html = $tmp_html; + @imgs = @tmp_imgs; last; } die "couldn't convert $file:\n$errors" unless $converted; - $html; + if ( wantarray ) { + ( $html, @imgs ); + } else { + $html; + } } diff --git a/lib/HTML/AutoConvert/OpenOffice.pm b/lib/HTML/AutoConvert/OpenOffice.pm index e09a9e4..7b35595 100644 --- a/lib/HTML/AutoConvert/OpenOffice.pm +++ b/lib/HTML/AutoConvert/OpenOffice.pm @@ -34,24 +34,66 @@ use strict; use vars qw( %info ); #$slept ); use IPC::Run qw( run timeout io ); use File::Slurp qw( slurp ); +use Archive::Zip qw( :ERROR_CODES :CONSTANTS ); %info = ( - 'types' => [qw( doc rtf odt sxw )], - 'weight' => 80, - 'url' => 'http://wvware.sourceforge.net/', + 'types' => [qw( doc rtf odt sxw )], + 'weight' => 10, + 'returns_images' => 1, + 'url' => 'http://www.openoffice.org/', ); #$slept = 0; #sub program { ( 'openoffice', '-headless' ); } -#half-ass using DocumentConverter.py for now -#need to recode with OpenOffice::UNO - sub html_convert { my( $self, $file ) = ( shift, shift ); my $opt = ref($_[0]) ? shift : { @_ }; + my $outfile = $self->odconvert($file, 'html'); + my $html = slurp($outfile); + unlink($outfile) or warn "can't unlink $outfile"; + + return $html unless wantarray; + + my @images = $self->extract_images($file, $opt); + + ( $html, @images ); + +} + +#http://cdriga.kfacts.com/open-source-world/tutorial-extract-original-images-from-ms-word-doc-using-openofficeorg/2007/11/04/ +sub extract_images { + my( $self, $file ) = ( shift, shift ); + my $opt = ref($_[0]) ? shift : { @_ }; + + my $zipfile = $self->odconvert($file, 'odt'); + my $zip = Archive::Zip->new(); + + unless ( $zip->read( $zipfile ) == AZ_OK ) { + die "error reading $zipfile for images"; + } + + my @members = $zip->membersMatching( '^Pictures/*' ); + + my @images = map { + ( my $filename = $_->fileName ) =~ s/^.*\///; + [ $filename, $zip->contents($_) ]; + } + @members; + + unlink($zipfile); + + @images; +} + +#half-ass using DocumentConverter.py for now +#need to recode with OpenOffice::UNO +sub odconvert { + my( $self, $file, $suffix ) = ( shift, shift, shift ); + my $opt = ref($_[0]) ? shift : { @_ }; + $self->start_openoffice($opt); my $program = 'DocumentConverter.py'; @@ -59,20 +101,27 @@ sub html_convert { my $timeout = 60; #? use File::Temp qw/ tempfile /; - my($fh, $outfile) = tempfile(SUFFIX => '.html'); + my($fh, $outfile) = tempfile(SUFFIX => ".$suffix"); #hmm, it gets overwritten so $fh is bunk my($out, $err) = ( '', '' ); local($SIG{CHLD}) = sub {}; - run( [ $program, $file, $outfile ], \undef, \$out, \$err, timeout($timeout) ) - or die "$program failed with exit status ". ( $? >> 8 ). ": $out\n"; - - my $html = slurp($outfile); - - $html; - + eval { + run( [ $program, $file, $outfile ], \undef, \$out, \$err, timeout($timeout) ) + or do { + unlink($outfile) or warn "$!\n"; + die "$program failed with exit status ". ( $? >> 8 ). ": $out\n"; + }; + }; + if ( $@ ) { + unlink($outfile) or warn "$!\n"; + die "$program failed: $@\n"; + } + + $outfile; } + sub start_openoffice { my( $self ) = ( shift, shift ); my $opt = ref($_[0]) ? shift : { @_ }; diff --git a/lib/HTML/AutoConvert/poppler.pm b/lib/HTML/AutoConvert/poppler.pm index cca5b0d..a75a54f 100644 --- a/lib/HTML/AutoConvert/poppler.pm +++ b/lib/HTML/AutoConvert/poppler.pm @@ -13,36 +13,55 @@ poppler can be downloaded from http://poppler.freedesktop.org/ use strict; use vars qw( %info ); use base 'HTML::AutoConvert::Run'; +use File::Temp qw( tempdir ); +use File::Slurp qw( slurp ); +use IPC::Run qw( run timeout ); %info = ( - 'types' => 'pdf', - 'weight' => 10, - 'url' => 'http://poppler.freedesktop.org/', + 'types' => 'pdf', + 'weight' => 10, + 'returns_images' => 1, + 'url' => 'http://poppler.freedesktop.org/', ); sub program { ( 'pdftohtml', '-stdout' ) } -#false laziness w/OpenOffice.pm -#sub html_convert { -# my( $self, $file ) = ( shift, shift ); -# my $opt = ref($_[0]) ? shift : { @_ }; -# -# my $program = 'pdftohtml'; -# -# my $timeout = 60; #? -# -# my($out, $err) = ( '', '' ); -# local($SIG{CHLD}) = sub {}; -# run( [ $program, $file ], \undef, \$out, \$err, timeout($timeout) ) -# or die "$program failed with exit status ". ( $? >> 8 ). ": $out\n"; -# -# ( my $outfile = $file ) =~ s/\.pdf$/.html/i -# or die "poppler.pm called with non-PDF file?!"; -# -# my $html = slurp($outfile); -# -# $html; -# -#} +#some false laziness "in spirit" w/OpenOffice.pm +sub html_convert { + my( $self, $file ) = ( shift, shift ); + my $opt = ref($_[0]) ? shift : { @_ }; + + my $html = $self->SUPER::html_convert($file, $opt); + return $html unless wantarray; + + my @images = $self->extract_images($file, $opt); + + ( $html, @images); +} + +sub extract_images { + my( $self, $file ) = ( shift, shift ); + my $opt = ref($_[0]) ? shift : { @_ }; + + my $imgdir = tempdir( CLEANUP=>1 ).'/'; + + #some false laziness w/Run::html_convert :( + my @program = ( 'pdfimages' ); + my $program = $program[0]; + + my $timeout = 60; #? + + my( $out, $err ) = ( '', ''); + local($SIG{CHLD}) = sub {}; + run( [ @program, $file, $imgdir ], \undef, \$out, \$err, timeout($timeout) ) + or die "$program failed with exit status ". ( $? >> 8 ). ": $err\n"; + + map { + ( my $filename = $_ ) =~ s/^.*\/\-?//; + [ $filename, scalar(slurp($_)) ]; + } + glob("$imgdir*"); + +} 1; diff --git a/t/01-doc.t b/t/01-doc.t index cbb8286..a2a788d 100644 --- a/t/01-doc.t +++ b/t/01-doc.t @@ -1,5 +1,7 @@ #!perl +BEGIN { chomp($pwd=`pwd`); $ENV{PATH} .= ":$pwd/bin"; }; + use Test::More tests => 2; use HTML::AutoConvert; diff --git a/t/34-doc_images-OpenOffice.t b/t/34-doc_images-OpenOffice.t new file mode 100644 index 0000000..ba32b2d --- /dev/null +++ b/t/34-doc_images-OpenOffice.t @@ -0,0 +1,35 @@ +#!perl + +BEGIN { chomp($pwd=`pwd`); $ENV{PATH} .= ":$pwd/bin"; }; + +use Test::More tests => 5; + +use HTML::AutoConvert; + +my $c = new HTML::AutoConvert; + +my $force = 'OpenOffice'; +#$c->{'handlers'}{'doc'}{$force}{'weight'} = -1; +my @del = grep { $_ ne $force } keys %{ $c->{'handlers'}{'doc'} }; +delete($c->{'handlers'}{'doc'}{$_}) foreach @del; + +my( $html, @images ) = $c->html_convert('t/HeatherElko.doc'); + +ok( scalar(@images) == 2, 'got two images' ); + +#save em off +#foreach my $image (@images) { +# my( $file, $data) = @$image; +# open(FILE, ">t/$file") or die $!; +# print FILE $data; +# close FILE or die $!; +#} + +#check the names & lengths at least +is( $images[0]->[0], '10000000000000C80000009688B0FEF3.png', '1st image name'); +ok( length($images[0]->[1]) == 8704, '1st image size'); + +is( $images[1]->[0], '100000000000009D0000009F54B4BCB3.png', '2nd image name'); +ok( length($images[1]->[1]) == 2125, '2nd image size'); + + diff --git a/t/46-pdf_images-poppler.t b/t/46-pdf_images-poppler.t new file mode 100644 index 0000000..9bc1fc1 --- /dev/null +++ b/t/46-pdf_images-poppler.t @@ -0,0 +1,38 @@ +#!perl + +use Test::More tests => 9; + +use HTML::AutoConvert; + +my $c = new HTML::AutoConvert; + +my $force = 'poppler'; +#$c->{'handlers'}{'doc'}{$force}{'weight'} = -1; +my @del = grep { $_ ne $force } keys %{ $c->{'handlers'}{'pdf'} }; +delete($c->{'handlers'}{'pdf'}{$_}) foreach @del; + +my( $html, @images ) = $c->html_convert('t/attitude.pdf'); + +ok( scalar(@images) == 21, 'got 21 images' ); + +#save em off +#foreach my $image (@images) { +# my( $file, $data) = @$image; +# open(FILE, ">t/$file") or die $!; +# print FILE $data; +# close FILE or die $!; +#} + +#check the names & lengths at least +is( $images[0]->[0], '000.ppm', '1st image name'); +ok( length($images[0]->[1]) == 25949, '1st image size'); + +is( $images[1]->[0], '001.ppm', '2nd image name'); +ok( length($images[1]->[1]) == 43664, '1st image size'); + +is( $images[2]->[0], '002.ppm', '3rd image name'); +ok( length($images[2]->[1]) == 46833, '1st image size'); + +is( $images[9]->[0], '009.ppm', '10th image name'); +ok( length($images[9]->[1]) == 46374, '10th image size'); + diff --git a/t/HeatherElko.doc b/t/HeatherElko.doc new file mode 100644 index 0000000000000000000000000000000000000000..4af5af7ceda150250526fda1d295b3b108357557 GIT binary patch literal 36864 zcmeIb2|Sct-#C8R_k9Wh+ud{D&vW1J`#$gg_j`Vh&-Xg}Ip6Jk&pC6QbGh3n@#1iiVmtPW zTZnOCKX3tzcU(FLge}J6;uvNHVU+d*$8nUr2mpc;?*AeV^yFQ`R$~I(7{)Jo6$uf; zK(gQ|;HTS9#q)W9!pj2~lo{gVAJ@kZ?;mw}K3H`d;m|7*Kr zal{wo#_Ij*qjIv4|7$zH#u0!1$jycN@{r*$>W|g?Gk*-PvGx!?+oA3lUSoZX<^S5w zSpMJj$J!g~KLPqg`V=e5y>tEkN{%so8uMR3elg^a)%&~s{d>yWSed)qSh*6c9PF2f zh%9zBw{SOiAUJxs*;Iha{0-4IB5r~8UT3C1lWV5Rh z!pQ;52fgH|ZftUj6K04)OG6+xsx3Y`>AA~FG? zH6%K86UYukl9?f7DuK*okSIZ{9wMN%@fZ_GVI+DmiQPv)2!Rnoi$v5w@Bh#pff3DM zlEQ!uW4#8_2qDBZBm#{N)M|?e0PzeGA!t0i$YJ3$I+I9c68uR4#0ba+Rfig5IA&f|kCIpg$f=G0b2Z2IbLjsA0(`kVb0Z2B#WsR~OdNv2~jQ0IU zO#&s*JCjbM(!!85n6z+OFr644f+93J0lI*G$pLg4BY*~yWp}DYAQ6Gnzx50v8;{v< z-T%}(L=Na54bcQ*0f|xx;S^vhgTRP@wtxi)7a)zwQWi)eGYD+{f|jwBV^j@(uOdLC zBN-HeL?J99(y0sz$R-k+Bl?5FLDc{{s1;B-CRDa+P-wJOz*SNdlMea=>c=35ktswv zssTj-4N!(fAVV4zlp~0RTo{ePL`o0rXD|`NK^Is9=%>1{w9b|-<3KX73>X8w041!P zpL*j@8m%5U4#Thr1~~vxgM7f27RJ`|-zTy77)+zXxCnzBTc$c=92ICRk|>~Q!E9Yd zqaENE&0$7|{~mD|hM>pHkO;=FMI(vPfIN-Bq!XzOU<+bI7)lt!b_wu@6h#aR2X3?I zBoXW)C}bi?nW+WRrqCj_MMOk6JVQX}FqT1KM-2oLQZR{1qJww>$5e_O1#$#kN6Ii} zSO_pqkjk*6%3zR#sc6hVHid?ajup~HR%8?b(6rx5QE4Fj(FR#K6DiOhk%ERL8XRof zg>)J89wl2Hj1GFUe% zs|-jC@p=?*cH3G%6_IU?0Z(KhS@I2_Q9-36SR>}23^q^{CX03kF$~I*!${C5ldy_J z3TN3cYB2N;xVzEDEIuNJjWrB>sKhFdSn<;Wj~O%;w;<;yPJjon4w*U@*CIi&kx>Fe z%~DtPB=9Rfp!en=m=L6@h^sJm$Cv5kUvL&$2~hTFT)*o@Y>ebp#o- z=Qq2`8kDGwpDGSW+cF7(G*ATSl|AxN0hT_Z{s6y7S_B0YmI6cJCoU{iMB@nTA=^SB zk=uef9H$&G(Nf6FXe2h60Kr7T)JXy=#p)1podW`*oKFtJOL~Y!R#MIL%5Gg5%muh%?V&PX@SvfeAq?U z3=SmIN4p<2oUDcrd7~B}fC!UlG%aGZVU|=_R56%DI?J;DWHj^(6j4Y)Kpl+^#scOj zU~dQ*z!*?j7%r$H1JT3Q6@(>gU~03*UI;mur3z376&$VoTVgnw84zO3#(+i*Mq2Z` zqLkkfQLCU`EORraQb7?^7NZ%zDG4wa8B@R(5DKTUv>(z!(3JFBDFTBT4YEY5FEl?P ztj8>>In-tw6tMN|ITlS{VQ8BCO%=zigePlZ#o`H(9!7%|0?2Z7K87V9m{!mS0*k7j zN(-YHkfBgO{}At3x`d_)WZaNxh2T$qLL4*zwLeBPIgA)Q%E;dhrBPI2Mx-JhjH(BS z-5n|u1Ca8;0tQhECXXHn7yzLmqe!#XrEJpz#wl!U%0-osP6OXr%0nW|o@b~FYdvV5 z2uQ&LR9(>1KiMzs^92lT>h2BJo~4^6OG!5aAE z7wxER_R4S^VFVG7^r&>eXrW~`s4{~Fa~;?L8X^g48hQMnVW}RA=`f&1`$FU(CI?W6 zpv#C7G!LK=$JS3sVc{DM`!G-mm_Z=rx5dHF1t!oDKnjPN&|U~FoD>v+c1mGLq+}}6 zZuUkjk_;`Q&B7WYtYOi*7fe5ESv`g=gGQr5M`%Hd)+0hXCY2qLra z22_7HxQLX|^(IUbAj>hE$HoIFqY_w+P$RTAbZkT1%$JA zu|NAB<4pOcJ3Ul@wp;gp+lwZ=+(F(*CprOM0&D^-KaUqy-}p%H_(<2EMxKq&`Ca76_?-V=A}!-F z_%}u{cqxxbB;4i1;9Zg+i2i6GCJkVth8e11Mr!D7{m(GQ0fj({TxuM@rLp3izr_{) zx8#q;I);58{xsCz`>yL{`|~HQ5AQeLuDe-Pd8O>ac>et-hk1CD`*56xcV<6&YOES7$vnSkIb4l%o9+J&%1t&s$&6v!3<`~9IvF}Gkp zEE6yeR0F1`08FTVAq2VMd=Q55TvLQ6F9#<)<2Yc+Abu5+#W5V7{BW*pKIVgkVi6bx zBV$BN8w&;Es;B0XcA}367%> zVA+ddx)>IXVcRe`GKCpoSUj9E!2nroKL*HR<`|ZXVO|(!3+r19JHeEpdK$R=f-hI z9M{5eH5^yMaSt5djN>bC+zQ8~aeO_FN8orEj?cyM3LHesnUcVp5vvIr)?`y+x5{}of&bT$A=eKWoh6et52>;xN4|U^sAKqDyw^iWXPw=)X zysHWCYQ!6}@upLF)i%7l1@ElETZ-|P0{q)&{7D(!bPR9I#h*Z@pYi%kye0#$Nx`c& z<3n9v4)3rB8F2X9TU%mVd6aD+A{-Fr>}+Qd7OrbJTMyz~HW=eFhN;BcTAI5@`8KTZ z*wks0@%)*#x0ZK?+wG7wvpw}ih6X3k>BKjdZFQ*2^eK3oy`cR4+#%-|#UE!FbtL!X zjP#^l-0-8lTkb=Q{{AFu>%vyU29*&Bm8h+z!@9gtJmo_xFULF-dcC@^@lZZKD1PB% z)AOMtX8A-crvogKm;U#Nh*RK1xWN};!kL25A zQku7gMA~;#=fc#*Md`fyX`a?t;M}wlJ~2|fjREa^&GGN6X%EfsUD_SC?tEgQh_b^K zb*E`bVrQQCie-7q)zzPBc^y=KVc6Z~S=*fIcS*bhHQ#3|eB|ICs>0Rvyf(gRUa{ee z_qI+O`{~PfGvcYjTdFn`w(+T*vF=|zvnbV5U*&82q_%0PN3vfW(3_H_>%4A(Ohkjx zJ1lz2tI(uP3)&tJtP_{1Q1sovEvx6=(K55pX@Rl*mq+_jZar+iwceyx?27dy>r_7c zh>OOvReUFSc{$%EH@Ds|8LGVJZk;)6kK*@wRWkXZrppfd&V$ok8=HI{L`C|3nUug` zv{`E5g`QVhsU+e_0WIAMjkMB;%kOqb&vWOI@M+{-Oxv0g!j~Z;isf!O+_`QzN+y-B z*#1aeCLf;W|GjTR+v>7IeI4%S?CYiJ(st{Qtba6T88c+1yW|z3ijIqts7UI%N!9^v z(Z?&ra&Bhis5HMYrpif)RDB3K)$1Ype7>8~YPBc+EAtFnTrXtTCgl>gcJQ0L4>xkX zc)=?~s@}E|Z#MIFxKtW$=GQ%GTjjjCd2c=sBpvz6xBs@INv`2tMe~Ea?_$G`O@3Xr zQ1R_ov$R03X|niRDuGYh)=>C9|1(3T`Anf2>)mmcyfH^^JihNjO02xHKS_E+s)>g}*b(IEJl4sq=sL9{#cXS%nxqEoXCOj?rCXy!ST#({_ z&*zBxOs?DklNy2c?U5-YedFXP^Vlc)$(I5zq#df)|G=SM7sTw@(z$Do+Ke+=xwlMS zt_Y*-HWx?=UFD`I2OwvznbX1OMj6&gE~!h$D}u$W&?LD zqJEsR`%bfSd_65&-84%*@ASN#g4gdRCmrRz9z)Rj*qvO^9UJq7ysIpQBqiw^lX-jS z)T;Q|7diK?-c;pz^!`YpRPL$$5e&{XJZ4&j@{L+enFkHhxcGL8h%P@t@0oqz=`GbD zmxKb+^#!MHJaG4F9z1nEN0z%q1zZqF=eb@P6N9WuYl|?WdLIc+kQeWyJY?(!6*WLvs~2 zzHd*C63{x>*;$xHC~+|Kyu8O|#3x)#lHOy->zOwFUyZR5w!cJVN*b3i;u95JaKVZrBqY#oolyTCU51^X}rH%LdIri z*aCs%wQGDFihSoK@;E)zEA%iF(X;a8eQcD{v`+EcX({J*?)ywHUiFSP=U-}C`GtSk z_H@;(ysNx(I6r!G#kI~+yhb}vIjpC8l1Fv_hR-XipHq5hLGOF+8Qi36N7>Tx zBI@JN(|JjoG(8TR30JygUAZc{qQ7nuA(m^SNZA(RxdOlD#8=mt$%M9xSBcf0>5Ue5YKG`riH=M4{qsC_)!mMrtzg+#%?L02;L1IG zG(7CA#3HR+uNN;IWJrrVgog0*RAr?)#jV$_%<$>+wh;-dvEvsbDqTO+`{Vv+Hy&+4 z?zGt2{=-eHD&vkNV%%v_>Ex>i$yClksWnM~*2|tA@ANl|Nqohh5gL8GfA5vbMsp9P zEz1~fOQ&gNiVJXF6Pg^o-nQmmzERPv`mZDFU$ps1pG)eN8*H;ANGV^|*eH7eX7U*v7kyhqOlpdH33#ZF%)m^+ z`jh0EEWH!5?w9uOt=;xr@z&#rUWHoC1Gu5bR#UewJ?Z$xp^u7g2U|@(X=3Pn@$p%{ z5W<73eRZ)3YL9;C4f?c9THFX|y+>a&*I;Z-(1`SG-QF)istCeIm3m!mxSMjtk{ z-gI{H%J)n(i|XsvO2}C9O{8p^yVlmUeJWpF(`i^#qSRBeI=-K z9`lK~bAI-&O~ zBU^H`ew@@&{!o34{D`_i?h$&7SM;JxXtP7|r|9(^H;o8?n)V>r6sD)3iZw>{~19?Fhi zXVuixBdNMjjqk=(-Vd>cj(gf|dJRfHCnn@w{cx)2kn{H3VM?=Kn)@7YpUDI{bJ8S4}euYRLTP-7B;<8E(vsFHBs)@{$MP`m3jFV<$i>ji=G14SaM zRvb~8q_{J&cWI7Rry)9UqgYjoZf!O1nJZ8N?MlzWG8 z-ZPOevmnTx^ZA;&Z?C+9m5U0WrmwAkP2j7(iWT9fl}ofYKKAEGmv3)a_7G0@t*(AA zbzQ*s!v=wiCvFJE>kc**l~X+`X%4jXr9!lYhpyRd`INmU>hk=Wh40#TovtFDXvkAg zdUNXY@@9uQCW)eU&s@#rh53!V`RE0NZ99#ONRnJ$Cy(t`>AJ8p_QYQ0@Wi#Yx9w6K z@)Yp7dXJ7@YFTi&;RGY#i=X=Rxl`Rkn#@&7D!zS|7C+AuPpn#fg|OdA&Le5P$;?^$ zI|kMEEq2KW2=`E%#CtU3CC{cX!nb#1+O0RAYBQ&t4EDJDDM>xQD)#Hq^jq=V``Z1? zl(3A7Z}%Nfa!iqY*x*#J@yW+4R@HY)ioprxPt}L5u2w5QF1InrzSzC}8pmmay|W`c zO^Vh9Z@3gYWA0ZkZvRb|-!HhlT&|odyCi-{=N3863nzpt3EA1&8ou*dZ5+K$KrR2| zf(Pp-NhjYFGsUOhnl3)9!Jo$WF#qjaweYYgfgRuYmwhvtedgQ&oA_;X-87XIU1HJF zr)HfBfp%K6)EYH7IP zu#nM=o_nW-C};SuujC_MESpzU@LinVbj*4qLGJ10Fm?q-EwwwK=^~*57Ba=MdM1etq4ytGly;mi?WLA(VRJGniQCKXfVc}M z-S}GevK94n_E9Xa-&e4?LZKaEa6f*sq-)tXy3dkyO5md4WmDrkRk+?PGcPtzXf5SG zvHe{XVG?D@od&;WyDQ&Gto74MzSg>FC8zjl`sI7IdduCqNA~nBvAV8jD=WKpq}w~= zH0P=%@>BQL4Rb%8S<^u6c!g;NV}pJxRG0GxcoyIBzs4huKyml-J zo_?V5*`9;njP)eq9!QjYJG5#5yPrV}SH zvNQVWkGP)j6*~@AUjCf&QG8iLt6bl7qxqHdR5!UzJ?whe?8^<)?1rH&eHNw%o*2nw z@(P}5yRg~xOjl$`y7SVhkc_1-o-Tl@Ag+-R){2i|pw+iaM7vn~$d3bygUt9yZ&5*J;wJ{kzqPMh6xKrjR|U z!de2`r*`x9YHL24di8zMwa7{7VQN|?&1H#Z@}dTLn-mAW%uIaLcthw=ZnOb(b9OuDgR&SA<*1L0hacRoU~4%@i*iIi?BA>BZq)E-4C54Vy#ezb>~bHYKPm zzEAg=J85yZ#$;-2l3UBx5}TJKos}u-@3b)9?5Pip9?z)`PU{i7ReI1Qz3wJ$%5jM( zty_NNoMS>)Pp**=*sd%qmvd7u@|IM^(HX%DW(>#LeBulAp8l*rxBF?4GiSqFjy~1& zIVs1FbMs#NXeMMYsi0<_S-HzOHcBYwBX{k7tGm+2>A6{S8+> zbUN%fMc%)^rAPKpGs^}Qlv|B2k+IEB0`_qm*TwPZ#j*Kirnxl3BH z&s%r}96Hk9bLfVgx}09Sv+kRl+Bdhtf`b%IIj;B=Ih)YT#A;*A_PanBe#qv8`r-od zs%x>7n@QbXD@X1ux!w|&)n7_?S$-xxdYkmkyG!}~7D*-yJmeNQCvp;_aCwCY*BGR! zKmNGLW(}u8==42V&mJFhTlhtO&Y+CzUPH4`CF8jKbQARqt{E+v)O!!*t0Md~_T z8tSP6lDrNXaaWG`&yIGED~T=Hbz1xE8n^Q&*NNt?@K(KcXWNexDK#}_^UtZjk`qhz zR%_iQS2vQE$a`Ny_LZsYL;t0&P50i#96HEvI;&eW`RHVhk6&dS4#Vl3{X<@#9+^4_ z`7wNU?5xDExEo&DYZ_SQp=1+Je17q(sb?3PSg`S@drMceaG-9yv1a~YM_Rz7IE6YL=!v?MP7VRG|l zrK73mmxX9`WIytIJSSu6R$5+)yXtz0OpfP^%^vF>e0yN`!LDqw@8v|IcklU7S@rI= z-FHvjm|R`;w(!vTmbCenE=8AzH%C<*vA?)+A2asumXnEvo;Kg~<#KdtrWe`guDCIG zcVR@bgzDR+IXqd8fg9>lviL)r)+OF1yx!iRUC9wgu`K3T^orw%6{r24oEP!7ix=() z{lq&hu;s^v_9Kh#RhQelT`x zSNNhb*=A?AH=bH8p|ZNXTu%ColrA-YW=wsPY+Dh_%N@*pwCrGiEgi^ zlYW2BtN12^8{NyA)yTY;T~7$E$}n3{9kzREt6T@^$&S}eLQe*tJKU<5KJo4|_06+z zKix|Eh#}3L{nQ;V1!|%j7hX)SQ>;CCsYBm4UnbJ#Bhw;fQIuG4HlDlIEp-F+6&vPxZ=e7CqH}rRKM&^%L_7 z;lkSK7aDg}>`1utd`gpKmX=7|hHaCky;#|{Lubw$TL}R{p5xu8-twN!li{>DpdBpE zpPg_~eGVQOnX~?u^pbP24~zEUB?gY7#ckyx+?ldlbic1$ik+gvYY^QdTO}qiG6P^V^^Oj zsNEoMeedn2Ze8{5N*sm@ctfRJUn_{-G~=-&6kiR!MOf{hBx-x$@zFWPkMNmIQqL4? zCl^dfouyDyR+f>q?sKGVmd^d$q_m|=N%)V(vo~IFb_>TkPr9yUwpL8DXuAi0r|+_0 zSLve`lr)OnX0CjLk~HV3F8MYEdKnJdMrDm=J?}U#-e$(uw@z9v|3#o+w^>Q0V}8u> z?X!(Fwi`|fzo75+(Y>dMCU~>-aL>VIPh@Nh8@(hyFFUE^B$OsPsqD1YPn+hs*O_{| zcjg-9eEq;3zrUhxtz7-} it^U{`Bci+(s+hK9NctOkKU}s7a_Zp|U2g0z20WSCC zV`VA_vvL}b^Av}~e%PB*6a5j^TP91IOzi1`35n|~wDuUf$Y0$T<&?*f7NIP5bmaf^Jv?ju;%D-F)P z$-gFdaKt}mWMoh;PUIME;W@X$$z%@k22;b{lYL%KF~gSz2b_=06WID-p~UgvO2xb8 zF(>l1$a}Bl*OP3P|4>#s%9(hGv{uNE->hQNNdJNQ({&xIOu2P>IycK|P9`tKM2#+g zo8M7%^?HhPhvsd`gfG{(EHH6jE|FS1X`Yf)T}A%-(_;4)$1jknsV1m|7^q#Wn{_)c ztVL*N{W>2<&w#A?m6pl+7gy1_zr0Wt$~3xk%tp8&`xHm>#~ZIA=3cAV`ud2DpGZid zS+>QZ;BC&;ouYZ#`AYLlCvi#eG;3P4-mE2Ly=Xb7M`|cn6!4}gR!Pq4esOK9H=(&x z>t=~p@;#2Nyupsl4Z`bX=YGCo`!YC8^O0+zZ))&Ci90$*^S(YG(2-o0%0U*L@?#oxtMn?i?j&VB4OAaepe&zHm5&)xAlIJg4WpDcXFYn=sy8CEEIQk#VLF#l zRb@#mwNSZgPwW)ySAiw#s*Y9ON<%b ze`a^*Tfvg^^TplkbCw4R?<-oJT*=E%nK2&Q`wiN>RZJw zGN`MJS}@dlWOjA7bd&I#SjuUoFOwUxO?wMOjZA+`(;7avPFT>OiMO_m_kG;~B~Pbi zHc!>lg3}+Ah`MW=#b20NWzJ*2rC_kVYPbZa%O&X`H%1|tE6*D3kF7l_ux ze%tTQ2b>yIquB->#tSoT}0K=o4@)yDui+ow@5zd-gY-AD<{h7eLbql!!VK?%+@$S<>I~|Q~$DO*K1YjTC?ITUBMq8BkNRJ6`a4<)Y0>9{7O8#hkKWnH%Pl;SA(G6Qf8CJx?h&1BC4QR69-NbSGSqp_x@rG& zLb?51;XUO&S)0pfN{w7`I|g>H9bD#X-M0M1>|&GXEh8_yzMhneNm6tT6)EF-b!*N0 zs~4>Zr!wz$w!ir9X0(vgwM%C9qag0=e8>8#1!8grVn-KDUM2R@a>eE<$sn?#>%*?| zUs|h|-l#h>Ntn6uO0L-YPH}HWq|xVlm(|o>^2R(hXYzG*txD-?R`-1nc6d|!o8tSS zY9GYQ@7eMk7bttX&!UmTR^YiE2c*ahm)MdZ+iXpGH7sS7AHr^$q=C3(VZPE#4d8N5cpJDh{ zUDA~_dLX*`h^f19|5-()D?$v-n#6`VckKYX+DqZeYg4H&E%b_7vu3?f zarDH!uU^)|~X|CVaTJ zsG+1)Bq3_0s`{Oe>aUjlwp;M7`>@1{sEt#i zO^PBfo~z)`+EBL^e%WX`e>r^4P)Hj-w35qf%d#p%o*I=7?!7AMs|>X&=Et{dd{(^Q zDlV_SQreu6JG_&_eQiN=K}hq0A6!axLr30S60yu_*p<+VOI%OqFPxJop}6;T&*jg( zDy~h$!VUAT#Q3ZGsqvO%H{IHzx~H@KLl2hf`YlA7@1X6*yDLfpIbT0le^dJA(yLIC z6TLT;d+>fot6kZxS2%TaYFg^zfpnGVLwmxe#LX#<*tb6>;HJu!PP53*ZqG|M zai^Z`scZV$7Wv)g$(I>9YworfIENa~liVV4dt_nrO|h6&rUMNZ5;#MxWV$!meUvhN z&ExyrYH;=aFP*gRjTo`ts@3p9&K-Cwzm;jVjJcS7kAz?q6%N12lIYmDyYvz0?hD*| zL$`|jNpSlYu4uybJh)P~nCK4|<19!FBAqNE7=&U0X2B#{0GSDwWzba>5y5fycYc?n zV*wX;Ea2M*aEpsb(a|%~pDQ9LvOl1db->Wp%E_`|kzc$S9Ao{nBQJ9VTm;^5z2O2H z{;B)*$P4E`9C_h{`hVrfO8^{sp<`<>(jOdof#WQ!BQM+F7z!_Mp2+G~n-@KOX_%Qu ze96rhgJVIE4rMUIsR&v2kr!V``-hT$aOCA6)W~5Sd3m6#2?uhyu+bwgv5*mq#j-r` zAq0+_oBR9s1jw0i6CC&t;6P<%CHiC;Iv|BS@ZkY&cWQSCB>a^lN8Fw4BS*bH_VjLD z;qKx~c;dIBEb-}PuCB+LN_sjPRt470y&=XwZ+-c*=e>;$BRwJ;bqq86SE=N@X*bLo zbjqKj(jcaC!4Q{}`WWur(z-y2->0Tbb#=Y^urhbb2Ye~Y>0s==DZO40cH4^4&1`jqC_v+5i=+SR8THNCPf&XpzF zE^JCCa8WLvndGOxCiiK~{Zk9~4ZL017n!hsCzIe6drNg;x40ltT}ly8dPd)|rpKme z6Ipfj;|I;NbKN^aF5IMw8S^ifvkKfrNzMrhIOg=N=wro8V~zQFexKx`p9DWvTUL0c z$=@b--MZ=?&Ni{~2l$0^WluhFE`PecW!ncO#vVNnRV%e`^7LUX3m@Z~HymG`T~}KA z-tth%KL$K-0B+H5E3FGpu@xgkk*4m)UZs7TXpl4~ooXXl#^U#dz% zzU#15oKxDm)q}qGF1DsW_HXQsR+qA+D_c!V9*FkdQ?XLRIQHiIl!GR7>{H786K~r% zYQ)WpQJER2$6;%yQZxV2XNC2eee!j^J16b6rgXozT=x3nwqn(_dcFEr&Ur0add;aA zll*w9Q7%u}DlvR;SZ#~`lJ!kCB)J*tX^NAJmh}7Ne7CE5o>}@n-E-#f)G5B_*AIW( z&_&YMs?G~~`e4|yv{3oBO6zl(;-gv9KU6j@%}JvlS({TT`XaQaLVT;&^PMA3t_<7P z!JSN|Q-z48D2es z`*hw$QVr_Nk{`eDVCXDZmLc+>|N7%$L-(9ga@fJA&3r|2d%myed2!>wu2(U0D)i?) zcq-tsYge{=Sk>|e8gD*5dVKGKmK@D*M6>+LTBW%aXR0jrDuk@$DK~m>Hfr+wT#YCb zLHfQcyYg$6q=uP%_wmq=jM4pE{Y1Sjcf|?eFlE~}fsvtiOWS7GXb05jSv-AhabcEI zYedJ1!dV}Tr=@RKPuX%|dgmkFmcCNzS^Z0A%9-VrN2VWIuW*$sl-hYqCgPapu`aa>}V!@HkC;HzXyb@q_Xhh+v_uamA=RZHH?%J&oO|!7x_o}a$mY~cb zapKH@ul1%ol#Tba4VGtY2+o^>XF3jM-`<<0l>g{rPLs=_CwJ_=$Y{>ptXgv7Ies8${nzRj!*BO0wb)4E6gQ+VXji?AMH!-0%eOhTd)cE?` zUFJ)g8zWZ@qxMiS*Ux&J1h$fr$(Ho|@tks*X(l*!oEmmK>HS z;lJbV*1d}!F1sVKJs!@<+Mg6c%OyF0nE;WjKW$hR(J%%qjAmgWtaHI(aMYFx%Onz< zSB}7RunmwK2?uWjuvL%~4(U2@VmJ))7_3xnsK34Q<&PlZ$pKroa>jDrO_ zq%+}3VU-Jkazsc)B0#ItKW8Rd82&Lc|9A14_3vaiT*>=|$S`0_80HU8#3XHC7Gmgc zj1Pp=a7d$qzLG&l+4?!2&Hs$4KL6(apm}hTaK2p-HWA*K80%}5C=E`wn+@Qc;0wcz zGAE#b4s;^5V1Ln495}Lv||S~1uz?+n8>1?3Q7hy zO`yIU^aVH4c-1)AH`dIiqZ?~PH4gaF=;*~nAO1KZTuuDdnPBE-gZN?01lpDb14IIq zC9>=S6WVr$Fyd@5>lqG`B4CSIyl4KdA#KdVEEgL9DX}q(X4wro&_)J1t%9<`*ut3{ zi)M0$!$J@(4&iLv)II9#1Xw+mQlC@u!UH;||S0apMIfMWpd0IvX4p|CRmYF}!ZG%gc6{)rlz z0i|XGY=#nim=e1F2hI*49>5yVIS6nIwzp3Jo&kIWm<+_J0Z0Mv!2pv4p)Y_wfaO9M z)&ifkYzMFg0xkofUfcl?ew_d>0j7@P77sqVcSY!(3jp~D=Klep{9ggbVFwGmumkBK z)5Dgf4NJag9IP{I+? z0zgYp>om|ICZu^nhz^>H>PJ8d4ReK_$D30ElqRx_D}hxDwMc^!5r7W~dSHTfBMZQQ zR{j<4qR~$=LDIv98yd_bZN$nbyF1!mW7YSigR-Ea`ABUaB^{P z`E8uA%IC12piC^WqS=W6!X&{|OaRuion)%;uKv3|Iai-o|z!mtPX zT&LUacb$&HTrdDJj0t(0V9;;^+tEnp5|0Ic%&~6rj+qH;3K!Nt#>=r16aHW206*U$ zo)WACi;I)gg=^B70t_e&o*1SFx6S^#W1H_^%Olk_<-l>c-_CNvFuhrUKDe`dSI}51 z7{vBqh}iC54dcH%P)D1c@FqAg!GQ@5OmJX=0}~vW;J|;E13zGk%l&ib|66NlHTxnl zXK;8)i1_JreoRyS+#^_3vHr6HT1}z+Gyt?L+6w^NNbD>CY;`fXkBz~%MX(0|u&u;i z0H8H_4*=TRp~|q$Wc`N-w4Bm{IBYdB8}MP9iMfN1_R44$E9endKJcM8aUv7nu(o%mb1L;4@ zkA`9Ui#ee$W>U1_qo)`d<-_LyFiTQ6d~-MqecBMd3hN9Xm1TXwk<}K$X$}@nL}d}4 z@SS=1%p?uIMeGS5uq7C2>tYl$NXJfizeRjwg-IPiBk@MrsfwD076*zhn* zdy@FhJ+S}R8ZNMeG@f75mciy)8UXFv(LNi!6}Q1;XS0Gs_a#gOiy|hJOS- zIE3H^|7-(x<`}$}MSJB~0OTXwkdIymA%C_EKMMy4{;5A1kVAC+!*2`1kbw|Fn|wr| zJDEu#v6@B2F*E3Z8wJ2=0H8!y090i;0P4SZG>+2H?;k6jZ7o+?d)QjCauL<66dUwU zE8KokZw~2B0BfNEhlY zW~EqQ^B^=2TMw{ru0Khe3U?SP4Gr zR~(+m9|H)o!ejU>g81+889PVzzk3MjSGCxU4?bXqa z6zvMoJ9)InNBefPr~d(G;?dq+8bAgB?cdQJAH63>$1l-$|0e+`0Vo5Y9VS}s69A?F zr~*s{Py?6-FdaZ00PUM+0HF3Y!Pf$q1%P&*Ism!=X!nHj=YT&KKp(&Wzz_iLF~IJY zosTZdm;fvQSO{PWUWHyZ|n?Y2ol{IM49nL*p)Vt^p;+q6~0j=knRHzyB1(IDht!u=p!2 zpfe|5kph964F`CMa3q`p$IkyNP83ED2UHwo!C&fsCh7xDo3|B=7zB%yz#@yK5O9sZB?#O@W{ zHDSUXCNvf)e+va!(7rL+|E&M9aX03V?*QrU|0}j`Jf9}QYz}aM^toVyN3maRH=4hH uIlE>_3y466-dMj^JG!CsY>>nn%qXlqmalDueVA9gdc3Ahg#S