From 693a7ddc090565612170778a81e81328418f49ff Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Tue, 6 Dec 2011 04:31:04 +0000 Subject: [PATCH] Patch from Fabian from bug #52285 - support Smart Tags in XWPF paragraphs, with test (and fixing indents) git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1210774 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../poi/xwpf/usermodel/XWPFParagraph.java | 401 +++++++++--------- .../poi/xwpf/usermodel/TestXWPFSmartTag.java | 39 ++ test-data/document/smarttag-snippet.docx | Bin 0 -> 11862 bytes 4 files changed, 245 insertions(+), 196 deletions(-) create mode 100644 src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFSmartTag.java create mode 100644 test-data/document/smarttag-snippet.docx diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 6db3227e4..12a3c9a4e 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 52285 - Support XWPF smart tags text in Paragraphs 51875 - More XSSF new-line in formula support POIFS EntryUtils.copyNodes(POFS,POIFS) now uses FilteringDirectoryNode, so can exclude from copying nodes not just directly under the root POIFS Helper FilteringDirectoryNode, which wraps a DirectoryEntry and allows certain parts to be ignored diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java index 5190f2284..383fa13e6 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java @@ -41,6 +41,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRunTrackChange; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtContentRun; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtRun; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSimpleField; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSmartTagRun; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTString; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; @@ -73,87 +74,95 @@ public class XWPFParagraph implements IBodyElement { throw new NullPointerException(); } + // Build up the character runs runs = new ArrayList(); + buildRunsInOrderFromXml(paragraph); - // Get all our child nodes in order, and process them - // into XWPFRuns where we can - XmlCursor c = paragraph.newCursor(); - c.selectPath("child::*"); - while (c.toNextSelection()) { - XmlObject o = c.getObject(); - if(o instanceof CTR) { - runs.add(new XWPFRun((CTR)o, this)); - } - if(o instanceof CTHyperlink) { - CTHyperlink link = (CTHyperlink)o; - for(CTR r : link.getRList()) { - runs.add(new XWPFHyperlinkRun(link, r, this)); - } - } - if(o instanceof CTSdtRun) { - CTSdtContentRun run = ((CTSdtRun)o).getSdtContent(); - for(CTR r : run.getRList()) { - runs.add(new XWPFRun(r, this)); - } - } - if(o instanceof CTRunTrackChange) { - for(CTR r : ((CTRunTrackChange)o).getRList()) { - runs.add(new XWPFRun(r, this)); - } - } - if(o instanceof CTSimpleField) { - for(CTR r : ((CTSimpleField)o).getRList()) { - runs.add(new XWPFRun(r, this)); - } - } - } + // Look for bits associated with the runs + for(XWPFRun run : runs) { + CTR r = run.getCTR(); - c.dispose(); - - // Look for bits associated with the runs - for(XWPFRun run : runs) { - CTR r = run.getCTR(); - - // Check for bits that only apply when - // attached to a core document - // TODO Make this nicer by tracking the XWPFFootnotes directly - if(document != null) { - c = r.newCursor(); - c.selectPath("child::*"); - while (c.toNextSelection()) { + // Check for bits that only apply when attached to a core document + // TODO Make this nicer by tracking the XWPFFootnotes directly + XmlCursor c = r.newCursor(); + c.selectPath("child::*"); + while (c.toNextSelection()) { XmlObject o = c.getObject(); if(o instanceof CTFtnEdnRef) { - CTFtnEdnRef ftn = (CTFtnEdnRef)o; - footnoteText.append("[").append(ftn.getId()).append(": "); - XWPFFootnote footnote = - ftn.getDomNode().getLocalName().equals("footnoteReference") ? + CTFtnEdnRef ftn = (CTFtnEdnRef)o; + footnoteText.append("[").append(ftn.getId()).append(": "); + XWPFFootnote footnote = + ftn.getDomNode().getLocalName().equals("footnoteReference") ? document.getFootnoteByID(ftn.getId().intValue()) : document.getEndnoteByID(ftn.getId().intValue()); - - boolean first = true; - for (XWPFParagraph p : footnote.getParagraphs()) { - if (!first) { - footnoteText.append("\n"); - first = false; - } - footnoteText.append(p.getText()); - } - - footnoteText.append("]"); + + boolean first = true; + for (XWPFParagraph p : footnote.getParagraphs()) { + if (!first) { + footnoteText.append("\n"); + first = false; + } + footnoteText.append(p.getText()); + } + + footnoteText.append("]"); } - } - c.dispose(); - } - } + } + c.dispose(); + } } + /** + * Identifies (in order) the parts of the paragraph / + * sub-paragraph that correspond to character text + * runs, and builds the appropriate runs for these. + */ + private void buildRunsInOrderFromXml(XmlObject object) { + XmlCursor c = object.newCursor(); + c.selectPath("child::*"); + while (c.toNextSelection()) { + XmlObject o = c.getObject(); + if (o instanceof CTR) { + runs.add(new XWPFRun((CTR) o, this)); + } + if (o instanceof CTHyperlink) { + CTHyperlink link = (CTHyperlink) o; + for (CTR r : link.getRList()) { + runs.add(new XWPFHyperlinkRun(link, r, this)); + } + } + if (o instanceof CTSdtRun) { + CTSdtContentRun run = ((CTSdtRun) o).getSdtContent(); + for (CTR r : run.getRList()) { + runs.add(new XWPFRun(r, this)); + } + } + if (o instanceof CTRunTrackChange) { + for (CTR r : ((CTRunTrackChange) o).getRList()) { + runs.add(new XWPFRun(r, this)); + } + } + if (o instanceof CTSimpleField) { + for (CTR r : ((CTSimpleField) o).getRList()) { + runs.add(new XWPFRun(r, this)); + } + } + if (o instanceof CTSmartTagRun) { + // Smart Tags can be nested many times. + // This implementation does not preserve the tagging information + buildRunsInOrderFromXml(o); + } + } + c.dispose(); + } + @Internal public CTP getCTP() { return paragraph; } public List getRuns(){ - return Collections.unmodifiableList(runs); + return Collections.unmodifiableList(runs); } public boolean isEmpty(){ @@ -176,35 +185,35 @@ public class XWPFParagraph implements IBodyElement { out.append(footnoteText); return out.toString(); } - - /** - * Return styleID of the paragraph if style exist for this paragraph - * if not, null will be returned - * @return styleID as String - */ + + /** + * Return styleID of the paragraph if style exist for this paragraph + * if not, null will be returned + * @return styleID as String + */ public String getStyleID(){ - if (paragraph.getPPr() != null){ - if(paragraph.getPPr().getPStyle()!= null){ - if (paragraph.getPPr().getPStyle().getVal()!= null) - return paragraph.getPPr().getPStyle().getVal(); - } - } - return null; - } + if (paragraph.getPPr() != null){ + if(paragraph.getPPr().getPStyle()!= null){ + if (paragraph.getPPr().getPStyle().getVal()!= null) + return paragraph.getPPr().getPStyle().getVal(); + } + } + return null; + } /** * If style exist for this paragraph * NumId of the paragraph will be returned. - * If style not exist null will be returned - * @return NumID as BigInteger + * If style not exist null will be returned + * @return NumID as BigInteger */ public BigInteger getNumID(){ - if(paragraph.getPPr()!=null){ - if(paragraph.getPPr().getNumPr()!=null){ - if(paragraph.getPPr().getNumPr().getNumId()!=null) - return paragraph.getPPr().getNumPr().getNumId().getVal(); - } - } - return null; + if(paragraph.getPPr()!=null){ + if(paragraph.getPPr().getNumPr()!=null){ + if(paragraph.getPPr().getNumPr().getNumId()!=null) + return paragraph.getPPr().getNumPr().getNumId().getVal(); + } + } + return null; } /** @@ -212,14 +221,14 @@ public class XWPFParagraph implements IBodyElement { * @param numPos */ public void setNumID(BigInteger numPos) { - if(paragraph.getPPr()==null) - paragraph.addNewPPr(); - if(paragraph.getPPr().getNumPr()==null) - paragraph.getPPr().addNewNumPr(); - if(paragraph.getPPr().getNumPr().getNumId()==null){ - paragraph.getPPr().getNumPr().addNewNumId(); - } - paragraph.getPPr().getNumPr().getNumId().setVal(numPos); + if(paragraph.getPPr()==null) + paragraph.addNewPPr(); + if(paragraph.getPPr().getNumPr()==null) + paragraph.getPPr().addNewNumPr(); + if(paragraph.getPPr().getNumPr().getNumId()==null){ + paragraph.getPPr().getNumPr().addNewNumId(); + } + paragraph.getPPr().getNumPr().getNumId().setVal(numPos); } /** @@ -1027,18 +1036,18 @@ public class XWPFParagraph implements IBodyElement { * @param newStyle */ public void setStyle(String newStyle) { - CTPPr pr = getCTPPr(); - CTString style = pr.getPStyle() != null ? pr.getPStyle() : pr.addNewPStyle(); - style.setVal(newStyle); + CTPPr pr = getCTPPr(); + CTString style = pr.getPStyle() != null ? pr.getPStyle() : pr.addNewPStyle(); + style.setVal(newStyle); } /** * @return the style of the paragraph */ public String getStyle() { - CTPPr pr = getCTPPr(); - CTString style = pr.isSetPStyle() ? pr.getPStyle() : null; - return style != null ? style.getVal() : null; + CTPPr pr = getCTPPr(); + CTString style = pr.isSetPStyle() ? pr.getPStyle() : null; + return style != null ? style.getVal() : null; } /** @@ -1094,10 +1103,10 @@ public class XWPFParagraph implements IBodyElement { * @param run */ protected void addRun(CTR run){ - int pos; - pos = paragraph.getRList().size(); - paragraph.addNewR(); - paragraph.setRArray(pos, run); + int pos; + pos = paragraph.getRList().size(); + paragraph.addNewR(); + paragraph.setRArray(pos, run); } /** @@ -1108,65 +1117,65 @@ public class XWPFParagraph implements IBodyElement { * @param startPos */ public TextSegement searchText(String searched,PositionInParagraph startPos){ - - int startRun = startPos.getRun(), - startText = startPos.getText(), - startChar = startPos.getChar(); - int beginRunPos = 0, candCharPos = 0; - boolean newList = false; - for (int runPos=startRun; runPos=startText){ - String candidate = ((CTText)o).getStringValue(); - if(runPos==startRun) - charPos= startChar; - else - charPos = 0; - for(; charPos=startText){ + String candidate = ((CTText)o).getStringValue(); + if(runPos==startRun) + charPos= startChar; + else + charPos = 0; + for(; charPos= 0 && pos <= paragraph.sizeOfRArray()) { - CTR ctRun = paragraph.insertNewR(pos); - XWPFRun newRun = new XWPFRun(ctRun, this); - runs.add(pos, newRun); - return newRun; - } - return null; + if (pos >= 0 && pos <= paragraph.sizeOfRArray()) { + CTR ctRun = paragraph.insertNewR(pos); + XWPFRun newRun = new XWPFRun(ctRun, this); + runs.add(pos, newRun); + return newRun; + } + return null; } @@ -1196,27 +1205,27 @@ public class XWPFParagraph implements IBodyElement { int charBegin = segment.getBeginChar(); int runEnd = segment.getEndRun(); int textEnd = segment.getEndText(); - int charEnd = segment.getEndChar(); + int charEnd = segment.getEndChar(); StringBuffer out = new StringBuffer(); - for(int i=runBegin; i<=runEnd;i++){ - int startText=0, endText = paragraph.getRArray(i).getTList().size()-1; - if(i==runBegin) - startText=textBegin; - if(i==runEnd) - endText = textEnd; - for(int j=startText;j<=endText;j++){ - String tmpText = paragraph.getRArray(i).getTArray(j).getStringValue(); - int startChar=0, endChar = tmpText.length()-1; - if((j==textBegin)&&(i==runBegin)) - startChar=charBegin; - if((j==textEnd)&&(i==runEnd)){ - endChar = charEnd; - } - out.append(tmpText.substring(startChar, endChar+1)); - - } - } - return out.toString(); + for(int i=runBegin; i<=runEnd;i++){ + int startText=0, endText = paragraph.getRArray(i).getTList().size()-1; + if(i==runBegin) + startText=textBegin; + if(i==runEnd) + endText = textEnd; + for(int j=startText;j<=endText;j++){ + String tmpText = paragraph.getRArray(i).getTArray(j).getStringValue(); + int startChar=0, endChar = tmpText.length()-1; + if((j==textBegin)&&(i==runBegin)) + startChar=charBegin; + if((j==textEnd)&&(i==runEnd)){ + endChar = charEnd; + } + out.append(tmpText.substring(startChar, endChar+1)); + + } + } + return out.toString(); } /** @@ -1225,12 +1234,12 @@ public class XWPFParagraph implements IBodyElement { * @return true if the run was removed */ public boolean removeRun(int pos){ - if (pos >= 0 && pos < paragraph.sizeOfRArray()){ - getCTP().removeR(pos); - runs.remove(pos); - return true; - } - return false; + if (pos >= 0 && pos < paragraph.sizeOfRArray()){ + getCTP().removeR(pos); + runs.remove(pos); + return true; + } + return false; } /** diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFSmartTag.java b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFSmartTag.java new file mode 100644 index 000000000..f12e16805 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFSmartTag.java @@ -0,0 +1,39 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.xwpf.usermodel; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.apache.poi.xwpf.XWPFTestDataSamples; + +/** + * Tests for reading SmartTags from Word docx. + * + * @author Fabian Lange + */ +public final class TestXWPFSmartTag extends TestCase { + + public void testSmartTags() throws IOException { + XWPFDocument doc = XWPFTestDataSamples.openSampleDocument("smarttag-snippet.docx"); + XWPFParagraph p = doc.getParagraphArray(0); + assertTrue(p.getText().contains("Carnegie Mellon University School of Computer Science")); + p = doc.getParagraphArray(2); + assertTrue(p.getText().contains("Alice's Adventures")); + } +} diff --git a/test-data/document/smarttag-snippet.docx b/test-data/document/smarttag-snippet.docx new file mode 100644 index 0000000000000000000000000000000000000000..2c54f18d038c8bd11be22564c0384663cffba8ae GIT binary patch literal 11862 zcmeHtbyQu+v+lv&-3h^+;BJTD?(XjH65QS0B|vZp!6mr6b8v!(1h+@#-gz^VJL|3Y z|NZTCy4Trj_gCG!yJ}bWSEV2g0f`BK2EYOUfH#2k#}rO8FaUrQ3IMoR)S+K?7Pf>Y-Mz+c<{=lWkf1C1)9cD>A~?dnH@z)rP@g0PPtVF~ZK znk7m|3{H?VuB5Lo?;q^IfvO0?w0r0@n&)FDC;dUwE15QMESBd|3rt@05L6vAc53x; z!_t0}^})R~hV4O#wxo5_7xz}o%a9Uyu5Bvy+(sTCzF}^S1bC8e#gfZu_~J#b9pNE) z76`x9<8{YxM1g&T)`%R0e8Lpi&t{w}1e7iLubnlO)y~gM@!yPL?F6YgiMRGJwr@*a=Mp_u^<~k@2 z1FSzm?>mog8?aXjNOlK}H~2ThAdN|mMEXTUq~mjCovUAJ9VU^fG5dC8HjIMd)T%_!6;}BX=etSBZ*JcTH4t!F!3cM7w|>1 zR2FVGgbI@jkeRX87U_fxb*g-cCzOO!j!PILxQed)(mD839U?QMbVU=%()?VgX;6bh ze*syS6W5RIwY0N6}s1(^8=j zg;_g51p2tU&YU;2zU8V*CdQTnmmPyU$IC|A%ZCWysT&kzsm5p@ zSOpk#bHgX+j}H}r5-TMuUN8|$V3A)Rug$KJ)i6kU3#(X+u)-p!!;h#+rZ5#OlRKdY zc{5N_C>cdRS=n;%z_8=RmCray>mYU0wEcpyM`-CU4pCYOuM!R1_P zzS@zbuP+`k!@Y6F+Pl}$;!1c81y0f7OAy2QK3BBw+-h?QbV&t1oMlm`XsncW>}|MT zfvIrz!qY>?UGmG&Xeue!x~WFa)6^ZxtE&7@t$RKWV6J%8IQrMeXs<2)(7M0t+ljii z{VX%4pZK<*hUb>0EYmUW^opgrkWqP|7+>3jq|yFT!YNxaj34iy+zc9gQEgN_sN~b> zEw6jtr1)T-ldxJolDZQ*qSsSEfC=sXyBx@KoHXw+FqYJjba)Cw^r{}n!PzqT*pJj| zRb#?hfv7V;y0!5thC*gG`k?I0Tw0-RvqBd+202&!eHHz*uf)op!VLOOJ*PZ`*AFnl z5Ji;26{d4SCwtkJrYc732@(`==2M8La2-)=_q(1&^=Y_PsO3S4lsh4o)0cu`q8JU> zCazg%l{$@^s$UsrjAZGe#Tl)d=t_1(=%5EcN1h$6!h}EVlh(PWgl*(b3Pse-DkbGw z*SbHS+%i6Afv%W63>kP`6gWfknM%{8rLk((HR5S|b)q+A!N($^riW?v^&8mwgm$*S zmHXFqY4U{yr}oK5f>$wTuNgQ9#koT|#sIMZ6n?x_p8 zEf8@BuOZVHOQ4|^<;2EEJj8;WZ5a^(4w3DIYTTMgiXj<(1feDYE3=mnhtQ79%FY(E zENTZ%0!z!yD(;Y%K`0uTMo21XunuDUaAx4Hp3=^9y{$)nLI^Nkc>%f%S3$vMUhkWN zlSK!o56>281g=rYR&*m*HNR7%`IGAFedw0EWb$yTV?0vvCzbC^TjR8Adp?)-FM0gF ze)egSdo&qc@W!NUjTfWcjiljPRb(QbKLwjDgEiI$NdijR=(9Z*5yD66KJvN1QR~9D z#PZ-zJlEqG1d*yoxRK&TZJ#<#uT`T615lpZz#piS36(R8{%Syy%&{QPZ|? zax&xLi#@$l8KHi@%d!tKHBLGA3TgAVH%8`7fZTjJkKR_T;C?^til-p{5vFNSpnf3* zF^j>XB~o&pIl8$9#OeMGWIj}V-mtjAW`8=?IE#T(D%a}@U&LxT6pV zjt83a&DonGy_YaX&h8T%=L%crWA3xI`PuJ1QAweCWzOQIbnObEeZD?o+w(y$gJHK_ zpQ?UF@<>!(jI%D)2u|~jqgpyF-B6{DXoaBEx@iktj@yM|LEb`p2>c|Wf^Ypk&c;xk z$FZG`9y`Ptyh8L{3zT(Lvd?k`%7JCI!?WV%xoBh5?bzzXJjQ|h)HJ#FP5oc(I<={K zI~5uLP#^#Ru>Q1b7YkEcQ>H&^mS4W)L{r{=l>@U2Wyzh?#on084Ijoea;YwhL6)H3 zE{xbRB6UfjP-sTdmtbWl0IcteVHnnuZJ43<2kfPi2exG%8=|S2T5+YsuNr!Z97~ba z0_5m-I}b0!Bo&p5XM>1D=x=D`SM;8o#zo1$dkQ7ho~{#LGgW&^&}KuXe-Cc#ph?(h zin-Zri(FI%8U-5ok}W!}MkPxvrll66^l9kr(?}ZFjfq8LwMRP8px(kB4%k?>s%h&+ zvdHo!gK&p7@Z&1vrxcQYayV`CL)SzkdUcx{dQFPnlB87*7~$alwu z?si_H_9^uMS1U8}S{fZ);cCpI(zR-v8$a3mEq}2QcBr-Sh;&e4uD&6trqhPX~oLedAWxm{wwFfMa+a*OWP3$X=q#h;Npp{dOkFYGe3NK*8sViXyx2|4q+(0 z;Z*7tzkAUN&nGm{g1S(;0rv-xA+iZ~bEgmmfMlXE4Ow9+HLw&$eYWjEPQ8G5!P8TM z$Gbb)`eqzKQh!RNsRKnB5s#(l=IHLGCRb2w5G9vL@b=s7R^JYcpq9wQN89Y+JyoWz zr<2J+k|M!ezaN*=IP)Hw126Y|R9%mcp6sYI=9CjrOnHyjC&N)S0#8qE;#e25AH+>> z;|G9+S-Y^OKX5Tw)f_3P-a2+-^})_wMvjvwbM<6OLp|k268;>gG0Si#I!&T^phiR#Vr9D*E`gmnP4MWbXc>zCpv~~)lI*qGH&PlnHTKc?+7^W|*wrBk z@5+iIE&kmONXT-7D~E|JDS|oU{1^}qe|n~LwB(w0A1E!LcE~73Bts|ruJ|ogrdd?8 zC8A%3+Bd{!H=71;7+0>!8Lue9FA_uWW3<5}==`A3wCkOtrFBu5k;E#~?H{RZSL8H9=cyZ$YFnZK&FUoldpkpu{;cCLqO$cMO#Tz_?QDvIJO4V6i7 zZTRCgPA|KWYYw*_brj-R+GH(|^SFz!WIPtQ)e3k% zTs8QJ*bBkXkix6rYI-3*TFsDPFvcYEPwj3t)))p5aK4e@Z3UPJ%y>;cl-p$|4@S1a z1uRuuBna&07&W1{q*6XWuT5+hziS%+cWb@b=$H0{oNO=taSi0DFV4=U7Z$gznb)!N z8XB}*CFsk*Cf>npXLOwm(}cwl?L_A|k(V_~+GDkEcKW(TUf#zZ()5tsD@P;|qN1(w zo`Y>n1sj}xI=kv|RF?TzjK<*Y6MM0SW{|<{VljaC%O{E*mhY;|%Oz;GybqZvN&<>N z#6l2k%o6a#*r5wqb&4t<{S&2(*c6jPTxqp!>DqQSh3?(;2E&+G(yrTQ z)3e*}{eW}89@t&6HC_86E=*s2%NNo9AEh4vl0liV0UW$Jmb}wbXM6+> z;aJ9RSINhp?>m|cT-Z*^wZ;WP)^@zl!cLuiueM4`lip${IyAerBP?bZs9hx1TAlI< z+LoqG0BQTBfZTo72hk-zw(f5>KglKZOZB|t27{-3+v%p&L#BD+7_-|*83IZ?pKAoU z_Pg^cfzAfM@%67P)6D}LG!I;Q2*v_z2gPEN+d+kynv(>2jqGMt9PaC1Weg%Q$?0#3 z6Zo~C^p{EJI;psytHOpO!~HtzqEoRStU3%p%8tpiq@t-F3}NKO_!?7CHw;ic)xtj@ZQE+mKdFVA`Yg z5j3z8h+tt0IRWEiyBs@W!Oj(==P^cDzh)8ZPqYEP62qwkm@njmA4zj72hRHG4wJM> zd9K4!Z=&~3TgJ}}H=zq2EwRm32;tuVX|tN$yi;#uQxxRsNmh9V!4KQm#>gDvD(+>{ zcf+o8nX_S|k@dABOM1{s9*bvt_1*A3)PRJ{LVDst`s0!MVY zt!ZS0W^2f^0I4Hx;ynTama-BH(IJ82LlvIT&NZd}q1Ci0KfRY}fL;486ZF)&Nutj{ zpm1Ibtkt_~@SZ&;WjPEW8lB65S((KWP$5I`c_Q?)%Ul{6j>t zF7n_jk66QWP>aOEK0jBEmQdiQDI^PJlmI_%p!GJ=2A&bTdL@2(O!<5QZd0nq&9@ZU zV|r}=z*3IXD{=*6_F|MnDB(fz^eZj7 z8`A|n+0ScRdq8;uViRdh}jkw4un0ARqG5LTpafnr7og|9dO6AyQ zDZ*Q@aJWA67{MRP5zWHqc8alfiBO<+suiH(@Q&NulnN&}YV_rhlslQ#!8aVbcuuSL z7i4eqoH!rjrq=By9fW@tfpiY~M7Mj5F!f;Z>n_6vgtYDlkFNy(^_!%{ z<^?wOO1==hZm`k+++3TzMjTWOjciPReU#dhSM7?JQA5w^u888#TX02CBBVaDt?FDC z9ti|Mh}P~xT*gmzJ|S~Z(3v)9dnux#KN6$tOupPcZ^>330+37CxurjGMRj8~C!3`; z=+|VCxYPHlr7KZelbW9goEUm~I&L!1T5CmHboPXdYOXhrlf~AR3nR4GBjMs#R?QZc z$hSn{gRx^?MR|y*#h?)Ju9QW^ zhM>y9kNeyA&0Gcj_WBT$B-;28D^X7}k1pbc`SQG}FRV|7aUI!8;4;Y-pCr%e2jh*v#paC=EzcVw zeD)8DBuz;bQf^|o=01fckaXHK1-4vx4a`rDSG?(j8W~+Wh5|-X5HqFuCqyD zZn1_Q)anl0Bv~fcx6lq`^=Elcv6;`eNVGH3wT>l~$S~osbo-;neZWNBr$7+>vO9uK z?Kok48Y_>B>iIZwtGj#8{$JOB>R|yV;C20ZFadzq-+z%N?xse{e{)MG=^ekq41+gP zzF|Y&lKB+D;Q_(4rE(0ufF!Q|#M3AnQLWt0G{}m^5+%HH9QoF@w3lbkFIfy5#jw{h z1jw{+`-FkurB2kJSIl-@JHALu7|^RiVdZPQ-^h3qb$2>mB}vfiX}Dli3RfHTu3|#M z)HWxyV3>}w0#9fhb3ke;QR{Jj6?G)>-~{b5$7Ce5h&obq9I~mrTcw?2cd7bLx&_)r z(RwI@g&G*ynOCfnz->i;yFu6`oOB6ZgG;ru(_>v6z@jY!C4JjS-|)Ur=DSG1H~RQ|CzGmc-p+K{G;}VJiHEJXQGL~y>gG1$rm~+36$p)1AE0L#gH=zIaHfQr_bBuCPg|Ts5szib1-`-iPgEZ3#+9q3bS6}N5@0%PVWxF z#@8vS`Mp}lc()AuD)J3)sG1PK;vPAsk<>3WHw(E>?^;;MZw366xb??q^#XL}PleXF*LfZ5V)iI8M(zt{u0lN5K8>+Iil;}SHju1aZLD`&+H)QroA#9S2l zc!$~{Q!}fa%r%*8P@={RByg|*#>GLeVd`k7YuI*s!7;*-@4?%JT5NUDFuVDHVL5kz z6oj)awOL4{cn3qj%X7eFV>kuUUC1y`IzY(CiUYNaK}0iEHh5rx#?%Gidqfjnc+cOx zYEvGZNol<&r0!$jIBgf#%@H;ta~?*ACo~paYt9DQd!O4~R+CZIgV);Am>@4cQ8! zl|gV=liKO?Zaxfp?~pcjH+a#_3b$<}dXR74V7mLLl!>CLF}lB)@9E-V%0*hb%!oh+ zcZX@4$-CDi*qN>OHZv)@c$a=$zHGO6=mF}@`M5C54_Q6iqLokA_S?u)X3n>9Gym=) z^Le+@df)&6H%S2CSLET3xPr5br;X_!hqf^tY3Efg^iKQvN0?N(hOp?XdXthu85*~C zxs)VFvbKF0#rUr*Vpfv0u?LIxMPN!kA5O5po$R7&lTPV(U9giEU7g*K@J`+{_6{LE zdfH!TcbUU&!YzDy;X3DE)9-3M)Ks*2uwF&*VL9&zFTZ(s(0{%KncIPQPTMBd!BAVMz6OYqNANKOwTU%n~53 z?M`yl*k~TvctEOi-q($$2)S~`M`3T?=6B9t-h!M>S67FkM>w3erCY9lq<*`@bFnkz z8Ot*%>~dY)o~*tgA*&OJ z_3s$4nL^J2fd)Zg!nQDtk0O?mayQXHREN~w1Y7O>T~sWm?pfM^;iehlYlk8? z{c~8NO>Kk$5E5Y#JtyZRst$?0xg@BGAG4B9VVe0!8^6S?-MmWi6*8Z5|N)^=@Li ztjU4U!(D1v*^NGyZ$!v2Otb)GR%hg}Du(vAPFcEynaB3$;%X1}|-AMyYL z^w7*V&qhbyI7H(7T!v2KK-|f-rfg0&2V$(fBoq=jvEIUh7)0K<$kHE@^Stk)SFr@3 z724p309NYoctW9%%z2(OnG!IT`h<8IHd=C(xTQoH8D%zRavwY>@{7Jma{1%A)o|mC zxF!YkA}h4op(`v+)6CTiCptk^O&n2zjViE@v5Yv;4pO_VkAas^fdz`K$8&;}6I@aP zTtzn^^RIW;5od@zF>06A;+?Eg+u?+`LVlT&QcYz0rN*7zlk{{LstOKS>}8=uAX za(+qtH4g#3v2q_2lz*QIW)|%vPB7TAY(Q{ZI7aE~kPQ@nO^lWN#yIJE0w-A1I1){n z+3NuMOT#d!r8gi+IXH*fK0D~Md8;<1Ho=6F=Wnn|*s;fhg@uwTf;D~E?0+9Tr9qKt zAJzydLvKQgKNDoN9Y*R=KNAIN+9JS14@TVk#K+lfy~*SuO*MPPnz`WeX6?n%a=pAW zcZNr%;;~xquxO+iXt) zgS)D?+Civ6PTSXQX$CtG7X_&wMKQ7O;cyBnGZ$To4DPj>VLswe`6$KH$`X5LDgCQTa4#n}F5bL@&*VJkT|g^Kv?w-gtmc{?zhzFNtXw<35z-4W?v` z{}McEu0H(agUwrr{+Q|=i@dvN>t#$uJExG%pQdj`1Qov}n}7GVFc0gkZFqt2C0IVsVJKe?BXKSJ>=Wyg z=bQe?rI73G1;kH#x)ZF-1}jbQ-ByZ4?v27{)}YGU4a_w|@t*mTA>1{>0%1;{Gli?k z=%%Oych}uGQ`7Q>ciL)+b!Uayyu+nsd|&tcBUPcwhC+Rg)jh85b6WS4^u*GCTGErG z=c%217`fcw$1aE{i|~6cWFw(Wg|$oVDWQ-$F@$j zIv(<%OFGv4;-Co-e6qEhq-*5bntx{p!d@L-=jL1N<9c(0z?3Y-y@Hd~0>TWOeiG|G zn%vnz7p_52+K2ARezba!^uPv;19=|OPsIr~Lx%*yE?L&l+2YJ}vuH|;C%thtV;{eQ zO^^O6zOt}Gw@f1qDS;bz;QaqeK$DmSBEwt>`1;4F=>r~!2z6%kenFC?pBMS{L zZr~Rr<@d@S-s()IeB9!$hP(iD6NE>l7s~T@T)~Vkaz3pnmU!u|HC7+HfvL7`wuh}^U9=||Y9DiMXfF004o_P+cS^x>c)k2Z zh=GAKypkXPo`Czup8Rjszog?TNdHs7KT`z%h6Uukrl?ako#Y7G^GEA{~mz