From 70935d923c2767beaafa0c226bb93b79f26b966c Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 5 May 2021 14:59:38 +0200 Subject: [PATCH 1/3] Implement line color filter --- .../technology/tabula/CommandLineApp.java | 27 ++++++++++++- .../technology/tabula/ObjectExtractor.java | 8 +++- .../tabula/ObjectExtractorStreamEngine.java | 40 +++++++++++++++---- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/main/java/technology/tabula/CommandLineApp.java b/src/main/java/technology/tabula/CommandLineApp.java index 0228df4b..75a23cf2 100644 --- a/src/main/java/technology/tabula/CommandLineApp.java +++ b/src/main/java/technology/tabula/CommandLineApp.java @@ -44,6 +44,7 @@ public class CommandLineApp { private OutputFormat outputFormat; private String password; private TableExtractor tableExtractor; + private Integer lineColorFilter; public CommandLineApp(Appendable defaultOutput, CommandLine line) throws ParseException { this.defaultOutput = defaultOutput; @@ -51,6 +52,7 @@ public CommandLineApp(Appendable defaultOutput, CommandLine line) throws ParseEx this.pages = CommandLineApp.whichPages(line); this.outputFormat = CommandLineApp.whichOutputFormat(line); this.tableExtractor = CommandLineApp.createExtractor(line); + this.lineColorFilter = CommandLineApp.whichLineColorFilter(line); if (line.hasOption('s')) { this.password = line.getOptionValue('s'); @@ -195,7 +197,7 @@ private void extractFile(File pdfFile, Appendable outFile) throws ParseException } private PageIterator getPageIterator(PDDocument pdfDocument) throws IOException { - ObjectExtractor extractor = new ObjectExtractor(pdfDocument); + ObjectExtractor extractor = new ObjectExtractor(pdfDocument, lineColorFilter); return (pages == null) ? extractor.extract() : extractor.extract(pages); @@ -260,6 +262,23 @@ private static ExtractionMethod whichExtractionMethod(CommandLine line) { return ExtractionMethod.DECIDE; } + private static Integer whichLineColorFilter(CommandLine line) throws ParseException { + if (!line.hasOption("line-color-filter")) { + return null; + } + + Integer result; + try { + result = Integer.parseInt(line.getOptionValue("line-color-filter")); + } catch (NumberFormatException e) { + throw new ParseException("line-color-filter parameter must be a hexadecimal number"); + } + if (result < 0 || result > 0xFFFFFF) { + throw new ParseException("line-color-filter parameter must be at most FFFFFF"); + } + return result; + } + private static TableExtractor createExtractor(CommandLine line) throws ParseException { TableExtractor extractor = new TableExtractor(); extractor.setGuess(line.hasOption('g')); @@ -358,6 +377,12 @@ public static Options buildOptions() { .hasArg() .argName("PAGES") .build()); + o.addOption(Option.builder(null) + .longOpt("line-color-filter") + .desc("Only consider lines of this color to be lattice lines. Example: --line-color-filter DEADBE .") + .hasArg() + .argName("COLOR") + .build()); return o; } diff --git a/src/main/java/technology/tabula/ObjectExtractor.java b/src/main/java/technology/tabula/ObjectExtractor.java index 9f3f6a03..358d24d0 100644 --- a/src/main/java/technology/tabula/ObjectExtractor.java +++ b/src/main/java/technology/tabula/ObjectExtractor.java @@ -8,9 +8,15 @@ public class ObjectExtractor implements java.io.Closeable { private final PDDocument pdfDocument; + private final Integer lineColorFilter; public ObjectExtractor(PDDocument pdfDocument) { + this(pdfDocument, null); + } + + public ObjectExtractor(PDDocument pdfDocument, Integer lineColorFilter) { this.pdfDocument = pdfDocument; + this.lineColorFilter = lineColorFilter; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // @@ -20,7 +26,7 @@ protected Page extractPage(Integer pageNumber) throws IOException { } PDPage page = pdfDocument.getPage(pageNumber - 1); - ObjectExtractorStreamEngine streamEngine = new ObjectExtractorStreamEngine(page); + ObjectExtractorStreamEngine streamEngine = new ObjectExtractorStreamEngine(page, lineColorFilter); streamEngine.processPage(page); TextStripper textStripper = new TextStripper(pdfDocument, pageNumber); diff --git a/src/main/java/technology/tabula/ObjectExtractorStreamEngine.java b/src/main/java/technology/tabula/ObjectExtractorStreamEngine.java index 9907eca1..d1fb45bb 100644 --- a/src/main/java/technology/tabula/ObjectExtractorStreamEngine.java +++ b/src/main/java/technology/tabula/ObjectExtractorStreamEngine.java @@ -7,6 +7,7 @@ import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -15,7 +16,9 @@ import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; import org.apache.pdfbox.pdmodel.graphics.image.PDImage; +import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +26,7 @@ class ObjectExtractorStreamEngine extends PDFGraphicsStreamEngine { + private Integer lineColorFilter; protected List rulings; private AffineTransform pageTransform; private boolean extractRulingLines = true; @@ -32,8 +36,9 @@ class ObjectExtractorStreamEngine extends PDFGraphicsStreamEngine { private static final float RULING_MINIMUM_LENGTH = 0.01f; - protected ObjectExtractorStreamEngine(PDPage page) { + protected ObjectExtractorStreamEngine(PDPage page, Integer lineColorFilter) { super(page); + this.lineColorFilter = lineColorFilter; logger = LoggerFactory.getLogger(ObjectExtractorStreamEngine.class); rulings = new ArrayList<>(); @@ -130,16 +135,11 @@ public void strokePath() { } private void strokeOrFillPath(boolean isFill) { - if (!extractRulingLines) { + if (!extractRulingLines || filterPathByColor(isFill) || filterPathBySegmentType()) { currentPath.reset(); return; } - boolean didNotPassedTheFilter = filterPathBySegmentType(); - if (didNotPassedTheFilter) return; - - // TODO: how to implement color filter? - // Skip the first path operation and save it as the starting point. PathIterator pathIterator = currentPath.getPathIterator(getPageTransform()); @@ -191,6 +191,32 @@ private void strokeOrFillPath(boolean isFill) { currentPath.reset(); } + private boolean filterPathByColor (boolean isFill) { + if (lineColorFilter == null) { + return false; + } + + try { + PDGraphicsState state = getGraphicsState(); + PDColor currentColor; + if (isFill) { + currentColor = state.getNonStrokingColor(); + } else { + currentColor = state.getStrokingColor(); + } + return currentColor.toRGB() != lineColorFilter; + } catch (IOException e) { + System.err.println("Color conversion failed:"); + e.printStackTrace(); + return false; + } catch (IllegalStateException e) { + System.err.println("Cannot convert pattern color:"); + e.printStackTrace(); + return false; + } + // TODO: if the toRGB() method throws an exception, should the color be valid or not? + } + private boolean filterPathBySegmentType() { PathIterator pathIterator = currentPath.getPathIterator(pageTransform); float[] coordinates = new float[6]; From ffa6ebace64ce4c30754da05777b943e747469c7 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 11 May 2021 15:15:02 +0200 Subject: [PATCH 2/3] Parse --line-color-filter as hex --- src/main/java/technology/tabula/CommandLineApp.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/technology/tabula/CommandLineApp.java b/src/main/java/technology/tabula/CommandLineApp.java index 75a23cf2..c153b3a5 100644 --- a/src/main/java/technology/tabula/CommandLineApp.java +++ b/src/main/java/technology/tabula/CommandLineApp.java @@ -269,7 +269,7 @@ private static Integer whichLineColorFilter(CommandLine line) throws ParseExcept Integer result; try { - result = Integer.parseInt(line.getOptionValue("line-color-filter")); + result = Integer.parseInt(line.getOptionValue("line-color-filter"), 16); } catch (NumberFormatException e) { throw new ParseException("line-color-filter parameter must be a hexadecimal number"); } From 945d20e4564f8a8560cc388abe9e5cd0aeaa7c3f Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 11 May 2021 15:47:44 +0200 Subject: [PATCH 3/3] Add test for line color filter --- .../technology/tabula/TestObjectExtractor.java | 13 +++++++++++++ .../tabula/should_filter_rulings_by_color.pdf | Bin 0 -> 8955 bytes 2 files changed, 13 insertions(+) create mode 100644 src/test/resources/technology/tabula/should_filter_rulings_by_color.pdf diff --git a/src/test/java/technology/tabula/TestObjectExtractor.java b/src/test/java/technology/tabula/TestObjectExtractor.java index 9db7ad18..d2aa870a 100644 --- a/src/test/java/technology/tabula/TestObjectExtractor.java +++ b/src/test/java/technology/tabula/TestObjectExtractor.java @@ -83,6 +83,19 @@ public void testShouldDetectRulings() throws IOException { } } + @Test + public void testShouldFilterRulingsByColor() throws IOException { + PDDocument pdf_document = PDDocument.load(new File("src/test/resources/technology/tabula/should_filter_rulings_by_color.pdf")); + try (ObjectExtractor oe = new ObjectExtractor(pdf_document, 0)) { + PageIterator pi = oe.extract(); + + Page page = pi.next(); + List rulings = page.getRulings(); + + assertEquals(7, rulings.size()); + } + } + @Test public void testDontThrowNPEInShfill() throws IOException { PDDocument pdf_document = PDDocument.load(new File("src/test/resources/technology/tabula/labor.pdf")); diff --git a/src/test/resources/technology/tabula/should_filter_rulings_by_color.pdf b/src/test/resources/technology/tabula/should_filter_rulings_by_color.pdf new file mode 100644 index 0000000000000000000000000000000000000000..639a1aa82d6ccc1d2c5d236cd18e50b47822698a GIT binary patch literal 8955 zcmaia2UJr_*Y*V!kSaw$YG~4gR1!k(y$DDb5JD&d2_~Tz3rg=*5KyE^6QqhXsZymV z2#O*CqDb$ZKk@p$*L&~#t^cewWX_&BGkZV#*>e`_IYO7!G$g>1Qq)52dF_quwe1h7 zApj5n>u{Y~P7bJt#yAsP08paE7^vauPC(;;8t(Q4v>F2*%y$7T+>nxoG8@|KY)=)Mz<3m+AN)?N6`qac7;vUbx5K$mT#U z<5sm!;ZwE&_@OtEO)#4%jn8(H?W71X*mn``Pb^a4RAm_G4IC@S^xe|XYx!W{YN_Dr z=uk7!tSMg2TBx6;zSFxUFEe+S$GFaR-@WjV=EDa8Rt>fbjkXTCgii{BxB&{=Sa$$r z7F8PaEG~NU#j^4V1Jh6vZ9K{vm?5qKRkj_NbBxNl*$45HPs1l)9hl2Ao>zqBY+Zc~ zP83agxO!1Eg&K`<{FMM=kCZcNQmRNDC;~S3=?cM?!xk91H-1KycXa>7BC- z2xQr+QEW+{&`i|dPSDq?)b>v0W9*mJC07=t(M}yYU8POssH_EVc(CYD*|2yCunV|l z#$i;!&zi3@YSv20%)=CTHMh*xBmZ&TcI`u3n%bJWG0NoPAQ$*fKUSFUa^F$la?{Dj ziT-}%ywB^stf`&Msq_i`OD{e#$grPg<@!WsnCEUr#_$K0{9KZcYP9o%PZDbTm;R{A zMT{U;Uq<{;JoTCUj|^1M(F6aR%Adq3hUUog0);>C?8;1*^C>Q|??0h`^m%_5PPfi0 zpeO)3*RV!sijh)ECEMFOvM(=$yhCd=Tk`O(PO^VTIfJ`3R;$y(KspL zw+GjK5{!u^MJ7~~G`o>KM`bhbS*#VS`{$6z?W;5*&w$7Y0S&?M9w_5UGy%0QP01Z@;Y@>ix3JH)w%rLU&cw zH_@1(&Boj_fQer8EzdZm&!I_EO$GOn9L1}G?iWTc&NhcY<6h2Kh`fp5c5YI>aCE+L zZR=_G!7SY_T<&(pR4B7=&^ezj{r)q%@HZ5EU#BL@D(IT;xdq-oZO^vg(~fdHU#jf- z#o|;jmEBhYm3RA&J#@LPN43W5a4>#<)F}GU`kOQGv^}?z@rmj2?wR`oM?A(eiXmwh z;$6qIT!j~{h25oM7OlQohF)l3OAmO&V92m1)aA$4uuAQ6T+Xm%wn*c&ER3UzIk66; z={gqgAHs)sS;uykn7xpdO|7Y`d13X``u*f)j8IHe7TuOt6syd2wyUxy8Gcgu`77q8 z$!RI6EQ5{371u&RADspuoOJuuZ?((fe0Ou}iC(rKuK?x)jjgkBgb=za4=hf_|WXY~EVW$d$wNuf^NJXlUPy)w6# zXweTrI9C^|0X$Kho!gkgCtEe!RG@Ppy>p z_p%Zhp@Nq^lyV}iEmp1YcMed4h3Cf;AiyOmdVcq+9?&H6%Pk(qywW<|5{trgxC5)ow|FMLhaBX4mKV#T4QDqeJy^Zxnle- zUs6K3DBi?|C)er=&3a<-Ic1fSA_@ycWl#KVh^w6-=Anh>9hKaOa>k*U#X@D5Z)V5v z)0IPHaY_hQA!>We!r3R9ZPP}OG0;`pP}XdWo<&9WMH4Y*{zOX)Sh@|eH1t}4u#{21 zAU^B#N83&vF}Fttky!SHr-pX{2TWG7hIfJv7gyas;oj0$wb}s1oyY7`&ckWF_Wgz2 z>N>*?o~Er-j|+xzLKj(OjxhC$=W;BaC90(-tIblq;59V*W#Y2)Vz&h;9DS}Tx1%w4 z4RqV+SRds{46SEH-}B0SOm?P``KyrpY^(XZ0Qtxq0_n?QPT9C8{A$VwhFO3p|ygwk+RisY6mN zal;!~ckDcA$ZT=XW#(#nRxc-?2Z?;ud75g|rcXv=PtHki2!h;Ctm=lGzv>E}6Q1bm z3r*9b-B7j4DGJi^mK32U6R_jIeYGvNUV~=o%z7wm#m+j#(usUlBf-Ji9Xn=EIWgQ^xsgXUHRpZlhg{paf~Nrq z0lpNmUfHu`TvvL#jV(o>js4x(@8gG!gZi{7Ujn%2p4?jh#7KUnAS<46H5PO>oeylp zm(@*18|xXt`06e^4C0{z*FtZyQ(S;(R`%w22_gx4VyFujE5A|cWViba3X*Gj38q+t z4j0EnYIcA2Ol(gaVppTn7r3CJneaa?mjvlB6|=lhNhf()Lvre2yvAn5w zuM5AFlt#m_tx+}x`hFw^J|p`2WG-Z2QFL^y1#4s)gp4`39E=21_#dK{%Zr;Xll#b) z%5?Xghm4AuI!St>^JP^1oo1M_tl8c^^XF#N=o*y0etDK9m8OaOf!I0jLgw*B_vSwC z*8kS#MoXdqC1amzoV?QXIQ1`^g!C`) z_r|-zzNvlg=%4WQHc)tGZe~6piGC`EE^!*HFm*C`lgwoQLZ&^_uwbs_c}v$+JTct0_JAxe*Y=TIGB3?dU}j5Xh3~-oA)?#lhu9V`CLKh z9G(VB)BNl@zHg29qpW+>uE6v??sqKT%hWeg)fv+o)*AW>cQI*aKE}F1qBWAtP$Q1> zdAZ|xJ=aWZFk_xFVbNdDiH2nszFXtM!Nc^Z8D`_P=qCQ zC%=kv3N-?~HYlYPZ5S>ci+7us_mA4#T`OsjtzDG$)>9oXb88B_DPz_p$4obvy3F~Z zz#kg?qvk{6vx{cec&eWNczJrFVt85)Q7kpF8>3qYu1w{s;TU)KtT(A*99>)#Y*KhL zH^!N0ktQp;QGGAks+){v!hpAbd3;|fw@X{Z5BLlZBR1@U_xSu)kVnh1wJRn`Epj_EQZYg^O6%J6 z!wHAa(yBtU50|Hds^Mib^XVm~ox2`STEPON?gqm@lB6Y}0{Z!;^^>Cef{#&2TvqZZ zTzz_ft6rrHqw{M~+F@JA(+$y8!?CTVy=&*6-1}~-k<+X2D%*bWTr}=E{(i~FlAAaB zJWLVI$1j?^HJac*@L#i**d*O~e>{H*mw2YRJ>vtBU#@c$!QDqnwC8=h!u6_n)auZQ z6OW2{+nZ#6eF@EMVwU` hPG^!7)WzH!6-FXi$1iU9HL8yhzRzJ&m*Ja5wm+mS8D zy)9?_D(l;%lW#yKb$?{*YMOPqxhu@o218c@S^Iwa#0^U2>GYQPC23H1o`^sP=eY-+}Ak!YyM2|5(f)bK_*G z8&yuOunVsH#;K#uvz{sE{VdCt8})diGc3z6FWHifu$ULwJq$y`XZux&K$9pfFLv|*XtqJm4$9>3S!X=xN2-E1qK7_G^> z?wxa0)jZvOT{1_z@1-8Atx#S_itf~Sd!Oob_6s}{5bH+Hqcd$EHVBsx?dg36jnNqEO--9yC=`0|rvHp&-9)_E zEUplJr)%!@=kN>xQ7#Lvxw}ANYzlKx=3)UXnfYBh4q|0s)!TC$2eB~d?Tu*8%mfM- zissm;9-LWXcT4_CR+Hk!)P%?J@;jyhE~b$=@FVm-S=<|-?an5gW7QP26}?7TI!do9 zuBWhC#J;(1k!19WQ12zPUjfm+_yv-%aS?Kzzd=9mgIceCUcZ`|-Bs_hEHf$_5sZU- zw$!e<5eDT^sEf#C7a2udR~d-qB!7YP%Rd;zR}m6A$Iz zGusKq#+*pJ_R!rdR=Ao*|8Rb<;$`c+N`~!>Q#W*S&7|H+lsu!yY5&y6;U!H0Qf3 zC^;716|XBlQyh_%=Bt$c#xs{OMk04GctwDLp>}ax@Kc(M!*QSZO7KwiyvGeq1-w(= ze(ug>;707+vsUY|3k=Q2T|Kp$Jjm*=!Oo8#+J!Y;J>M}rG(7lKNPFDQ_M+eB_7dmHDEF)Uqs80< zA4S2Q6*^#x%i#e6%UriS#bHwygqL3WZf={hEPa3QLfp^`pz7rOkS*rUb#vXhz`z+5TU<~`Wubc)1set8fEo^9jMOc9lL%b1}3&f#Sy z^Efm8>DTpj{N`g>;V`KOg`Ja5HMA}dLmjV&Kbd%H&onN!;$o5wO?s4Z{bC6_(~zy& z6|Fk-`1p0Tl)k8wxmfl&l;2>&hs(yCNswlc9#-pzE=GRTTZo}8n?Iv+fjX}Os zOK~Z$?VHEh?4GYfJAjYgYFyN#Ov@D+d@`2**+9>zs*M&Ei*#=K;O>-^IHFc(Tc`U< zu!Nw^k;?WF*U!9?oCG#;6mBcXe>sGq@>Z}h5lMobaB#>hg^pg`zUQNO6#0bl({^ET ztb%97m+;+o27|?;uVoCuD`Or3-9^wf>F<0Vf@6MDxTmgE(Y5LeO#-flsx8Ov%CFqO+Wr-Aex2oy(?=+dB_WGA>4$a zZ_)KzCdG3V8O+ldIdhQIwa$a!WO=>~UrEgifI-nph3E8cm2~7Tc2t^Lkcb4vb=iV= zztkpr#W+vb$*svaQ<+h#XwB?ls`KM)%!cx(-rL4?plHLmZFmNRb9$be>2+x-#c>&x z!iGwQF3rBYrGkHg4^t_rm|8Y_+eztAQ3c$U zzZP~si%_XB&K7+JaS+rv$h@)o$caKe77?HqitAAdLiOW zdycU}{W7jer6GIjU1mb6xmqHjvxQJVm-AIA<5Eo96!iAr@Pz7B8f^n z$bGD5o@$nyYRA=}2NhZ8QDEKLJv^2DjV|!{&ItSNlNO*z*P#&$g=(OtYk=s=ddFc7 z`^wq9U35~YL{pP;`9tcaW0(Gr-f4?J+OIY!aIgd^m&-k{khyTgBJv(}OcxwnB+R-I zH9f>17ml}gq{(f)?FnuHpWCF^jp0ey+3UP!M@1O`UrT$xD!tHJb5~`W?i((og?(l@ zX1`Mj^!8QMa|hc^&5))@8U@zXj@cB3UDWWScWcQGNiFP+!_{vCy z0!QpFxchw61E83L#aTe7=LtoNR*h2Wnr9xN;xl@M%5+e90lf7{{`q`cI9rYk0&M@ZaO zPe;^WvSGx+-K3*Dk6$d{!Z&PJf7onkSz%EM+i-JUIyM5JU_cvd&Jwv7kx?pAcDX=_ zJtX12g&4(wZaeWmCst?ZTh((P?9Vy)Vki`MSJ9s+d1mFc{P<`Y$4?s-pH~tEwmf=y zsnPsP+GA=tKZ|H9d#)AD03K^E_APIfdgN$JRGfv}#0ez>E(Z@=_mB z0{2}s8hE&}7ShAKXH@{k4GI=R>Z>_#d?}#f{jT`ycJW#s zAt2x3T_XfzzPm~3=a0JO{8@;*_*E@oT>l%bxo`f!s5^z>ej>*anGBy@q8I#)KE*xM z?=|353S_F<771___?UjXrzW@ALuO!6{SwpYNXgrahWU(n?Xc~$n>sEUv%5zyvGnz! ztWP1VM}%xOE;1`-o(!Xw#A)^zZ*rlBm14*?s+44J_qLo&9B+H99zYmtu8Rj z%Zv!-D3|W^3D@|MTe&J&S!vb+F>&ZTsy>+nrKi|hq0epxzP0XAY^_UkU^a20dlMeG zt38V}GP19>F|0}*SC8O`JvxQsiNsnwiyD$-F>$!?sDWSUha~yN>X}N~+$6>P>ivS{ z^W~K$Iw|-2j$e7>t};e`erv;+6^;G_l8iDopo|2sc(VIDjN%!_@Nu}ofqa^ef#_sQQkwzsr?UA(q;Qo=U!nm)5mrO zWy-Kw^7!pZ%ds0{wo7t9`YVI?d!BDGuZP9PaVjqCqJ#*6yJ3oiLuUcPAx~G?icLVl z46VIvaFE1@HQvSNrYBFgY(j0h6kk_LPx;?#l$M#=uPhnbZ)@qVj(sTjG@-luGnFhv zVS$|&Y${VGeBH1*lj`9(?#ncDn4wE$worlqWvrH_#SvrWfpkJF^U;mRqr7GE{>vh= zB1@_4+8ikcy6zLx@7w!t7v>kH4;LCL4kXK)9`HdZH>s2l6SnRk@RJjfA@|f<)#|GSQ4bXCY_gT_uM-uYl z$EIWf+WFnP5a@(~wxkGh=C{;lxN~LB&WzD)R>Vx06(EF@G z7A?9rH58w|d;54XGn?*|D;eaz_LW^P&z;rE?Z=M`7zccXz4zmy(V+-SBrecv}VfpFP zZbOW=4iTbij7w^p0%C8_Ww%era~9Yw{)g5DIsDY%tNH-D?Fqj&s zYVWCqc6D|k4kD1$KobJm!weuze1x<-QBXre03>zBn3|}y{70^l{wsoC`WplZ{r{+M zHVJ*0y8xEZ9l0}o-yQgEoio^zicFZvCW&jWbn#Jf^CVm^3GjsFy ze;$s$CCk*rPNq>D!K1M7y7Obf7F++LOX$4fC%1;1Hs1c24-TcWd1dyzy82+)^7!;h z14YI)t}x?W{%fQ1ly6GTYV}yC_Z1XTiL3$H*jgdchgApz4p~u@pB2&|KB{US=DCbi zRX}IsXJOk{PXjjssr?Uw=>yjT<1!z8y_1>qmCy4_Jw2A+?i5Ns?)F-X{xv(pesZ?) z&t=S0#Z#SwRH9SSH)h)JO-~+nFi;+_5nfslV8{a>yCO}rckB;s7b zd7*z=RqXL-Qo8<6%DCe21XUM%9El2g_P_dII5p7R)sf(Ww?;^T0B|@QASLx@gCGz; z`~S3&?ti!anomvowfU1We=!+ABDyg(@Cw!x<4O`^0VLl1jYwe7U-4;x0iQK%zJdyn^;6*!<4V-=6x3&t-dOH1M)Lj>LKxX#t=y8jtnD zq0o2$m^9fKsE>AZwI>x5X#**wlq9j7kWd5&gpfwS0Man9q_i{=0f!MaR1cHJUe%3z0`p<|hEUk!J{E7?!viT1{KYjY$NfKhDLyWlBf6*kKL;r-3`Y#NL z5Bvm?v^t3wx~`6RYrxNKNw>cu{T+vY?xAW=JioBcM4z0AIr^D1YM`Mf8l#LNorHge z005F=z<9a4|M>?*T-(r#;O>ec<@G1le*^6|m06YeO#tz*{RJ0{1Q$tARwXtl5hfzN z9KBHJ|BjRFNMeH!`9N$WSYbq%;qYILG)7v3XeSPXphP=Fo%jnQuJv;(qR(o?j}k=G zWFR027$gM-gQZ{)DQR&KL>L4T7X1wiUmV(r8U!F#Cvl};7XU6L1(gCg0e;yK2q+Ov z(gnc$#|8p{iS_dj8w^gw?(a5fqMQG;fnd`Ang^GL{SVu}pGAPA{8F( zL%0L$PsaXD}xarbu}rNiWIT<;czgq?m