From 3c6ce72dda6d58f29370b0ee660e5194d097d327 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Fri, 21 Oct 2011 13:17:33 +0000 Subject: [PATCH] initial support for rendering pptx slides into images with a PPTX2PNG command-line utility git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1187328 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + src/ooxml/java/org/apache/poi/util/Units.java | 3 +- .../xslf/model/CharacterPropertyFetcher.java | 45 + .../xslf/model/ParagraphPropertyFetcher.java | 52 + .../poi/xslf/model/PropertyFetcher.java | 48 + .../xslf/model/TextBodyPropertyFetcher.java | 52 + .../poi/xslf/usermodel/XSLFAutoShape.java | 81 +- .../poi/xslf/usermodel/XSLFBackground.java | 70 + .../apache/poi/xslf/usermodel/XSLFColor.java | 54 + .../xslf/usermodel/XSLFConnectorShape.java | 36 + .../poi/xslf/usermodel/XSLFFreeformShape.java | 46 +- .../poi/xslf/usermodel/XSLFGraphicFrame.java | 84 +- .../poi/xslf/usermodel/XSLFGroupShape.java | 79 + .../poi/xslf/usermodel/XSLFPictureShape.java | 47 + .../xslf/usermodel/XSLFPresetGeometry.java | 637 + .../poi/xslf/usermodel/XSLFRenderingHint.java | 42 + .../apache/poi/xslf/usermodel/XSLFShadow.java | 106 + .../apache/poi/xslf/usermodel/XSLFShape.java | 75 +- .../apache/poi/xslf/usermodel/XSLFSheet.java | 121 +- .../poi/xslf/usermodel/XSLFSimpleShape.java | 589 +- .../apache/poi/xslf/usermodel/XSLFSlide.java | 46 +- .../poi/xslf/usermodel/XSLFSlideLayout.java | 46 +- .../poi/xslf/usermodel/XSLFSlideMaster.java | 22 + .../apache/poi/xslf/usermodel/XSLFTable.java | 28 - .../poi/xslf/usermodel/XSLFTextBox.java | 16 +- .../poi/xslf/usermodel/XSLFTextParagraph.java | 446 +- .../poi/xslf/usermodel/XSLFTextRun.java | 136 +- .../poi/xslf/usermodel/XSLFTextShape.java | 275 +- .../apache/poi/xslf/usermodel/XSLFTheme.java | 202 +- .../org/apache/poi/xslf/util/PPTX2PNG.java | 101 + .../poi/xslf/usermodel/TestXMLSlideShow.java | 1 - .../poi/xslf/usermodel/TestXSLFAutoShape.java | 31 +- .../xslf/usermodel/TestXSLFSimpleShape.java | 105 + .../poi/xslf/usermodel/TestXSLFTextBox.java | 6 +- .../poi/xslf/usermodel/TestXSLFTextShape.java | 609 + .../poi/xslf/usermodel/TestXSLFTheme.java | 39 + .../xslf/usermodel/presetShapeDefinitions.xml | 19915 ++++++++++++++++ test-data/slideshow/layouts.pptx | Bin 0 -> 62901 bytes test-data/slideshow/shapes.pptx | Bin 67435 -> 68822 bytes 39 files changed, 23958 insertions(+), 334 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xslf/model/CharacterPropertyFetcher.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/model/PropertyFetcher.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFBackground.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFColor.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPresetGeometry.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShadow.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java create mode 100644 src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java create mode 100644 src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTheme.java create mode 100755 src/resources/scratchpad/org/apache/poi/xslf/usermodel/presetShapeDefinitions.xml create mode 100755 test-data/slideshow/layouts.pptx diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 06bd108fd..aa8117fee 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + support for converting pptx files into images with a PPTX2PNG tool 52050 - Support for the Excel RATE function 51566 - HSLF fix for finishing parsing the picture stream on the first non-valid type 51974 - Avoid HWPF issue when identifying the picture type diff --git a/src/ooxml/java/org/apache/poi/util/Units.java b/src/ooxml/java/org/apache/poi/util/Units.java index 77652004e..3a400399c 100755 --- a/src/ooxml/java/org/apache/poi/util/Units.java +++ b/src/ooxml/java/org/apache/poi/util/Units.java @@ -13,7 +13,8 @@ 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.util; +==================================================================== */ +package org.apache.poi.util; /** * @author Yegor Kozlov diff --git a/src/ooxml/java/org/apache/poi/xslf/model/CharacterPropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/CharacterPropertyFetcher.java new file mode 100644 index 000000000..09674abfe --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/model/CharacterPropertyFetcher.java @@ -0,0 +1,45 @@ +/* + * ==================================================================== + * 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.xslf.model; + +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; + +/** + * + * @author Yegor Kozlov + */ +public abstract class CharacterPropertyFetcher extends ParagraphPropertyFetcher { + + public CharacterPropertyFetcher(int level) { + super(level); + } + + public boolean fetch(CTTextParagraphProperties props) { + if (props.isSetDefRPr()) { + return fetch(props.getDefRPr()); + } + + return false; + } + + public abstract boolean fetch(CTTextCharacterProperties props); + +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java new file mode 100644 index 000000000..65c19e6d7 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java @@ -0,0 +1,52 @@ +/* + * ==================================================================== + * 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.xslf.model; + +import org.apache.poi.xslf.usermodel.XSLFSimpleShape; +import org.apache.xmlbeans.XmlObject; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; + +/** + * + * @author Yegor Kozlov + */ +public abstract class ParagraphPropertyFetcher extends PropertyFetcher { + int _level; + + public ParagraphPropertyFetcher(int level) { + _level = level; + } + + public boolean fetch(XSLFSimpleShape shape) { + + XmlObject[] o = shape.getXmlObject().selectPath( + "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' " + + "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " + + ".//p:txBody/a:lstStyle/a:lvl" + (_level + 1) + "pPr" + ); + if (o.length == 1) { + CTTextParagraphProperties props = (CTTextParagraphProperties) o[0]; + return fetch(props); + } + return false; + } + + public abstract boolean fetch(CTTextParagraphProperties props); +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/model/PropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/PropertyFetcher.java new file mode 100644 index 000000000..d446ccedd --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/model/PropertyFetcher.java @@ -0,0 +1,48 @@ +/* + * ==================================================================== + * 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.xslf.model; + +import org.apache.poi.xslf.usermodel.XSLFSimpleShape; +import org.apache.poi.util.Internal; + +/** + * Used internally to navigate the PresentationML text style hierarchy and fetch properties + * + * @author Yegor Kozlov +*/ +@Internal +public abstract class PropertyFetcher { + private T _value; + + /** + * + * @param shape the shape being examined + * @return true if the desired property was fetched + */ + public abstract boolean fetch(XSLFSimpleShape shape) ; + + public T getValue(){ + return _value; + } + + public void setValue(T val){ + _value = val; + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java new file mode 100644 index 000000000..b66a1e5f7 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java @@ -0,0 +1,52 @@ +/* + * ==================================================================== + * 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.xslf.model; + +import org.apache.poi.xslf.usermodel.XSLFSimpleShape; +import org.apache.xmlbeans.XmlObject; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBodyProperties; + +/** + * Created by IntelliJ IDEA. + * User: yegor + * Date: Oct 21, 2011 + * Time: 1:18:52 PM + * To change this template use File | Settings | File Templates. + */ +public abstract class TextBodyPropertyFetcher extends PropertyFetcher { + + public boolean fetch(XSLFSimpleShape shape) { + + XmlObject[] o = shape.getXmlObject().selectPath( + "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' " + + "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " + + ".//p:txBody/a:bodyPr" + ); + if (o.length == 1) { + CTTextBodyProperties props = (CTTextBodyProperties) o[0]; + return fetch(props); + } + + return false; + } + + public abstract boolean fetch(CTTextBodyProperties props); + +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFAutoShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFAutoShape.java index feb77a404..e37259459 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFAutoShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFAutoShape.java @@ -21,6 +21,8 @@ package org.apache.poi.xslf.usermodel; import org.apache.poi.util.Beta; import org.apache.poi.util.Units; +import org.openxmlformats.schemas.drawingml.x2006.main.CTGeomGuide; +import org.openxmlformats.schemas.drawingml.x2006.main.CTGeomGuideList; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetGeometry2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; @@ -39,8 +41,12 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.Matcher; /** * Represents a preset geometric shape. @@ -49,6 +55,7 @@ import java.util.List; */ @Beta public class XSLFAutoShape extends XSLFTextShape { + private static final Pattern adjPtrn = Pattern.compile("val\\s+(\\d+)"); /*package*/ XSLFAutoShape(CTShape shape, XSLFSheet sheet) { super(shape, sheet); @@ -83,44 +90,6 @@ public class XSLFAutoShape extends XSLFTextShape { return ct; } - /** - * Specifies a solid color fill. The shape is filled entirely with the specified color. - * - * @param color the solid color fill. - * The value of null unsets the solidFIll attribute from the underlying xml - */ - public void setFillColor(Color color) { - CTShapeProperties spPr = getSpPr(); - if (color == null) { - if(spPr.isSetSolidFill()) spPr.unsetSolidFill(); - } - else { - CTSolidColorFillProperties fill = spPr.isSetSolidFill() ? spPr.getSolidFill() : spPr.addNewSolidFill(); - - CTSRgbColor rgb = CTSRgbColor.Factory.newInstance(); - rgb.setVal(new byte[]{(byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()}); - - fill.setSrgbClr(rgb); - } - } - - /** - * - * @return solid fill color of null if not set - */ - public Color getFillColor(){ - CTShapeProperties spPr = getSpPr(); - if(!spPr.isSetSolidFill() ) return null; - - CTSolidColorFillProperties fill = spPr.getSolidFill(); - if(!fill.isSetSrgbClr()) { - // TODO for now return null for all colors except explicit RGB - return null; - } - byte[] val = fill.getSrgbClr().getVal(); - return new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2]); - } - protected CTTextBody getTextBody(boolean create){ CTShape shape = (CTShape) getXmlObject(); CTTextBody txBody = shape.getTxBody(); @@ -132,4 +101,38 @@ public class XSLFAutoShape extends XSLFTextShape { return txBody; } -} \ No newline at end of file + int getAdjustValue(String name, int defaultValue){ + /* + CTShape shape = (CTShape) getXmlObject(); + CTGeomGuideList av = shape.getSpPr().getPrstGeom().getAvLst(); + if(av != null){ + for(CTGeomGuide gd : av.getGdList()){ + if(gd.getName().equals(name)) { + String fmla = gd.getFmla(); + Matcher m = adjPtrn.matcher(fmla); + if(m.matches()){ + int val = Integer.parseInt(m.group(1)); + return 21600*val/100000; + } + } + } + } + */ + return defaultValue; + } + + @Override + protected java.awt.Shape getOutline(){ + java.awt.Shape outline = XSLFPresetGeometry.getOutline(this); + Rectangle2D anchor = getAnchor(); + + AffineTransform at = new AffineTransform(); + at.translate(anchor.getX(), anchor.getY()); + at.scale( + 1.0f/21600*anchor.getWidth(), + 1.0f/21600*anchor.getHeight() + ); + return outline == null ? anchor : at.createTransformedShape(outline); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFBackground.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFBackground.java new file mode 100644 index 000000000..646c5a40a --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFBackground.java @@ -0,0 +1,70 @@ +/* ==================================================================== + 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.xslf.usermodel; + +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.openxmlformats.schemas.presentationml.x2006.main.CTBackground; +import org.openxmlformats.schemas.presentationml.x2006.main.CTBackgroundProperties; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * Background shape + * + * @author Yegor Kozlov + */ +public class XSLFBackground extends XSLFSimpleShape { + + /* package */XSLFBackground(CTBackground shape, XSLFSheet sheet) { + super(shape, sheet); + } + + public void draw(Graphics2D graphics) { + Dimension pg = getSheet().getSlideShow().getPageSize(); + Rectangle anchor = new Rectangle(0, 0, pg.width, pg.height); + CTBackgroundProperties pr = ((CTBackground) getXmlObject()).getBgPr(); + if (pr == null) return; + + XSLFTheme theme = getSheet().getTheme(); + if (pr.isSetSolidFill()) { + Color color = theme.getSolidFillColor(pr.getSolidFill()); + graphics.setPaint(color); + graphics.fill(anchor); + } + if (pr.isSetBlipFill()) { + + String blipId = pr.getBlipFill().getBlip().getEmbed(); + PackagePart p = getSheet().getPackagePart(); + PackageRelationship rel = p.getRelationship(blipId); + if (rel != null) { + try { + BufferedImage img = ImageIO.read(p.getRelatedPart(rel).getInputStream()); + graphics.drawImage(img, (int) anchor.getX(), (int) anchor.getY(), + (int) anchor.getWidth(), (int) anchor.getHeight(), null); + } + catch (Exception e) { + return; + } + } + } + } + +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFColor.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFColor.java new file mode 100644 index 000000000..614262a9e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFColor.java @@ -0,0 +1,54 @@ +/* + * ==================================================================== + * 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.xslf.usermodel; + +import java.awt.Color; + +import org.apache.poi.util.Internal; +import org.openxmlformats.schemas.drawingml.x2006.main.CTColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTOfficeStyleSheet; + +public class XSLFColor { + private final CTColor _ctColor; + + XSLFColor(CTColor ctColor){ + _ctColor = ctColor; + } + + @Internal + public CTColor getXmlObject() { + return _ctColor; + } + + public Color getColor(){ + return getColor(0xFF); + } + + public Color getColor(int alpha){ + Color color = Color.black; + if(_ctColor.isSetSrgbClr()){ + byte[] val = _ctColor.getSrgbClr().getVal(); + color = new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2], alpha); + } else if (_ctColor.isSetSysClr()){ + byte[] val = _ctColor.getSysClr().getLastClr(); + color = new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2], alpha); + } + return color; + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFConnectorShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFConnectorShape.java index d93c1351f..6eb49b4b9 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFConnectorShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFConnectorShape.java @@ -32,6 +32,10 @@ import org.openxmlformats.schemas.drawingml.x2006.main.STLineEndLength; import org.openxmlformats.schemas.presentationml.x2006.main.CTConnector; import org.openxmlformats.schemas.presentationml.x2006.main.CTConnectorNonVisual; +import java.awt.*; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; + /** * * Specifies a connection shape. @@ -192,4 +196,36 @@ public class XSLFConnectorShape extends XSLFSimpleShape { return len == null ? null : LineEndLength.values()[len.intValue() - 1]; } + @Override + public void draw(Graphics2D graphics){ + java.awt.Shape outline = getOutline(); + + // shadow + XSLFShadow shadow = getShadow(); + if(shadow != null) shadow.draw(graphics); + + //border + Color lineColor = getLineColor(); + if (lineColor != null){ + graphics.setColor(lineColor); + applyStroke(graphics); + graphics.draw(outline); + } + } + + @Override + protected java.awt.Shape getOutline(){ + Rectangle2D anchor = getAnchor(); + double x1 = anchor.getX(), + y1 = anchor.getY(), + x2 = anchor.getX() + anchor.getWidth(), + y2 = anchor.getY() + anchor.getHeight(); + + GeneralPath line = new GeneralPath(); + line.moveTo((float)x1, (float)y1); + line.lineTo((float)x2, (float)y2); + + return line; + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFreeformShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFreeformShape.java index a0b3ab055..594aa277b 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFreeformShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFreeformShape.java @@ -111,34 +111,45 @@ public class XSLFFreeformShape extends XSLFAutoShape { return numPoints; } + /** + * Gets the shape path. + *

+ * The path is translated in the shape's coordinate system, i.e. + * freeform.getPath().getBounds2D() equals to freeform.getAnchor() + * (small discrepancies are possible due to rounding errors) + *

+ * + * @return the path + */ public GeneralPath getPath() { GeneralPath path = new GeneralPath(); Rectangle2D bounds = getAnchor(); - int x0 = Units.toEMU(bounds.getX()); - int y0 = Units.toEMU(bounds.getY()); + CTCustomGeometry2D geom = getSpPr().getCustGeom(); for(CTPath2D spPath : geom.getPathLst().getPathList()){ + double scaleW = bounds.getWidth() / Units.toPoints(spPath.getW()); + double scaleH = bounds.getHeight() / Units.toPoints(spPath.getH()); for(XmlObject ch : spPath.selectPath("*")){ if(ch instanceof CTPath2DMoveTo){ CTAdjPoint2D pt = ((CTPath2DMoveTo)ch).getPt(); - path.moveTo((float)Units.toPoints((Long)pt.getX() + x0), - (float)Units.toPoints((Long)pt.getY() + y0)); + path.moveTo((float)Units.toPoints((Long)pt.getX())*scaleW, + (float)Units.toPoints((Long)pt.getY())*scaleH); } else if (ch instanceof CTPath2DLineTo){ CTAdjPoint2D pt = ((CTPath2DLineTo)ch).getPt(); - path.lineTo((float)Units.toPoints((Long)pt.getX() + x0), - (float)Units.toPoints((Long)pt.getY() + y0)); + path.lineTo((float)Units.toPoints((Long)pt.getX()), + (float)Units.toPoints((Long)pt.getY())); } else if (ch instanceof CTPath2DCubicBezierTo){ CTPath2DCubicBezierTo bez = ((CTPath2DCubicBezierTo)ch); CTAdjPoint2D pt1 = bez.getPtArray(0); CTAdjPoint2D pt2 = bez.getPtArray(1); CTAdjPoint2D pt3 = bez.getPtArray(2); path.curveTo( - (float)Units.toPoints((Long) pt1.getX() + x0), - (float)Units.toPoints((Long) pt1.getY() + y0), - (float)Units.toPoints((Long) pt2.getX() + x0), - (float)Units.toPoints((Long) pt2.getY() + y0), - (float)Units.toPoints((Long) pt3.getX() + x0), - (float)Units.toPoints((Long) pt3.getY() + y0) + (float)Units.toPoints((Long) pt1.getX())*scaleW, + (float)Units.toPoints((Long) pt1.getY())*scaleH, + (float)Units.toPoints((Long) pt2.getX())*scaleW, + (float)Units.toPoints((Long) pt2.getY())*scaleH, + (float)Units.toPoints((Long) pt3.getX())*scaleW, + (float)Units.toPoints((Long) pt3.getY())*scaleH ); } else if (ch instanceof CTPath2DClose){ @@ -147,7 +158,11 @@ public class XSLFFreeformShape extends XSLFAutoShape { } } - return path; + // the created path starts at (x=0, y=0). + // The returned path should fit in the bounding rectangle + AffineTransform at = new AffineTransform(); + at.translate(bounds.getX(), bounds.getY()); + return new GeneralPath(at.createTransformedShape(path)); } /** * @param shapeId 1-based shapeId @@ -175,4 +190,9 @@ public class XSLFFreeformShape extends XSLFAutoShape { return ct; } + @Override + protected java.awt.Shape getOutline(){ + return getPath(); + } + } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java index 7e0990331..9077e256f 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGraphicFrame.java @@ -23,8 +23,16 @@ import org.apache.poi.sl.usermodel.Shape; import org.apache.poi.sl.usermodel.ShapeContainer; import org.apache.poi.sl.usermodel.ShapeGroup; import org.apache.poi.util.Beta; +import org.apache.poi.util.Units; import org.openxmlformats.schemas.presentationml.x2006.main.CTGraphicalObjectFrame; +import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTShapeProperties; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; /** @@ -61,11 +69,31 @@ public class XSLFGraphicFrame extends XSLFShape { } public Rectangle2D getAnchor(){ - throw new RuntimeException("NotImplemented"); + CTTransform2D xfrm = _shape.getXfrm(); + CTPoint2D off = xfrm.getOff(); + long x = off.getX(); + long y = off.getY(); + CTPositiveSize2D ext = xfrm.getExt(); + long cx = ext.getCx(); + long cy = ext.getCy(); + return new Rectangle2D.Double( + Units.toPoints(x), Units.toPoints(y), + Units.toPoints(cx), Units.toPoints(cy)); } public void setAnchor(Rectangle2D anchor){ - throw new RuntimeException("NotImplemented"); + CTTransform2D xfrm = _shape.getXfrm(); + CTPoint2D off = xfrm.isSetOff() ? xfrm.getOff() : xfrm.addNewOff(); + long x = Units.toEMU(anchor.getX()); + long y = Units.toEMU(anchor.getY()); + off.setX(x); + off.setY(y); + CTPositiveSize2D ext = xfrm.isSetExt() ? xfrm.getExt() : xfrm + .addNewExt(); + long cx = Units.toEMU(anchor.getWidth()); + long cy = Units.toEMU(anchor.getHeight()); + ext.setCx(cx); + ext.setCy(cy); } @@ -78,4 +106,56 @@ public class XSLFGraphicFrame extends XSLFShape { } } + /** + * Rotate this shape. + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @param theta the rotation angle in degrees. + */ + public void setRotation(double theta){ + throw new IllegalArgumentException("Operation not supported"); + } + + /** + * Rotation angle in degrees + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @return rotation angle in degrees + */ + public double getRotation(){ + return 0; + } + + public void setFlipHorizontal(boolean flip){ + throw new IllegalArgumentException("Operation not supported"); + } + + public void setFlipVertical(boolean flip){ + throw new IllegalArgumentException("Operation not supported"); + } + + /** + * Whether the shape is horizontally flipped + * + * @return whether the shape is horizontally flipped + */ + public boolean getFlipHorizontal(){ + return false; + } + + public boolean getFlipVertical(){ + return false; + } + + public void draw(Graphics2D graphics){ + + } + + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java index 804420854..3d5b2f6a5 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFGroupShape.java @@ -33,11 +33,14 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupTransform2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; import org.openxmlformats.schemas.presentationml.x2006.main.CTConnector; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShapeNonVisual; import org.openxmlformats.schemas.presentationml.x2006.main.CTShape; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.List; import java.util.regex.Pattern; @@ -217,4 +220,80 @@ public class XSLFGroupShape extends XSLFShape { return sh; } + + public void setFlipHorizontal(boolean flip){ + _spPr.getXfrm().setFlipH(flip); + } + + public void setFlipVertical(boolean flip){ + _spPr.getXfrm().setFlipV(flip); + } + /** + * Whether the shape is horizontally flipped + * + * @return whether the shape is horizontally flipped + */ + public boolean getFlipHorizontal(){ + return _spPr.getXfrm().getFlipH(); + } + + public boolean getFlipVertical(){ + return _spPr.getXfrm().getFlipV(); + } + + /** + * Rotate this shape. + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @param theta the rotation angle in degrees. + */ + public void setRotation(double theta){ + _spPr.getXfrm().setRot((int)(theta*60000)); + } + + /** + * Rotation angle in degrees + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @return rotation angle in degrees + */ + public double getRotation(){ + return (double)_spPr.getXfrm().getRot()/60000; + } + + public void draw(Graphics2D graphics){ + + // the coordinate system of this group of shape + Rectangle2D interior = getInteriorAnchor(); + // anchor of this group relative to the parent shape + Rectangle2D exterior = getAnchor(); + + graphics.translate(exterior.getX(), exterior.getY()); + double scaleX = exterior.getWidth() / interior.getWidth(); + double scaleY = exterior.getHeight() / interior.getHeight(); + graphics.scale(scaleX, scaleY); + graphics.translate(-interior.getX(), -interior.getY()); + + for (XSLFShape shape : getShapes()) { + // remember the initial transform and restore it after we are done with the drawing + AffineTransform at0 = graphics.getTransform(); + graphics.setRenderingHint(XSLFRenderingHint.GSAVE, true); + + // apply rotation and flipping + shape.applyTransform(graphics); + + shape.draw(graphics); + + // restore the coordinate system + graphics.setTransform(at0); + graphics.setRenderingHint(XSLFRenderingHint.GRESTORE, true); + } + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java index c2ce2bd96..4e17d62c0 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java @@ -22,16 +22,24 @@ package org.apache.poi.xslf.usermodel; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.sl.usermodel.ShapeContainer; import org.apache.poi.util.Beta; +import org.apache.poi.util.POILogger; import org.openxmlformats.schemas.drawingml.x2006.main.CTBlip; import org.openxmlformats.schemas.drawingml.x2006.main.CTBlipFillProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetGeometry2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; import org.openxmlformats.schemas.drawingml.x2006.main.CTShapeProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillProperties; import org.openxmlformats.schemas.drawingml.x2006.main.STShapeType; import org.openxmlformats.schemas.presentationml.x2006.main.CTPicture; import org.openxmlformats.schemas.presentationml.x2006.main.CTPictureNonVisual; import javax.imageio.ImageIO; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; @@ -104,4 +112,43 @@ public class XSLFPictureShape extends XSLFSimpleShape { } return _data; } + + @Override + public void draw(Graphics2D graphics){ + java.awt.Shape outline = getOutline(); + + //fill + Color fillColor = getFillColor(); + if (fillColor != null) { + graphics.setColor(fillColor); + applyFill(graphics); + graphics.fill(outline); + } + + // text + + XSLFPictureData data = getPictureData(); + if(data == null) return; + + BufferedImage img; + try { + img = ImageIO.read(new ByteArrayInputStream(data.getData())); + } + catch (Exception e){ + return; + } + Rectangle2D anchor = getAnchor(); + graphics.drawImage(img, (int)anchor.getX(), (int)anchor.getY(), + (int)anchor.getWidth(), (int)anchor.getHeight(), null); + + + //border overlays the image + Color lineColor = getLineColor(); + if (lineColor != null){ + graphics.setColor(lineColor); + applyStroke(graphics); + graphics.draw(outline); + } + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPresetGeometry.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPresetGeometry.java new file mode 100644 index 000000000..dcd4910b8 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPresetGeometry.java @@ -0,0 +1,637 @@ +/* + * ==================================================================== + * 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.xslf.usermodel; + +import java.awt.*; +import java.awt.geom.*; + +/** + * TODO: re-write and initialize from presetShapeDefinitions.xml + * + * @author Yegor Kozlov + */ +public class XSLFPresetGeometry { + public static final int LINE = 1; + public static final int LINE_INV = 2; + public static final int TRIANGLE = 3; + public static final int RT_TRIANGLE = 4; + public static final int RECT = 5; + public static final int DIAMOND = 6; + public static final int PARALLELOGRAM = 7; + public static final int TRAPEZOID = 8; + public static final int NON_ISOSCELES_TRAPEZOID = 9; + public static final int PENTAGON = 10; + public static final int HEXAGON = 11; + public static final int HEPTAGON = 12; + public static final int OCTAGON = 13; + public static final int DECAGON = 14; + public static final int DODECAGON = 15; + public static final int STAR_4 = 16; + public static final int STAR_5 = 17; + public static final int STAR_6 = 18; + public static final int STAR_7 = 19; + public static final int STAR_8 = 20; + public static final int STAR_10 = 21; + public static final int STAR_12 = 22; + public static final int STAR_16 = 23; + public static final int STAR_24 = 24; + public static final int STAR_32 = 25; + public static final int ROUND_RECT = 26; + public static final int ROUND_1_RECT = 27; + public static final int ROUND_2_SAME_RECT = 28; + public static final int ROUND_2_DIAG_RECT = 29; + public static final int SNIP_ROUND_RECT = 30; + public static final int SNIP_1_RECT = 31; + public static final int SNIP_2_SAME_RECT = 32; + public static final int SNIP_2_DIAG_RECT = 33; + public static final int PLAQUE = 34; + public static final int ELLIPSE = 35; + public static final int TEARDROP = 36; + public static final int HOME_PLATE = 37; + public static final int CHEVRON = 38; + public static final int PIE_WEDGE = 39; + public static final int PIE = 40; + public static final int BLOCK_ARC = 41; + public static final int DONUT = 42; + public static final int NO_SMOKING = 43; + public static final int RIGHT_ARROW = 44; + public static final int LEFT_ARROW = 45; + public static final int UP_ARROW = 46; + public static final int DOWN_ARROW = 47; + public static final int STRIPED_RIGHT_ARROW = 48; + public static final int NOTCHED_RIGHT_ARROW = 49; + public static final int BENT_UP_ARROW = 50; + public static final int LEFT_RIGHT_ARROW = 51; + public static final int UP_DOWN_ARROW = 52; + public static final int LEFT_UP_ARROW = 53; + public static final int LEFT_RIGHT_UP_ARROW = 54; + public static final int QUAD_ARROW = 55; + public static final int LEFT_ARROW_CALLOUT = 56; + public static final int RIGHT_ARROW_CALLOUT = 57; + public static final int UP_ARROW_CALLOUT = 58; + public static final int DOWN_ARROW_CALLOUT = 59; + public static final int LEFT_RIGHT_ARROW_CALLOUT = 60; + public static final int UP_DOWN_ARROW_CALLOUT = 61; + public static final int QUAD_ARROW_CALLOUT = 62; + public static final int BENT_ARROW = 63; + public static final int UTURN_ARROW = 64; + public static final int CIRCULAR_ARROW = 65; + public static final int LEFT_CIRCULAR_ARROW = 66; + public static final int LEFT_RIGHT_CIRCULAR_ARROW = 67; + public static final int CURVED_RIGHT_ARROW = 68; + public static final int CURVED_LEFT_ARROW = 69; + public static final int CURVED_UP_ARROW = 70; + public static final int CURVED_DOWN_ARROW = 71; + public static final int SWOOSH_ARROW = 72; + public static final int CUBE = 73; + public static final int CAN = 74; + public static final int LIGHTNING_BOLT = 75; + public static final int HEART = 76; + public static final int SUN = 77; + public static final int MOON = 78; + public static final int SMILEY_FACE = 79; + public static final int IRREGULAR_SEAL_1 = 80; + public static final int IRREGULAR_SEAL_2 = 81; + public static final int FOLDED_CORNER = 82; + public static final int BEVEL = 83; + public static final int FRAME = 84; + public static final int HALF_FRAME = 85; + public static final int CORNER = 86; + public static final int DIAG_STRIPE = 87; + public static final int CHORD = 88; + public static final int ARC = 89; + public static final int LEFT_BRACKET = 90; + public static final int RIGHT_BRACKET = 91; + public static final int LEFT_BRACE = 92; + public static final int RIGHT_BRACE = 93; + public static final int BRACKET_PAIR = 94; + public static final int BRACE_PAIR = 95; + public static final int STRAIGHT_CONNECTOR_1 = 96; + public static final int BENT_CONNECTOR_2 = 97; + public static final int BENT_CONNECTOR_3 = 98; + public static final int BENT_CONNECTOR_4 = 99; + public static final int BENT_CONNECTOR_5 = 100; + public static final int CURVED_CONNECTOR_2 = 101; + public static final int CURVED_CONNECTOR_3 = 102; + public static final int CURVED_CONNECTOR_4 = 103; + public static final int CURVED_CONNECTOR_5 = 104; + public static final int CALLOUT_1 = 105; + public static final int CALLOUT_2 = 106; + public static final int CALLOUT_3 = 107; + public static final int ACCENT_CALLOUT_1 = 108; + public static final int ACCENT_CALLOUT_2 = 109; + public static final int ACCENT_CALLOUT_3 = 110; + public static final int BORDER_CALLOUT_1 = 111; + public static final int BORDER_CALLOUT_2 = 112; + public static final int BORDER_CALLOUT_3 = 113; + public static final int ACCENT_BORDER_CALLOUT_1 = 114; + public static final int ACCENT_BORDER_CALLOUT_2 = 115; + public static final int ACCENT_BORDER_CALLOUT_3 = 116; + public static final int WEDGE_RECT_CALLOUT = 117; + public static final int WEDGE_ROUND_RECT_CALLOUT = 118; + public static final int WEDGE_ELLIPSE_CALLOUT = 119; + public static final int CLOUD_CALLOUT = 120; + public static final int CLOUD = 121; + public static final int RIBBON = 122; + public static final int RIBBON_2 = 123; + public static final int ELLIPSE_RIBBON = 124; + public static final int ELLIPSE_RIBBON_2 = 125; + public static final int LEFT_RIGHT_RIBBON = 126; + public static final int VERTICAL_SCROLL = 127; + public static final int HORIZONTAL_SCROLL = 128; + public static final int WAVE = 129; + public static final int DOUBLE_WAVE = 130; + public static final int PLUS = 131; + public static final int FLOW_CHART_PROCESS = 132; + public static final int FLOW_CHART_DECISION = 133; + public static final int FLOW_CHART_INPUT_OUTPUT = 134; + public static final int FLOW_CHART_PREDEFINED_PROCESS = 135; + public static final int FLOW_CHART_INTERNAL_STORAGE = 136; + public static final int FLOW_CHART_DOCUMENT = 137; + public static final int FLOW_CHART_MULTIDOCUMENT = 138; + public static final int FLOW_CHART_TERMINATOR = 139; + public static final int FLOW_CHART_PREPARATION = 140; + public static final int FLOW_CHART_MANUAL_INPUT = 141; + public static final int FLOW_CHART_MANUAL_OPERATION = 142; + public static final int FLOW_CHART_CONNECTOR = 143; + public static final int FLOW_CHART_PUNCHED_CARD = 144; + public static final int FLOW_CHART_PUNCHED_TAPE = 145; + public static final int FLOW_CHART_SUMMING_JUNCTION = 146; + public static final int FLOW_CHART_OR = 147; + public static final int FLOW_CHART_COLLATE = 148; + public static final int FLOW_CHART_SORT = 149; + public static final int FLOW_CHART_EXTRACT = 150; + public static final int FLOW_CHART_MERGE = 151; + public static final int FLOW_CHART_OFFLINE_STORAGE = 152; + public static final int FLOW_CHART_ONLINE_STORAGE = 153; + public static final int FLOW_CHART_MAGNETIC_TAPE = 154; + public static final int FLOW_CHART_MAGNETIC_DISK = 155; + public static final int FLOW_CHART_MAGNETIC_DRUM = 156; + public static final int FLOW_CHART_DISPLAY = 157; + public static final int FLOW_CHART_DELAY = 158; + public static final int FLOW_CHART_ALTERNATE_PROCESS = 159; + public static final int FLOW_CHART_OFFPAGE_CONNECTOR = 160; + public static final int ACTION_BUTTON_BLANK = 161; + public static final int ACTION_BUTTON_HOME = 162; + public static final int ACTION_BUTTON_HELP = 163; + public static final int ACTION_BUTTON_INFORMATION = 164; + public static final int ACTION_BUTTON_FORWARD_NEXT = 165; + public static final int ACTION_BUTTON_BACK_PREVIOUS = 166; + public static final int ACTION_BUTTON_END = 167; + public static final int ACTION_BUTTON_BEGINNING = 168; + public static final int ACTION_BUTTON_RETURN = 169; + public static final int ACTION_BUTTON_DOCUMENT = 170; + public static final int ACTION_BUTTON_SOUND = 171; + public static final int ACTION_BUTTON_MOVIE = 172; + public static final int GEAR_6 = 173; + public static final int GEAR_9 = 174; + public static final int FUNNEL = 175; + public static final int MATH_PLUS = 176; + public static final int MATH_MINUS = 177; + public static final int MATH_MULTIPLY = 178; + public static final int MATH_DIVIDE = 179; + public static final int MATH_EQUAL = 180; + public static final int MATH_NOT_EQUAL = 181; + public static final int CORNER_TABS = 182; + public static final int SQUARE_TABS = 183; + public static final int PLAQUE_TABS = 184; + public static final int CHART_X = 185; + public static final int CHART_STAR = 186; + public static final int CHART_PLUS = 187; + + private static interface ShapeOutline { + java.awt.Shape getOutline(XSLFAutoShape shape); + } + + + static ShapeOutline[] shapes; + static { + shapes = new ShapeOutline[255]; + + shapes[RECT] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + Rectangle2D path = new Rectangle2D.Float(0, 0, 21600, 21600); + return path; + } + }; + + shapes[ROUND_RECT] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + RoundRectangle2D path = new RoundRectangle2D.Float(0, 0, 21600, 21600, adjval, adjval); + return path; + } + }; + + shapes[ELLIPSE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + Ellipse2D path = new Ellipse2D.Float(0, 0, 21600, 21600); + return path; + } + }; + + shapes[DIAMOND] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + GeneralPath path = new GeneralPath(); + path.moveTo(10800, 0); + path.lineTo(21600, 10800); + path.lineTo(10800, 21600); + path.lineTo(0, 10800); + path.closePath(); + return path; + } + }; + + //m@0,l,21600r21600 + shapes[TRIANGLE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(0, 21600); + path.lineTo(21600, 21600); + path.closePath(); + return path; + } + }; + + shapes[RT_TRIANGLE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + path.lineTo(21600, 21600); + path.lineTo(0, 21600); + path.closePath(); + return path; + } + }; + + shapes[PARALLELOGRAM] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(21600, 0); + path.lineTo(21600 - adjval, 21600); + path.lineTo(0, 21600); + path.closePath(); + return path; + } + }; + + shapes[TRAPEZOID] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + path.lineTo(adjval, 21600); + path.lineTo(21600 - adjval, 21600); + path.lineTo(21600, 0); + path.closePath(); + return path; + } + }; + + shapes[HEXAGON] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(21600 - adjval, 0); + path.lineTo(21600, 10800); + path.lineTo(21600 - adjval, 21600); + path.lineTo(adjval, 21600); + path.lineTo(0, 10800); + path.closePath(); + return path; + } + }; + + shapes[OCTAGON] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 6324); + + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(21600 - adjval, 0); + path.lineTo(21600, adjval); + path.lineTo(21600, 21600-adjval); + path.lineTo(21600-adjval, 21600); + path.lineTo(adjval, 21600); + path.lineTo(0, 21600-adjval); + path.lineTo(0, adjval); + path.closePath(); + return path; + } + }; + + shapes[PLUS] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(21600 - adjval, 0); + path.lineTo(21600 - adjval, adjval); + path.lineTo(21600, adjval); + path.lineTo(21600, 21600-adjval); + path.lineTo(21600-adjval, 21600-adjval); + path.lineTo(21600-adjval, 21600); + path.lineTo(adjval, 21600); + path.lineTo(adjval, 21600-adjval); + path.lineTo(0, 21600-adjval); + path.lineTo(0, adjval); + path.lineTo(adjval, adjval); + path.closePath(); + return path; + } + }; + + shapes[PENTAGON] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + + GeneralPath path = new GeneralPath(); + path.moveTo(10800, 0); + path.lineTo(21600, 8259); + path.lineTo(21600 - 4200, 21600); + path.lineTo(4200, 21600); + path.lineTo(0, 8259); + path.closePath(); + return path; + } + }; + + shapes[HOME_PLATE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + + GeneralPath path = new GeneralPath(); + int adjval = shape.getAdjustValue("adj1", 16200); + path.moveTo(0, 0); + path.lineTo(adjval, 0 ); + path.lineTo(21600, 10800); + path.lineTo(adjval, 21600); + path.lineTo(0, 21600); + path.closePath(); + return path; + } + }; + + shapes[CHEVRON] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + GeneralPath path = new GeneralPath(); + int adjval = shape.getAdjustValue("adj1", 16200); + path.moveTo(0, 0); + path.lineTo(adjval, 0 ); + path.lineTo(21600, 10800); + path.lineTo(adjval, 21600); + path.lineTo(0, 21600); + path.lineTo(21600 - adjval, 10800); + path.closePath(); + return path; + } + }; + + shapes[CAN] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + int adjval = shape.getAdjustValue("adj1", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + path.lineTo(21600, 0); + path.lineTo(21600, 21600); + path.lineTo(0, 21600); + + //path.lineTo(21600, adjval); + + path.closePath(); + path.moveTo(10800, 0); + //path.append(new Arc2D.Float(10800, 0, 10800, adjval, 0, 90, Arc2D.OPEN), true); + path.moveTo(10800, adjval/2); + path.append(new Arc2D.Float(10800, adjval/2, 10800, adjval, 90, 180, Arc2D.OPEN), true); + //path.append(new Arc2D.Float(0, adjval/2, 10800, adjval, 180, 270, Arc2D.OPEN), true); + //path.append(new Arc2D.Float(0, 0, 10800, adjval, 270, 360, Arc2D.OPEN), true); + return path; + } + }; + + shapes[DOWN_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m0@0 l@1@0 @1,0 @2,0 @2@0,21600@0,10800,21600xe + int adjval = shape.getAdjustValue("adj1", 16200); + int adjval2 = shape.getAdjustValue("adj2", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(0, adjval); + path.lineTo(adjval2, adjval); + path.lineTo(adjval2, 0); + path.lineTo(21600-adjval2, 0); + path.lineTo(21600-adjval2, adjval); + path.lineTo(21600, adjval); + path.lineTo(10800, 21600); + path.closePath(); + return path; + } + }; + + shapes[UP_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m0@0 l@1@0 @1,21600@2,21600@2@0,21600@0,10800,xe + int adjval = shape.getAdjustValue("adj1", 5400); + int adjval2 = shape.getAdjustValue("adj2", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(0, adjval); + path.lineTo(adjval2, adjval); + path.lineTo(adjval2, 21600); + path.lineTo(21600-adjval2, 21600); + path.lineTo(21600-adjval2, adjval); + path.lineTo(21600, adjval); + path.lineTo(10800, 0); + path.closePath(); + return path; + } + }; + + shapes[RIGHT_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m@0, l@0@1 ,0@1,0@2@0@2@0,21600,21600,10800xe + int adjval = shape.getAdjustValue("adj1", 16200); + int adjval2 = shape.getAdjustValue("adj2", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(adjval, adjval2); + path.lineTo(0, adjval2); + path.lineTo(0, 21600-adjval2); + path.lineTo(adjval, 21600-adjval2); + path.lineTo(adjval, 21600); + path.lineTo(21600, 10800); + path.closePath(); + return path; + } + }; + + shapes[LEFT_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m@0, l@0@1,21600@1,21600@2@0@2@0,21600,,10800xe + int adjval = shape.getAdjustValue("adj1", 5400); + int adjval2 = shape.getAdjustValue("adj2", 5400); + GeneralPath path = new GeneralPath(); + path.moveTo(adjval, 0); + path.lineTo(adjval, adjval2); + path.lineTo(21600, adjval2); + path.lineTo(21600, 21600-adjval2); + path.lineTo(adjval, 21600-adjval2); + path.lineTo(adjval, 21600); + path.lineTo(0, 10800); + path.closePath(); + return path; + } + }; + shapes[LEFT_BRACE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m21600,qx10800@0l10800@2qy0@11,10800@3l10800@1qy21600,21600e + int adjval = shape.getAdjustValue("adj1", 1800); + int adjval2 = shape.getAdjustValue("adj2", 10800); + + GeneralPath path = new GeneralPath(); + path.moveTo(21600, 0); + + path.append(new Arc2D.Float(10800, 0, 21600, adjval*2, 90, 90, Arc2D.OPEN), false); + path.moveTo(10800, adjval); + + path.lineTo(10800, adjval2 - adjval); + + path.append(new Arc2D.Float(-10800, adjval2 - 2*adjval, 21600, adjval*2, 270, 90, Arc2D.OPEN), false); + path.moveTo(0, adjval2); + + path.append(new Arc2D.Float(-10800, adjval2, 21600, adjval*2, 0, 90, Arc2D.OPEN), false); + path.moveTo(10800, adjval2 + adjval); + + path.lineTo(10800, 21600 - adjval); + + path.append(new Arc2D.Float(10800, 21600 - 2*adjval, 21600, adjval*2, 180, 90, Arc2D.OPEN), false); + + return path; + } + }; + + shapes[RIGHT_BRACE] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m,qx10800@0 l10800@2qy21600@11,10800@3l10800@1qy,21600e + int adjval = shape.getAdjustValue("adj1", 1800); + int adjval2 = shape.getAdjustValue("adj2", 10800); + + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + + path.append(new Arc2D.Float(-10800, 0, 21600, adjval*2, 0, 90, Arc2D.OPEN), false); + path.moveTo(10800, adjval); + + path.lineTo(10800, adjval2 - adjval); + + path.append(new Arc2D.Float(10800, adjval2 - 2*adjval, 21600, adjval*2, 180, 90, Arc2D.OPEN), false); + path.moveTo(21600, adjval2); + + path.append(new Arc2D.Float(10800, adjval2, 21600, adjval*2, 90, 90, Arc2D.OPEN), false); + path.moveTo(10800, adjval2 + adjval); + + path.lineTo(10800, 21600 - adjval); + + path.append(new Arc2D.Float(-10800, 21600 - 2*adjval, 21600, adjval*2, 270, 90, Arc2D.OPEN), false); + + return path; + } + }; + + + shapes[LEFT_RIGHT_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m,10800l@0,21600@0@3@2@3@2,21600,21600,10800@2,0@2@1@0@1@0,xe + int adjval = shape.getAdjustValue("adj1", 4320); + int adjval2 = shape.getAdjustValue("adj2", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(0, 10800); + path.lineTo(adjval, 0); + path.lineTo(adjval, adjval2); + path.lineTo(21600 - adjval, adjval2); + path.lineTo(21600 - adjval, 0); + path.lineTo(21600, 10800); + path.lineTo(21600 - adjval, 21600); + path.lineTo(21600 - adjval, 21600 - adjval2); + path.lineTo(adjval, 21600 - adjval2); + path.lineTo(adjval, 21600); + path.closePath(); + return path; + } + }; + + shapes[UP_DOWN_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m10800,l21600@0@3@0@3@2,21600@2,10800,21600,0@2@1@2@1@0,0@0xe + int adjval1 = shape.getAdjustValue("adj1", 5400); + int adjval2 = shape.getAdjustValue("adj2", 4320); + + GeneralPath path = new GeneralPath(); + path.moveTo(10800, 0); + path.lineTo(21600, adjval2); + path.lineTo(21600 - adjval1, adjval2); + path.lineTo(21600 - adjval1, 21600 - adjval2); + path.lineTo(21600, 21600 - adjval2); + + path.lineTo(10800, 21600); + path.lineTo(0, 21600 - adjval2); + path.lineTo(adjval1, 21600 - adjval2); + path.lineTo(adjval1, adjval2); + path.lineTo(0, adjval2); + + path.closePath(); + return path; + } + }; + + shapes[NOTCHED_RIGHT_ARROW] = new ShapeOutline(){ + public java.awt.Shape getOutline(XSLFAutoShape shape){ + //m@0,l@0@1,0@1@5,10800,0@2@0@2@0,21600,21600,10800xe + int adjval1 = shape.getAdjustValue("adj1", 16200); + int adjval2 = shape.getAdjustValue("adj2", 5400); + + GeneralPath path = new GeneralPath(); + path.moveTo(adjval1, 0); + path.lineTo(adjval1, adjval2); + path.lineTo(0, adjval2); + //The notch at the end stays adjusted so that it matches the shape of the arrowhead. + int notch = (21600-2*adjval2)*(21600-adjval1)/21600; + path.lineTo(notch, 10800); + path.lineTo(0, 21600 - adjval2); + path.lineTo(adjval1, 21600 - adjval2); + path.lineTo(adjval1, 21600); + path.lineTo(21600, 10800); + path.closePath(); + return path; + } + }; + } + + static Shape getOutline(XSLFAutoShape shape){ + ShapeOutline outline = shapes[shape.getShapeType()]; + return outline == null ? null : outline.getOutline(shape); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java new file mode 100644 index 000000000..aff8c90c4 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRenderingHint.java @@ -0,0 +1,42 @@ +/* + * ==================================================================== + * 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.xslf.usermodel; + +import java.awt.*; + +/** + * + * @author Yegor Kozlov + */ +public class XSLFRenderingHint extends RenderingHints.Key { + + public XSLFRenderingHint(int i){ + super(i); + } + + @Override + public boolean isCompatibleValue(Object val) { + return true; + } + + public static final XSLFRenderingHint GSAVE = new XSLFRenderingHint(1); + public static final XSLFRenderingHint GRESTORE = new XSLFRenderingHint(2); + public static final XSLFRenderingHint SKIP_PLACEHOLDERS = new XSLFRenderingHint(3); +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShadow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShadow.java new file mode 100644 index 000000000..e897e29fb --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShadow.java @@ -0,0 +1,106 @@ +/* ==================================================================== + 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.xslf.usermodel; + +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.util.Units; +import org.openxmlformats.schemas.presentationml.x2006.main.CTBackground; +import org.openxmlformats.schemas.presentationml.x2006.main.CTBackgroundProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTOuterShadowEffect; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetColor; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +/** + * Represents a shadow of a shape. For now supports only outer shadows. + * + * @author Yegor Kozlov + */ +public class XSLFShadow extends XSLFSimpleShape { + private XSLFSimpleShape _parent; + + /* package */XSLFShadow(CTOuterShadowEffect shape, XSLFSimpleShape parentShape) { + super(shape, parentShape.getSheet()); + + _parent = parentShape; + } + + public void draw(Graphics2D graphics) { + Shape outline = _parent.getOutline(); + + double angle = getAngle(); + double dist = getDistance(); + double dx = dist * Math.cos( Math.toRadians(angle)); + double dy = dist * Math.sin( Math.toRadians(angle)); + + graphics.translate(dx, dy); + + //fill + Color fillColor = getFillColor(); + if (fillColor != null) { + graphics.setColor(fillColor); + graphics.fill(outline); + } + + graphics.translate(-dx, -dy); + } + + @Override + public Rectangle2D getAnchor(){ + return _parent.getAnchor(); + } + + @Override + public void setAnchor(Rectangle2D anchor){ + throw new IllegalStateException("You can't set anchor of a shadow"); + } + + public double getDistance(){ + CTOuterShadowEffect ct = (CTOuterShadowEffect)getXmlObject(); + return ct.isSetDist() ? Units.toPoints(ct.getDist()) : 0; + } + + public double getAngle(){ + CTOuterShadowEffect ct = (CTOuterShadowEffect)getXmlObject(); + return ct.isSetDir() ? (double)ct.getDir() / 60000 : 0; + } + + public double getBlur(){ + CTOuterShadowEffect ct = (CTOuterShadowEffect)getXmlObject(); + return ct.isSetBlurRad() ? Units.toPoints(ct.getBlurRad()) : 0; + } + + @Override + public Color getFillColor() { + XSLFTheme theme = getSheet().getTheme(); + CTOuterShadowEffect ct = (CTOuterShadowEffect)getXmlObject(); + if(ct.isSetSchemeClr()) { + return theme.getSchemeColor(ct.getSchemeClr()); + } + else if (ct.isSetPrstClr()) { + return theme.getPresetColor(ct.getPrstClr()); + } + + return Color.black; + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java index dfb659557..2c676caf4 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java @@ -19,10 +19,11 @@ package org.apache.poi.xslf.usermodel; -import org.apache.poi.sl.usermodel.Shape; import org.apache.poi.util.Beta; import org.apache.xmlbeans.XmlObject; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; /** @@ -41,4 +42,76 @@ public abstract class XSLFShape { public abstract String getShapeName(); public abstract int getShapeId(); + + /** + * Rotate this shape. + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @param theta the rotation angle in degrees. + */ + public abstract void setRotation(double theta); + + /** + * Rotation angle in degrees + *

+ * Positive angles are clockwise (i.e., towards the positive y axis); + * negative angles are counter-clockwise (i.e., towards the negative y axis). + *

+ * + * @return rotation angle in degrees + */ + public abstract double getRotation(); + + public abstract void setFlipHorizontal(boolean flip); + + public abstract void setFlipVertical(boolean flip); + + /** + * Whether the shape is horizontally flipped + * + * @return whether the shape is horizontally flipped + */ + public abstract boolean getFlipHorizontal(); + + public abstract boolean getFlipVertical(); + + public abstract void draw(Graphics2D graphics); + + protected java.awt.Shape getOutline(){ + return getAnchor(); + } + + protected void applyTransform(Graphics2D graphics){ + Rectangle2D anchor = getAnchor(); + + // rotation + double rotation = getRotation(); + if(rotation != 0.) { + // PowerPoint rotates shapes relative to the geometric center + double centerX = anchor.getX() + anchor.getWidth()/2; + double centerY = anchor.getY() + anchor.getHeight()/2; + + graphics.translate(centerX, centerY); + graphics.rotate(Math.toRadians(rotation)); + graphics.translate(-centerX, -centerY); + } + + //flip horizontal + if(getFlipHorizontal()){ + graphics.translate(anchor.getX() + anchor.getWidth(), anchor.getY()); + graphics.scale(-1, 1); + graphics.translate(-anchor.getX() , -anchor.getY()); + } + + //flip vertical + if(getFlipVertical()){ + graphics.translate(anchor.getX(), anchor.getY() + anchor.getHeight()); + graphics.scale(1, -1); + graphics.translate(-anchor.getX(), -anchor.getY()); + } + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java index 58e26eb87..f4a46fc90 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java @@ -21,6 +21,7 @@ import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.TargetMode; import org.apache.poi.util.Beta; +import org.apache.poi.util.Internal; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; import org.openxmlformats.schemas.officeDocument.x2006.relationships.STRelationshipId; @@ -30,9 +31,13 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CTGraphicalObjectFra import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape; import org.openxmlformats.schemas.presentationml.x2006.main.CTPicture; import org.openxmlformats.schemas.presentationml.x2006.main.CTShape; -import org.openxmlformats.schemas.presentationml.x2006.main.CTSlide; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import javax.xml.namespace.QName; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -47,6 +52,8 @@ public abstract class XSLFSheet extends POIXMLDocumentPart { private XSLFDrawing _drawing; private List _shapes; private CTGroupShape _spTree; + private Map _placeholderByIdMap; + private Map _placeholderByTypeMap; public XSLFSheet() { super(); @@ -187,6 +194,10 @@ public abstract class XSLFSheet extends POIXMLDocumentPart { return getShapeList().remove(xShape); } + public XSLFBackground getBackground(){ + return null; + } + protected abstract String getRootElementName(); protected CTGroupShape getSpTree(){ @@ -232,4 +243,112 @@ public abstract class XSLFSheet extends POIXMLDocumentPart { getXmlObject().set(src.getXmlObject()); } + public XSLFTheme getTheme(){ + return null; + } + + public XSLFSlideMaster getSlideMaster(){ + return null; + } + + public XSLFSlideLayout getSlideLayout(){ + return null; + } + + protected CTTextListStyle getTextProperties(Placeholder textType) { + return null; + } + + protected XSLFTextShape getTextShapeByType(Placeholder type){ + for(XSLFShape shape : this.getShapes()){ + if(shape instanceof XSLFTextShape) { + XSLFTextShape txt = (XSLFTextShape)shape; + if(txt.getTextType() == type) { + return txt; + } + } + } + return null; + } + + XSLFSimpleShape getPlaceholder(CTPlaceholder ph) { + XSLFSimpleShape shape = null; + if(ph.isSetIdx()) shape = getPlaceholderById((int)ph.getIdx()); + + if (shape == null && ph.isSetType()) { + shape = getPlaceholderByType(ph.getType().intValue()); + } + return shape; + } + + XSLFSimpleShape getPlaceholderById(int id) { + if(_placeholderByIdMap == null) { + _placeholderByIdMap = new HashMap(); + for(XSLFShape sh : getShapes()){ + if(sh instanceof XSLFSimpleShape){ + XSLFSimpleShape sShape = (XSLFSimpleShape)sh; + CTPlaceholder ph = sShape.getCTPlaceholder(); + if(ph != null && ph.isSetIdx()){ + int idx = (int)ph.getIdx(); + _placeholderByIdMap.put(idx, sShape); + } + } + } + } + return _placeholderByIdMap.get(id); + } + + XSLFSimpleShape getPlaceholderByType(int ordinal) { + if(_placeholderByTypeMap == null) { + _placeholderByTypeMap = new HashMap(); + for(XSLFShape sh : getShapes()){ + if(sh instanceof XSLFSimpleShape){ + XSLFSimpleShape sShape = (XSLFSimpleShape)sh; + CTPlaceholder ph = sShape.getCTPlaceholder(); + if(ph != null && ph.isSetType()){ + _placeholderByTypeMap.put(ph.getType().intValue(), sShape); + } + } + } + } + return _placeholderByTypeMap.get(ordinal); + } + + /** + * Checks if this sheet displays the specified shape. + * + * Subclasses can override it and skip certain shapes from drawings. + */ + protected boolean canDraw(XSLFShape shape){ + return true; + } + + /** + * Render this sheet into the supplied graphics object + * + * @param graphics + */ + public void draw(Graphics2D graphics){ + XSLFBackground bg = getBackground(); + if(bg != null) bg.draw(graphics); + + for(XSLFShape shape : getShapeList()) { + if(!canDraw(shape)) continue; + + // remember the initial transform and restore it after we are done with drawing + AffineTransform at0 = graphics.getTransform(); + + graphics.setRenderingHint(XSLFRenderingHint.GSAVE, true); + + // apply rotation and flipping + shape.applyTransform(graphics); + + shape.draw(graphics); + + // restore the coordinate system + graphics.setTransform(at0); + graphics.setRenderingHint(XSLFRenderingHint.GRESTORE, true); + + } + } } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java index 3b624dc0a..8e3b38a22 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java @@ -21,6 +21,7 @@ package org.apache.poi.xslf.usermodel; import org.apache.poi.xslf.usermodel.LineCap; import org.apache.poi.xslf.usermodel.LineDash; +import org.apache.poi.xslf.model.PropertyFetcher; import org.apache.poi.util.Beta; import org.apache.poi.util.Units; import org.apache.xmlbeans.XmlObject; @@ -28,14 +29,22 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTLineProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetGeometry2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetLineDashProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSchemeColor; import org.openxmlformats.schemas.drawingml.x2006.main.CTShapeProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTShapeStyle; import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTStyleMatrix; import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; import org.openxmlformats.schemas.drawingml.x2006.main.STLineCap; import org.openxmlformats.schemas.drawingml.x2006.main.STPresetLineDashVal; import org.openxmlformats.schemas.drawingml.x2006.main.STShapeType; +import org.openxmlformats.schemas.drawingml.x2006.main.CTOuterShadowEffect; +import org.openxmlformats.schemas.drawingml.x2006.main.CTEffectStyleItem; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; import java.awt.*; import java.awt.geom.Rectangle2D; @@ -48,26 +57,31 @@ public abstract class XSLFSimpleShape extends XSLFShape { private final XmlObject _shape; private final XSLFSheet _sheet; private CTShapeProperties _spPr; + private CTShapeStyle _spStyle; private CTNonVisualDrawingProps _nvPr; + private CTPlaceholder _ph; - /*package*/ XSLFSimpleShape(XmlObject shape, XSLFSheet sheet){ + /* package */XSLFSimpleShape(XmlObject shape, XSLFSheet sheet) { _shape = shape; _sheet = sheet; } - public XmlObject getXmlObject(){ + public XmlObject getXmlObject() { return _shape; } - - public XSLFSheet getSheet(){ + + public XSLFSheet getSheet() { return _sheet; } + /** - * TODO match STShapeType with {@link org.apache.poi.sl.usermodel.ShapeTypes} + * TODO match STShapeType with + * {@link org.apache.poi.sl.usermodel.ShapeTypes} */ public int getShapeType() { - STShapeType.Enum stEnum = getSpPr().getPrstGeom().getPrst(); - return stEnum.intValue(); + CTPresetGeometry2D prst = getSpPr().getPrstGeom(); + STShapeType.Enum stEnum = prst == null ? null : prst.getPrst(); + return stEnum == null ? 0 : stEnum.intValue(); } public String getShapeName() { @@ -75,36 +89,75 @@ public abstract class XSLFSimpleShape extends XSLFShape { } public int getShapeId() { - return (int)getNvPr().getId(); + return (int) getNvPr().getId(); } - protected CTNonVisualDrawingProps getNvPr(){ - if(_nvPr == null){ - XmlObject[] rs = _shape.selectPath( - "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' .//*/p:cNvPr"); - if(rs.length != 0) { - _nvPr = (CTNonVisualDrawingProps)rs[0]; + protected CTNonVisualDrawingProps getNvPr() { + if (_nvPr == null) { + XmlObject[] rs = _shape + .selectPath("declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' .//*/p:cNvPr"); + if (rs.length != 0) { + _nvPr = (CTNonVisualDrawingProps) rs[0]; } } return _nvPr; } - protected CTShapeProperties getSpPr(){ - if(_spPr == null) { - for(XmlObject obj : _shape.selectPath("*")){ - if(obj instanceof CTShapeProperties){ - _spPr = (CTShapeProperties)obj; + protected CTShapeProperties getSpPr() { + if (_spPr == null) { + for (XmlObject obj : _shape.selectPath("*")) { + if (obj instanceof CTShapeProperties) { + _spPr = (CTShapeProperties) obj; } } } - if(_spPr == null) { + if (_spPr == null) { throw new IllegalStateException("CTShapeProperties was not found."); } return _spPr; } - public Rectangle2D getAnchor(){ - CTTransform2D xfrm = getSpPr().getXfrm(); + protected CTShapeStyle getSpStyle() { + if (_spStyle == null) { + for (XmlObject obj : _shape.selectPath("*")) { + if (obj instanceof CTShapeStyle) { + _spStyle = (CTShapeStyle) obj; + } + } + } + return _spStyle; + } + + protected CTPlaceholder getCTPlaceholder(){ + if(_ph == null){ + XmlObject[] obj = _shape.selectPath( + "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' .//*/p:nvPr/p:ph"); + if(obj.length == 1){ + _ph = (CTPlaceholder)obj[0]; + } + } + return _ph; + } + + private CTTransform2D getXfrm(){ + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties pr = shape.getSpPr(); + if(pr.isSetXfrm()){ + setValue(pr.getXfrm()); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue(); + } + + public Rectangle2D getAnchor() { + + CTTransform2D xfrm = getXfrm(); + CTPoint2D off = xfrm.getOff(); long x = off.getX(); long y = off.getY(); @@ -116,14 +169,16 @@ public abstract class XSLFSimpleShape extends XSLFShape { Units.toPoints(cx), Units.toPoints(cy)); } - public void setAnchor(Rectangle2D anchor){ - CTTransform2D xfrm = getSpPr().isSetXfrm() ? getSpPr().getXfrm() : getSpPr().addNewXfrm(); + public void setAnchor(Rectangle2D anchor) { + CTShapeProperties spPr = getSpPr(); + CTTransform2D xfrm = spPr.isSetXfrm() ? spPr.getXfrm() : spPr.addNewXfrm(); CTPoint2D off = xfrm.isSetOff() ? xfrm.getOff() : xfrm.addNewOff(); long x = Units.toEMU(anchor.getX()); long y = Units.toEMU(anchor.getY()); off.setX(x); off.setY(y); - CTPositiveSize2D ext = xfrm.isSetExt() ? xfrm.getExt() : xfrm.addNewExt(); + CTPositiveSize2D ext = xfrm.isSetExt() ? xfrm.getExt() : xfrm + .addNewExt(); long cx = Units.toEMU(anchor.getWidth()); long cy = Units.toEMU(anchor.getHeight()); ext.setCx(cx); @@ -134,135 +189,489 @@ public abstract class XSLFSimpleShape extends XSLFShape { * Rotate this shape. *

* Positive angles are clockwise (i.e., towards the positive y axis); - * negative angles are counter-clockwise (i.e., towards the negative y axis). + * negative angles are counter-clockwise (i.e., towards the negative y + * axis). *

* * @param theta the rotation angle in degrees. */ - public void setRotation(double theta){ - CTTransform2D xfrm = getSpPr().getXfrm(); - xfrm.setRot((int)(theta*60000)); + public void setRotation(double theta) { + CTShapeProperties spPr = getSpPr(); + CTTransform2D xfrm = spPr.isSetXfrm() ? spPr.getXfrm() : spPr.addNewXfrm(); + xfrm.setRot((int) (theta * 60000)); } /** * Rotation angle in degrees *

* Positive angles are clockwise (i.e., towards the positive y axis); - * negative angles are counter-clockwise (i.e., towards the negative y axis). + * negative angles are counter-clockwise (i.e., towards the negative y + * axis). *

* * @return rotation angle in degrees */ - public double getRotation(){ - CTTransform2D xfrm = getSpPr().getXfrm(); - return (double)xfrm.getRot()/60000; + public double getRotation() { + CTTransform2D xfrm = getXfrm(); + return (double) xfrm.getRot() / 60000; } - public void setFlipHorizontal(boolean flip){ - CTTransform2D xfrm = getSpPr().getXfrm(); + public void setFlipHorizontal(boolean flip) { + CTShapeProperties spPr = getSpPr(); + CTTransform2D xfrm = spPr.isSetXfrm() ? spPr.getXfrm() : spPr.addNewXfrm(); xfrm.setFlipH(flip); } - public void setFlipVertical(boolean flip){ - CTTransform2D xfrm = getSpPr().getXfrm(); + public void setFlipVertical(boolean flip) { + CTShapeProperties spPr = getSpPr(); + CTTransform2D xfrm = spPr.isSetXfrm() ? spPr.getXfrm() : spPr.addNewXfrm(); xfrm.setFlipV(flip); } + /** * Whether the shape is horizontally flipped * * @return whether the shape is horizontally flipped */ - public boolean getFlipHorizontal(){ - return getSpPr().getXfrm().getFlipH(); + public boolean getFlipHorizontal() { + return getXfrm().getFlipH(); } - public boolean getFlipVertical(){ - return getSpPr().getXfrm().getFlipV(); + public boolean getFlipVertical() { + return getXfrm().getFlipV(); } - public void setLineColor(Color color){ - CTShapeProperties spPr = getSpPr(); - if(color == null) { - if(spPr.isSetLn() && spPr.getLn().isSetSolidFill()) spPr.getLn().unsetSolidFill(); + /** + * Get line properties defined in the theme (if any) + * + * @return line propeties from the theme of null + */ + CTLineProperties getDefaultLineProperties() { + CTLineProperties ln = null; + CTShapeStyle style = getSpStyle(); + if (style != null) { + // 1-based index of a line style within the style matrix + int idx = (int) style.getLnRef().getIdx(); + CTStyleMatrix styleMatrix = _sheet.getTheme().getXmlObject().getThemeElements().getFmtScheme(); + ln = styleMatrix.getLnStyleLst().getLnArray(idx - 1); } - else { - CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr.addNewLn(); + return ln; + } + + public void setLineColor(Color color) { + CTShapeProperties spPr = getSpPr(); + if (color == null) { + if (spPr.isSetLn() && spPr.getLn().isSetSolidFill()) + spPr.getLn().unsetSolidFill(); + } else { + CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr + .addNewLn(); CTSRgbColor rgb = CTSRgbColor.Factory.newInstance(); - rgb.setVal(new byte[]{(byte)color.getRed(), (byte)color.getGreen(), (byte)color.getBlue()}); + rgb.setVal(new byte[]{(byte) color.getRed(), + (byte) color.getGreen(), (byte) color.getBlue()}); - CTSolidColorFillProperties fill = ln.isSetSolidFill() ? ln.getSolidFill() : ln.addNewSolidFill(); + CTSolidColorFillProperties fill = ln.isSetSolidFill() ? ln + .getSolidFill() : ln.addNewSolidFill(); fill.setSrgbClr(rgb); } } - public Color getLineColor(){ - CTShapeProperties spPr = getSpPr(); - if(!spPr.isSetLn() || !spPr.getLn().isSetSolidFill()) return null; + public Color getLineColor() { + final XSLFTheme theme = _sheet.getTheme(); - CTSRgbColor rgb = spPr.getLn().getSolidFill().getSrgbClr(); - byte[] val = rgb.getVal(); - return new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2]); + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + CTLineProperties ln = spPr.getLn(); + if (ln != null) { + if (ln.isSetNoFill()) { + setValue(null); + return true; + } + CTSolidColorFillProperties solidLine = ln.getSolidFill(); + if (solidLine != null) { + setValue( theme.getSolidFillColor(ln.getSolidFill()) ); + return true; + } + } + return false; + } + }; + fetchShapeProperty(fetcher); + + Color color = fetcher.getValue(); + if(color == null){ + // line color was not found, check if it is defined in the theme + CTShapeStyle style = getSpStyle(); + if (style != null) { + CTSchemeColor schemeColor = style.getLnRef().getSchemeClr(); + if (schemeColor != null) { + color = theme.getSchemeColor(schemeColor); + } + } + } + return color; } - public void setLineWidth(double width){ + public void setLineWidth(double width) { CTShapeProperties spPr = getSpPr(); - if(width == 0.) { - if(spPr.isSetLn()) spPr.getLn().unsetW(); - } - else { - CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr.addNewLn(); + if (width == 0.) { + if (spPr.isSetLn()) + spPr.getLn().unsetW(); + } else { + CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr + .addNewLn(); ln.setW(Units.toEMU(width)); } } - public double getLineWidth(){ - CTShapeProperties spPr = getSpPr(); - CTLineProperties ln = spPr.getLn(); - if(ln == null || !ln.isSetW()) return 0; + public double getLineWidth() { + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + CTLineProperties ln = spPr.getLn(); + if (ln != null) { + if (ln.isSetNoFill()) { + setValue(0.); + return true; + } - return Units.toPoints(ln.getW()); + if (ln.isSetW()) { + setValue( Units.toPoints(ln.getW()) ); + return true; + } + } + return false; + } + }; + fetchShapeProperty(fetcher); + + double lineWidth = 0; + if(fetcher.getValue() == null) { + CTLineProperties defaultLn = getDefaultLineProperties(); + if (defaultLn != null) { + if (defaultLn.isSetW()) lineWidth = Units.toPoints(defaultLn.getW()); + } + } else { + lineWidth = fetcher.getValue(); + } + + return lineWidth; } - public void setLineDash(LineDash dash){ + public void setLineDash(LineDash dash) { CTShapeProperties spPr = getSpPr(); - if(dash == null) { - if(spPr.isSetLn()) spPr.getLn().unsetPrstDash(); - } - else { - CTPresetLineDashProperties val = CTPresetLineDashProperties.Factory.newInstance(); + if (dash == null) { + if (spPr.isSetLn()) + spPr.getLn().unsetPrstDash(); + } else { + CTPresetLineDashProperties val = CTPresetLineDashProperties.Factory + .newInstance(); val.setVal(STPresetLineDashVal.Enum.forInt(dash.ordinal() + 1)); - CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr.addNewLn(); + CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr + .addNewLn(); ln.setPrstDash(val); } } - public LineDash getLineDash(){ - CTShapeProperties spPr = getSpPr(); - CTLineProperties ln = spPr.getLn(); - if(ln == null || !ln.isSetPrstDash()) return null; + public LineDash getLineDash() { - CTPresetLineDashProperties dash = ln.getPrstDash(); - return LineDash.values()[dash.getVal().intValue() - 1]; + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + CTLineProperties ln = spPr.getLn(); + if (ln != null) { + CTPresetLineDashProperties ctDash = ln.getPrstDash(); + if (ctDash != null) { + setValue( LineDash.values()[ctDash.getVal().intValue() - 1] ); + return true; + } + } + return false; + } + }; + fetchShapeProperty(fetcher); + + LineDash dash = fetcher.getValue(); + if(dash == null){ + CTLineProperties defaultLn = getDefaultLineProperties(); + if (defaultLn != null) { + CTPresetLineDashProperties ctDash = defaultLn.getPrstDash(); + if (ctDash != null) { + dash = LineDash.values()[ctDash.getVal().intValue() - 1]; + } + } + } + return dash; } - public void setLineCap(LineCap cap){ + public void setLineCap(LineCap cap) { CTShapeProperties spPr = getSpPr(); - if(cap == null) { - if(spPr.isSetLn()) spPr.getLn().unsetCap(); - } - else { - CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr.addNewLn(); + if (cap == null) { + if (spPr.isSetLn()) + spPr.getLn().unsetCap(); + } else { + CTLineProperties ln = spPr.isSetLn() ? spPr.getLn() : spPr + .addNewLn(); ln.setCap(STLineCap.Enum.forInt(cap.ordinal() + 1)); } } - public LineCap getLineCap(){ - CTShapeProperties spPr = getSpPr(); - CTLineProperties ln = spPr.getLn(); - if(ln == null || !ln.isSetCap()) return null; + public LineCap getLineCap() { + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + CTLineProperties ln = spPr.getLn(); + if (ln != null) { + STLineCap.Enum stCap = ln.getCap(); + if (stCap != null) { + setValue( LineCap.values()[stCap.intValue() - 1] ); + return true; + } + } + return false; + } + }; + fetchShapeProperty(fetcher); - return LineCap.values()[ln.getCap().intValue() - 1]; + LineCap cap = fetcher.getValue(); + if(cap == null){ + CTLineProperties defaultLn = getDefaultLineProperties(); + if (defaultLn != null) { + STLineCap.Enum stCap = defaultLn.getCap(); + if (stCap != null) { + cap = LineCap.values()[stCap.intValue() - 1]; + } + } + } + return cap; + } + + /** + * Specifies a solid color fill. The shape is filled entirely with the + * specified color. + * + * @param color the solid color fill. The value of null unsets + * the solidFIll attribute from the underlying xml + */ + public void setFillColor(Color color) { + CTShapeProperties spPr = getSpPr(); + if (color == null) { + if (spPr.isSetSolidFill()) spPr.unsetSolidFill(); + + if(!spPr.isSetNoFill()) spPr.addNewNoFill(); + } else { + if(spPr.isSetNoFill()) spPr.unsetNoFill(); + + CTSolidColorFillProperties fill = spPr.isSetSolidFill() ? spPr + .getSolidFill() : spPr.addNewSolidFill(); + + CTSRgbColor rgb = CTSRgbColor.Factory.newInstance(); + rgb.setVal(new byte[]{(byte) color.getRed(), + (byte) color.getGreen(), (byte) color.getBlue()}); + + fill.setSrgbClr(rgb); + } + } + + /** + * @return solid fill color of null if not set + */ + public Color getFillColor() { + final XSLFTheme theme = _sheet.getTheme(); + + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + if (spPr.isSetNoFill()) { + setValue(null); + return true; + } + if (spPr.isSetSolidFill()) { + setValue( theme.getSolidFillColor(spPr.getSolidFill()) ); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + + Color color = fetcher.getValue(); + if(color == null){ + // fill color was not found, check if it is defined in the theme + CTShapeStyle style = getSpStyle(); + if (style != null) { + CTSchemeColor schemeColor = style.getFillRef().getSchemeClr(); + if (schemeColor != null) { + color = theme.getSchemeColor(schemeColor); + } + } + } + return color; + } + + public XSLFShadow getShadow(){ + PropertyFetcher fetcher = new PropertyFetcher(){ + public boolean fetch(XSLFSimpleShape shape){ + CTShapeProperties spPr = shape.getSpPr(); + if (spPr.isSetEffectLst()) { + CTOuterShadowEffect obj = spPr.getEffectLst().getOuterShdw(); + setValue(obj); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + + CTOuterShadowEffect obj = fetcher.getValue(); + if(obj == null){ + // fill color was not found, check if it is defined in the theme + CTShapeStyle style = getSpStyle(); + if (style != null) { + // 1-based index of a shadow style within the style matrix + int idx = (int) style.getEffectRef().getIdx(); + + CTStyleMatrix styleMatrix = _sheet.getTheme().getXmlObject().getThemeElements().getFmtScheme(); + CTEffectStyleItem ef = styleMatrix.getEffectStyleLst().getEffectStyleArray(idx - 1); + obj = ef.getEffectLst().getOuterShdw(); + } + } + return obj == null ? null : new XSLFShadow(obj, this); + } + + public void draw(Graphics2D graphics) { + + } + + protected void applyFill(Graphics2D graphics) { + + } + + protected float[] getDashPattern(LineDash lineDash, float lineWidth) { + float[] dash = null; + switch (lineDash) { + case SYS_DOT: + dash = new float[]{lineWidth, lineWidth}; + break; + case SYS_DASH: + dash = new float[]{2 * lineWidth, 2 * lineWidth}; + break; + case DASH: + dash = new float[]{3 * lineWidth, 4 * lineWidth}; + break; + case DASH_DOT: + dash = new float[]{4 * lineWidth, 3 * lineWidth, lineWidth, + 3 * lineWidth}; + break; + case LG_DASH: + dash = new float[]{8 * lineWidth, 3 * lineWidth}; + break; + case LG_DASH_DOT: + dash = new float[]{8 * lineWidth, 3 * lineWidth, lineWidth, + 3 * lineWidth}; + break; + case LG_DASH_DOT_DOT: + dash = new float[]{8 * lineWidth, 3 * lineWidth, lineWidth, + 3 * lineWidth, lineWidth, 3 * lineWidth}; + break; + } + return dash; + } + + protected void applyStroke(Graphics2D graphics) { + + float lineWidth = (float) getLineWidth(); + LineDash lineDash = getLineDash(); + float[] dash = null; + float dash_phase = 0; + if (lineDash != null) { + dash = getDashPattern(lineDash, lineWidth); + } + + int cap = BasicStroke.CAP_BUTT; + LineCap lineCap = getLineCap(); + if (lineCap != null) { + switch (lineCap) { + case ROUND: + cap = BasicStroke.CAP_ROUND; + break; + case SQUARE: + cap = BasicStroke.CAP_SQUARE; + break; + default: + cap = BasicStroke.CAP_BUTT; + break; + } + } + + int meter = BasicStroke.JOIN_ROUND; + + Stroke stroke = new BasicStroke(lineWidth, cap, meter, 0.0f, dash, + dash_phase); + graphics.setStroke(stroke); + } + + /** + * Walk up the inheritance tree and fetch properties. + * + * slide <-- slideLayout <-- slideMaster + * + * + * @param visitor the object that collects the desired property + * @return true if the property was fetched + */ + boolean fetchShapeProperty(PropertyFetcher visitor){ + boolean ok = visitor.fetch(this); + + XSLFSimpleShape masterShape; + if(!ok){ + + // first try to fetch from the slide layout + XSLFSlideLayout layout = getSheet().getSlideLayout(); + if(layout != null) { + CTPlaceholder ph = getCTPlaceholder(); + if (ph != null) { + masterShape = layout.getPlaceholder(ph); + if (masterShape != null) { + ok = visitor.fetch(masterShape); + } + } + } + } + + // try slide master + if (!ok) { + int textType; + CTPlaceholder ph = getCTPlaceholder(); + if(ph == null || !ph.isSetType()) textType = STPlaceholderType.INT_BODY; + else { + switch(ph.getType().intValue()){ + case STPlaceholderType.INT_TITLE: + case STPlaceholderType.INT_CTR_TITLE: + textType = STPlaceholderType.INT_TITLE; + break; + case STPlaceholderType.INT_FTR: + case STPlaceholderType.INT_SLD_NUM: + case STPlaceholderType.INT_DT: + textType = ph.getType().intValue(); + break; + default: + textType = STPlaceholderType.INT_BODY; + break; + } + } + XSLFSlideMaster master = getSheet().getSlideMaster(); + if(master != null) { + masterShape = master.getPlaceholderByType(textType); + if (masterShape != null) { + ok = visitor.fetch(masterShape); + } + } + } + + return ok; } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java index f0a8e9b8e..d9f7c88e6 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java @@ -16,6 +16,7 @@ ==================================================================== */ package org.apache.poi.xslf.usermodel; +import java.awt.*; import java.io.IOException; import org.apache.poi.POIXMLDocumentPart; @@ -23,16 +24,20 @@ import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.util.Beta; import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupShapeProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupTransform2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import org.openxmlformats.schemas.presentationml.x2006.main.CTCommonSlideData; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShapeNonVisual; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlide; import org.openxmlformats.schemas.presentationml.x2006.main.SldDocument; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; @Beta public final class XSLFSlide extends XSLFSheet { @@ -112,6 +117,7 @@ public final class XSLFSlide extends XSLFSheet { return getSlideLayout().getSlideMaster(); } + @Override public XSLFSlideLayout getSlideLayout(){ if(_layout == null){ for (POIXMLDocumentPart p : getRelations()) { @@ -125,7 +131,12 @@ public final class XSLFSlide extends XSLFSheet { } return _layout; } - + + @Override + public XSLFSlideMaster getSlideMaster(){ + return getSlideLayout().getSlideMaster(); + } + public XSLFComments getComments() { if(_comments == null) { for (POIXMLDocumentPart p : getRelations()) { @@ -165,4 +176,37 @@ public final class XSLFSlide extends XSLFSheet { public boolean getFollowMasterBackground(){ return !_slide.isSetShowMasterSp() || _slide.getShowMasterSp(); } + + /** + * + * @return title of this slide or empty string if title is not set + */ + public String getTitle(){ + XSLFTextShape txt = getTextShapeByType(Placeholder.TITLE); + return txt == null ? "" : txt.getText(); + } + + public XSLFTheme getTheme(){ + return getSlideLayout().getSlideMaster().getTheme(); + } + + @Override + public void draw(Graphics2D graphics){ + + if (getFollowMasterBackground()){ + XSLFSlideLayout layout = getSlideLayout(); + layout.draw(graphics); + } + + super.draw(graphics); + } + + @Override + public XSLFBackground getBackground(){ + if(_slide.getCSld().isSetBg()) { + return new XSLFBackground(_slide.getCSld().getBg(), this); + } + return null; + } + } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java index 2d385382f..ab6661125 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java @@ -22,11 +22,18 @@ import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.util.Beta; import org.apache.poi.util.Internal; import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideLayout; import org.openxmlformats.schemas.presentationml.x2006.main.SldLayoutDocument; -import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMaster; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import java.io.IOException; +import java.util.Map; +import java.util.HashMap; +import java.awt.*; +import java.awt.geom.AffineTransform; @Beta public class XSLFSlideLayout extends XSLFSheet { @@ -84,6 +91,7 @@ public class XSLFSlideLayout extends XSLFSheet { * @return slide master. Never null. * @throws IllegalStateException if slide master was not found */ + @Override public XSLFSlideMaster getSlideMaster(){ if(_master == null){ for (POIXMLDocumentPart p : getRelations()) { @@ -101,4 +109,40 @@ public class XSLFSlideLayout extends XSLFSheet { public XMLSlideShow getSlideShow() { return (XMLSlideShow)getParent().getParent(); } + + public XSLFTheme getTheme(){ + return getSlideMaster().getTheme(); + } + + + @Override + protected CTTextListStyle getTextProperties(Placeholder textType) { + XSLFTextShape lp = getTextShapeByType(textType); + CTTextListStyle props = lp.getTextBody(false).getLstStyle(); + return props; + } + + /** + * Render this sheet into the supplied graphics object + * + */ + @Override + protected boolean canDraw(XSLFShape shape){ + if(shape instanceof XSLFSimpleShape){ + XSLFSimpleShape txt = (XSLFSimpleShape)shape; + CTPlaceholder ph = txt.getCTPlaceholder(); + if(ph != null) { + return false; + } + } + return true; + } + + @Override + public XSLFBackground getBackground(){ + if(_layout.getCSld().isSetBg()) { + return new XSLFBackground(_layout.getCSld().getBg(), this); + } + return null; + } } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java index b7dcad7ce..269b76cd1 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java @@ -24,6 +24,8 @@ import org.apache.poi.util.Beta; import org.apache.xmlbeans.XmlException; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMaster; import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; +import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMasterTextStyles; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import java.io.IOException; import java.util.HashMap; @@ -101,4 +103,24 @@ import java.util.Map; } return _theme; } + + protected CTTextListStyle getTextProperties(Placeholder textType) { + CTTextListStyle props; + CTSlideMasterTextStyles txStyles = getXmlObject().getTxStyles(); + switch (textType){ + case TITLE: + case CENTERED_TITLE: + case SUBTITLE: + props = txStyles.getTitleStyle(); + break; + case BODY: + props = txStyles.getBodyStyle(); + break; + default: + props = txStyles.getOtherStyle(); + break; + } + return props; + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTable.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTable.java index dce9cc238..173d6c856 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTable.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTable.java @@ -126,32 +126,4 @@ public class XSLFTable extends XSLFGraphicFrame implements Iterable{ private final CTTextParagraph _p; private final List _runs; private final XSLFTextShape _shape; + private List _lines; + private TextFragment _bullet; XSLFTextParagraph(CTTextParagraph p, XSLFTextShape shape){ _p = p; _runs = new ArrayList(); _shape = shape; + for (CTRegularTextRun r : _p.getRList()) { _runs.add(new XSLFTextRun(r, this)); } + + for (CTTextField f : _p.getFldList()) { + CTRegularTextRun r = CTRegularTextRun.Factory.newInstance(); + r.setT(f.getT()); + _runs.add(new XSLFTextRun(r, this)); + } } public String getText(){ StringBuilder out = new StringBuilder(); - for (CTRegularTextRun r : _p.getRList()) { - out.append(r.getT()); + for (XSLFTextRun r : _runs) { + out.append(r.getText()); } return out.toString(); } @@ -95,10 +117,18 @@ public class XSLFTextParagraph implements Iterable{ * @return ??? alignment that is applied to the paragraph */ public TextAlign getTextAlign(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetAlgn()) return TextAlign.LEFT; - - return TextAlign.values()[pr.getAlgn().intValue() - 1]; + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetAlgn()){ + TextAlign val = TextAlign.values()[props.getAlgn().intValue() - 1]; + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? TextAlign.LEFT : fetcher.getValue(); } /** @@ -117,6 +147,74 @@ public class XSLFTextParagraph implements Iterable{ } } + + /** + * @return the font to be used on bullet characters within a given paragraph + */ + public String getBulletFont(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuFont()){ + setValue(props.getBuFont().getTypeface()); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + /** + * @return the character to be used in place of the standard bullet point + */ + public String getBulletCharacter(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuChar()){ + setValue(props.getBuChar().getChar()); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + public Color getBulletFontColor(){ + final XSLFTheme theme = getParentShape().getSheet().getTheme(); + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuClr()){ + setValue(theme.getColor(props.getBuClr())); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue(); + } + + public double getBulletFontSize(){ + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuSzPct()){ + setValue(props.getBuSzPct().getVal() * 0.001); + return true; + } + if(props.isSetBuSzPts()){ + setValue( - props.getBuSzPts().getVal() * 0.01); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 100 : fetcher.getValue(); + } + /** * Specifies the indent size that will be applied to the first line of text in the paragraph. * @@ -137,10 +235,19 @@ public class XSLFTextParagraph implements Iterable{ * @return the indent applied to the first line of text in the paragraph. */ public double getIndent(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetIndent()) return 0; - return Units.toPoints(pr.getIndent()); + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetIndent()){ + setValue(Units.toPoints(props.getIndent())); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -160,10 +267,18 @@ public class XSLFTextParagraph implements Iterable{ * @return the left margin of the paragraph */ public double getLeftMargin(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetMarL()) return 0; - - return Units.toPoints(pr.getMarL()); + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetMarL()){ + double val = Units.toPoints(props.getMarL()); + setValue(val); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -206,13 +321,20 @@ public class XSLFTextParagraph implements Iterable{ * @return the vertical line spacing. */ public double getLineSpacing(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetLnSpc()) return 100; // TODO fetch from master + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetLnSpc()){ + CTTextSpacing spc = props.getLnSpc(); - CTTextSpacing spc = pr.getLnSpc(); - if(spc.isSetSpcPct()) return spc.getSpcPct().getVal()*0.001; - else if (spc.isSetSpcPts()) return -spc.getSpcPts().getVal()*0.01; - else return 100; + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 100 : fetcher.getValue(); } /** @@ -253,13 +375,20 @@ public class XSLFTextParagraph implements Iterable{ * @return the vertical white space before the paragraph */ public double getSpaceBefore(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetSpcBef()) return 0; // TODO fetch from master + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetSpcBef()){ + CTTextSpacing spc = props.getSpcBef(); - CTTextSpacing spc = pr.getSpcBef(); - if(spc.isSetSpcPct()) return spc.getSpcPct().getVal()*0.001; - else if (spc.isSetSpcPts()) return -spc.getSpcPts().getVal()*0.01; - else return 0; + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -300,13 +429,20 @@ public class XSLFTextParagraph implements Iterable{ * @return the vertical white space after the paragraph */ public double getSpaceAfter(){ - CTTextParagraphProperties pr = _p.getPPr(); - if(pr == null || !pr.isSetSpcAft()) return 0; // TODO fetch from master + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetSpcAft()){ + CTTextSpacing spc = props.getSpcAft(); - CTTextSpacing spc = pr.getSpcAft(); - if(spc.isSetSpcPct()) return spc.getSpcPct().getVal()*0.001; - else if (spc.isSetSpcPts()) return -spc.getSpcPts().getVal()*0.01; - else return 0; + if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); + else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -334,8 +470,256 @@ public class XSLFTextParagraph implements Iterable{ } + /** + * Returns whether this paragraph has bullets + */ + public boolean isBullet() { + ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getLevel()){ + public boolean fetch(CTTextParagraphProperties props){ + if(props.isSetBuNone()) { + setValue(false); + return true; + } + if(props.isSetBuFont()){ + setValue(true); + return true; + } + return false; + } + }; + fetchParagraphProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); + } + @Override public String toString(){ return "[" + getClass() + "]" + getText(); } + + public List getTextLines(){ + return _lines; + } + + public double draw(Graphics2D graphics, double x, double y){ + double marginLeft = _shape.getMarginLeft(); + double marginRight = _shape.getMarginRight(); + Rectangle2D anchor = _shape.getAnchor(); + double penY = y; + + double textOffset = getLeftMargin(); + for(TextFragment line : _lines){ + double penX = x; + switch (getTextAlign()) { + case CENTER: + penX += textOffset + (anchor.getWidth() - textOffset - line.getWidth() - marginLeft - marginRight) / 2; + break; + case RIGHT: + penX += (anchor.getWidth() - line.getWidth() - marginLeft - marginRight); + break; + default: + penX = x + textOffset; + break; + } + + if(_bullet != null){ + _bullet.draw(graphics, penX + getIndent(), penY); + } + line.draw(graphics, penX, penY); + + //The vertical line spacing + double spacing = getLineSpacing(); + if(spacing > 0) { + // If linespacing >= 0, then linespacing is a percentage of normal line height. + penY += spacing*0.01*line.getHeight(); + } else { + // positive value means absolute spacing in points + penY += -spacing; + } + } + return penY - y; + } + + static class TextFragment { + private TextLayout _layout; + private AttributedString _str; + + TextFragment(TextLayout layout, AttributedString str){ + _layout = layout; + _str = str; + } + + void draw(Graphics2D graphics, double x, double y){ + double yBaseline = y + _layout.getAscent(); + + graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline ); + + } + + public float getHeight(){ + return _layout.getAscent() + _layout.getDescent() + _layout.getLeading(); + } + public float getWidth(){ + return _layout.getAdvance(); + } + + } + + public AttributedString getAttributedString(){ + String text = getText(); + + AttributedString string = new AttributedString(text); + + int startIndex = 0; + for (XSLFTextRun run : _runs){ + int length = run.getText().length(); + if(length == 0) { + // skip empty runs + continue; + } + int endIndex = startIndex + length; + + string.addAttribute(TextAttribute.FOREGROUND, run.getFontColor(), startIndex, endIndex); + string.addAttribute(TextAttribute.FAMILY, run.getFontFamily(), startIndex, endIndex); + string.addAttribute(TextAttribute.SIZE, run.getFontSize(), startIndex, endIndex); + if(run.isBold()) { + string.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, startIndex, endIndex); + } + if(run.isItalic()) { + string.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, startIndex, endIndex); + } + if(run.isUnderline()) { + string.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, startIndex, endIndex); + } + if(run.isStrikethrough()) { + string.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, startIndex, endIndex); + } + + startIndex = endIndex; + } + + return string; + } + + void breakText(Graphics2D graphics){ + _lines = new ArrayList(); + + AttributedString at = getAttributedString(); + AttributedCharacterIterator it = at.getIterator(); + if(it.getBeginIndex() == it.getEndIndex()) { + return; + } + LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext()); + for (;;) { + int startIndex = measurer.getPosition(); + double wrappingWidth = getWrappingWidth() + 1; // add a pixel to compensate rounding errors + TextLayout layout = measurer.nextLayout((float)wrappingWidth, it.getEndIndex(), true); + if (layout == null) { + // layout can be null if the entire word at the current position + // does not fit within the wrapping width. Try with requireNextWord=false. + layout = measurer.nextLayout((float)wrappingWidth, it.getEndIndex(), false); + } + + int endIndex = measurer.getPosition(); + + if(getTextAlign() == TextAlign.JUSTIFY) { + layout = layout.getJustifiedLayout((float)wrappingWidth); + } + + AttributedString str = new AttributedString(it, startIndex, endIndex); + TextFragment line = new TextFragment(layout, str); + _lines.add(line); + + if(endIndex == it.getEndIndex()) break; + } + + if(isBullet()) { + String buCharacter = getBulletCharacter(); + String buFont = getBulletFont(); + if(buCharacter != null && buFont != null && _lines.size() > 0) { + AttributedString str = new AttributedString(buCharacter); + + TextFragment firstLine = _lines.get(0); + AttributedCharacterIterator bit = firstLine._str.getIterator(); + + Color buColor = getBulletFontColor(); + str.addAttribute(TextAttribute.FOREGROUND, buColor == null ? + bit.getAttribute(TextAttribute.FOREGROUND) : buColor); + str.addAttribute(TextAttribute.FAMILY, buFont); + + double fontSize = (Double)bit.getAttribute(TextAttribute.SIZE); + double buSz = getBulletFontSize(); + if(buSz > 0) fontSize *= buSz* 0.01; + else fontSize = -buSz; + + str.addAttribute(TextAttribute.SIZE, fontSize); + + TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext()); + _bullet = new TextFragment(layout, str); + } + } + + } + + double getWrappingWidth(){ + double width; + if(!_shape.getWordWrap()) { + width = _shape.getSheet().getSlideShow().getPageSize().getWidth(); + } else { + width = _shape.getAnchor().getWidth() - + _shape.getMarginLeft() - _shape.getMarginRight() - getLeftMargin(); + } + return width; + } + + CTTextParagraphProperties getDefaultStyle(){ + CTPlaceholder ph = _shape.getCTPlaceholder(); + String defaultStyleSelector; + if(ph == null) defaultStyleSelector = "otherStyle"; + else { + switch(ph.getType().intValue()){ + case STPlaceholderType.INT_TITLE: + case STPlaceholderType.INT_CTR_TITLE: + defaultStyleSelector = "titleStyle"; + break; + case STPlaceholderType.INT_FTR: + case STPlaceholderType.INT_SLD_NUM: + case STPlaceholderType.INT_DT: + defaultStyleSelector = "otherStyle"; + break; + default: + defaultStyleSelector = "bodyStyle"; + break; + } + } + + int level = getLevel(); + XmlObject[] o = _shape.getSheet().getSlideMaster().getXmlObject().selectPath( + "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' " + + "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " + + ".//p:txStyles/p:" + defaultStyleSelector +"/a:lvl" +(level+1)+ "pPr"); + if(o.length == 1){ + return (CTTextParagraphProperties)o[0]; + } + throw new IllegalArgumentException("Failed to fetch default style for " + + defaultStyleSelector + " and level=" + level); + } + + + private boolean fetchParagraphProperty(ParagraphPropertyFetcher visitor){ + boolean ok = false; + + if(_p.isSetPPr()) ok = visitor.fetch(_p.getPPr()); + + if(!ok) { + XSLFTextShape shape = getParentShape(); + ok = shape.fetchShapeProperty(visitor); + if(!ok) { + CTTextParagraphProperties defaultProps = getDefaultStyle(); + if(defaultProps != null) ok = visitor.fetch(defaultProps); + } + } + + return ok; + } + } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java index c62775e08..558dbdd45 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java @@ -17,6 +17,8 @@ package org.apache.poi.xslf.usermodel; import org.apache.poi.util.Beta; +import org.apache.poi.xslf.model.CharacterPropertyFetcher; +import org.apache.xmlbeans.XmlObject; import org.openxmlformats.schemas.drawingml.x2006.main.CTRegularTextRun; import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillProperties; @@ -24,6 +26,9 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTTextCharacterProperties import org.openxmlformats.schemas.drawingml.x2006.main.CTTextFont; import org.openxmlformats.schemas.drawingml.x2006.main.STTextStrikeType; import org.openxmlformats.schemas.drawingml.x2006.main.STTextUnderlineType; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraphProperties; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; import java.awt.*; @@ -66,6 +71,23 @@ public class XSLFTextRun { clr.setVal(new byte[]{(byte)color.getRed(), (byte)color.getGreen(), (byte)color.getBlue()}); } + public Color getFontColor(){ + final XSLFTheme theme = _p.getParentShape().getSheet().getTheme(); + + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + CTSolidColorFillProperties solidFill = props.getSolidFill(); + if(solidFill != null){ + setValue(theme.getSolidFillColor(solidFill)); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue(); + } + /** * * @param fontSize font size in points. @@ -84,9 +106,17 @@ public class XSLFTextRun { * @return font size in points or -1 if font size is not set. */ public double getFontSize(){ - if(!_r.isSetRPr() || !_r.getRPr().isSetSz()) return -1; - - return _r.getRPr().getSz()*0.01; + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + if(props.isSetSz()){ + setValue(props.getSz()*0.01); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue() == null ? -1 : fetcher.getValue(); } /** @@ -120,12 +150,30 @@ public class XSLFTextRun { } /** - * @return font family or null if niot set + * @return font family or null if not set */ public String getFontFamily(){ - if(!_r.isSetRPr() || !_r.getRPr().isSetLatin()) return null; + final XSLFTheme theme = _p.getParentShape().getSheet().getTheme(); - return _r.getRPr().getLatin().getTypeface(); + CharacterPropertyFetcher visitor = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + CTTextFont font = props.getLatin(); + if(font != null){ + String typeface = font.getTypeface(); + if("+mj-lt".equals(typeface)) { + typeface = theme.getMajorFont(); + } else if ("+mn-lt".equals(typeface)){ + typeface = theme.getMinorFont(); + } + setValue(typeface); + return true; + } + return false; + } + }; + fetchCharacterProperty(visitor); + + return visitor.getValue(); } /** @@ -140,10 +188,18 @@ public class XSLFTextRun { /** * @return whether a run of text will be formatted as strikethrough text. Default is false. */ - public boolean isStrikethrough(){ - if(!_r.isSetRPr()) return false; - - return _r.getRPr().getStrike() == STTextStrikeType.SNG_STRIKE; + public boolean isStrikethrough() { + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + if(props.isSetStrike()){ + setValue(props.getStrike() != STTextStrikeType.NO_STRIKE); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); } /** @@ -159,9 +215,17 @@ public class XSLFTextRun { * @return whether this run of text is formatted as bold text */ public boolean isBold(){ - if(!_r.isSetRPr()) return false; - - return _r.getRPr().getB(); + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + if(props.isSetB()){ + setValue(props.getB()); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); } /** @@ -175,9 +239,17 @@ public class XSLFTextRun { * @return whether this run of text is formatted as italic text */ public boolean isItalic(){ - if(!_r.isSetRPr()) return false; - - return _r.getRPr().getI(); + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + if(props.isSetI()){ + setValue(props.getI()); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); } /** @@ -191,9 +263,17 @@ public class XSLFTextRun { * @return whether this run of text is formatted as underlined text */ public boolean isUnderline(){ - if(!_r.isSetRPr() || !_r.getRPr().isSetU()) return false; - - return _r.getRPr().getU() != STTextUnderlineType.NONE; + CharacterPropertyFetcher fetcher = new CharacterPropertyFetcher(_p.getLevel()){ + public boolean fetch(CTTextCharacterProperties props){ + if(props.isSetU()){ + setValue(props.getU() != STTextUnderlineType.NONE); + return true; + } + return false; + } + }; + fetchCharacterProperty(fetcher); + return fetcher.getValue() == null ? false : fetcher.getValue(); } protected CTTextCharacterProperties getRpR(){ @@ -216,4 +296,22 @@ public class XSLFTextRun { return new XSLFHyperlink(_r.getRPr().getHlinkClick(), this); } + + private boolean fetchCharacterProperty(CharacterPropertyFetcher fetcher){ + boolean ok = false; + + if(_r.isSetRPr()) ok = fetcher.fetch(_r.getRPr()); + + if(!ok) { + XSLFTextShape shape = _p.getParentShape(); + ok = shape.fetchShapeProperty(fetcher); + if(!ok) { + CTTextParagraphProperties defaultProps = _p.getDefaultStyle(); + if(defaultProps != null) ok = fetcher.fetch(defaultProps); + } + } + + return ok; + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java index 5bb48b600..3d303002e 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java @@ -21,18 +21,20 @@ package org.apache.poi.xslf.usermodel; import org.apache.poi.util.Beta; import org.apache.poi.util.Units; +import org.apache.poi.xslf.model.PropertyFetcher; +import org.apache.poi.xslf.model.TextBodyPropertyFetcher; import org.apache.xmlbeans.XmlObject; -import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; -import org.openxmlformats.schemas.drawingml.x2006.main.CTShapeProperties; -import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBody; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBodyProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextParagraph; import org.openxmlformats.schemas.drawingml.x2006.main.STTextAnchoringType; import org.openxmlformats.schemas.drawingml.x2006.main.STTextVerticalType; import org.openxmlformats.schemas.drawingml.x2006.main.STTextWrappingType; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; @@ -79,43 +81,6 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { return paragraph; } - /** - * Specifies a solid color fill. The shape is filled entirely with the specified color. - * - * @param color the solid color fill. - * The value of null unsets the solidFIll attribute from the underlying xml - */ - public void setFillColor(Color color) { - CTShapeProperties spPr = getSpPr(); - if (color == null) { - if(spPr.isSetSolidFill()) spPr.unsetSolidFill(); - } - else { - CTSolidColorFillProperties fill = spPr.isSetSolidFill() ? spPr.getSolidFill() : spPr.addNewSolidFill(); - - CTSRgbColor rgb = CTSRgbColor.Factory.newInstance(); - rgb.setVal(new byte[]{(byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()}); - - fill.setSrgbClr(rgb); - } - } - - /** - * - * @return solid fill color of null if not set - */ - public Color getFillColor(){ - CTShapeProperties spPr = getSpPr(); - if(!spPr.isSetSolidFill() ) return null; - - CTSolidColorFillProperties fill = spPr.getSolidFill(); - if(!fill.isSetSrgbClr()) { - // TODO for now return null for all colors except explicit RGB - return null; - } - byte[] val = fill.getSrgbClr().getVal(); - return new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2]); - } /** * Sets the type of vertical alignment for the text. @@ -140,14 +105,18 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the type of alignment */ public VerticalAlignment getVerticalAlignment(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - STTextAnchoringType.Enum val = bodyPr.getAnchor(); - if(val != null){ - return VerticalAlignment.values()[val.intValue() - 1]; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetAnchor()){ + int val = props.getAnchor().intValue(); + setValue(VerticalAlignment.values()[val - 1]); + return true; + } + return false; } - } - return VerticalAlignment.TOP; + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? VerticalAlignment.TOP : fetcher.getValue(); } /** @@ -185,11 +154,18 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the bottom margin or -1 if not set */ public double getMarginBottom(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - return bodyPr.isSetBIns() ? Units.toPoints(bodyPr.getBIns()) : -1; - } - return -1; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetBIns()){ + double val = Units.toPoints(props.getBIns()); + setValue(val); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -200,11 +176,18 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the left margin */ public double getMarginLeft(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - return bodyPr.isSetLIns() ? Units.toPoints(bodyPr.getLIns()) : -1; - } - return -1; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetLIns()){ + double val = Units.toPoints(props.getLIns()); + setValue(val); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -215,11 +198,18 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the right margin */ public double getMarginRight(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - return bodyPr.isSetRIns() ? Units.toPoints(bodyPr.getRIns()) : -1; - } - return -1; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetRIns()){ + double val = Units.toPoints(props.getRIns()); + setValue(val); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -229,11 +219,18 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the top margin */ public double getMarginTop(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - return bodyPr.isSetTIns() ? Units.toPoints(bodyPr.getTIns()) : -1; - } - return -1; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetTIns()){ + double val = Units.toPoints(props.getTIns()); + setValue(val); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? 0 : fetcher.getValue(); } /** @@ -300,11 +297,17 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { * @return the value indicating word wrap */ public boolean getWordWrap(){ - CTTextBodyProperties bodyPr = getTextBodyPr(); - if (bodyPr != null) { - return bodyPr.getWrap() == STTextWrappingType.SQUARE; - } - return false; + PropertyFetcher fetcher = new TextBodyPropertyFetcher(){ + public boolean fetch(CTTextBodyProperties props){ + if(props.isSetWrap()){ + setValue(props.getWrap() == STTextWrappingType.SQUARE); + return true; + } + return false; + } + }; + fetchShapeProperty(fetcher); + return fetcher.getValue() == null ? true : fetcher.getValue(); } /** @@ -362,4 +365,132 @@ public abstract class XSLFTextShape extends XSLFSimpleShape { protected abstract CTTextBody getTextBody(boolean create); + + + public Placeholder getTextType(){ + CTPlaceholder ph; + XmlObject[] obj = getXmlObject().selectPath( + "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' .//*/p:nvPr/p:ph"); + if(obj.length == 1){ + ph = (CTPlaceholder)obj[0]; + int val = ph.getType().intValue(); + return Placeholder.values()[val - 1]; + } + else { + return null; + } + } + + @Override + public void draw(Graphics2D graphics){ + java.awt.Shape outline = getOutline(); + + // shadow + XSLFShadow shadow = getShadow(); + if(shadow != null) shadow.draw(graphics); + + //fill + Color fillColor = getFillColor(); + if (fillColor != null) { + graphics.setColor(fillColor); + applyFill(graphics); + graphics.fill(outline); + } + + //border + Color lineColor = getLineColor(); + if (lineColor != null){ + graphics.setColor(lineColor); + applyStroke(graphics); + graphics.draw(outline); + } + + // text + if(getText().length() > 0) drawText(graphics); + } + + /** + * Compute the cumulative height occupied by the text + */ + private double getTextHeight(){ + // dry-run in a 1x1 image and return the vertical advance + BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + return drawParagraphs(img.createGraphics(), 0, 0); + } + + void breakText(Graphics2D graphics){ + for(XSLFTextParagraph p : _paragraphs) p.breakText(graphics); + } + + public void drawText(Graphics2D graphics) { + breakText(graphics); + + Rectangle2D anchor = getAnchor(); + double x = anchor.getX() + getMarginLeft(); + double y = anchor.getY(); + + // first dry-run to calculate the total height of the text + double textHeight = getTextHeight(); + + switch (getVerticalAlignment()){ + case TOP: + y += getMarginTop(); + break; + case BOTTOM: + y += anchor.getHeight() - textHeight - getMarginBottom(); + break; + default: + case MIDDLE: + double delta = anchor.getHeight() - textHeight - + getMarginTop() - getMarginBottom(); + y += getMarginTop() + delta/2; + break; + } + + drawParagraphs(graphics, x, y); + + } + + + /** + * pain the paragraphs starting from top left (x,y) + * + * @return the vertical advance, i.e. the cumulative space occupied by the text + */ + private double drawParagraphs(Graphics2D graphics, double x, double y) { + double y0 = y; + for(int i = 0; i < _paragraphs.size(); i++){ + XSLFTextParagraph p = _paragraphs.get(i); + java.util.List lines = p.getTextLines(); + + if(i > 0 && lines.size() > 0) { + // the amount of vertical white space before the paragraph + double spaceBefore = p.getSpaceBefore(); + if(spaceBefore > 0) { + // positive value means percentage spacing of the height of the first line, e.g. + // the higher the first line, the bigger the space before the paragraph + y += spaceBefore*0.01*lines.get(0).getHeight(); + } else { + // negative value means the absolute spacing in points + y += -spaceBefore; + } + } + + y += p.draw(graphics, x, y); + + if(i < _paragraphs.size() - 1) { + double spaceAfter = p.getSpaceAfter(); + if(spaceAfter > 0) { + // positive value means percentage spacing of the height of the last line, e.g. + // the higher the last line, the bigger the space after the paragraph + y += spaceAfter*0.01*lines.get(lines.size() - 1).getHeight(); + } else { + // negative value means the absolute spacing in points + y += -spaceAfter; + } + } + } + return y - y0; + } + } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTheme.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTheme.java index d107b3a27..a648dbdf7 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTheme.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTheme.java @@ -22,15 +22,21 @@ import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.util.Beta; import org.apache.poi.util.Internal; import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; -import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideLayout; -import org.openxmlformats.schemas.presentationml.x2006.main.SldLayoutDocument; -import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMaster; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSchemeColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTBaseStyles; +import org.openxmlformats.schemas.drawingml.x2006.main.CTColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTColorScheme; import org.openxmlformats.schemas.drawingml.x2006.main.ThemeDocument; import org.openxmlformats.schemas.drawingml.x2006.main.CTOfficeStyleSheet; -import org.openxmlformats.schemas.officeDocument.x2006.relationships.STRelationshipId; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSRgbColor; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillProperties; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPresetColor; import javax.xml.namespace.QName; + +import java.awt.Color; import java.io.IOException; import java.io.OutputStream; import java.util.Map; @@ -39,7 +45,8 @@ import java.util.HashMap; @Beta public class XSLFTheme extends POIXMLDocumentPart { private CTOfficeStyleSheet _theme; - + private Map _schemeColors; + XSLFTheme() { super(); _theme = CTOfficeStyleSheet.Factory.newInstance(); @@ -50,8 +57,24 @@ public class XSLFTheme extends POIXMLDocumentPart { ThemeDocument doc = ThemeDocument.Factory.parse(getPackagePart().getInputStream()); _theme = doc.getTheme(); + initialize(); } + private void initialize(){ + CTBaseStyles elems = _theme.getThemeElements(); + CTColorScheme scheme = elems.getClrScheme(); + // The color scheme is responsible for defining a list of twelve colors. + _schemeColors = new HashMap(12); + for(XmlObject o : scheme.selectPath("*")){ + CTColor c = (CTColor)o; + String name = c.getDomNode().getLocalName(); + _schemeColors.put(name, new XSLFColor(c)); + } + _schemeColors.put("bg1", _schemeColors.get("lt1")); + _schemeColors.put("bg2", _schemeColors.get("lt2")); + _schemeColors.put("tx1", _schemeColors.get("dk1")); + _schemeColors.put("tx2", _schemeColors.get("dk2")); + } public String getName(){ return _theme.getName(); @@ -62,6 +85,146 @@ public class XSLFTheme extends POIXMLDocumentPart { } /** + * Get a color from the theme's color scheme by name + * + * @return a theme color or null if not found + */ + public XSLFColor getColor(String name){ + return _schemeColors.get(name); + } + + Color getSchemeColor(CTSchemeColor schemeColor){ + String colorRef = schemeColor.getVal().toString(); + int alpha = 0xFF; + if(schemeColor.sizeOfAlphaArray() > 0){ + int aval = schemeColor.getAlphaArray(0).getVal(); + alpha = Math.round(255 * aval / 100000f); + } + Color themeColor = _schemeColors.get(colorRef).getColor(alpha); + + int lumMod = 100, lumOff = 0; + if (schemeColor.sizeOfLumModArray() > 0) { + lumMod = schemeColor.getLumModArray(0).getVal() / 1000; + } + if (schemeColor.sizeOfLumOffArray() > 0) { + lumOff = schemeColor.getLumOffArray(0).getVal() / 1000; + } + if(schemeColor.sizeOfShadeArray() > 0) { + lumMod = schemeColor.getShadeArray(0).getVal() / 1000; + } + Color color = modulateLuminanace(themeColor, lumMod, lumOff); + + if(schemeColor.sizeOfTintArray() > 0) { + float tint = schemeColor.getTintArray(0).getVal() / 100000f; + int red = Math.round(tint * themeColor.getRed() + (1 - tint) * 255); + int green = Math.round(tint * themeColor.getGreen() + (1 - tint) * 255); + int blue = Math.round(tint * themeColor.getBlue() + (1 - tint) * 255); + color = new Color(red, green, blue); + } + + return color; + } + + /** + * TODO get rid of code duplication. Re-write to use xpath instead of beans + */ + Color getPresetColor(CTPresetColor presetColor){ + String colorName = presetColor.getVal().toString(); + Color color; + try { + color = (Color)Color.class.getField(colorName).get(null); + } catch (Exception e){ + color = Color.black; + } + if(presetColor.sizeOfAlphaArray() > 0){ + int aval = presetColor.getAlphaArray(0).getVal(); + int alpha = Math.round(255 * aval / 100000f); + color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + int lumMod = 100, lumOff = 0; + if (presetColor.sizeOfLumModArray() > 0) { + lumMod = presetColor.getLumModArray(0).getVal() / 1000; + } + if (presetColor.sizeOfLumOffArray() > 0) { + lumOff = presetColor.getLumOffArray(0).getVal() / 1000; + } + if(presetColor.sizeOfShadeArray() > 0) { + lumMod = presetColor.getShadeArray(0).getVal() / 1000; + } + color = modulateLuminanace(color, lumMod, lumOff); + + if(presetColor.sizeOfTintArray() > 0) { + float tint = presetColor.getTintArray(0).getVal() / 100000f; + int red = Math.round(tint * color.getRed() + (1 - tint) * 255); + int green = Math.round(tint * color.getGreen() + (1 - tint) * 255); + int blue = Math.round(tint * color.getBlue() + (1 - tint) * 255); + color = new Color(red, green, blue); + } + + return color; + } + + public Color brighter(Color color, double tint) { + int r = color.getRed(); + int g = color.getGreen(); + int b = color.getBlue(); + + /* From 2D group: + * 1. black.brighter() should return grey + * 2. applying brighter to blue will always return blue, brighter + * 3. non pure color (non zero rgb) will eventually return white + */ + int i = (int)(1.0/(1.0-tint)); + if ( r == 0 && g == 0 && b == 0) { + return new Color(i, i, i); + } + if ( r > 0 && r < i ) r = i; + if ( g > 0 && g < i ) g = i; + if ( b > 0 && b < i ) b = i; + + return new Color(Math.min((int)(r/tint), 255), + Math.min((int)(g/tint), 255), + Math.min((int)(b/tint), 255)); + } + + Color getSrgbColor(CTSRgbColor srgb){ + byte[] val = srgb.getVal(); + int alpha = 0xFF; + if(srgb.sizeOfAlphaArray() > 0){ + int aval = srgb.getAlphaArray(0).getVal(); + alpha = Math.round(255 * aval / 100000f); + } + return new Color(0xFF & val[0], 0xFF & val[1], 0xFF & val[2], alpha); + } + + Color getSolidFillColor(CTSolidColorFillProperties solidFill){ + Color color; + if (solidFill.isSetSrgbClr()) { + color = getSrgbColor(solidFill.getSrgbClr()); + } else if (solidFill.isSetSchemeClr()) { + color = getSchemeColor(solidFill.getSchemeClr()); + } else { + // TODO support other types + color = Color.black; + } + return color; + } + + Color getColor(CTColor solidFill){ + Color color; + if (solidFill.isSetSrgbClr()) { + color = getSrgbColor(solidFill.getSrgbClr()); + } else if (solidFill.isSetSchemeClr()) { + color = getSchemeColor(solidFill.getSchemeClr()); + } else { + // TODO support other types + color = Color.black; + } + return color; + } + + /** * While developing only! */ @Internal @@ -84,4 +247,31 @@ public class XSLFTheme extends POIXMLDocumentPart { out.close(); } -} \ No newline at end of file + public static Color modulateLuminanace(Color c, int lumMod, int lumOff) { + Color color; + if (lumOff > 0) { + color = new Color( + (int) (Math.round((255 - c.getRed()) * (100.0 - lumMod) / 100.0 + c.getRed())), + (int) (Math.round((255 - c.getGreen()) * lumOff / 100.0 + c.getGreen())), + (int) (Math.round((255 - c.getBlue()) * lumOff / 100.0 + c.getBlue())), + c.getAlpha() + ); + } else { + color = new Color( + (int) (Math.round(c.getRed() * lumMod / 100.0)), + (int) (Math.round(c.getGreen() * lumMod / 100.0)), + (int) (Math.round(c.getBlue() * lumMod / 100.0)), + c.getAlpha() + ); + } + return color; + } + + public String getMajorFont(){ + return _theme.getThemeElements().getFontScheme().getMajorFont().getLatin().getTypeface(); + } + + public String getMinorFont(){ + return _theme.getThemeElements().getFontScheme().getMinorFont().getLatin().getTypeface(); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java new file mode 100644 index 000000000..f4e6955e6 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java @@ -0,0 +1,101 @@ +/* + * ==================================================================== + * 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.xslf.util; + +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.apache.poi.xslf.usermodel.XSLFSlide; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; + +/** + * Date: 10/11/11 + * + * @author Yegor Kozlov + */ +public class PPTX2PNG { + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.out.println("Usage: PPTX2PNG [options] "); + return; + } + + int slidenum = -1; + float scale = 1; + String file = null; + + for (int i = 0; i < args.length; i++) { + if (args[i].startsWith("-")) { + if ("-scale".equals(args[i])) { + scale = Float.parseFloat(args[++i]); + } else if ("-slide".equals(args[i])) { + slidenum = Integer.parseInt(args[++i]); + } + } else { + file = args[i]; + } + } + + System.out.println("Processing " + file); + XMLSlideShow ppt = new XMLSlideShow(OPCPackage.open(file)); + + Dimension pgsize = ppt.getPageSize(); + int width = (int) (pgsize.width * scale); + int height = (int) (pgsize.height * scale); + + XSLFSlide[] slide = ppt.getSlides(); + for (int i = 0; i < slide.length; i++) { + if (slidenum != -1 && slidenum != (i + 1)) continue; + + String title = slide[i].getTitle(); + System.out.println("Rendering slide " + (i + 1) + (title == null ? "" : ": " + title)); + + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = img.createGraphics(); + + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + graphics.setPaint(Color.white); + graphics.fill(new Rectangle2D.Float(0, 0, width, height)); + + graphics.scale((double) width / pgsize.width, (double) height / pgsize.height); + + slide[i].draw(graphics); + + String fname = file.replaceAll("\\.pptx", "-" + (i + 1) + ".png"); + FileOutputStream out = new FileOutputStream(fname); + ImageIO.write(img, "png", out); + out.close(); + } + System.out.println("Done"); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java index 61eb7cd0a..e522c375d 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java @@ -41,7 +41,6 @@ public class TestXMLSlideShow extends TestCase { if(part.getContentType().equals(XSLFRelation.MAIN.getContentType())) { found = true; } - //System.out.println(part); } assertTrue(found); } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFAutoShape.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFAutoShape.java index efed3ca03..7cab8ea4e 100755 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFAutoShape.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFAutoShape.java @@ -33,11 +33,11 @@ public class TestXSLFAutoShape extends TestCase { XSLFAutoShape shape = slide.createAutoShape(); shape.addNewTextParagraph().addNewTextRun().setText("POI"); - // margins - assertEquals(-1., shape.getMarginBottom()); - assertEquals(-1., shape.getMarginTop()); - assertEquals(-1., shape.getMarginLeft()); - assertEquals(-1., shape.getMarginRight()); + // default margins from slide master + assertEquals(3.6, shape.getMarginBottom()); + assertEquals(3.6, shape.getMarginTop()); + assertEquals(7.2, shape.getMarginLeft()); + assertEquals(7.2, shape.getMarginRight()); shape.setMarginBottom(1.0); assertEquals(1.0, shape.getMarginBottom()); @@ -57,21 +57,22 @@ public class TestXSLFAutoShape extends TestCase { shape.setMarginRight(0.0); assertEquals(0.0, shape.getMarginRight()); + // unset to defauls shape.setMarginBottom(-1); - assertEquals(-1., shape.getMarginBottom()); + assertEquals(3.6, shape.getMarginBottom()); shape.setMarginTop(-1); - assertEquals(-1.0, shape.getMarginTop()); + assertEquals(3.6, shape.getMarginTop()); shape.setMarginLeft(-1); - assertEquals(-1.0, shape.getMarginLeft()); + assertEquals(7.2, shape.getMarginLeft()); shape.setMarginRight(-1); - assertEquals(-1.0, shape.getMarginRight()); + assertEquals(7.2, shape.getMarginRight()); // shape - assertFalse(shape.getWordWrap()); - shape.setWordWrap(true); assertTrue(shape.getWordWrap()); shape.setWordWrap(false); assertFalse(shape.getWordWrap()); + shape.setWordWrap(true); + assertTrue(shape.getWordWrap()); // shape assertEquals(TextAutofit.NORMAL, shape.getTextAutofit()); @@ -209,7 +210,7 @@ public class TestXSLFAutoShape extends TestCase { assertEquals(1, p.getTextRuns().size()); assertSame(r, p.getTextRuns().get(0)); - assertEquals(-1.0, r.getFontSize()); + assertEquals(18.0, r.getFontSize()); // default font size for text boxes assertFalse(r.getXmlObject().getRPr().isSetSz()); r.setFontSize(10.0); assertTrue(r.getXmlObject().isSetRPr()); @@ -220,9 +221,9 @@ public class TestXSLFAutoShape extends TestCase { assertFalse(r.getXmlObject().getRPr().isSetSz()); assertFalse(r.getXmlObject().getRPr().isSetLatin()); - assertNull(r.getFontFamily()); + assertEquals("Calibri", r.getFontFamily()); // comes from the slide master r.setFontFamily(null); - assertNull(r.getFontFamily()); + assertEquals("Calibri", r.getFontFamily()); // comes from the slide master r.setFontFamily("Arial"); assertEquals("Arial", r.getFontFamily()); assertEquals("Arial", r.getXmlObject().getRPr().getLatin().getTypeface()); @@ -230,7 +231,7 @@ public class TestXSLFAutoShape extends TestCase { assertEquals("Symbol", r.getFontFamily()); assertEquals("Symbol", r.getXmlObject().getRPr().getLatin().getTypeface()); r.setFontFamily(null); - assertNull(r.getFontFamily()); + assertEquals("Calibri", r.getFontFamily()); // comes from the slide master assertFalse(r.getXmlObject().getRPr().isSetLatin()); assertFalse(r.isStrikethrough()); diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java index d012f99ab..53562d967 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java @@ -21,8 +21,10 @@ import junit.framework.TestCase; import org.apache.poi.util.Units; import org.apache.poi.xslf.usermodel.LineCap; import org.apache.poi.xslf.usermodel.LineDash; +import org.apache.poi.xslf.XSLFTestDataSamples; import org.openxmlformats.schemas.drawingml.x2006.main.STLineCap; import org.openxmlformats.schemas.drawingml.x2006.main.STPresetLineDashVal; +import org.openxmlformats.schemas.drawingml.x2006.main.CTSchemeColor; import java.awt.*; @@ -129,4 +131,107 @@ public class TestXSLFSimpleShape extends TestCase { assertFalse(shape.getSpPr().isSetSolidFill()); } + public void testDefaultProperties() { + XMLSlideShow ppt = XSLFTestDataSamples.openSampleDocument("shapes.pptx"); + + XSLFSlide slide6 = ppt.getSlides()[5]; + XSLFShape[] shapes = slide6.getShapes(); + for(int i = 1; i < shapes.length; i++){ + XSLFSimpleShape s = (XSLFSimpleShape) shapes[i]; + // all shapes have a theme color="accent1" + assertEquals("accent1", s.getSpStyle().getFillRef().getSchemeClr().getVal().toString()); + assertEquals(2.0, s.getLineWidth()); + assertEquals(LineCap.FLAT, s.getLineCap()); + // YK: calculated color is slightly different from PowerPoint + assertEquals(new Color(40, 65, 95), s.getLineColor()); + } + + XSLFSimpleShape s0 = (XSLFSimpleShape) shapes[0]; + // fill is not set + assertNull(s0.getSpPr().getSolidFill()); + assertEquals(slide6.getTheme().getColor("accent1").getColor(), s0.getFillColor()); + assertEquals(new Color(79, 129, 189), s0.getFillColor()); + + // lighter 80% + XSLFSimpleShape s1 = (XSLFSimpleShape)shapes[1]; + CTSchemeColor ref1 = s1.getSpPr().getSolidFill().getSchemeClr(); + assertEquals(1, ref1.sizeOfLumModArray()); + assertEquals(1, ref1.sizeOfLumOffArray()); + assertEquals(20000, ref1.getLumModArray(0).getVal()); + assertEquals(80000, ref1.getLumOffArray(0).getVal()); + assertEquals("accent1", ref1.getVal().toString()); + assertEquals(new Color(220, 230, 242), s1.getFillColor()); + + // lighter 60% + XSLFSimpleShape s2 = (XSLFSimpleShape)shapes[2]; + CTSchemeColor ref2 = s2.getSpPr().getSolidFill().getSchemeClr(); + assertEquals(1, ref2.sizeOfLumModArray()); + assertEquals(1, ref2.sizeOfLumOffArray()); + assertEquals(40000, ref2.getLumModArray(0).getVal()); + assertEquals(60000, ref2.getLumOffArray(0).getVal()); + assertEquals("accent1", ref2.getVal().toString()); + assertEquals(new Color(185, 205, 229), s2.getFillColor()); + + // lighter 40% + XSLFSimpleShape s3 = (XSLFSimpleShape)shapes[3]; + CTSchemeColor ref3 = s3.getSpPr().getSolidFill().getSchemeClr(); + assertEquals(1, ref3.sizeOfLumModArray()); + assertEquals(1, ref3.sizeOfLumOffArray()); + assertEquals(60000, ref3.getLumModArray(0).getVal()); + assertEquals(40000, ref3.getLumOffArray(0).getVal()); + assertEquals("accent1", ref3.getVal().toString()); + assertEquals(new Color(149, 179, 215), s3.getFillColor()); + + // darker 25% + XSLFSimpleShape s4 = (XSLFSimpleShape)shapes[4]; + CTSchemeColor ref4 = s4.getSpPr().getSolidFill().getSchemeClr(); + assertEquals(1, ref4.sizeOfLumModArray()); + assertEquals(0, ref4.sizeOfLumOffArray()); + assertEquals(75000, ref4.getLumModArray(0).getVal()); + assertEquals("accent1", ref3.getVal().toString()); + // YK: calculated color is slightly different from PowerPoint + assertEquals(new Color(59, 97, 142), s4.getFillColor()); + + XSLFSimpleShape s5 = (XSLFSimpleShape)shapes[5]; + CTSchemeColor ref5 = s5.getSpPr().getSolidFill().getSchemeClr(); + assertEquals(1, ref5.sizeOfLumModArray()); + assertEquals(0, ref5.sizeOfLumOffArray()); + assertEquals(50000, ref5.getLumModArray(0).getVal()); + assertEquals("accent1", ref5.getVal().toString()); + // YK: calculated color is slightly different from PowerPoint + assertEquals(new Color(40, 65, 95), s5.getFillColor()); + } + + public void testAnchor(){ + XMLSlideShow ppt = XSLFTestDataSamples.openSampleDocument("shapes.pptx"); + XSLFSlide[] slide = ppt.getSlides(); + + XSLFSlide slide2 = slide[1]; + XSLFSlideLayout layout2 = slide2.getSlideLayout(); + XSLFShape[] shapes2 = slide2.getShapes(); + XSLFTextShape sh1 = (XSLFTextShape)shapes2[0]; + assertEquals(Placeholder.CENTERED_TITLE, sh1.getTextType()); + assertEquals("PPTX Title", sh1.getText()); + assertNull(sh1.getSpPr().getXfrm()); // xfrm is not set, the query is delegated to the slide layout + assertEquals(sh1.getAnchor(), layout2.getTextShapeByType(Placeholder.CENTERED_TITLE).getAnchor()); + + XSLFTextShape sh2 = (XSLFTextShape)shapes2[1]; + assertEquals("Subtitle\nAnd second line", sh2.getText()); + assertEquals(Placeholder.SUBTITLE, sh2.getTextType()); + assertNull(sh2.getSpPr().getXfrm()); // xfrm is not set, the query is delegated to the slide layout + assertEquals(sh2.getAnchor(), layout2.getTextShapeByType(Placeholder.SUBTITLE).getAnchor()); + + XSLFSlide slide5 = slide[4]; + XSLFSlideLayout layout5 = slide5.getSlideLayout(); + XSLFTextShape shTitle = slide5.getTextShapeByType(Placeholder.TITLE); + assertEquals("Hyperlinks", shTitle.getText()); + // xfrm is not set, the query is delegated to the slide layout + assertNull(shTitle.getSpPr().getXfrm()); + // xfrm is not set, the query is delegated to the slide master + assertNull(layout5.getTextShapeByType(Placeholder.TITLE).getSpPr().getXfrm()); + assertNotNull(layout5.getSlideMaster().getTextShapeByType(Placeholder.TITLE).getSpPr().getXfrm()); + assertEquals(shTitle.getAnchor(), layout5.getSlideMaster().getTextShapeByType(Placeholder.TITLE).getAnchor()); + + } + } \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextBox.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextBox.java index cc54111a1..06b5f1b9a 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextBox.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextBox.java @@ -28,10 +28,10 @@ public class TestXSLFTextBox extends TestCase { XSLFSlide slide = ppt.createSlide(); XSLFTextBox shape = slide.createTextBox(); - assertNull(shape.getPlaceholder()); + assertNull(shape.getTextType()); shape.setPlaceholder(Placeholder.TITLE); - assertEquals(Placeholder.TITLE, shape.getPlaceholder()); + assertEquals(Placeholder.TITLE, shape.getTextType()); shape.setPlaceholder(null); - assertNull(shape.getPlaceholder()); + assertNull(shape.getTextType()); } } \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java new file mode 100644 index 000000000..7524b2529 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java @@ -0,0 +1,609 @@ +/* ==================================================================== + 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.xslf.usermodel; + +import junit.framework.TestCase; +import org.apache.poi.xslf.XSLFTestDataSamples; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBodyProperties; + +import java.awt.*; + +/** + * @author Yegor Kozlov + */ +public class TestXSLFTextShape extends TestCase { + + public void testLayouts(){ + XMLSlideShow ppt = XSLFTestDataSamples.openSampleDocument("layouts.pptx"); + + XSLFSlide[] slide = ppt.getSlides(); + + verifySlide1(slide[0]); + verifySlide2(slide[1]); + verifySlide3(slide[2]); + verifySlide4(slide[3]); + verifySlide7(slide[6]); + verifySlide8(slide[7]); + verifySlide10(slide[9]); + } + + void verifySlide1(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Title Slide",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.CTR_TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + XSLFTextShape masterShape1 = (XSLFTextShape)layout.getPlaceholder(ph1); + assertNotNull(masterShape1.getSpPr().getXfrm()); + assertEquals(masterShape1.getAnchor(), shape1.getAnchor()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.MIDDLE, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Centered Title", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(44.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + CTPlaceholder ph2 = shape2.getCTPlaceholder(); + assertEquals(STPlaceholderType.SUB_TITLE, ph2.getType()); + // anchor is not defined in the shape + assertNull(shape2.getSpPr().getXfrm()); + + XSLFTextShape masterShape2 = (XSLFTextShape)layout.getPlaceholder(ph2); + assertNotNull(masterShape2.getSpPr().getXfrm()); + assertEquals(masterShape2.getAnchor(), shape2.getAnchor()); + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape2.getVerticalAlignment()); + + assertEquals("subtitle", shape2.getText()); + XSLFTextRun r2 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals("Calibri", r2.getFontFamily()); + assertEquals(32.0, r2.getFontSize()); + // TODO fix calculation of tint + //assertEquals(new Color(137, 137, 137), r2.getFontColor()); + } + + void verifySlide2(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Title and Content",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + XSLFTextShape masterShape1 = (XSLFTextShape)layout.getPlaceholder(ph1); + // layout does not have anchor info either, it is in the slide master + assertNull(masterShape1.getSpPr().getXfrm()); + masterShape1 = (XSLFTextShape)layout.getSlideMaster().getPlaceholder(ph1); + assertNotNull(masterShape1.getSpPr().getXfrm()); + assertEquals(masterShape1.getAnchor(), shape1.getAnchor()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.MIDDLE, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Title", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(44.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + CTPlaceholder ph2 = shape2.getCTPlaceholder(); + assertFalse(ph2.isSetType()); // + assertTrue(ph2.isSetIdx()); + assertEquals(1, ph2.getIdx()); + // anchor is not defined in the shape + assertNull(shape2.getSpPr().getXfrm()); + + XSLFTextShape masterShape2 = (XSLFTextShape)layout.getPlaceholder(ph2); + // anchor of the body text is missing in the slide layout, llokup in the slide master + assertNull(masterShape2.getSpPr().getXfrm()); + masterShape2 = (XSLFTextShape)layout.getSlideMaster().getPlaceholder(ph2); + assertNotNull(masterShape2.getSpPr().getXfrm()); + assertEquals(masterShape2.getAnchor(), shape2.getAnchor()); + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape2.getVerticalAlignment()); + + XSLFTextRun pr1 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr1.getParentParagraph().getLevel()); + assertEquals("Content", pr1.getText()); + assertEquals("Calibri", pr1.getFontFamily()); + assertEquals(32.0, pr1.getFontSize()); + assertEquals(27.0, pr1.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr1.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr1.getParentParagraph().getBulletFont()); + + XSLFTextRun pr2 = shape2.getTextParagraphs().get(1).getTextRuns().get(0); + assertEquals(1, pr2.getParentParagraph().getLevel()); + assertEquals("Level 2", pr2.getText()); + assertEquals("Calibri", pr2.getFontFamily()); + assertEquals(28.0, pr2.getFontSize()); + assertEquals(58.5, pr2.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr2.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr2.getParentParagraph().getBulletFont()); + + XSLFTextRun pr3 = shape2.getTextParagraphs().get(2).getTextRuns().get(0); + assertEquals(2, pr3.getParentParagraph().getLevel()); + assertEquals("Level 3", pr3.getText()); + assertEquals("Calibri", pr3.getFontFamily()); + assertEquals(24.0, pr3.getFontSize()); + assertEquals(90.0, pr3.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr3.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr3.getParentParagraph().getBulletFont()); + + XSLFTextRun pr4 = shape2.getTextParagraphs().get(3).getTextRuns().get(0); + assertEquals(3, pr4.getParentParagraph().getLevel()); + assertEquals("Level 4", pr4.getText()); + assertEquals("Calibri", pr4.getFontFamily()); + assertEquals(20.0, pr4.getFontSize()); + assertEquals(126.0, pr4.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr4.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr4.getParentParagraph().getBulletFont()); + + XSLFTextRun pr5 = shape2.getTextParagraphs().get(4).getTextRuns().get(0); + assertEquals(4, pr5.getParentParagraph().getLevel()); + assertEquals("Level 5", pr5.getText()); + assertEquals("Calibri", pr5.getFontFamily()); + assertEquals(20.0, pr5.getFontSize()); + assertEquals(162.0, pr5.getParentParagraph().getLeftMargin()); + assertEquals("\u00bb", pr5.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr5.getParentParagraph().getBulletFont()); + + } + + void verifySlide3(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Section Header",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + XSLFTextShape masterShape1 = (XSLFTextShape)layout.getPlaceholder(ph1); + assertNotNull(masterShape1.getSpPr().getXfrm()); + assertEquals(masterShape1.getAnchor(), shape1.getAnchor()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Section Title", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.LEFT, r1.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(40.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + assertTrue(r1.isBold()); + assertFalse(r1.isItalic()); + assertFalse(r1.isUnderline()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + CTPlaceholder ph2 = shape2.getCTPlaceholder(); + assertEquals(STPlaceholderType.BODY, ph2.getType()); + // anchor is not defined in the shape + assertNull(shape2.getSpPr().getXfrm()); + + XSLFTextShape masterShape2 = (XSLFTextShape)layout.getPlaceholder(ph2); + assertNotNull(masterShape2.getSpPr().getXfrm()); + assertEquals(masterShape2.getAnchor(), shape2.getAnchor()); + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.BOTTOM, shape2.getVerticalAlignment()); + + assertEquals("Section Header", shape2.getText()); + XSLFTextRun r2 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.LEFT, r2.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r2.getFontFamily()); + assertEquals(20.0, r2.getFontSize()); + // TODO fix calculation of tint + //assertEquals(new Color(137, 137, 137), r2.getFontColor()); + } + + void verifySlide4(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Two Content",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + XSLFTextShape masterShape1 = (XSLFTextShape)layout.getPlaceholder(ph1); + // layout does not have anchor info either, it is in the slide master + assertNull(masterShape1.getSpPr().getXfrm()); + masterShape1 = (XSLFTextShape)layout.getSlideMaster().getPlaceholder(ph1); + assertNotNull(masterShape1.getSpPr().getXfrm()); + assertEquals(masterShape1.getAnchor(), shape1.getAnchor()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.MIDDLE, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Title", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.CENTER, r1.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(44.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + CTPlaceholder ph2 = shape2.getCTPlaceholder(); + assertFalse(ph2.isSetType()); + assertTrue(ph2.isSetIdx()); + assertEquals(1, ph2.getIdx()); // + // anchor is not defined in the shape + assertNull(shape2.getSpPr().getXfrm()); + + XSLFTextShape masterShape2 = (XSLFTextShape)layout.getPlaceholder(ph2); + assertNotNull(masterShape2.getSpPr().getXfrm()); + assertEquals(masterShape2.getAnchor(), shape2.getAnchor()); + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape2.getVerticalAlignment()); + + XSLFTextRun pr1 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr1.getParentParagraph().getLevel()); + assertEquals("Left", pr1.getText()); + assertEquals("Calibri", pr1.getFontFamily()); + assertEquals(28.0, pr1.getFontSize()); + assertEquals(27.0, pr1.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr1.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr1.getParentParagraph().getBulletFont()); + + XSLFTextRun pr2 = shape2.getTextParagraphs().get(1).getTextRuns().get(0); + assertEquals(1, pr2.getParentParagraph().getLevel()); + assertEquals("Level 2", pr2.getParentParagraph().getText()); + assertEquals("Calibri", pr2.getFontFamily()); + assertEquals(24.0, pr2.getFontSize()); + assertEquals(58.5, pr2.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr2.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr2.getParentParagraph().getBulletFont()); + + XSLFTextRun pr3 = shape2.getTextParagraphs().get(2).getTextRuns().get(0); + assertEquals(2, pr3.getParentParagraph().getLevel()); + assertEquals("Level 3", pr3.getParentParagraph().getText()); + assertEquals("Calibri", pr3.getFontFamily()); + assertEquals(20.0, pr3.getFontSize()); + assertEquals(90.0, pr3.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr3.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr3.getParentParagraph().getBulletFont()); + + XSLFTextRun pr4 = shape2.getTextParagraphs().get(3).getTextRuns().get(0); + assertEquals(3, pr4.getParentParagraph().getLevel()); + assertEquals("Level 4", pr4.getParentParagraph().getText()); + assertEquals("Calibri", pr4.getFontFamily()); + assertEquals(18.0, pr4.getFontSize()); + assertEquals(126.0, pr4.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr4.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr4.getParentParagraph().getBulletFont()); + + XSLFTextShape shape3 = (XSLFTextShape)shapes[2]; + XSLFTextRun pr5 = shape3.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr5.getParentParagraph().getLevel()); + assertEquals("Right", pr5.getText()); + assertEquals("Calibri", pr5.getFontFamily()); + assertEquals(Color.black, pr5.getFontColor()); + } + + void verifySlide5(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + // TODO + } + + void verifySlide7(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Blank",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.MIDDLE, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Blank with Default Title", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.CENTER, r1.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(44.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + assertFalse(r1.isBold()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape2.getVerticalAlignment()); + + XSLFTextRun pr1 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr1.getParentParagraph().getLevel()); + assertEquals("Default Text", pr1.getText()); + assertEquals("Calibri", pr1.getFontFamily()); + assertEquals(18.0, pr1.getFontSize()); + + XSLFTextShape shape3 = (XSLFTextShape)shapes[2]; + assertEquals("Default", shape3.getTextParagraphs().get(0).getText()); + assertEquals("Text with levels", shape3.getTextParagraphs().get(1).getText()); + assertEquals("Level 1", shape3.getTextParagraphs().get(2).getText()); + assertEquals("Level 2", shape3.getTextParagraphs().get(3).getText()); + assertEquals("Level 3", shape3.getTextParagraphs().get(4).getText()); + + for(int p = 0; p < 5; p++) { + XSLFTextParagraph pr = shape3.getTextParagraphs().get(p); + assertEquals("Calibri", pr.getTextRuns().get(0).getFontFamily()); + assertEquals(18.0, pr.getTextRuns().get(0).getFontSize()); + } + } + + void verifySlide8(XSLFSlide slide){ + XSLFSlideLayout layout = slide.getSlideLayout(); + XSLFShape[] shapes = slide.getShapes(); + assertEquals("Content with Caption",layout.getName()); + + XSLFTextShape shape1 = (XSLFTextShape)shapes[0]; + CTPlaceholder ph1 = shape1.getCTPlaceholder(); + assertEquals(STPlaceholderType.TITLE, ph1.getType()); + // anchor is not defined in the shape + assertNull(shape1.getSpPr().getXfrm()); + + XSLFTextShape masterShape1 = (XSLFTextShape)layout.getPlaceholder(ph1); + // layout does not have anchor info either, it is in the slide master + assertNotNull(masterShape1.getSpPr().getXfrm()); + assertEquals(masterShape1.getAnchor(), shape1.getAnchor()); + + CTTextBodyProperties bodyPr1 = shape1.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr1.isSetLIns() && !bodyPr1.isSetRIns() && + !bodyPr1.isSetBIns() && !bodyPr1.isSetTIns() && + !bodyPr1.isSetAnchor() + ); + assertEquals(7.2, shape1.getMarginLeft()); // 0.1" + assertEquals(7.2, shape1.getMarginRight()); // 0.1" + assertEquals(3.6, shape1.getMarginTop()); // 0.05" + assertEquals(3.6, shape1.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.BOTTOM, shape1.getVerticalAlignment()); + + // now check text properties + assertEquals("Caption", shape1.getText()); + XSLFTextRun r1 = shape1.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.LEFT, r1.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(20.0, r1.getFontSize()); + assertEquals(Color.black, r1.getFontColor()); + assertTrue(r1.isBold()); + + XSLFTextShape shape2 = (XSLFTextShape)shapes[1]; + CTPlaceholder ph2 = shape2.getCTPlaceholder(); + assertFalse(ph2.isSetType()); + assertTrue(ph2.isSetIdx()); + assertEquals(1, ph2.getIdx()); + // anchor is not defined in the shape + assertNull(shape2.getSpPr().getXfrm()); + + XSLFTextShape masterShape2 = (XSLFTextShape)layout.getPlaceholder(ph2); + assertNotNull(masterShape2.getSpPr().getXfrm()); + assertEquals(masterShape2.getAnchor(), shape2.getAnchor()); + + CTTextBodyProperties bodyPr2 = shape2.getTextBodyPr(); + // none of the following properties are set in the shapes and fetched from the master shape + assertTrue( + !bodyPr2.isSetLIns() && !bodyPr2.isSetRIns() && + !bodyPr2.isSetBIns() && !bodyPr2.isSetTIns() && + !bodyPr2.isSetAnchor() + ); + assertEquals(7.2, shape2.getMarginLeft()); // 0.1" + assertEquals(7.2, shape2.getMarginRight()); // 0.1" + assertEquals(3.6, shape2.getMarginTop()); // 0.05" + assertEquals(3.6, shape2.getMarginBottom()); // 0.05" + assertEquals(VerticalAlignment.TOP, shape2.getVerticalAlignment()); + + XSLFTextRun pr1 = shape2.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr1.getParentParagraph().getLevel()); + assertEquals("Level 1", pr1.getText()); + assertEquals("Calibri", pr1.getFontFamily()); + assertEquals(32.0, pr1.getFontSize()); + assertEquals(27.0, pr1.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr1.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr1.getParentParagraph().getBulletFont()); + + XSLFTextRun pr2 = shape2.getTextParagraphs().get(1).getTextRuns().get(0); + assertEquals(1, pr2.getParentParagraph().getLevel()); + assertEquals("Level 2", pr2.getParentParagraph().getText()); + assertEquals("Calibri", pr2.getFontFamily()); + assertEquals(28.0, pr2.getFontSize()); + assertEquals(58.5, pr2.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr2.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr2.getParentParagraph().getBulletFont()); + + XSLFTextRun pr3 = shape2.getTextParagraphs().get(2).getTextRuns().get(0); + assertEquals(2, pr3.getParentParagraph().getLevel()); + assertEquals("Level 3", pr3.getParentParagraph().getText()); + assertEquals("Calibri", pr3.getFontFamily()); + assertEquals(24.0, pr3.getFontSize()); + assertEquals(90.0, pr3.getParentParagraph().getLeftMargin()); + assertEquals("\u2022", pr3.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr3.getParentParagraph().getBulletFont()); + + XSLFTextRun pr4 = shape2.getTextParagraphs().get(3).getTextRuns().get(0); + assertEquals(3, pr4.getParentParagraph().getLevel()); + assertEquals("Level 4", pr4.getParentParagraph().getText()); + assertEquals("Calibri", pr4.getFontFamily()); + assertEquals(20.0, pr4.getFontSize()); + assertEquals(126.0, pr4.getParentParagraph().getLeftMargin()); + assertEquals("\u2013", pr4.getParentParagraph().getBulletCharacter()); + assertEquals("Arial", pr4.getParentParagraph().getBulletFont()); + + XSLFTextShape shape3 = (XSLFTextShape)shapes[2]; + assertEquals(VerticalAlignment.TOP, shape3.getVerticalAlignment()); + assertEquals("Content with caption", shape3.getText()); + + pr1 = shape3.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(0, pr1.getParentParagraph().getLevel()); + assertEquals("Content with caption", pr1.getText()); + assertEquals("Calibri", pr1.getFontFamily()); + assertEquals(14.0, pr1.getFontSize()); + + } + + void verifySlide10(XSLFSlide slide){ + XSLFTextShape footer = (XSLFTextShape)slide.getPlaceholderByType(STPlaceholderType.INT_FTR); + + // now check text properties + assertEquals("Apache Software Foundation", footer.getText()); + assertEquals(VerticalAlignment.MIDDLE, footer.getVerticalAlignment()); + + XSLFTextRun r1 = footer.getTextParagraphs().get(0).getTextRuns().get(0); + assertEquals(TextAlign.CENTER, r1.getParentParagraph().getTextAlign()); + assertEquals("Calibri", r1.getFontFamily()); + assertEquals(12.0, r1.getFontSize()); + // TODO calculation of tint is incorrect + assertEquals(new Color(64,64,64), r1.getFontColor()); + + XSLFTextShape dt = (XSLFTextShape)slide.getPlaceholderByType(STPlaceholderType.INT_DT); + assertEquals("Friday, October 21, 2011", dt.getText()); + + XSLFTextShape sldNum = (XSLFTextShape)slide.getPlaceholderByType(STPlaceholderType.INT_SLD_NUM); + assertEquals("10", sldNum.getText()); + } +} \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTheme.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTheme.java new file mode 100644 index 000000000..746098be0 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTheme.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.xslf.usermodel; + +import junit.framework.TestCase; + +import org.apache.poi.xslf.XSLFTestDataSamples; + +/** + * test common properties for sheets (slides, masters, layouts, etc.) + * + * @author Yegor Kozlov + */ +public class TestXSLFTheme extends TestCase { + public void testRead(){ + XMLSlideShow ppt = new XMLSlideShow(); + XSLFSlide slide = ppt.createSlide(); + XSLFTheme theme = slide.getSlideLayout().getSlideMaster().getTheme(); + assertNotNull(theme); + + assertEquals("Office Theme", theme.getName()); + XSLFColor accent1 = theme.getColor("accent1"); + assertNotNull(accent1); + } +} diff --git a/src/resources/scratchpad/org/apache/poi/xslf/usermodel/presetShapeDefinitions.xml b/src/resources/scratchpad/org/apache/poi/xslf/usermodel/presetShapeDefinitions.xml new file mode 100755 index 000000000..d65e93595 --- /dev/null +++ b/src/resources/scratchpad/org/apache/poi/xslf/usermodel/presetShapeDefinitions.xml @@ -0,0 +1,19915 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-data/slideshow/layouts.pptx b/test-data/slideshow/layouts.pptx new file mode 100755 index 0000000000000000000000000000000000000000..9087579a0b8b13cbffcaf76bc171750715fe6361 GIT binary patch literal 62901 zcmeFZV|XU*wk;gnwvCR{v2CMc+v(W0ZQJa0+_7!j?3iD=H`cq)>UGxse(t@_^COj~ zs&eI;bB>97+@nfP5*P#p01N;E004jx;K!gk04WdvKt2)x05SjskcOa*wWE=>qmGiR zt&xM)Cl@PAf;*3>+H7k@qZ|-E}BQ5ibPNb1qU)g*j80 zDA9l9Wd(L)#nGPBgaYveu3#B7e8DZUl-IxB8G36cbCshf9%{agY^;vBEJL%!ZkQ7h zG=sBDQ6QbbTs}NhKmj-OhgCMGUE?+$Pkhv!5^oK5J78Cu=^k-{KX#$-JVTyr^Vv-@ zd+DRXrd5V2oz}I?!_jbCb(M6q=h$OMVboDlA+ZWm-m5qI!tkldt`o#SLh>OVv%v=1 zibljUo~1#juOSz*#}2AnJw_@+;F5u(+ttxeb_()S9-Z5?V+hn*;?`{|JHS7wXzQ?2N>Ac}W4)O%kn-3|N^iIgUoqv!#Q;hX>ZJkH z49){Xpc(p0$`57)FDc(IP_iW{#bG& zaLUXX>zY>N1dX?ROJK0f_KRbD%f^75(N1@E-+TOXL1u}wfx^%>W+PL`-T?ss-rs=% zCHzDsd})g@GtGOS-4 zT>q1Hx5IPIH_%wj_HG6j1=}2$^!;o7S0Q41%whjXbrjTWRG85#nc)g>`;>Bop=1g4 zIKfYP$3{Z0LEJ;iS{s8mioj{1MT=OVW)@X^4Z&)x@1hzU`pknAU&R_iN@GZy_Gt6^ zW|67s`FWEZd7P92v39v8kK6dTPFKT4ImdH6=E@etUTyeBI)MYL>eJ5s*V(VR4E!>j zJec|h8Oer?PEDUn@xSwsBb=qS5jre0M_k2=$$YAR{P4ryXAO2SfM)l{mCqlUL-295 zwRNO(urxC?a-jRS1p57j`B!iLok{`G{N+FB5dtp!-U(0F3V4Ks((Z`-E@kp~b;X;k zYNQ2Nd9>f#*;Np@#^HXB$*jG}=0D{3esY1;Q1>OU@%!4bVB4Ab85|*FN(0BA9F~cp zVa!cg$E;UVfa}{}o*b72hJBQ1;UWw(;RwPIdNH)w4^6{kWuc#4S+larXUbm~b6gF= z!slW>IYDo;4otsf;(i~;23L0KoT){uQmzk_jKJuKC_N9Hy zO8bxITgG5m{d4n~{-OE&|Fg}PyyZUa{HM`p{EPYBP#@73`iMTlowWjYh?pWB5k!{I ze+ED7)!m_jz$F8Be@w>HCEDLbULU}>+h>I4SY-!*FYXEaowf4yTjC6`0D{xO9`jJ?-E%i`cQPf3xAjzc>9A7gKzN!;+y zW6$tcyP4ONH7%le)Q!ms05A3)6wm-!_YD1zn)s#PpFph2< zVcM@{khv^m-eFCm$6DHH&_0#Upbf~`iddD7? zU-n~Be%SARP+4N+im$X%vbK8nVgKJ}KGzS@wSR6t%U{g@ubD6RKO6ce7My|q9Qv$( zG5>!O^oRfN&}V|f1NrB%|NIx@|4ZyU{KJ^fgh944g}l4sELU{X11vq--u@%=(=)%n zW{X^jhH716HPr#}uY;U;RBw54+Q1R8O$ zOVnbT3j9L84El6H%iwJJ2lBZ~v&)uC8w~k=_M@Pe6Kb8J^;$(0K5}w8W->WK?X$;D z;8USQk5AK$Z>2(`Daxw*Cya2?^=T*F^RqO%rLVO8G4r25p%4Cf?AiWe{{N)d5B=X` zj~H5Ev;5(f4#59g$7laf<11puBtRH`b^Hs2Cu_!faQUTCZ6UQwk`rKNaappH-C#$R zjONeVMqp@dE*0ia<#~6()%4-ARHurwlOu2-!toW+GCj{nd*^^4su5@hDB?W=Fu_}Q z#_u9b^LM@R6Es+)bwQ=`F9j&+p+I#~srs!Er63j3XvK_a@QwveTF%wOX@EUJlu;^z z?8~?zGjST%8>h@s>yr+Q$$Yiip>jM8r(nI?CFU^-pmliOui9Dlz+c-%-m_U;?vk|a z#FM{M)?cev_U2nszcDMO&js835fqnY8BB{GSH?g5Lilm~^CZMT|2N0Pc7OB(n4k;) zcK<0)r#L~(d>J|exy8~myskwF{iMNu`+@7bTR32FsMgaW9f53~$+ye)(QRntXFjYi zOFn#c-L6r;iAkpj3h#$ z#36)fefv-|rKv?(ro7`c<6?y|AFm!1(PBbeE%5ESe)~roCk!x~E0)C*zl4hnITXfg z*E=}qxdOy`Fv@aP)vwgyoF&g+&b-a9qNiHbdsuAR%6Q7D96OE8e+IsR{i#hb+t^gt zeYgY%5&!@n00QurTmEe_w6!;K_+W4J9L;R3|I;ZSOD(_;0_Woo{|Jtwcn`Gu#NsIm0-*3-MrmLhSCqES>`y>dxJ)nqY(Rv+IE&KK< zlqZC;=^>U#9TZY2+|{~yrPM%VDz#KJ_$8bHF6n!De|Gnm;CM%}mh_LU&Z9TdI2a{9 zNU_RI!mt1y-dHl#kUS97uqCdSRR_2r9ST`-f@SS2!KD3}uw-<;pmP?f^Z35(R!_;~ zGQFe9pN}5`KV=3!Pi3|T^7X!i z7wV7bj&!Ls%0hix;t(EChIYi>fR9_0PbD5Y(prjV@j*M;W=6d~p!WB}ja2qkK+Fdb z@Cpk6fb#Fj{D0#0G>r%OaMqt`M@H)?^}$;pi7iPsab){>sVs(CpPyQFaJRh_T5E1$zYe-1y1A>rkKvBoTb#jht<>6?w1$(H%jHn9ngzQz80$YI7p=AS=&BiV0$s_GNTX|Yt-qP6HB6}abo(f`3I*CvfS!Dl0rs=+;GB`9flpTFfS zb+l^=cOwGeh9==koKQF5i-^UT6zmibtrtxerignvIKWryQN*3?pdRG`CnZ?xk(VV) zFYRwgL$Ict76yvK=~G9~w4;~7_wgm+#^(op4-l3pf=W!0K42kS4s%)*)AEa*85Nq` zJn*#R<*NbCG|8Xpt598&R43@d6uX$pMg7B!id=QkbBaPJR%{yl=Z3T-yjKMnEmRJ8v{9{t>6HP z|3mQ|G(U-+Xt=3K!Gy|}_4X~vk(1ADg5H3JmyTv5Wd=8CkKO87Wai{gAk5-4W#-8F%Hh4ic8I8n^~*mC ziD%YzT;xDn!?Y#ZpV>|-hrJswSuze6tH%gmF@{tdA9U~8+oRD;^xI{a%#3(=yoL~x zg&~)vT3ls&DfwQ_xs*_s6~=OM=G4KpvLjPHb~VH$VzW%uWUIn%QAP#t{4`~a+BVaf zjVYvt%chmPBo45D4KHc!N2Qe#q3BsnMH&6k;hv@S<(%H_?T^(o*32<9_XCRsANx3e z!s2g&pjSoSW`zL3E4$&{$3-@iEh=42yo}7MNL&k>_6S}#WpeI=m zw`*Gp%GS7>TKyla{LDMO0{cZ2iI7uD$)*xazJHoW?MHTUUoole0!~dTVUatU$=FX{UCdgMD!r;s8kuu@(Hy z^4A8SqWt|VC}~#d8ukL}1gZ(VxSOo>7y--?RMDoB9OE@W6j%h^|i{$()p)|FpbV)Q&FB+o}O&)0=zQj1O zpt937ymR@C3QjeQca}g4xz|S*j;YzG&s|HcvJNH za_KNqJH5u!T@70Sbub)h$h=n|-zlF3S~1j^E#%E^!#P&XZ8g=6!GzMBcp!r4CH_4R zS#s+T!UpYehKlY&yJm0i7J6RZ8Nk?m3;&AOxxO zLJS<<5FL7*TmtudY>BYpZ(Sja%e0{M6?E1h_x`BX7ELAF=rSM5w{BgFmQa9LJva)VdyRkNiFt(LyKhIcr{uwrr!L z3>_H;^YB9~2FbKrzJ4m?Q3p`}67j6G+MhJeTID+qyg1b2q;Y`nOQpLXlK4_T(HFYu z0p&CuUSi3gBPIp;n*c;Ew#RaFAz43;z}T7t_h7!_$<5_~C>_Lkv51Lv9c?aL9x9_o zu#b+n4acrg>GpFdMryVnJwjZAvE3RjjR_fRfWJVuy%LGH)JnGQjUof-#iK;@03ROo z8c`ruwvvjd=T;s-q)yAQ6UU6eMmn8#!}!8Ln@p+c5@{evTWd4#`oyAW3N#QhKUB21 zr*^Z^vnBoz@X7@xPuPnyk;*@}^GLghSq!MAFTmRsCV1b7;V_|oHkaY9y|g`5ZS%b@ ztIw~*)M|X} z@zKU}o56RgZa4pVw8NQvVq>%R!#eD1hlce8dCLW)D9Pr#vRKFBsY^$)RrNO_4|Lb( zW6mnZm#-*bC@E);t(SQ9*wcW@>i8uBebKM9ebhrq=_#hb54&R#ORZ)Ap(F=SjD)tiZo;L#Dak43K zgh`6U968k(X~~=Qy4oWrHMv%3Ir_O*C}0HE#?@;Fs@9hC@qb%%;6^ry9}J$QsUgYB zHI)ZM*#K82%?|-z8a;oIWlSdW7vPb==NEiitV=>E)=&XI1I+1u&7Ch>&MV%(`Qor& zsE5*buQDgeV9AZV+bW{zlR5vg%w^wU8K|CK>7q_hpTyU%CO`JwZTiY|!9w>~H+4?U z(8A(@rEQHsCWV!$-gp)eu+a|a#}?oNW)q@Bi#N+pITflM3_`6`$b1a_mTuE1Ud0T4 zpM3alarp970luBYJ_cQ%BZY?RB`YqQO+YmS?SQC!?kdYJ)$2X7Ogl+A5LfxyawUTx zX+eM^LR|$LieE&O*p2HB_bYBulY#C$E3AZDE1i(23)Vx2fQw^Rtj1^E?=<2GzFd->jPG`;67o&!E26X(U2z;Fy z*lGV}0$3}=)wRkRV(ND*AkN_Z5hsGq{#WMH+dNINl4jOug_UB#re#5 zqC1?`l>MKS9VV?UP(8QHzO$|fU^?Mx0*kJkO=t#ovJK3QH0Ar69k(G0h|OWj@g32-!8SxIi^H;#o9;A}{a9(BGntgcqKO zDeoRbrhMvkNbZc#FBx#Q>B#JhuyM;j#@ zRVr%>3Nj9!BZI!*FbVnsvPYw~qX-73q>#Wj6idJ6(aoKs0eAyc>@0#BG%_;e4E6GD z0(xcxPzG=X20(|>S|QUMgM9t1+Q77g5+5--Eg6ZD^MFfQUL%B zTM7BG(Oa>P(Ip}mW2ZG&tV{f{J_i}YZV)Rn%w_XAqzH*IcDN~v7`}*^Q;PxZ*tk;# zTpBEa-?Oxqin568%n{2`g~JFY)J1(+5)wUk@=YthfI!x#S1N8ddDd~w@$vTIm^CT_ zgUoU_EtVr!<`Y=@+nQ5jviM5L7wooIg3y*)>W|(9>{}xaGyajO>~>H_c@B*~D25tu zBVs5|GnjzBR52`<${NGa(_{#r`Gy*bGe1665GP}V>Pdz)LBjG-J53;;ZXi1jH17t~ zNnHn+%|4^k<6^-K1FJKiZhQ>e4rm>SGbMT)$v*_fnriOud(6)_>j=0ibDspSpB$;Y zEON(#5|drtBm`&1)UfB(-3Fwe9}7yXzii8sp!?@4c%#Ll z)>fex?l#fTHpA%mQkX3@yk(-2CI?{e@?mG{&m7sC+4W0PHJ|avIO^%YIOfgd_lyte zvmMfay;7u2h>-t$TcU$u)k9Gn^8mkVIU>=!6u(#5RgxrDbmucdRI<{|*`=M4*zZ7w zY5mHnj`R%!KL5M~JFq=Ao-_cOpVbDR28IO|Z|@0OKDqFMD#s>pI_sGbG}GMZbpR=u zgE^@4k4xubh2FI2kA`;k4@SG+du*#8quneQLi;<(diV7!YiGJ;UQtP_5u$=`SQ5A` zt_||mV$zwk^DShk$<~w$Qol@HL!bmI0m-+euVhTf*$(|X>|PxwY{#jrl_;oDQ34Su zId#^3(t#T~l_t;;5u|x!noufR+)NLTL-p@b%4RN+1Yc6(l)lxfB=YN;_HgJ&eN1@k zRt4uFfuf$svuUdW^HJb-7{>gfUwj8#UK3nemKqW=#lN4W)CeMtvDMZ%%rp#3EzK&{ zedI~O)zp2HBhcU>Y4uvCylNUlGt#SM<)^v8yXuV_bgV{}%fMIy+(|$GtG>g1e9&** zL@o$kOh!a|*7>dR1`0r|4dwJPoz9&ILK02&A#fs3V-o4jyqASz;rK4#GvK=5!OR+=y|u?CtS(Bn9*i=9u!%wH_~TFdBdgX)jjrqH zD>W~>O~)LahA{#4abKIuCT%_et~E9Bn^+1qtrEdMU_jL8ETklaKKYqmpHHpAaZ&1_ z1L!W((tvy6o3L}&z#!4>!I9p(hiC>WIYkt9zP-B(R_3hTvL+16lIXTH#~gRu>V^54T4Jsx-zk*K+X!vO`h~ z1)J0ON+AyMFw90&<2WfbzsVPCKvVXQ`j4LX*~mW<(008G4v*tgrN5CG#am|K7tmgt zUdb)9Nuj$Y!A^^qWszTPZ!FwpRrJW;99$KD|B=W^)p00aLdn9co=j_GjWWQP4?7Rm|M2RYwvDRp z)jpK>}FLjl5zzFbQ0&@2$mJQ#M~Uw%(IRh{9FF_bf3BPt@I1cI#)pvSwEvh^JL0h0iEo zZxi2Gp%#+PjmpYY7eOLKY)ETqNj5Uu8$_3!`ssN-Ul7g^l1Op;nYP9`2*rC!<~ z^Htr2uc$6#_EWvp4+CgQ2)vY;I`^JIaq3&zM81>eM&p@C9_?Afg zH*Eo*K(BCz4G-J`<+~%HaR@b9H0gobSEmn+*{3>0$@qf(XG{&~)oDTrb+lpka${lQ z1lnuaTr3yeGkf@V=~busgL4ZB5)vg)MswcuBCdg+2;<(o2itU6kW20ZMuUpFP&K)7 z8%<9?UEVY2G!tma8A4T9KxBt4R3ewqpQg=xD?e;Fi5D z9#A$W4tDf-@y9rSYJWd0Fxi36oawv#R?mi4V8(-=x$plIAH&1VOavDC=c!`GNX>&K zno0(i(Of^SyfW#SoOHu?0}*Pxee>!SxP!EZ36})8{n<_E;ghjM%981zqaNpK34_3kHQE(9ph5QV0T7}=+1tlCm4dEi7fIjIf?w;G5 zI61lAZcU2U9rQ!Z(Zy}k$U2@vOHuNcaW?uDx4khw$J@$-i#cv zL2= zfkA|InHZDhltVhy?>|N3aYx@n9zz+$Yx85D1&p0guD-OPFG^kZo$1U@P z5SRp?sn0$~0h*OQEM1#=mq-Dteo<`LrQEOwmQofZI_sN2jVg-&>32}Ij)pX*d%xS6 zKpRuh3=pJ1Fa6jK;`d!AxOxOzcT)qXc3yYPO^HV ztO@e+hK|9L5B{wA$X;7!#>|V8_NTqdtnh2|{iosMDSLptYmsm^+OTWsU?)FJqq*@+ zhB+g_@_|ra6}oFX?bA{m>Tr94M!BREq9!ha>~fY<>OrC$i>!&MqWc{piXo03T#NQC zxWo26xy>0s^XJjXYKK$9%VUezpLkRT-4`)vj~&Yg#pdwa@2Y3J>A=X3vh-qm)-+eU z-e=X7$82_d&-kp$hm~v|2A*G2Lj6Hh%OCd&vMQ&Q<*R0{jd`S#Pe#HWJ?*rUZq1u2 zCT!Y4SV#t8;2qu!JgRFRc|gr?dm~kd+CI|>tMRG)R9BZVCVm`bj0jGlCFpey5dXXZE=fOGk|Aqp5I#{5i{h>&_w+M?TnRgf{d; zUWB%hYtANJ_K=Kk6fylGXC45OQ)u!Xe2ig%dGG6_q@^ zQ5K0iQ@SCDib~0vnGP>5t7Om-8RTUo0@O)6LF^vwZ!cy>jN}p^bHhd?1Bv%!l8U0# zWTDumJt!HO4T-%y)!fB}q!T_=!W?*T2c;yMTUF~`1=`s%8Fd=c*(8b`ptVv6@S|FN zktb85qLUFuaJu@pItlYR5qLtFDkf1$wKa;j=oWCsHak~>?ad*{o9H58Vl|+}Yy8P* z-lzrLWUs|9tHiOV_I38{M3|<6$vFOx(hmZNLyLXH%V0+q2`a75_GVx2Wt^6E0ARE~ z!Ipgyw%LntXBRyO70gFhwxaT#c9-_%C}2K>xz=#K$vyA(+Xt|r+QkMQe*l= z=GpKqo}%%lbX)@2;?<4hkApmb??9x%0%{Nk+q?vP28F~OR=RfSD%qVEjgE=;BF6C5 zKDWF%Q5ls9H3p4xafyKoDz%rTd3@7p$cZwvrIR~<>}JKlNbca=EIr2sNPjSuO*(i` zm8Ut_8Osw3n{rH-vPo7Q8gt^2Y^on}{7#AXIelo&28JcyOoTQ(@}Uu*0;0oAf2ed$ zCi}(5Jhf>i2J3aSVu}l{*K|=@Sz0KHd}qj`1qa?lHS@WP@%ntpFQ8+pv}Pk@X>Lj@ zMQ7raEC@!*^1i>ClAjZ1@Xz2?X0EJ{1m{jFLp(gsUU1x*ENM_=b1j>2w z?BNIn157Cq=YHSOA~S7ib;cYGhPibO3inRHuiy9GJ|s=Y&zpzDEf=YITx@zSP+3$6 zBYgV&C+@))g;tJ@pg+|LUS(rb0v>#5lvFsvdD4YVy~jq6FV;S^AAip<&P~N#b$mGG zm*Ab|-<|V&LtRSG)#j_?f04ca+o^vff1Y+jh%M3nHvsM4 zIiWfEe-DH{lPZ6XOcU9AWh#=Q{YiyFk_XUXek+!>q(E_*Cdtw|`Df^6b zAk~A!I@q+tkoSoctBKM$?+WIX8|BT$eeS|3WmE(?mauwnaV39xI~{VE93qSw5M_HS zQQ&13GT9d?)ugx$V-;j*|Gyvps)GNk9`b*WA87wucwqQj zz63=7OFQCkTFC#cj`;6Yh`)(t|F!+_j}+7Ydn&|#4-bD8&j0T$5dS?t{G+EQ{?+jC z*N15SmGSV`#|ZzG{qVnYfQWT)?8^FJjhN{Ej|V8E^&A|H>>d8?xzd08^4kNaUu>+m zMNr0b&tyGb+0Q~q<;T#3 zridZ0GgZ6lb@05swdPl7gdEaRlT5&F#-eD=GnptDuNl`vn0Snbshag8r=%A=W{A6t z6^~JvbOK(qm@Zc)ZDiE5G2A|RWZxbcA_aS-1ckH97U;)0bdzLsru3=H+mGCBCGE8V zur|2uhz;0wUJ3|3aA%cGz7&NG zwN2DI%05tR_^6nrWq82e5pa=ozE2oL)0JsQzBVv}frE?bx^lP_uFw#ir2O@tMQPHFHD_$~prY z?OL6RXc83(aZQdEn9|t9%taYP9LiZlzNFmGR-)g{t!}Vr(?q5G|#d#8aY2vN8LL{bkRsY2hk!pjx>E%~^q@ zhE0%0ocQSJUWq0F??|~eS1re|py@b&HIAzv*YVLW*bE8NkFI6WpZjPLcTs12+r#_3 z0LFQfM*_VibbBBJ3mujU|r??vlI?4EJmSz8VA;>ZrpOKg7`dM>-uq|c^b-+Xc;#05dp$adKA z`(}j4H|UfO-<&S}4Pr8OYv;_D(RMSE8lA9h(YO}$Dto9a4HAk{bQc8i)Nee`41k7! z2%k>;_Gc9Cp_9c{*}a zcVExTQ{|L_POOH-!@SAC&)xCZ028llRL2j_yOqT+AC&5^H$A-#U9)hREiGJ@ZAHn+ zBXf*0Sv>}D;eyGDn{Do?;k9%2Npo-y?Jl;zb$Yx#2h(N0v->O*nG2)-yi{v~5QyzG zN(F=kfJj3Ma}Dw3X8P_|d^Ow8P!h5Sm6b>BqOS*`1)ehrzXvf|9vjwADVhVKplHnw zgqeVKHH;X7W!_6C+GT7)BvVCz_=9dHrC{5HMUF?WU!XmM$tm;GZaM_j5;if4h1G~v ztM5)LZi!^;V*TKT(DxMZbXrxLNr4{?MRSGAGGAhp`&18nIL-KZP%EQ?KaTdOm3ML< z$0F29Z%M{GTS$lDjbObB~Z=l<>${K$4^#$4f`+!x8S&}!@F{3#P z0Nhi1g_@`9YNBbGEWra0a%FIiBw{drq-5dJt@Go@UDu|m`p-jNwr{iF(ch2B z`$E=LnbbMTNuZNY)*h$wj(eLygc9%t4OmOr7Lonf?iW(q?5*-}QY@RK4mj_jMVwIg znrFgYIN$8s23f@mDU*jwIC>Nu!54yjw&0(m$0&?hHc+YTb&MO3N)bUDv1Qf&sm2F9 z2B-mLLPN)fqlqrF(+418`pv}a@g+G<2UZld5+7*55IsuV06a>3r<^U0eNl~?vSQf> z^{Ew#n)5Q}95A^;va~pIF1xv=&Of-W1z0~2z-(c!qLwPXm8Is?CoJIraNeq?4u6y% z4}nHV%ztmbYg%jW0BRm>M4b`z-otXK zx>>}R+U53^{ZWh?B~;i?$km6Ec;neUXm4hB>)YLEIJM}mm*i{MtzJk+1s!>^(txb! z;wrk}D3aZ$a&QwNjU?%?KnN{FStgy7%Ws7JoeVc)rsVOrh50z^v^Oj|+04whRa1?* zTLO9=UZd$>iftYZ?sxwDc9kmHehKyuvRV(}|7h<2L7V-BL;Ky_N7b}`Z4r6VO>!a} z*U!gQ@Iu$3H=*8>rh}ZE0%C+V1r~ZqOjWJrb!nG#k7pGy6_~QCqo<>ZY@r9|1?Ai9 zr*Dn!?!R{I8V(3)%+4*^Z?L23DXSa^H{xkzXQ4S+#;QzH6}MF_E^^e&yYW6=1*eml zxq6Ah->M)@$~QvdYG6~Q8`7KB!%~|)S6QopUZ^xulm>{_;_dpqL4B9xxx^vINoXjk zIB#pgM0(x^&WUic=ZxhxaR)aR5JxY$*%>}eDB4n=*K|!M+rX8$wts9s|rc7lvtU~VG@d9QiLbvXF zNDc+c)+}n#;}AOpP7R`5&W8`$m(z6eD6TwEInJ8A_Bgb5=8Y4j>UG0fZbsW;M^iLAe=0uP>vAx4W3?x`{2Pyf2>2<|OU>y)4xXkRuMA)45Yyh%1<^3rd2t7~r9-0rq$iw! zv&}2|9wTT097Ep8#~-w)VX!|gNNCNKWA&BiRd^Pg{f)O~| zIUw;ewKRv&5+GhC0Gz+r+Uo*(Pl$rnT+ur56Lf-mN%FgD-@SC7Y^mXH>`a7}d6wV= zLz~dv1SvCnCx5u~Bh+zR+1qz|gM+fR;P66wk~{J;-(&ZrdY11OWh#?82K6tk>HYQ0 z_C%bxW9Us6QZ{F)yzpm_6f(sK*WIg_uX}xtGYHsRGkvUtm zvpKUpEH>lNdSJ3~ir7Fa9)y~FU8#9}+MCGk@OnG}VkloW@}bfC#nV1MPy3eWj9 zyhj)Q3S^ANl7)WQ;4@raJH#>lJ66ieAL;sqvr%|=F zxI7V*4q4^(+;Gj-X^4>7!}$^eYh#n5yk`K~2H>TA^2zUMN@aJAaRW=_L+r4Xhu^8k zM}@6Xu1=~aE}XHf*KLsbS=%UfXmGJ^GR+XOz|=8{Cj@tP^yjZ&6eN;@qzozXu{gD{ zE<}0X9pj%1Aug6aX-ER$m>t#UB8sOzuUTUXap~=~(i~8}G!KUbh(D-vIrsQ^f}IOF z(#|-xLWV2mYwI+qXIHA&brq}E$hc5Z$N52%b-yu&{Zy#mvs%3NOXka0!A~b~W_L-a z_13Y3&C~4zvz9Xe$pUvkj7uQ3HHa%j!WcYI6EXZ@-yo>+O(b?ErhB6i|uu>h^- zA{QzNTx?kp{r6jZ7O7V$Krr;R_`Qj=(8m?!*a+(sctpaLT)@zkEpi%to(%~krO*C7 zYXojw{;+wiOovtL05pp>Q0x=b(H&n0rq5~gDcN$}tEnjb3-8r0OG(VYyz-tC-Mf24 z;&Ug&vPBD^I)%!xR9{QCbFT`+N?9H8AyA;3o38t-vl}mqgBR@LMq!su0qdTHWX2+%X12IHOJFqlz4V=faB+~3M%Ud$~7zm zA);1Y?!y-FG$%aG^T=I%p({{}Ic~I(n*fwI+_A&X4OJ-DqQjD4-&!V!3z_LT_hn3J zj*@rC5x%@S&k|sBy6mJ;9aS5-9MI=1vwK;(0t{fD$G^{fWhDpD=j$gu7J!;_(vOo( z-jE-+-2Oh{FS!V1TNSKJ3K-;B-&R*Fr>HEMD9WvT8)=UBFsV`YqdhP3fNO9%2BKpt z548QblKK!`UbDeO71j>)nMMPh{&gA4#z0S75Wjl7t|!G^k)^&djh22+no<$ax3*ez zE<6ZR1|h~40~8UN8h9Llz?Lf3yd-zGB)p}}1x2UrHvdTEEK9JQhd1IS|H`-(*!+7n z8e3e=wl*%qHgmuXoOYhs$Sp7qlrk%Jnn<)GlnIf3F0%Z(ITd%l#ue_-QK-ng2eA20 zRRQWoENj-G>6oay=W!D$&I_LfOSXwC#cd@F*EZ+WPTE8r7ySjP2YFiM<5Lrs#=?qo z-U$nST%*ijY$y40^`mC-d!5G%?>?|&6OVKWFc_Xgr+1Zo1Cn&=uBb#o!_P(ulwljk z5S3z3k7YJ$p~)R^`0=dZ1gMhw6DYi>OTYs^M0nv9Yacr{^Tnl)=2#8iSt7!9%-kv3W?xt+Nt&;>Naw2+H8i4_%XdI@jn0 zU1tNhU7FOyC|C$E*6B2G7Qr33=0M9_|9ZM~W;MWcq&0B1G;?(0WE@?2)!`+1!v%U$ zvtC8HQmym$xObB5y^&*_Sf^xzyNRTuP@;g+2cE=hJ&O|5%qS;|{FOTRo*n@`0FPiW zcjF}D?L(vU_q&_ARVDGIAG(5`k9RrzvvT>HrLIsu`}I`OE3erFf@|lD2OnjmPZ&ZS zuU5FoGeC2wU>=8)F*aB_*==Lfz)}3^GT~W2N9oK1@c3B!zOAwea3(o1W7fKOya~bhv{YQ0a11!revY+ zaHUH(s*hp4H?NP8Nlah%guS|>yED6sHNg7R=6|gm3^LUeb885KD6wfd0Ft?!szc)! zV%-?u*nP^LmbgH-YrOXoXj#1i!0EC)TVsy-Do%cSNRm@puNyO^P?I4+?n1Pv0+^>6 zcB;*;Nnn&OG{W4v?D#FHW2uxfQ{_o>XVa4(*%uY$R?yy;)B=@p$Z2YhI$`Cwq`7v` zStOqGrI|k@Wm>Y`VAeb^f`Dq0-(Yxw0JDE6)q9`ppnmr3d8P&X-UQg$iASoaeUdlx z@nA!20Yza>S$f92G3myw(hi3p&Xhh15D#N`ZH4qs>jX332i)97b_GYs4e3D}V{;L# z8v=D<@W-7#_wFMQi3b-SlLCK$H1S3h&L<6nH*4AIdU8~Oe9BHN6uh1sfsy%KS&Nu& zf+h+k)+dMFsY20ZVx5YHLuDBpswYq9e97w!j@=fBxjG5`$JVIq>o5KO%xv7S!PXcg z4$avz#pjdo`{y|evT=rKcRTc6X*xzJ4yWfj19w+j&#eQ&_@XK5oZru+LnR&Jmq%iT z;3qY0F4Y7Bvf(m{Q(;*iY8Q3TCMd!3pC6>WKG6L4IImf`L0bB_g8CuK{AW1-y$eyH ztno`r^YUvsa@w31jBf|B;!C^qDz2X|1J@;%KpH@#9Ora~25Mh<$Hp$6oRCmp231hv zWf``Ldp-&`boH2PXewJs1(L1%-rEy$EmQ!yA#*u)JB_JVz`RAN=Qjh%qYyYz{P9xJ za=F?YKb$@p;;v09666dEt-z}CxQryB%#bj9wUGdt&>-2CuZrRw08T@zT|a}Y#i63o zJsy)hVvU3a3}R0uhQGoRQ4(VvtGddUqE)&7qOZ>@vk))Eym}Q?w<>FFfK+{M6cRQENE13`g_cfv-9-Qc zz88LK5+>RrjNfyV5|{+SCByjQ#M~JpB;YnlQA!?PyhNhYj+DL|u9~;z1YELdr`S_w zd_$g@Q>b{d$y-+@2w9*VvMWRQ$)`A1XGXH>jy7e>aEeb}OVlFxg&++L zOAD#6RA?U577D-~=GfF!23XwnJ6W8Eo3G_VEP_)|-U^hSn&g8sp?hTv!xTy-rnu=R zH7HaNEz>mfoEOw6P(4gAVn0a_$yEsptVu)#<+)Bk*9or2J9dhuDCOpa#gqG-$dAm6uU5V&V|Ow4+^=>8hj-m)rvW!&q*$-a z*sRo}{8-#}^>@=^rkJTapFF^gtr1))xa)GXMfk34I7wBfCoC~FSJmd3E7=LHwV4Ze zs9IPHo^S9z9xUli4k4B?&{jzy?0k#{#|iL^qKlK_#-z~zj7dMML($q zGKHK!E!IO`@$vC4eOz|bJsK8e;b5majC4`S=*64X)+^xg46hZ9N~qktGhm)52lX0f zp6}4VnBo6AE)K+B@EpD(>k_d4`*HENRq9-Q-Ql+gf|soFhd>c`gIS=BTre#N8NJRr z>MA@&VIKvxIf{8NS((y>Eon}JZv15&_@Ff{1H2}x4u5DtLAMnCeWML;_i$XZkviU= z!iog=;Oe@;BY{+I*ZY|S`GJKdtyz*>L|HXe-8!w$)z{~ejK&1fJ|69xpjltyF|Njn znIJK)DU-_?b*Wo;Zeyr7_C0keu(!$V3;_{cU#{r%VttOO)Zj23ot&k%>=~21ncxlG zK^jCb@nIsW-VaZ_;x+@xv)Ure2|p2?F6(%;qMa(Y-Gr4f7@y3KYD=U{+Fh18kDVUA zXkgkr#!QTBjw1EcTj7s{l7vXcL6QmG2B%D3<3Ii(2a3f(KuC$`r&E7okjQ+<009Aq z(+4x*VJKh$-xS_!hYfFUY)Ng?iW+7eBQ}i+~84VwAH-P1wZCrl#rEk6d_tJ#h z=DTyjm?sNuD#3VV5?VJUksL*ve1? z+}gsMpRmpd5dwo0i{h9TbJ)UppFfbb30d53zts+?}>&}H!2*Lhh?YcBjsEA4WQDh&P0UA;;;}#F& zMwX0Im&V$G;sCJd5VyyZ-#|)ISPGS3w4V(X8E6`u$~G#?!Q3qR_lbey6&jVpywnPL zxOyg*18Zc-jk$2tHxKG@mR?v7wXT(FMGXv4Np)E8*RFi_?;TagzkDkCYiA`-P>vjXlSI_^a4}L! z@C&v#gi7Q2jVF&5lR1an53PMxw=b3wdcWNmR{GRse0>$@4+H^zEP+v-70E2rOAsjf%9i`BJSA=n}W;53d7<0 z`%IHA7t_M?WwdEaUi*V3haj~}Ku6uP{41-f`FbpLa}$(v?Ets#YPdHo0t5fXR&>09 ztg{mz)m1y!yMIE4U(p-k<0EtGk3FMjdZxPuFD1mkCt^SRE zJdBH;dxudh-wLA0Ulg7?M(6f~PDVZ+GyrVmpO$mPA`2u8!Q@5_GXTxl!}<%BNazUl z*HA~CdXaSo>s{{E-6_ld;#jx9aG3+|U_y3$> z&y>^{L;2Eg3?lyb6ZYTc^K-RLhu>@{-B(iE0(&|h?*Z1m$g4m&k0$J6C=Wm=nxL-6 z6G@W?U=`rWk9|!DWlYKF zO9QD2PyW~E04$=629fi?X?Qx2~^Xu21-x8m#c2EjX;9(@K3-0>I&?x9%VY0{8ASQwFGU0+7sjYJ2su z@6pM2Q!7cw)BDcNSu{8BdQBqEK+HyH09>nd(B0M85jyF6*p4Pag`ht*E3hbn%W~8n zlyk%Vvc}(rJI_swM-!*QyLEC^Tz)M5PEXN*4?xc?LzAo?q1mPwxZ3T(8)QUvpkhxn z6VO}d0C#u-`J-5wpRV+fLMiM_NuH<|`l7=9J#&`mHJn6`qd7WdZ2g;ZnrM(+L@3`WRxv^7Iz3!ogsaI?5$;ts#}0fkF$jU zn2dD)V0(T+^2f6CuqyG-O~T_6B4A3#>M5v+h-7dX95?Mfg3n_Jj@_C+56fQ>hR z1<)%nR09E6Jz^{a0P9@AU*|o<=wtiNw@Z^BU}Q-%AOj7sS#PCkQ~5hNgqi+YGMfE} z=d_y{`IWy@(r^~Y5x`?CNrz5u%WKB;$H=vC=#79YlL)=a zvd?FUwF)Y;rkb^H_7`f#$Wy$66dn#2#eBsbQe8PIs%NM$?bq$F3FHIqv6K{Dqo|PM( zC!5>O5)jOXd^0+aOW|Q2O2hJ)ZR-*tXO-?H0mm_H`1iU&92JJC`?$*&9CB@yxN3PL ziHOYH4C9|YC6Z21+B-UcQg{i+eP3SDbnCb1f<+ji9@AGH2WyBEGoweXtF>rOfq0Sv z7L3o;iQpI{wa>{dRWpRXzS+n;Uxlc(eJ8;HFH?c%d}V$Y^p+tp65;I8BdvdBD2 z1J+Q-`MI(PL;5zl!Jq$BHiFDRt`7T}Qkwn;rkcNfdMDNXod?vH_-PUs7#lcVYurl$ zue09d#-|qxUa&StTHx1PtkBexpk9OTMEZZNd z-$Ir}&X~>}%AH77)%1O6>MAD>HGq8u|10z{7s%)6&XqfpNX$FKI`yM)$ zkz$%9lcJ{GK-pGZ%m~f7UIxkV!m3Q{c`)!r_LlC6E%%m&h$}j(KnPa3esoW8=KF-~ zHScZ|Yd{~K7nG+sSas3-o+HC}D zHzl&$%wAx2a?8GeKZhs;71(jdFcOYhxYg!5#wM>K5PLzV5GZ}6G5Wdkta&VXH@dS)Uboffk}8u4FIAu-fR99=Cs`c zS^pDM;u?rJUXdrf^VU4teezBi8ctN&$(<$6{4MRoKa6DQ9HgdEZc;Av6ilSPItt!U zVSntKYUU2EW^v^;WOkn*b{AnF)9v;N#I`f#M*M>fNz71rECu+XLTqU=+l;INaGhlhC9!`_raqK^&bU>Y*@Bp-(ior@R?*X!H zM_muuIS#a!o41DwUY9&MW{qrS$tqfO>#yD2%cY2HPIChr45}^W#T{l`NzJm%EfyZ= z8Ti2~q?|6NU7t?HyKY#k0;Zg)lbqRJ-gB59$UV%MfeVfloqTl=KI+og=>$qyCI-n{ zRY|QnmJv!1drp8t`zq1sO=sNBQjTdtpk6@}o#t<6EhlNHU`;+FQK_Q1mgCt!v#NHF5I2Va}s%KU=zUT{Yd@1OikOOxGi>crkwEL2$uIDqE z-rL`}s+qfv-5PFhWz4wHaeI!WOBQa^*j+|E-c?Y3!a04a{>2)-|HT@0`kF{XX!juO z#cgFqp%lIJPMk%3-?}yhx(h@@7b5+@^EEO%(TDnF_GyFR_2o78Uu9X{Tg^o2FD>Vv zuh9AbPRH|ij7Fu3ro&&l%qywKT>BneKDR(OUs%1395Q`qbzcLRbCF~%aXGUR=3gou znU#gA_0m{^rn%R9A)d}oS8XGrr#dx`;VQzTkqy}g+O23rG>78DXgp`$Xuoh_#DmoR zDNPGatwS5DU$3^EBlzd{k+-gk1bzFAd_7dkl@K|iBNgKffA+!j@2qD=;844v!jL0A zVe1_&-e|X6>~k2Zug~9ug!ItK3;?c#(7EE`l=P2=P-drV!|b!?G_1jDe0CkU`GnS>4N{EEo_?cCIf8 z=hDn^EO}0p@ha&bCJA70b8s!7vm(JQJH|bBZ~&NQ+f4;#*j}I$?D&_VhYs}Kn6OG* zJq-4oI?30{(0yT>BLq7l>hoI;o}9mv;%+sUGocJD%5r-rhT4}ZrsyW4Z&zC|Xg37Y zD=0D5urw%ZJ4PNTWx9XTc(QFrz{xnM zShHJNhp5G=_Ok{0RxR6H>w3pgJZ1F+92s)9^cpp-PRUwZ31v)(e=tH!tW3WTz;F#* z&v!KrRLC2#Z}zR?v58uHI4wazeA~8;+Nj7;F*bloEU(WL{$)Tpx~X6bKl_Z^;ASaQ z)Mj3Aqc!rVmvY;Qz9~qDb=Y}fzH|}Bv6B4Qa*H9JwU*n4WE|2>SLcg*N|rSjKRqGV zVPYiF5SY7e{82j~dB!42$Ii*|1L#i2A`(7Jy=TB>ze&S;0y9K1x_5ifF;!4BzmhwM!0c>VF^({vCBvskR=!#)06qUH%S{ z^!N)1bKCA)VJqw_Q(7MIGe95aJlxh22Kd)n`P=g4(Z1w7t)?&eB%IJ2@Ni@F;%13# zi`S!f&Y;mq1k?DupUAc|Yri5NOuiHIEHABTdqtc=EsLIg@*h8!ouUWtr&n9oHr3%i z5H2H40RS;A58JFDp+n6A<$El7rDw_kchKjudK?1~UxYL{#VUHQ%i8Vn%V zOW8nY-hbf4{$=jQdp6Lt`$LLEm{yB3Luftjscsj&d~!KMq`QABNawXN1%m_zsVroc z^%Aot^&!;R?)`*22txy1-iB=`XG4av1vR&9{G8-1|G=B1>=`^l+q8UBi1 z&24^4yc`!afCF;@y_pXQ*+g@vUKW&(L$#AC?(9{rRO7I8y>`s0`6 zk1h-jcZwzxd@OiLNca=CE&ir{j-$FwpId!@LL8NjdBx2mT;}R|B;V`_|{H;0@__h8bKjHFRsR;pUhi}>Te{#XgZrsT`d09!HYx;#I!m)ir z2w1a!_80Jk7=&rN^3(Ve6XY-YE@S4xEO}u6RegX$SOmbiEkGd_fjdnDa1~81!BhUK z%ld}8cqw2p3MXRK`hc5hPDhCgBu=C+R=cE1a3!n79QDr$1;3TsM>Y!wnV$``Eq zS#WNY5;o?ELn`cTepLik&{zt$>jPP?HRNK!bzp$!do2a{u-s@zMM18N?*T-JW<(=% zKju##pdfw-!EqQH_O}^_*Cl?LtarYVGOf$y^S>1eHCp(W@}UQbo#vzuES~?0g?Sc| z3Ac1Z>pKeTIa$P&&8jYteN9NSaS?rdSBQ&6kyZlWGMaPhMu7J0L#IlPJD%V6DP z40fIiKCMbN-9l2f8OlLL6u|+_@pFT$!qmQQoq&#e2jp&OwsT%=8!4UGXXGJLBYF1} zJZtOh(28Y#ldfy!BKv!mj3~3CRb-WEuUxm$7HBCP=xy3c= zhm0^3{BR!(BA4FFUSHca+ufV0DAy1Osi#F`uIF75^uNp)3MlBS=#67y ze&-^}Qr+Xz(PAwt9J1`(vkU?RjG@G2DwL7kbEl;g4*wywg$!syMQ2sHR^9Lj>dgUk z6zd$nhN*tJwq*5i4CU0QD9bQq*I4!|!TiPL{Z27n5{p>Q^k0S~0vdMXgA>2N8bu40 zpOBDe(jRg-BQT_At3~24%q7sB1!mM52zk`gI7lTjjPYe`B+)dUG-^NzG=peln9AZ;&YR;z8977aWWG)D!|Zf}xO_x|Dp^k|9Z}@~bD4G7_YLKG2PMxGM4+(c zJA~K0{qqoNt)qiq_izr5(C4i<+hfk`o-Sjy@ST8kwPfW*uh(trHU^0i4$2a5jr%qu z>c`U9nO8beQ#wUeNI+{Tf167FbOkFbi@L3OY4gn$ltjE8#xt$7KA(hpz=&3m_Xhh} zdYv1>7i?69G!2r~ydZnon#|9-1t}k2T?8fcsZl~T^6h?wGBgX(usIO50m?#9c!pv% zb}aqN4lovtDw9M#vq2dy*6cZ|&rlT6!cr9XfFdGbOz2!%pg+)q@^aX?B>F?RXipbF z^(Lq#wakOod#sdZCO%b$S7p)d>6aAK;$!AOd4ZflW}wuoKJiM(uDKMAV2hPiAqz=S zzj!Uarb;rItso}dG)wy#gWkw z$Jgfag;HQ|Iv~Jy(#MhYJm8aQ0xXvG_F_7-er;ay<3dlu##tX2o1F|!2- z%D&tO|XtlBWUzBx|FCtlHvpXJt(PXkL8rH|_C=l$2?Hf#`<&n+2N<9bZCSHFS7#U_@pT&jgnv zt?+m%>Cf+1>aGo=mug~ST4bs9@pqN1h|9*Gdm%Qckm*{xteRq=7)6kFGvkGoz$1wE zBG5F`YCj~Nl9_b$pnRS;#5q3TR>7P-SFE|frX?-+X5ep3&C^FnjBfG?iYf&-^3(+1 zh$g7@XUm0VsboNV&8{|M_uKug{4l<2s*A1PkXE{>;48$Gun3US>FFm+N+2KvcKh4o z8HSq}kv653nq(pz$kFlLT4+eOK6IbvC*CBf}9uqO3&R+*)LAUe-l5+~8S`SOE~N*n`?tokc!N_A;C-6gsU{ znkd0nsL}W!m6%2qi*!rSw`snmj;r^nVwOu1p!Q&Qyc8S`titRlC6KBLC08o{e!Q+g zx91Pa9A_m>M}WkJ0KmcL<`^vP7B*fL&+Z1{$ouO9v+a+4gdX02#KXpsq#R?xh;z*M z^>PD@&{yN+GQW{fCO{oNO~4o>;lt^fLaqrz*02WEApc?zzPv+8m%~YjoC6iSkrk-U z9~X=~VgPk+(VR`0WhrJFVW>6&m4j3fs5pMOfjY`6cnKpneDTPCj_EyAzo@?(_U%)o1KtcRgjed+S8cc|0vno@Y)* zI2eBp>q`b&xN0_%u}B)n@W!`O+RMUPRYS@j5gDKFS|$xW5;*^yqqB+`e014{s}o7o zaRN8|Q2X6ty&yt{!|ch%a$o1Io-ICwznqC)EO7=sbAxVC-yzg&Nukte%n^4xU|{Yx z`xVVVo}q+Rb|R77KniNV2f9e}vkv=X>NHSK%QLCL*;Ncd^`yd(Ow>7*n|lWwX=t&;tff}(U2rHDzJ7Wfe(r0Hsbltq|uV&zj z+H;SA?}@W`_@*mkkSj5_luX~c==L8};HoBl!F8@YgpQBfCKuK0<1R=fe_25UI-Fbs zg}$<5&|}=myjHhpncFP38vqHkfK#q)hqZ6?(Zqu`bB%J@t3p5g-aZ4$KUV%f7WyS!=eArSl$N$B58NTJk>IMgn4T$tAtqLIv?9y zbQu4Xpfuy{$>k}J>`@pYmT3nHb0E+vhpxKxfvyfb8U8Q*NJ>J(sD2-0)aunKwhYrj z0S6Yoc=76}zSLL_W`xC{y2vT_S|_Gfs_{6J*6!N+qM?Q@-+m(}MsP*Y2P9kSniJ?4K^LTvawaI1X{ z{%PF*P`v)b6aVWB@DFO9bM+G&Y<7eX-Q26%)ejoBdAxWP{{#MzB@8H%9pX`|)n{VVb!AG_1w)MMC(Q|)5M(gp za{W?GlWOP74GWbsp%DsHu5eF~Sfqw&hI$gxKD>lVWRk0aO58VH>FLEpPsAZ2xv2Zw zFO3d6nkFK!=={ctgCc%7k5CS&)V&0$?wuy@iT<&0o)91lb;|PJ_ho>V12IW|q0vi6 z>um8fA&r}MA9v(N?A_~y+QqxNK}U;&)0QNk%NlEDPwVkEvSI3RH*%S>0hC*_{itdb zfnS2EzHy=!T8_1d{?M0&#Xk>T|_;-j494&K5-HsGbWKwtwYFgf+4%dVf6p-McZ1h>jNtoV(3RfBx4g4b*AgFvIYS% zy=WqUIQln4_~x)^C@@UJ)ih13whG0M9={eRu{htAi}o-j0gwl_YDLOhc(?B0q7~}N z)CVoyJ7a@g+6xZRNGpgu1dwrWn#F9nx(5<0%u|Ahe1s3&`BKCo>GQ>1L^0FQW^wrD zzAFl>zTNJ=D9d=>R-?#Rgikg&2&pKi=o?9pWKtN>WH@Q&lT=tWgHZ$wzoBK#ynA4h zak#~pO$B3)+VwikXmbU$vVr69dg4K92nYYCj-SIZDb;Sy{&(_i`wrfByGhcSM7$Zi zRPSZTsl}{&@=z|FIrD7U{m~^mu@4Fw*2B;-36p9gei)VCeH%sQKKZg{t+XD5sSR=2 zG-i63R;<;_j{|=%G3DSO6hET``V|5R^9^GTjU;e1WPtj_O#bw*Y@LJm@b)}-9%#M% zQ6Rr?rn^K;534z#zk1pt*wZo=4rY=LrnOavxU(>_?elVX6H|5jc`Bs_rmYzT=&>Uv zXsT9<(;I83e$CsqWe*V8czp3@`IE2Tad0m(70hlCD9>^{(gjrfSUViU2~|?=;*gb& z1?R+D+OeyO+3u_O1u>ndYFuo=5qo+j7)wt8iR64^tN1d@dAb|Nv_eSoEL0<4js?&~EN9&2e<1Y;IQ;^zQA$?$C*WUt+nnY@wb znqg5WH`RPKJNUv6MHZgb8eTZ!`$RaHub+>WsYX3|`#^IejGS>JgzRJHHTI+wo2?rJ z-nFX{Q}AO)IdU&T3e``0iZ;p@92elMIyjY7+0?HLofOTRbTID-0?oK8z?onNYOe$3 zfaoidLwIN{09`g#e0rC`V|yNDRluqq(0o;LwGSl&4_d_JpZY0vRYAVDQq{wQ4E-vlZ} zv&{UV;XtKrU=)xB1hZfKT0SBwh@2%vt_q5Q-1iI&p|4YK&c7Q-`3|DsP6$^WJ&AGxAot0`Z<{yTN~5=z5N$R z33XZ9bvBe9_*GW|CtE{u7c2O(*95T(2Dv%e{2Qi6yj0v%M@b%iE5MiP(zl`lYV(0hag48^rFLEQkUZGp{ zGn9lE-VCX^%VXdp=^MOwPy&ECJGPmfc)p|V{=;MCrZ`+o3ZYVVpUlQ1PT}NmJp4`{ zW1@na6lyZ6Ews*G6|ou=7$0aewW@O2;*6THI4lGS2jo}B9^*X}mPP!QicIIo4y(KZ z{F4+ci}~+LhE+X<70B-i+GMkYCf4vzpl@bkc$DyrG^kRIUFpw1sWn$ruT7dm%SbIH z&t48wswU7t6O;!O0}K4kySB}=*C~&&0g4f=IyEa7OEB)>zFzn&P4$W9Y%kdNGBORI z^)V5R9Ze=xyT2vxR?v)WoH?^hs1t^eSP5QN)p~6gm$BC?;I@2ey4!qhotir~ogr-b z(R#XluXHV|*tI%=engTbfHk_9_2kIY6BtgrQ14;`wo8q#zeDHeh0;wKOd>bYEE~MV zM(_eyV)cI@7n)FF8sg`-MquJXm1FKm`%5yrjHbW{rK87p=ayM`;1^>s8OU5sOFO37X+Uob-zDo*w*3pxL+#i@p)_n?xwvig~gvw6>|!~$pgU)uo-5k-ZvtdZZW{n z*U+iU3Pd7Tb1ciuPYxoJoNbA4pMn^b{Bi z?ZK0~Sa*hZ50>ItISUfbnW~a*Bql1uGKGFIkMYJ-frRjOv1)R~dupdrbPxC4DJ~W@ zRT+|xWRh`Dnp>Hpn&|r=BQ^FLn~Ho=Y(U!eL~FLv&Pgmov^!8-y~UI^&`WZrH%z%` z)B58ojZ2*1oH**#dg?8cLx^lUV)O#lxi6VK=Br;Of2cv@j54ZcOb2HEb{{;4`sw9s zBF80f7uH5W9`QUk|J|gLnXX`yOSqj>!bG^|VR$F4nX$xqcIm)`9eKqUcA@XbYs3{? zHrXraZ!dd@_R6l5rj^&3l`Z3E&*g9~`g%DZ=Nv<9qXoK=uk1RE##Xr|ksHay`pLEW zpk5e@XOrS_C?zE<{v(yWT&;3?4d77Ky!ljE!W=;+9hc@rd6QxxNJU!xgfp3GvKkYJb%7lDTa>C74npwr`47aAfci{98K&a{hZZA(#R z#f#rJpp*JSBFg*p^rer55$@>q4xBkEESK%X`XWKsE?tnPHw`-N;^^WL;lvJ!s~+=X zPKU*RR;FqKg=RtHpMY#rs!-con%7_TUeL%;HCWQP$b84xpbp(v@y8XpopCLi8T^x75jDg7^V&nJ z9AXGnqR8SEj2Vv9g|C^Phk9you78B_t!}K)`}SdGk^VfEjQ6Q;u$bIxj1mdtuaJ79 z{3{fLd#kH@5_OSLVI^^`G>tmZ{n*;PYQHmg+ebOjJezWH6>!)~qqnvzb?_*`&HcpN zsWQT$ElbCpt4mKSrT_&ST()o0HlKon;o3{p>=Uii4zt9D& zN0QUAxILJD=$mEY8$}vU9##E8r{HymjNbC=_@Gpr2SxVOMZ(aS@f!mX^PZTa*?jq@ z%9J*rOUL9dqE-9PiPqnKE9d`9wEmZ9{V&n_e?_#Ep8k;S%DlmuSVe|WvdXPwIp8kr z*SqAjx!A&Jq;YqUQ#~+kjB#!r5aoV;Fm(l#jPd$Ss%=!zReYuIeE7Ikc30=s^|a1T zF)UqdcdeIhG4I6WKD-kTM^?w`E0{LY+u0HHo5CV# zeF-f$L)b;ZdTK8T3uGe3l}L1ef)Hia64%_@MaVs`Ybb*!X?75+d~nkXS-G?GzN8q% zC=+ySox-d#|FWkxyU-Ftw|9MOXh%Dp+&C3sQAEMD2^wD;JoJSlqgM*(>rbEEBsdQ1 z9Y3@CSgG9I3BZJE+^mXmXu+V@DvU4+1c`qZJL@_wXt!6uqlO7L zBhLE5%uE^qQR|u|Eo=AD5%YEIfiz6KJ(Ny5XQ424NOak5B(`%|X_JHlN84tHp$(GK z0oKr_SO0z)T(KY}E;SK0mPsWvV4Ati%C`z>hO-}(a`I8rWJ7D`g zyIo1&xQAQWtSJX?(Q*`v(?RL#?4DSM%cH&aVVYXra9~0z(`_-n671 zi<>ziyE`~fzEmbsX@)*_YQMY?yi{)A{T7_5`Kq3O5Wc=VFaKT$0qf!jxr=lFz+daX z318O6M&|nT=GOYA#*B0pcBY@JpF02q2@!D-0MM8H!04+0KFLKoEgG2LUx-_Xi5{_2qy641aHcK|sNPz#$=^pn(8DVE?J<>#@HY!66_K z06-uhz#!lt;NV}~|9Z%;1_0t$6CoqgwSc@nGGrbRioIW49Whkj+%A(KDw=`=3AzC@ z3*y)HzWRayg93kD!tNcza&_C$VhOelGEeS!+}sF2rlH}%BKXkW^i z_45!Pllt9am^L zN`XZwkg>RcuA$F-`7vNr>*cwda}Cl&L}LMI^xQzC#t#EciKv770@P zL!pEclt@IPv(mnW6Jk<1Xo5GM*eu>w@!JfsBp5&_SGI>)zjR z|N3t!a3V>RwI}A1IAe(d@%W4LvJ8;q(rQxNQcvsWcQ4qmy*k?9@XJxbmWg*gVY_=$ zLe0&UBv49@5R#OW{F;p#qcr{msApsoO-@J!$H(o&#>rzwm+jz%xBcRb$4XS#;-c#% z>VwwdG;XW#k4(L!+Xxwj4=tSI6dZX*hN`- zRi#Kk`v3w2k9>Pq5%eP7BOLABV(0EBzmG^Sxx&$ZfXH}jjdU~g%}%HCP9xcv5CJ0ua4H#SwZt> zJ}G+xWJ((&7U7XZgSz*YcV~BUTw>t`0y%E^S7ZD`V51#Kh?V~~81cbsR z5^LSmu$s8Qd30O%t)rA18e)i_Y;r@h>aT}FVz1_ut2TG1)hCnl7uKYU%1;Bnl{hrS z2MiX;v|<^C{y}o5IU93Oe5Ncsc)C8h`RUb8a?3yu&KqT5#_vtcqG1C4EKW zkrcxm)zA+&<3=ySr0jYW)z3c|CZX zdxt5!^WNKQQlVl~0S)Aw434i=_^pzYMksU$8f2%+tE|h+JI!{hJR(oTM{0OjTbbhRiGgfURQrf-gLVz%g>;s61DzEK6SGb3D(a_B^IkK=JwVv%S~3Odi_JsdVe}z94KGb`KKc~?{XcLqK3@= z6eoyaIwv0`KllU;XJFobNzwTI4W}*Be?|n5itF(huq-4NTU241nCzLG;A$3Hzmm}y z$zq143>vKp%`K3|4@+V+labyhMMozr%5v@-jvnnFc%Y<2N+uBPJ>6-@4-Zu*c;)&} zZq7K5#5Pa3w$Q#DaNHjbV`9>0qmNGcUEX7rnnF}4e0GX;jE z$tg&eb;gW8bu)=nS`^smeFD<-0Fd}+BE)52a}h%ahoa*%g#2qfkjH!FhQyVREhbGS z3woRD-{8ydruk2$xq#uOj&v4~8g8R$+opQBWG(7T4M|ap$72h4B%OutZmTVRa{GJ_ zi_22c+OrZP*usGL7~6oG5Jc7XbJE9fTOQ!Y@D{MC@@QFWosSD&saV!zP1Ohh!SCP{ zqAvaov0UNAT9(HeyHKsJ1)cb!_!X28<@lWgXD*~GjFVZ<+1=_k_GK9(VFR7#UKIM|f&wJv z>F&H`Mh81HMqC;URVXrA!(d%Tp+)gi6QVtx0M3L zqcqu~*RnTP7+&#~k(sdd)@8Meg?>G}jjIdN<>7EY^Zx8ia7mH*N|fWEl( z^vUtE*)x51R$noAy3-U^*H-z>O9OP+wJ)bS4Q6h!(xA;@mHlp~AP#HgSHRA7xCvkU^(-Gas-wlJ}nzH z5J4?z1;L@HBL~IcH^js^D~wJ9b5*d)AGg_Vg8s=WUz{fVLE6fM7Ej+NX5Jsq)pv?R ztK?Q$;}Q3ewFO4!EYeP_Hw#?mbVv3`E^Yl#zG#MYeI#*G8B7hl3ll+4QfWxrmNb#) zWi4UcfY$N-?+<}e#;Z#DfC& zbcj=_h^B32urvv;Bg5a;LEuP>wAcuUNqk%9U3O*3i50**_ad?_6A{5Pd8FHC`3bQ8 z#Z)0OboCYZ++75m+<=HX!gFiK=G2Z<{TnO>`rMm6;&mE3p`ITf%Wsjls%K@zIbwXv za-R{1ymwlL6XD$_&)HjtCVQe$LWE3qa4vi?)tsAyE zu=BWlEl?>5Zby`=JjR0A>mD+`9HwKp>Dt5SzG($>)Le+*DIQ&cymhi8V@p~h_wLzC z>}5(4IA~E)b`jx&mh|&->N5oO4q=Lw10_(PbxbSZnD^*7m)jQ4ND8z(jWgA&{R6i3 zXahEC{NXBLF~N>&A$dvG25zaT*p9<%Zo%zcw$!oy=A}q&^a)m^q`$4?sDU0HLSZ!1 zM8ZNc%xtyvrpNilQwu-Q14H#U^pT2^Rbu%o0WrdjTNEJqB3J8?mGZi2+^%`dBQIby zp=Pi8-MvQmVYzGUS5)JGeiQGg#alBj!(PpYelupKNY_2Eze*dXHG$UfX{nd^^K!AK zn^uNS(eqU5#wURB{P*FWvKRlzT56)uhzN4VAiLc_b2xnzrhO~k;SUG0v5pakmpg5Y zk=FMzhW*a1B4N);b`0xH`s|>@xtGLhrDEc{OuFK*m}KuPOCFsZH8i#^ht?kKqpl~J z@sWB0=hA+?lRWzuzYVlSy7Vnslw&;nrxeM(ee?{WxlQ3@6{O;v$Q`c`U|${qX=h;n zZzFKZycG4xQE~N5Som~%UYtIH{)+kbNP&T2@E=3X1I|VP9yga=-CKLwBDnDcG@YTo zEs$_DnmOxFYAdQilxB6dAUYH7Bdy-LwW{ht%qz}e2?%JLsKU{ofP+14Qqo>(tl3IY zsy3uuWocx0bK3k^(6uBRR7A>NXDX_Vi??!2>4ZV?O=0_Wmkaj_E|eR8|FaKgg^3Jh zZoOBkGNgWiy_?F&%sK>TRjRxd^4Ii99tiPbO|hT7AwcRDL$su1@0MgEet60dxaqW+ zyKP3si~2hrNu~vlJ)&HNP91HYiJe*@wXyx#IZ>lT)YB}h)-KVA%e?6Ap4fcWRsz-iAEAV zy0x6gZAY8lW%DntM)%WsOD`!P#ZKASSh$m?(gGR7`gRDyMNMeLip)#r`t-?5jLU${=A;c+nXClgb=p5c3G?YSK-qU>W@Ov;!;DOO z(k{Z}PKU0X1E;jkB!n>cO!*1ut$B-DB1}?Xs}Py$f6R1Q9;^*7=7&j+KMzuC7kJ-D@<+ z(_@m9Qf{0h_Km|Cb7|s|?t(}f5piCNt+hcUGssoF>xqM*XTj%=jBRCJdB`U) zr1#Rofu*&rp?dB?uJ!RhqRO%-kM^*(rxosI;cbzjf>$MgBT&vT#o3{36sthHmU zJ!h}8mrYo-7%>M2xx5PaSe%^TEC#;tRh_ec^)9|}wdGcRSKYl1N8${(gop>3bJL9G z-%|Vp~Th7SICeIS%k6b#<(3y@mKBNOHIjdC0GiD7p!f1{a>&tbsg;R9+ zSFKG}4w}%T^EKsFL*lOEOMP{-FCJMf%06KC))SvDMM=7Z(A#`GYqZkw=DH~ny85ATJA$tmM0DK?vs-8yFW3<^y0>A4rIxBQAd@VBv}4gmv`b-hU2D(_e?L!V=Ukt+5yL`pHsj@?RI z7DHY>qJOQkp*g(4(7f7ci&eQX%RsE(re596g{ox1=Dsgw?o0luQw(aUK z*+^c=-AFkalYD;f*5nyx%S#-#Jql0EV&&Ci@4qAq59 zXuN6Dggs!#X0XfhHdoYUJ4M%tffUhccvUj7Hm8VLGURfVpq)B*t0eKHZd&aOhC*Mj z;Z?_z%fl|-&**HHm(x5Zlbo%K)v#9;Zzsj;@4AMeb25_gG=R?T@#nalgB&F69H2X& zPm}9Y^og)&n63wQ+m*+Lya<;TY@`hGi>cspZ%*Cj9>mnK;|-u2rL;tffb+gmcb^gU zFR(SA@ygr^N(xGcIL;2=qDLv5QgeH^mA!r@a&Qhxaj2Cch+2IMyJYe8)U_u_R^CQ- zbVe>!E&F7$dahyj6hcehI5;mo@^F-a{ituy=9mGux_usbS8TrPW?@`kQOV;aH1{Zh zPJ{^I8)H~<6e&cq9+{&B+C^sD*EGcTayPXxnFN@J$$q;nPjxv+($s(Xpn;Ajj4Grt@Z(&eM8X%aX?=Y z*O_EU0j*BSo*~nnG;4@5-nEoBihCE=j|&;`Nkw0Sh3`oI<|Gf!3~uq zGt}r`%>_&zixS)9VuiRb6pYEtMW&xvu2(m3rh7^F1tq-^LXg~f(Knp#eeF(_-tkt& z!&ZM=>55F08Ljbkgmb|1=agCLg6?pqogC5^74)0>eRMJUkYIWzNKNCN;{>-jQB&cJ+g&o97>ZT@rXdDR56U+eAN+ zjK??WN6EzV0)G)nw=-g})sntHQyMr~vrQ~R8lfrpxK&S|MM%tL{j|JDgL`&f<_;yR z-o2i7Sv8apXzC>cLasZYdoGOo(y7d(G7lJWq$my&4@2Pnm1H55ZAG!4%1FDJ@@gJS z-7X2yts{Hi-tpo!asSG7!A(uJ(if*%k`kpo9=$^mQs*Fw0DM9nB_#OTltW91*k+1jjI@(vNI!JoeUSWqUuE)`FfH$ zfMm;R4y0Ky*S|J^gn((Jp&ttmn!O(yH5NT3yQN)O?pvi_JTyCc>*(ymrY(zL8TK02 z@ayp*<{;VuTTBg0?Cag^L9tiGHKoFndxneQXO%Q(-H2J^m>}U?fyb>rr;rI@eb8{t ztk|L(0lDf*t-6g=NRn}F*F<;|k(DCClRH&4VHAfZ`b3pb*D|lMiGOw~<1~G(KHMOb zbNDr1SewEwQaV%XBw9G2+X3O~z^@sFA+Ng7psF_ku3s52jmSLjIp zz_|vEy6|XhTBQOz2|}&mCdqM0P4a2Om0EW@?Vt+oo2zQfZ`Q9tPq({El|5176Gev} zH$q5oL}TpWKX)Z zl)CXop@VTx=W?>5#%lHFILcRp_iJ39j>Su;S;?*$d=mTY(ET`Z&MZ1n=Nc`!Zn_`t z$~xu(^o&4->wsA^f>h=jSA1Av@ch=q;Sr5%V%1DDvAxvB4xe98XwUGNqNFwEr?FcT zZZjdI^}{Cl!m}tDVMP{rC9yZ)(}VTR<@Y?s2L^en7n0*c=#gU_Dw6{zR>LA>f|b2} z!2vGq(l;&Iub>>gw_@xf5en- z{XV8|Rfmn>a_4C`!68!#ShWNk%?e|z+-!-vk>^qqCPt@a>cB;1iD^eZV8X*-_<$X( zzso?0!9v%1lwbMMW*FclcYM~G*30u2?y;RH2M=CZahHE3Xoxwp{*c)#z(X$1 zT7+o}F9)u=LA;UEx2~j~Pr#ufe4t@$V@^G>%wgcaB$M4RrdWYAFcoslrMcSh(FO#S z6DOKfZe#UoDz%V%DO0?dQ#U?WTc%y~yiCZd44(~*^zG1M81ITM5-yC~Oz-);^F_-x zR-1-M>og5MoV>(Lv!Cc!}Mc6pq6Qi z+DsEZ^SNAg(Er+TvG2CHpP3jJ9*!?$f1@#nk6d5zMVl(LGmv3kO^w}~R!%-cMH_Vh z(sNSg`M5kI;|At17>~>}n&}hL+?;OE^-$NGW9FkmKQeP{f%gn^?)Jkg0y=gSx1J|D zy{i1`TAu2{%%2|>BPul4nCjfTk!^A$BL%le*SKrW{Kci&WI1>|Tl5kd$IMr>$Viw~ z#yP09K_-VuQk)0Xu8y={{^1)|G`Yiwb_xxfEc=`GeZ7@MC1>=w#dSj7Ro=;7Ckvc* zcy3-+By6iyWa^qGA6NLq<0fW89G3>3rFIV-W%Gzic|;w*a7!?K6S|>phx-Dxj9Jgc zL-Crp@-t;qubk^yTljNPZ!9+!9zod3eUMDOOWQu>xZ{#ihmQR8RQz4L(#u`b1jkO! z4n^i}hx05Z_$bLX$`bl3s>fW1pDJ-B2Oi>ot_tL!HAQVbwP6$3qE(Q^<{aw1LFYH3 zrkT$V?QJ)F=I{ly$xv=jd|^ zIc`3`faM{1gZK3Gt27hb0$8Tn*|E$ysIVn^VG-|*Y?5=XmzKuaf6)4J(jS^uKg-zP-N7K{|7Hl8Y*2s5;ek2+X$ zf=J;!lvI4{>HpsFitwRISD)~_Y@ss)2M=lDywdKISsQwo3Pg_HePXeSlNHNKPR(-{ zRkPH6ish^C8!AJD)sSFURpgvHNw~_msd*`5N56s-F#Eq1tAbnAm_oM}MUwvhp*~|x z!g{dDA>}J*{zS}i;q`^y8V5^DG`&*{G(D(E@aVf)RXNaOlq4Ln51LIR4U>=L6|vYu zVq49s(0#A_c}eG_md=AnzK;5?_~E}P=s@RVob!>ZrD76iWh1K{SncVBPjE*|LVy%Y#d zXJuiTshqhhkie+$<&lVDmY9`(7`RFj!r5DvOu8-r2BEqLqaB<&Q@%O%|3Y4;cMekA zD4emqUOD!$*mYShk}SDcw<@f@5bWarCLtL7HTLoe#ip2KyVfo>R@7al^i%_e$IrM> zt*M$0PdI#I)*@mfIYe`BKBDl;ItM9ljdPD49uInEI?cNlj&UlyoBbs7@Gwxqpv#ZZ zX%_NG(tfh2pFMhWrf-CCL^vwp1{9raegt0IT#l{V{P9wRQbL9N_7 z4n-L8XG@s3v7#dCU01X%^T92N1ljcwM6W&-q7&(4P`Ep;Ea($TN$Vc>50@C5!|J6S<{&N+Dj^uNlafR^`+Hr4+AXrFfoQUOwC=Vuu6tC&xnz**ft}0SY6hD@k5+3eR;Yk$A8Ogs6U8uk;_&vuBO! zt|n#^e3g!2_R^mh^ZIA3NRV?uRDd%Rg>J;-jU{cmq*#_J^or>FUtim9>`cyR!SI*w zJrV3-s3umottVj=bit;|JqHE1M7=04iNx>8q3$~em0r=+Qf^hCT2CC4L0ZQgcL^oT zr1vT>npR<@zD~(utbbuP=%OI3Ekt*eBV%a|K7=Jxm1l$+b)FDZ9->#2w@mxy-tT+y@#?DA)?oX3%iRPTjE15d zB2uuY?AqL3iA%>FA@0}4<%X@BWBIiZ$5BHWyQwn02Sv*A%l?i#vy%lHnk>L2#5KnU zB}ajnr2{F?v~I6wi*UO;6$SR^i6~?870Qa9x@+?}jM`{7zKr!a-sg+*&?Cau#QGGn zvxsD+I;fZ;QV5DLS1h?BEvj_elSPsRKii)cxUu6pxqzJ`pHGq=#Ma$}ntAgW-@$ZM z-$|t7Pl&y&QIJN=XF^=zOH1U;jeEHHvcGPoFTL%32;zhjLnJK!wWlTTu>i>-%lo34 z3N3~_8ZJj6nMr(UWreEFw4=46q6#_5OXr~e6E{`j=drnSpL|?;9W)fE!(aoeL9=w1 zVIgyS(XOD~D>=55PJ&*BUtL@F3@?S}O25RD9e3R3Tc@Y@Sm1-A3bY4zj%+&ZYvm37i2c~8$?&!-da9wh0P61(qond-Z! z>Aoq9QVZTNEi* zPmTFfj?ShwYz~7)nQjITL%DZp47CT1Go47I+wkvjn}C~*_(M0|RyCTDYA|G>;`ON& zo>cg*94KoMxykhe3b#45eQG?Onmfp!M%QAxNtDX&xU5H{RXl7cUuRr07hpNA4|Y|W z>|#GC1HT+dk9Dya6LJMU(V>|Z|8ZQKmQ-6f^Byx6W(v)%0(_B8mAjpgjg1nOn8mry zhSbC!&tg)kOuHpks{I=TF||LMa`@;Kp{q%r&ChO7Tvc@M1;~sv(O24!Hic&^gNfzde!sA1 zydLXl7XE$}0b2JZ2SMa)1g{}7t7WgLL5A!&<@j?WLtKPq_NvY9(5>+}MH@XT$6=k*VJ%fB{D zovpDdDJ~Lfd~H^4z$#yyAj0&HRXX(*g^0W6$x?~DypzQa0=!B)P!m=ZtW-bwln_Wb zi8Yal9cmn$B_R8@DSebGN0N|JZ4NSChkFolGs(-vN;V7y-S{*j_3yI^chd;^ACVv>Q*c zcY}q2tH+m77R2#$Yr5ASA>65louoL~$$VvOSsWPRRaBCa6!N17<5`zTfeBr&7<@_X{kMR*)F~eNjGk#VF3azK{W@7AP;XQB~5B9cqNBB!WVfC`k;+F;`0#2 zH+WUxOl6lnj`Z}&somY+y-geb+Iik?Rt*E6rg?Akgbw}!T_2H1`Ek`Mc#F`B$F($f zz?sC5l#lQ4GzixlkuE*X`xFuoW^ft`HG|7b-t|E<`{Y%(+U)P${1#C(JvSds9UO)N z512_2k=kHK^9#v$mVVK`K`DafND=aUT<3+LqN2H3YXf(N)fJ&&SnOega@n-@_{2&m zSPe`+Eu(o8L`!lh`(tU%fV30+n_cCKBmE|}Z#EY)P`d46G0tCy*=DF%tUNr>pI>IT z5Mfkadmo}69r=OC5l=IZf&FU9bxfXuNpUmWG!7?J1Ik5H{d8P=EN>UP z&DHazIui}77ExPgV)i%IK4~}vXH6-MU=F>`vm-D=v(*&RD#dVBBz{vEPLOW$5)N@k zEw6rOu`j!Er^>SPs`IOn1kv01$xjMmhvdlO6x$)HBMB;G8jwl_JFy)HEo1pWEtZ|W zwa#+M0Myd$QnDy0c*nW;Ak*dHI7PkY=7yP`x{KKBGCLwWqCy6Kb=5`tHLVr2`w&QU zSlUO%PdkrE0bsKg~TR zXqk-edx~g#n)6<6w*Upbt8UTCy+C99p$FGUkbQsxnL2i;B%O!GXr8nUrXHQ+^4vkm zt*m>jo-7f)2Rof*-xS*|5TH)Nti>e6udYg&qtdh^;to{b&8H~=*3D}%>bDgOXUIB@oKI6>4#HHo~lcpyNqQidxk$Ufq>!^HEkn8Ob; zD~O_uPPb;QyY`%YC!t1jxnJZRzI~!Gmyj`4-kYnrm59wkk^e~%bHv)*LD7n2@GN1x z%t6WZta#}$ja;s^#pM~z;K}#W<_q#t{6#B};2~KBAH%i?od_#ZC0PX*<}gP5(}K?1 zMR(QZcB*u46oE#0%2?t0scVZUB>HWEI)%II>88%ui$@s>bX|385X?C2ki*(-H@3vJzItT74t-XD*Wb(!*V6rrH}I$bZ@89t4TR6|(t16(N{)~bb;_)p8kpNNI!2MeS9#eZ0RY&{BPQVq&F18Qt zj4f?|IC!()0^xzs@dv;S1Vp(@{jWs1|1lo^8|05~{I9(DBhtP75(woI?axSeV5gt& z(d@qjwErub9Rm{!2!#i>lfV2esvQ*t4Gk3t<-g<&ybnUXj)qUeb>|9!nlU=910nab zn5?UGl9e4qkkNg5o`;UlF))d5kdTrwFfuW-fO+}&1q6kJrS9I7mXVc{SJ%+g($>+{ zGch%LWNu+;<>c(*>gMk8#LqtEG&f&OP_%-Nx!|G8hNgNo!ftBP6kd&ndN%5odt z;L;+2mkRtDj0Y;a?$*mmT~Qt`zAHo2 zn&ZP^4%dq?q+9MW7wcFh6i$~3Y_CJgf;v_ErgDUjge;0DUb&ZY&^mh{b1k8-%KDJm zp{em^q-uWQ=>~T#l_N-`HCZR}^sj4UNY_~0k*}NFgg%C+iHXWERi@i;fYYpmj7y4r zwbhKo;3&s+$oMl>gxKU6hZj2%h(|GR3o*hJm4I4PqggK!G+%;cF877|P_1WhPweut zDhYJzwMB@EK$T>=p4f-fVzm*%AI2!|+>2ULX8R+P`&w^YWQy`^nL-6}PuXjfP@2a=uHxG~;65Pxet#nmB@)@5DN zKSjQ@1QH>@1i_RSNb^`qTOJa0x+5f$5zneogw?y1kuJ(lo4W#&CS;Y%BU5)kqGtnf zTPizqr+Dtf1iOFtQ* zR_Jch&96t{HZ9DNM_GHIlKK8XA#;lpy?@~AS$zYi|61)?RZE`8ySiF~{buj=1r}fy zuOzI31G6f&9CyFRfF7z2zfY;G=2zkSD35{4x+cmlyP3M9tyW;W4@M4c3p+#c5<}+r zpuqQ5lDgJi^rNV~NcQb}_JTez!7G-+ys^|?jUF{1jP|$g!mz)DA^32jN;VrGB^3{r zt{=-4YzjvNM9fJ!dDs(X(s}{G{m;X_*nMXUj+Kz9zUCF-i`BlpV)jsp9TphDlG_gZ zvOT8P-Tl38&fvu>wg|dY0iSn^L^Y>|GE)08k=Tn}ZN!5sdwzKCt|z$E7TW!c>DJJ6 zT5QHSy=Q!sYoUG_fp3%6=ZwMI8_GK#_D|m(>bCCsSqFzEU`wn}ovk8f-5|8ZtwT$r1 z;9EuT>E8P|$U5E;Vz~Ko!ShyJFTqOKPEbghjSr6(1~M7p0%P>Hh{EeyvF5x22iwvu zQwQ?OHakHp2n^Q0eD26MEmmJGf~ZFbszu06B)Dx8n#e=K*PeVBhBec#Ft|W4H1gM! zdKHtJAnAl@o0cE?a*~rks>{aBTTwh|vuKA$$y!b(O8)eNo2+ms2kr`!l z)%Q_F*kWa0gbxYgUM0Nj)Ea@0O!i@MSJ#Iz1$Y^**awP}!V^~7$lN85PrJ!(bS{SI zb&F%cRDx~1C?!J0$qnFP>J!dL{VsvTC-aSH2mxGM>k~SsQ+1!Wu14gg=b%T{BeMca zVg)-ih5BJF630Fb@Rt1(+=%I^f;;sv)rM-gI%lwUUlRYPF5;y7bOcf_o6YaMuAdU{ zHSc2RT%g~Qbn-XbeMAyo{eFOh&pf1a^{cq2=6L?J=z3|jX!}DVopNnbOoek5Nb?bvt$FIq0&pJubXyg>yUZ^v*hUjf9^Mg?mBw;(ow-8!_*k(PghY za6=f%atgfFkj}7jzb+V|u>S>W9+KIfa$EtJ+Xu7mpH=oJ#2F@>jj#=^g?pj~GGAu~ zoP(?@;LOz9sj#@EPrx%X+)&@Dg|&pmjy)<17T5^1Zadv&WiNDguM@Z)A>$G}r^riS z-zZ3&5IzmKA(|_3&`b<>9{qQ5-HTMUgT9#eR+LmaPXt0*3Ei8P&$6H`p0|<7+123` zbb{anq5CEjvq>#`#?@rbjpP2nEQSn3Fzjm$P!qv#bRHYw`+$8$B=E(>;hDW>xS=2R^nh;fZ>}HIaZ=Fb zdu_dy9yU}E1h^bz*2|3;cnUjR*GcmU?w%4+Y5K-Q0cv4yE)(^Fv_SZ#Y67X&{ZWnZ z5+{wa``pFF(tITv@|a;7T99xnsq4yB?!ga>jhWOmyGpk}w}3_98-N zxY)z#gMxj;5FAsYlrR-2Q^?UGu9t2#@^!6ISZ4KE$^IPFHE6|l>^+Cqs);}=OE(>0>lkuffv(Cp%)qqMFwuaQ~{^|vi<&)-irqw0UNvz zr-MdCwc^4A0>r@xQfu33DZeBC(;c*c!jz=oMc-;>6yWVYeIN5TPp(ddK(rDh&F{MxQJLL zU5cHcD`8Wb;E}ljC&!|YuGK@<$;bJ55M4&rfJz+$#iHKpUC2ijCz)}}Fvzj^a5<@V z0hz|?x^mEGxzp=s9n>|8UF3^1fN*Vn^}ydOU1XGt5%thgX2=JMY2T%o%>C|Tac~>l z8z$ABlp*}ig&GZR-r>8imTuj#NXTnXZlxD65`^$rF4-fUhkx#Nz0pRn=XHA)^-Nqu zqS}5t=~QPugAQKoPRMLwK9=8%TCv%3e2Q%;dKz=p>_uUuK zMHX_LF>jrN=E$&<5VXJ-U-v;*Svm)y%OMjAHxpAn)FCUO0)`^yz&4G#hX`bahCVr- zUC=_jHr?~Mrq<@nugYFq1=?+iQ}ZF}5Id!&HnZBcwTT7U!!0IDH+|hrUvUWx3A4r7 z;l*P;WOSQ3C&AgUIO~39NSo;hoJISo4;Y@*a}v}o@$@9~=^4G(jY?#;4;Mma!iN>X zQ@_90Q)bl5p1mPX2one@m}q8);dc1q`p}D=<+Je`p=~`HKC`yx-(TD&I#qbA9fa%H9}v2kS5eKkpt-XZgUp7SQ!z zqnB6(R|?%Itk`%G2;?c?Wp*fccp6`TZ$9-^p}Esdq)7EYko7?SI>!tPb(R*SXdV@ywR43N!FPhV5E zbn#DW++=r>Nxm;Sn5^5vSstotuN?*4( zh3|9Z3F$HyjeYXhXfVSq>E+90D`43Q{+J+aRb$rwXscjXn({07o|riXvs~?vvO`Z} z%?<-$CO@TQ_{U|_iOkC4JXUyQ5G)L8_w85E2!fu9Vn09GyVT1Kp0Gc7w{|}%(yoGc zL2IrfgA(56Vtpe5r)|I2TT1@v0-Rb|&Zvu^JjX+17VfWNT)@8ftX5e@1(_r2QsC~k z+}14-Om^R|z%r#^`y2$bhh_$gvM-5Ae(s*q>sbugZN(D^GOul&6y?m{I=P><2G6-) zo!WKmJ3BQMyLk2Egx5W>yj+;Kx%vJ%sQrm+iCg)`8ODA@V4ZJyCZUlvu@CuZnT!4+ z0VV&bv^Z_k#LX&&rgY~GvW&bu%KMg?s={_z-X_OURSP482{j{uzSxHfmTZUiK_9M; zu`>j9^N-DLWDvfyVb80BIVY;Smt)+leHF?S)cx7A(Yci-BtE~EAs{{_cD}asis&@2 z&E610;Nvf41C6CL3my&qep{__;7Tsed3(#E!OoErJk2uNuc% zso>O|RH);ec0woQ3a&X+FXH}WmyqF;&f^fS3V<*?vjS@Yl(CYHzR#XM056%VHpLhY@0`4*{< zujDv6Ne0`>WYvqk&|Tj-dZadQ+i)#cAMQDxK>zF$We7Cx%0x&;hd}Z3o7pw(1%qqB zma-@YPk4doqN&M-A|#m2=XR9@Ao@S|3a`boEb9Vx$H4(s1c01n-<5NjY zaCIe>m0Nf%bTDeRMH(bBT9*UDYcn<_-k&)~0zPv6LiSdu$(ZfBx9w zF??t|ZdH{?70<^w_eJl3 zP>4!4z^K}0r27drqz3v-*gpDti&amH?75fUqhGEQW<6+rtCxRmVd!-6?c5r~;j4wm zv^)WRa8e2yJud6o68RAF%8&=f)o~SS$k??`^5PQULSY@3ohD=XKFsv2#{>*V8(~pG zP}HqF_r$^5O9UhC+Qja%Q)s6$h@G1gyan%HxfmNRdid%S_aqN*J7ZZen65@A9yAd9 zu?3h8A<}pvq$8db@5MkLf>6n#sA#evvLFMMEu6ruoD#uHfHxkC%TZItU=_`&rK zE01Vj#$e3H)KsSw+!BX7jT5R)VpjN=mwT^ZU^Q$|4-aSiI)+|JfpvA+0EuQ|>;*TB8 zoc^oDOQVOZx^Ch5?*+8_jT*+ViMZN4L>F<*dMAamgK|}08FdOb_CQ^Jv8B^NL5fr8 z@bw^f_2SznPS;HyKkRj6z{2CWb@j!z#>iSL$i715G1}#=!tIShde(N4>IdrL;-xO- z*wu={o6XMVm+k3om9ZW_R{nJL{cbyIEmmIgVci|lJLyJ4R7ZgsNE9yG=MK+N*YE*) z)O~D}J)NA!oPCfsL04}`H&y-XMY|KKxb!}&0m&x91%ob58HjC1^9tS^=< zuvTs{z8an-V^ku^BMGXf-C){Jxi#!*Ct`JCe&ggJXQ8_ge zWBZ`4tu^k&z8WN#qX71HFg01A>)q+62zrrQS{8gbP#;1()#Tv-y(qsR=c!Xdysi3y zh6&8x4y>7_j23Sxf*jUc%1;?&v$h49(@Ug{EvoVP=a0EcZ$T`omvJ&5QEq1Akt({p zUU=%bd^2=c(%to?;~US(m|OedIq9D4mgUur;-@cKHFR#}6uBDYKSW`R6d&I|%$2C^ zXAeV1A$t<%QwCqaD7>a1M$mk0Rl@j1&vct@ZxCBDp|l#KU~Bb!Gc zLfwQXDj#rVs3qRLLXjn~gU{|4*KBi0zuAz}hO9nCljaqDaTw%U8x;8ArBlqOmpE$O zhRqufxni=buA!94X_F@1x)Y$&k%yKmF1$|RE{62SG9g`P7{%c(zy^N65Brit8_i3W zZ3)ge(A7H@+GBr?>P<{=3Cyd%`X++w-0rQ@OaptMi%jsn6~=c?d<(G}f*8lXke8T- zLRJNP1aU^-B_}p4iqI`Q`p6L?%h1nH;KI1pP7FP2@faM~ByYSqKb29?D50gt5Jg^@ zKGa-1ORWuw&Wq(tmur(N&v^TKq`zUenz>(G{wj|7dh#6EzDC{Cqucv;W8A#d zy=izNPVLJJK9sUV?o~YVzOmUau2GcXh`!TmB)n0)DtK8`(H61#(#gPu&N^1Ef0FEg z*om@Gr7x!~kB#b>4#B6LeKhHNc5L+*cjpnZ?O+WIt*r0H@0Y=6V()17Kcnxb$?<|1 zT6Y3jfvC47;+yL%HPYhAjB$+kjbBB2HwUa18{0x+-){OKV++%(xZVs734NJ&a#SC7 zRZ6$%&?%ZEjA4_(U@i~P*kXB7PP~E2FvZ27Q%0mXO2w8kx>XWehcb+}+~UO$a9PshJf*HQ&LOh8aNk#aOLsD&98e-LK9b-(x13`ILJ5{y zOiwL0nQsaPk^%W(AI4rsbh$hrJ2jWOJNzQT`E<%$cvfth7y&xoPEQc4+-fW(mnQ!p z(NQrNR6xR)0B$BizpK!J#bk)>yesznQjA6xn*%N?ra*fdzca42Ev;)K2Cf0mR-(@h zRt%`C%Hm~~hH0bP4~cLAD!pqqW-|Dpe1FxH&{`zm&zeR6i8lXCzj^1>*jV_C0IzAKP@lIE`lppbY~wn%{bF z2HxLjT;FpP{t?G(Z(r&FpeTUuMg1GD7MM1FYla`A;M?387>~xzmiBgk)l@MP<{u3V zM=$(bLLxA2B>Gt#c*G7wvi;I4z7Nswef1(D#?m?D zo8!v`hTrdq2GV~bz7L*@2&05(iEmP*{X>cfihm-$kLHVrf|%s;X#mj%47A^+AO))V zq9WhT%SD7^j@5`Kur2FjV001xRu^=%|3v&~0x1t5MJiyT ze^(@r^PhN3`ijgxQE{nSUmqke77|h5jstgI+H+Cv;!js{9BV)@clr1 z&p@XBtN-}<%HK`v{3qnU zyQ*Kw?7v710woCkC*&WwlK*Iw{*KNN`j60mw|Bpyf3N8R;r|5vcfG>+$Q#)kpJ$G|4!}`|4+z&pE&%TJRBF9oH=P3+`X$;ydog zlRtXQ>7<34eE>%VOvQi4@hbm&+#gN#gYv?-BEZge0SQHZ$C;@Ad)yzL$E&$R{PzHk z2yh<1;}o?1J?@Vtd`<3z$`SDEaX_k*-*G0o{~q^8@0g~b*G~x4T{kdW`W;7T^zU(h z^!Zm)3CcqN91zBS0cURV?{R;eg&2L-k5>Y`<^kZlf0vHa;@{)`I4`oyHlm@r1OoK| zUHNz8*<}3-?qA*bm)}pnbml)={8RbyTjhY>5&x)0I@@2!f9TPFboJpluQRFvy)mwV zK(xR4COpS~kNe@3E)KYh3Bi9NQC$BU-gj4Yk#{k9@lRfX#}D4mS;Ie48DE6eT*wCd z6PDxk3+(q+`Og3ETIFJj)t}@-?>~?)GA{~xF*V*#oP_@m+{HwB7YP^h6#XO|hW>-_ zOBV*Z|M%mc86tiXnqz(tF6N8)A;V9?#j9(65<1)edacbxMJ}$k{e*l0e%yVV5not) zy9l_rqV*Fn@%{(kxAm=y#V;<8{4Cx(_oMi4izOF}|9*D=XJHUXdf}Hk_B(P9cP;%=`d=q2RTam9Irg!A6`YE zmy|MN_zC&16blHgi~ynx$s^Ui7hwZ0CEYF6LrL1NWTqS!C%!eUv!Ha{3!s!8NiAEx z(C*rxnQSOZEVQD4#I9Oky*r4)QR<0Yy6boAx*0J+Y;J9^E;9F_QICoOCXBYz&F{<~ z%RLd$IM)aql@Y_7@(E5CGq|uL)qU^ml4GFNJbcT*Sl=ds=nICP@I_J<&e8Z8w1+%9 z#Ic||;y*Qt;J7J!bqzxW!ERqD6=*OuVSo47DplpvzpL|>^RVPiVxt66XBQ+&{AP+E*w_y?gwAl`os&@P)e)ejrH^tdo3)k&Wk+l@EmFp4Iy8subN5ktF zO8TjjB(t`|@t4Jphw_d_j~xZnXy5U>1Z2{!dnXTpq1g&e%vVgo9R;ydzro45V_MhI zHZ%^7?ofgWocW9zS6fW;5dYD+82O3gXC`%y4&B&NJetaZxtJy`Gur`&a<;6w({8Rd zuVVe78!Ln_KI;x4wxh!~zC-cu_$!3=8)@-y4AEyg5B$Jz^b`)Q<%(L3f@NE`?&Lh! zZ=cx%3nViRC{DJBz$WmRb#zc}&|pvGWv~h|H8>M)x$YV<7zpGBXTdGOf72K65_ww_ zb})4ycxE6ddDJnO6;LMW-T z0aG*1;AS_@-VfD0n-04~q9#n<;^HEt#1{%bxL7c*IzF|0ry#Z+a9enX4EGZ-Ox@n0 z*D2mxfB&vrFW*Xom%3m^Pgo^N7n(`JNOo&qkxXM0R1c>qM>Q&}1IGy;lvY*?BR9#} z?vRbzFs}G)K%=<#+90JmhbI_yQU_hdl9a0k?4Tu^j$qdeQW{FQLZZt{CBtSWt0{H6 zgFKWdc&7L!wl)EUwo01+g;bnC@{n-nOCAgt-U!=A{BgD#)BcHCYEhb`hKz4P&uDi8r1!(+iw+p<7AKmY(dU>y`9kZRn@+XmZY_=Wu0h=H3jgiR2h**$|D^Ae%h=QX2aaN&~QY_9A98LZ-RTcsfI!LBM6a3$@2y`n{WH41g|u~8ht!TZy<(}d$Zk` zgW2^q)3c|mz5q} zhq=&hQzec}{pwJ^hjwh@uPYs0GSvj?S*;;iI#WC8=~0?WHS8m|)_&50Ny{@$F&th&LG!B54;&m|p4!6LenXV^!JvjbEX^J!)N}skiW@1KSY*rx2lS2$BGJ9{C=J=dR(XZ=WQul;^dTf;y$Qk#G7dFBfnxax2Q4UxY zLB%kM6<%3&HlF1{25Lj6^;fv)&Ob{JeDs7`cSz_4RreF^9KKGUY!Dr^Ecv<_1qlEs zV*&tJZ*Kr+XLlAidmD2LxBt0tGkZJOpXfL^&9b7t#Pxw9uG&ZCXPF~Y$n@|<5m zyc_gR2GC*AOx;_5=_0hfC({Y>Ui zI$Q;onr123CSs;VH$B|;K?f9&V z%UBvh53yeO8yo#jlx<DyR%ZR2bgCD=BF8^5GyVu!?|yv+XjpUt zAcZM5XfEXYx%F&h%1gWK+f8H|*C>F`zs!$X$JjkQ!vycx#Po7&QHf!i+-2FUXMQ~; zScGkm( z{uo(C&;A~wmP=YAPmstVAas2>+hQ6V*xQB|sOPHcDm-)sWqolz+Gxt}e`TGfogqnMn~yX4vY8Qe4h!-nLGA`^D4$d9 zQartm{~$s1n12Tf>64fNamK$G)47T~p@_4W;y zX=8IdF;neg(c+uh9wY~P$zL8VYGwDg*$ox2#p=@~SrTW5ujhTab;pZT>p%2Orp~4s zxujMH(-ua3*_b=-7z`V3@6ggzqax6)r_s>Lc1ZZx@HD2uBtJ`m@KiNz$(*=7ru@Xq z5_3>YJ&M0R*59^>O?}hOGBU5P*11XV2a=MnwtzRlP?)#gArs~LDVy&gGkHSrXB*e? z^Y8<~HP7&MhMdTdb?&^@u<%R|3tE^^06-sFq8vH+ohYWtbFf1f#uAY5wOjN3FIXV*BAz}AAe^On!*JNe&M z?w^wG?S)y`6Km{s6W?=FW~wlnyqc=c_0-H4*UV?W%bl*vaguYitk1B& z!fOQp24!O72DUe3_g7|WF9TsPy0uVEmkGfy9O~_~3aXLx?h_kM{Wan)%jjcB>p@0< zz!EcjM-@S|VfrqCn`LML)JL$fSTsmLdWbuHil`q?mo>eY`l)G{zC(=Ln7@e>f=n<5 z#c*0{xBjuqp zRl;Q!@3Zn=H!qdjt$-dtB`V3%A}85+;YWIyHya29neWa+nfzJoSEUczYqF+lLW^ps zbP}!B)0z8UJIlBfotg7w){j@~gAG}n4>$r2QeFUmWM^9g$2aPMH7XZTpWE@tua?&=e zw}Y#9SR+^Po?ebiTrYPozga{&UflxI=93G*Qv}F(fO922Urjc582r3NI#VLeruHcb zTScFe63Zct>RszVe1-l)n&H3G=deY-?Yaikw_R7KFF6MZid*-9Xe;5JAOm)ZTN*YZ z9XI{_f(6|G8jMYoAsTVRsGlpFm^!zs+R9U$L1|&%z3-!JXQc2S9U?qUGS)t}zYM^c zeG*B-fnxFO^%ow`!B5Xx zm>-edU$q-IxjJ5+N1Li&+X5^VVGTM_cRz1r#$pyebd4Uz@@-sL0dGREaU`f-hy%>6 zf11E8TfaKu`oaEr7`QX>MFhn+CU8~vD%%eM8gL^zNB88L{4~wcx|oqEwwAiB{F~@E9FIC(m&PkLhPyjJvJ0C1*5Ti@`pMKwG`C+Yi^T!RnZz=Y<1z zQ(t}w94WMUwoW$?81gpigSV&7S~ix>IQ30%k!dPb&Fbf?=aM;w&UBWAMV=ono^Boy z8Pd60Ek9-k$@tx{(@@hpI=eKnXCP;CiYW);(Qnz8@)*7IgPX?vrR#C=_vgctWWJy0 zdz%ZBzjINg5CjQIJw{x67Y4t0y2{oSWXrso03DVsWnf0({XPK%7o;^cOdICEtOhh< zrSvP7frQ^sn6VH-$B!@3jaqXWJ<#@79T!A2vUI&uu7gIb%zdb^(~z$ih#BoaXZ+AC zw$*~pEYh|h)v066L( zRd*plwez-m4nurQcBkB1jNVnfdg0o7gS3il-Bsa&-wq#F2B8w6#cvj&M@?WIDCpTu z=o_kIc~eeP9>3#&!pQ@`t3L+C48|_euuj_0YYSD zX~)eoZQ?f3ad}aJsojl!7tsCFZxkcpS2sYl;(QeS{&uZBS_nC4lpPWufXYdQd5sGB z$jd?)0b&Psk6r$ zviaMn8b&CLVO5EYN-nxepmMdf3qQ^5Fxh|*q~ItOfsv@K>@z~2epw@wm6e^S6y zTA|cKZPG7d)j*$sgJ{YS>Ila|3lkE3%Ozy^Nr^1pnPgJ75H=wJ!?s{y$~`l#YM4q4 z?5$7zZYL&Z8DAkV%nc^Zf957JE9IJjkG1d=?@x*}^0eQrhwGDP*F9BO@mH;PNB6@u z3!!R$C8V9b4o?w@K)+oKDl=hyPu4J4p%?j+GX4u|EMv;Hecyg<08Bb$Bj$vTp)+qY zb0%0-n}S|TO{W3KaN~ug<+&xc4#}*Ws-{d(F5+Zj0gGbc2HOV@oT(bsHPhtw0%vRE z;kK4dVpTAugev_O%U=d|i-&OPY~V9me+N%$a|1WQFgjv*SQ45+c8T9U<&8AJ(v8_B zSGX=He@kQnQG<cWWqb178Yq}m^1%9d(tv*GHlJL zj}Z-8Zh)M=>&2J2=)0->aZMrKXrQ!VyF;Q*;Z~*hHh6Iu0hIYtWrv>0w9PsFX{%Et z4uUCGw@^3jTJi;4431&zqZ~D=bI%mExC#p2Q!fIA0@0Ijj>fOz(WybyfZ*W$ ziM`3FNKZp6-L=*{qKL5Dm3cB7hNqBd>+p8?aTcYAjJlVnOTa?TCXp!5JW^NCFGaFV zo9*cifg)Ec^r#3M^HWV=q4Q6n-P=!PfC>1h&_6HsLyFvSvs-}1&o`vO>jTRLeXuTP%ca==1Xnn6y3AW5i* zFeR!fxcrFdff_B=M>{8nXFE+GnfEG%@xe@twqBVNhjBI}Mm!E?4bU2(k&?s+mu)TG z9l>N719RGy)h)G$LWm6%A--MUKS{eTj(D)2_zD0qva);z#uOuuVl5=TU-~R*T<@7u zV9~0wTc#3zBUyVuZs&OE!t6e#s2hQ$QH8 zy&W;PV;euSFbI(YK*xo9|NglH)KI`ztAJ*Dtt-#K*5lW7)LIA==~BiLqhA0k-zGGbRaz25zkP#zN=M-ELO zb}dyjGUg$3dqp(HAw`J|!ny&c0u1PeP`wL_bxhPmH^vu6U44>;P>s&S@a*wKrc zN!VyE>ZSQ`LS^ekX9{s;LT%T)-lF`2{x0Dtw*o5;f2~RlA;cC+OC8ipA(t zL)FHE$gOSMOua;Vx_#_KYJ(pgnEzvA64CJ@x0>haBJ2L@^JIgO0Q>CyS)5fLZu7&1wRCV*{K=O|PKQQj|XvDy!()MqHw)vWL3}{dtSPa}YQqu<(iO z_iv6gsa^AX7WSJ5iA86EUeK-aTusbstMn#5Z>-oV!A8$o4>9g(=P%bG{D5-um}OdD zL-P|CGIw*(Cq@7P#z(tp3TbdNYZ#4r-6TLiVgK7HXAFF_?qh@-;Ww?ptTQTfPK+t> zbCHlCg1SldMEB*0dQHgzRb19n6EtP79da48bA#rKbl4JIJNBc^nDy49N<9OR7- zK1g81bNq$F+s19YA647+72D3<0^c4iLGAu-H-f-pC4ISvIQ3T zGAm&&iuO@*cCc_Qxu={}rxx-feQ2r?Do%(a=}?|w(XSV9w+Io+VVwE_1_xAbwZEhHtL=~?LK^EI8s$|*GgW_2JIIT8lra)j~2s5%O1 zZjCdOnL%~q9DAK8UOJPV6-hDzODzo#COR&Pj#t`p#fB8_ZQIV25?LfsaKyT09tf^3 zhp@+Zt3!6PQI=BB{99(6+&jJ8UMPvtxP6>NSy>c(s$b?;oEbmLwN1gkm+$LW9RXN^ zjPGl7dvY;edqxqW;n`)pTBt+*`+LGBM~EeiYf+(iO62 z3D^-uq0*_6HuRr8fSC{hgu(py>9eA0*;SF0IfkSJ{*)nEd54YSxC`hn21EV~<_err zcGBqS2r1&w?aZ8tTq-Z{gF!Wj!D!u79aN5mTu65;u^hZ81M8bl-kkBu?I3j;;ph~l zt#RxL!H6&=ckU>w=WL#cwz~6X8+UjO57ilq+I6{>6`Q91)3aM+E!U729@=VebWBkl zisN`&10FPOPPz}5IF$YgrEP% z$(kmiL@a$<=e;n0oUCD!Lm-IqqJ0Z-ko%otApX^=foubgeI1!hmAZ`%;So-EE3crqHgWoW=LrM{La~y+;dxlrXXFxV%(Us zU5uHb2L8Y9L;lH|*JOY`EgpQkLI96sqv*0X%1R7=H+ z55`^IR^+ttt!Oc))M7N(2=P6xL`dB-w017Zd)xa4aBGaMS z>`^Plwv~yThSce$D}X{_Ez;q^H#uvC=G3dSOft6aQxf3Ho~D0|z?3N{!So3(B!|F* z&t(E$!S|`E5D-zCfclBB?@oc=EU}7fIQkrRx#+AdL6v6brEx9kXS5S?!#2RY5Pk`3 zD263aq6UUnE^R&ZTt#L0Y;x?5Fe0}4>tKlDlQHkq@oQ?0ic^H+p<%ZbeC1DW{!tn6 zCR2Q=ZIdQW`z%#`ghV%7^(6auLck4KKbWU)6@5Ps+Z#2NKyOj&Me$@BSduIRqsqAa zUcq+jXsO12_KKme6YL>^O?B|l>U$i7p=ni_RmEfWZ0c&tTGvS9H}}7&hG1qZ&VR4l^JcVu7WwX3Jk980)hovRvzs^Tz893~7=)8hR%XjyeYP01 zR`rYQ;|OPnIEcrk#AWM~7zy$#sdtHfgjDYbt?Z={j)M=%8#yvLbfpLI^=kQ=hv!__ zHNjXvdBEp{pP5HxP4RT6%193_4M4nG{h3LVd(h0O0KX)Bj&SA=owMe|eJ;Gty`-(4 zU+>L#ke{|z6n#`vEWjT*k%}K5lKI_WrnB@LH#=V*4_JOzZ!)XY?O!;uBwv(O=j~$_ z?I1Zi@>oXnKocI>SPFAF?}a5!7okw&_Zn^w{!c2;wk&tu{A~x*)dgCwLx2PqFDJI0 zZ`uwzLUl05Y({6f_u%lj^!~5x*A`7IEw@c+3elD10pJ9s`v&P17 zuNylzx|k6Tb?x0l$o;MM)#I@5S>Pzduyy@#1&JkXsnVIZzO{;{R$yEU6{@#}%QmL* znL%S8TGGpYRaofd<>uz@0E$7Vd~)gN3cC8JrRZEc$!Pb=vrr0?xnxS&tX-ogS%YZa z_`yD(bqvev0_Sh|>iwfpb=6zQB=&F6Sk@jN)QtGZ{v{9M9$J}air)0}2|uL7PM=^0PG7a-LvgGBoqP8J$~lXU*Bp#<&~8z}b=%dUl^5dP1Uot#&CQ{v#fb4w ziG2{WTzX*6q5od?YqPrXAxF;vS!oXoYFT6XOVXvvZDC9){~;Ut+)siqgf{3&MG%h8 zf*&^4sJjD(BYI4%#j4U|at1BsQbEv!P%Vj7H&CJ_$Z3dB{+SiBsLC1odPj}1k1R~E z_ZM@KGkMPrQj!`Xqp@MvBFVPay+d6{K&T&UH;tOnG0E!MiM}=heQ4Y3B zWMo=jCeZgZ5m#wv=v6^d+8Y(_3KWO_+=7zPd+ZG>RY~oRlm3W6xKiaMMTwFHy!I3ex8FOzCYgj^1A?*EpNN$cOBzj|tMW{H zFOZ@Wf6y~65%yH(jFoE`Y#ekE#%Q>RPfjIwi~5s-dodL3>f{De{IZ2a#lEVNFZxs< zQEwYmB7zetdz6#jJ9o02fN(FN6lLd)-F*<*`E!m)Ijicc@+0_*qo>U8ho#J=hk|n@ znJJSBxu(~6t@s6CGkt!@WImE$hoD1fnC6QQKWVh99b3UVvd0!MW=0)ii}Bs8c*%B+A_rF1`FtuT$l-M>WL=G()9>ldh%#qc;1g|)a6z#wX3Ae zf?JR`{Znl-vYY7X6rjP|7`>G3Bbcm><9&P1nRVv&#nWuQEF~EZH~X0QC#LCZOr_si zEB>#y=Omk*&x;UN7FN~7SKH{Da6_e3%)&KKHZ*%khCu-I9O^4A{v<;fDg%EDs6`qM7=p)-P}b0n=4F^ju3p4 zK6P^JbuwOatS2_vGuxWY(b|3rP2F+)v5_N7$;oPB3IF`KDKY!NIIE(`ZseBpI2Gi7 z|FE~oQ=(hQ_)8zQIf;c%gO2?rFe#pZD|EXOac)*}oqk&gu}?}I1$8)Y^El?OLj|Vi zVz0CDH2nm+fa!0je$%^LydCXm|2HS@X=CA~;_Bq=_9q{nq%QBY%8J;5vIIi)c5T6C8djl*F~q~Q+zm<(N#PVL_Uoz7&;eQi7xgl9#Z0EiFM3vo(z09 zj+olwuyy|M+W@)`wv10xIvNJKr+4=1ARZK2>7~~8>H-C(Dpdewro#{!4mc>YlYx2XWRaF~wR*X8CB-m~DTFAO_<}(;niI1ZfPE+z*Mm0((8S=|h4)M3&K56$>Kb zFLyN*+Bh7m(rrup5K~}ZctFT@fuPMlY6gFq9Z<~`6iTJOUaac(Y?b0tlq6}}#!K5Bd9&@6&kilI zo!RnhoOImqCM~t&1r%?$+P+z4$Ao_k2^g-_uoO^stjp-O5cfruhIc?3 z1Jhg#V}TyS3MaI-f*_O=dhR}|g{h3w{`v?fupOSpokmx&3W^Mqz|{tcwGyG zyXm!d7u#kF_)H2r|Cu#1rq1uqNX_QDMFf;8q`Aau^#w`X$`;nwb=dl#_S12}!K`wO zbhKX;eWfd!3QC9C_qq-2z>16sIa`vhLai@d)+7?^h6g&j5(aNwY_+$g zfZD#6E($oSo4cjOuQp6&Xsv=LV;p30iSDbC4|dZzCA~$b(gnPK#-b&T`wu7AIXUb)Fy@*f&pA) zsKbo~1Li_)JkRv1MvAq{V*TSU^;JtI0!X8q%ui?E5A-HH9l-Y8I;EPSvMFDoFc-zk zTEr*IKk`dOdMLmxSVLp21WR%F_-S5MZLM_893sbnjWMbV?7Lgx9WsyMW0*EYNA36V zMuP1e^g#Ph^K;E+Pr>s6G9QJznRuzw%y{1wt>;F>B?h4+t{2Ar$-& z19AE2?`(S0vehJ;lqM-qSTsL6vPDS9qehZc@I-m7JcA^aDMmKjC(Vk;QpHZkNDt{2Yd)e_`lV)2Yj%^ zBR+%!!dsOb{Qd#;Z|ex+pVl{7qu2u;@xQW00D$D}m;76Xk>2tZq3|Ss`Vs!4*L(Z& zO(gZdiXtlb@savXRfP|6f)0jxrUCy%Lq+@-;DiGfdJ=;1Y5&RdCX7JBgSdedH{S$J1(@Oup&n##557PV(^53A?Z#w^j%!hir`@j6{zsNC< ze~_0@Ko*DxpSLS8Z->f-@%!B8 zpEGCYJ#%)>%v^KTTpH{_0xY4bA`C1p03Lt{005`}ZFk5zTR;FH7`>K|1_WMK@8!UZ zILSOC6KL_U?yg54K*7+-vYbJPIs$#=9-&3~yxhhb^}dw4iR_cvi-NYNX2n~`QC3GW z6JhAZ)NYhVFE5JN(2FEr%-Z6i@;OcvA>>R%3vMZUP*&z;Q*&z~4vI*Q^$`uPR73H{ zJ8#6&)7;t|n_?$Jrgy21-Id@dlL5dl$F_jsxA|DyO&p^=zN;v^u}Qrf=b4>Z$?dv? zH5>l7rbT$1X_iRmXvW0bVmIN0W|hgcVr2=59(7`K>1yG)X`e6Qw8$fzw+Y`aSLSvaElDWnYAF#&T zty@dh!r&&@5FenWM2Uv!b9W|1mWJiT8eAgU@ufso<%90J&FizygNeNF_Pk-zlzHEk zPOnLoc<}QZtzC4v6Q_|@tbLfcc@gWV*P0n5iFAQfd{`u^Q!rxb(G#8oQy0t@Tp*RP z17m&yNixE#=YVyCgXAGEBF{7@0QsLM{ySX#CK?+MJfmnA#(@dg@XLJ?cdb^t{)#|y zt&Z%+km7je>5=@R!Iey>yW zv-?UbcydPbVk>lBte#d+-AzCAn-x#K(n!=|O4U=-eAUHzqn1`Qh+OMzF}Q(1f3hQ} znjoLMHE(6cl!!>97J}_d#EBB!t(|g;LMTzt$9zHMC`sJ_{8-Wd;hd`O9pZ#Sfru0_ z>DiaBA!+uXseD$_B{wWsL+fAU&{%95WDJN>as3dH+8>nrWMFqw75-|u+3_mw`igcB z@@?s8Lz3eJh7t)`0P@ZGp>8=+X7fm#rH^&SE&K(R8bRzjSN_Z({;F zQS)ye&FQrLNipXkb>MVK88*t1i}M4hG=vvK>nx3zD!|ZZp}^&;Stf6!^Fy|z#J`MW zgK&91FAq6#YXd*zQT!&R<`Zu3&^N5Jt8p6mMBuXyO7vtsTuQ;DeWEXy zQEi5wo){?^^Vl8dV`4sdS?evK>Qz!?H z53itv?BQNqD9Yr~zWyR+M9dxg1rC5Q-Xv_y{I2ORwRE(K*fJ6Ba4)))?BlGq5g`i! z_dv^-%tG-x$tIYi>;cLDyp?AaO+$CI9o~(JX^yC8oIxw#VME6QdP$F)0mzkP(8gMLCE1yg@ch z_^LB7=MS)G+~nfBl+SfB2tiw>yjfp=L@H|31|^ffD+;|%JbUMuR`doqh7gp;L5^Xw z{S2;{qfk_T@a`ac#L|6)gVd7X)&~%rAlKhwhr-s|Qx9Q7PWfKeGYJsFKuKVL$PgC( z$9f52YXszX+mVyFumC_G7R2l=HDpc-SG6|YA&$NVhVOXHF`4lYhM@`Z_;V^V5@>nv zOQ-f7-yN-RGOGk4>`?3NCpJcXEH-3qG?weow^ZsDEbL{G!Iu`P zOGyv`X+F~ZcBw~)Wg z840FdHsVD^vRJ-JwC^C2T8`}{b9M10**uq{wwD?yUdtP6EB(wXX_RSE=Aa%)E2rB~ zv)(h`@zG7Z1aYJWa@~!Z_*)c+^XoZmi!swH?sQg14Ac67OQ8D@3qWvz4Y{+5B-S{6 zo5;;Fya2WZ!YUIBZj%}2O`jwiAkyPV?|aeSJi^o|&TA^rObMbAio-Desy)(3U=cVO z>OY(NBO)O`tFiONhJP_h&!=$Bg_~f4pM6!HIc8HJZb#m9(x2)28C50a)9^$5Y0N&e zj~yj?xDBho?tCx6!O0smK+P(Qt`&hDAn4NM&8n3j2cR1sJX8Y@SU4q|0|1rkTFSCoFI%y!1omX5k)qKyer%E^2^#@9vq2X;o8fKk|L++t{6wOx9sS!v_lUNR4lK9%E0{twlV+7c->rR^$YJ z9!hsA6kxovsols`u5DE#l!x7pnx5!Wde{(ESXAgoY0EUlvYiE#SgCY5q6?M+}i=!|Oevh)RakQF6iD zGv&`8Y`4AI_Qc5EJ6%o~o)3zHSvVu{UONR8w1^DJLbVF4aMPx!>31#l)=u`YjU@;^ z;RE(Vr%3s;Cz32lI=4AILc3$_CzPrjM9Jb0hh3_VY7Xe-Un)G*zFLc@R!!BZX>-fJ z8sfU{0^giHWTW&~u(6Ec0fow9VaZ}rz=M}Z3SLgm9-uSsuaZ25sX=VWx3FrYKh7{EoaU>F_ z?EJ7Q)ym9SpEvB0%*dzUwivvK=J(Z;xlc{vZ)NPMnp@lgvKL*0+1R}?BKBlUIUm>s zhM_4Z3)%b+og|)yR=!`)EO&M?Cq3B&0w*$*i+8x34im1=D;ds7cOR>5y8^f$?x0R4 zPd>o0bokalib0wVJ`zTS%V6j{iCgPt7ibrWj4eXpUJx)cIn{?<_w{j^;}-q5ph4~x zCdpKS0RYaB{|x3#DGvlLI`D?pjQFg@+Ed}hyRIEK92~#qn@_lIJ|mRQT*{V;t`j$b z>UJZ0B#!UO?N}6OaJ|gedp&vd61=ck7?4f{y(1;f1NgG@3`UHztgBgVH7>_jt)BMQ zuBM|#dd^=CdKNDt_c`;~#1rRN`WIGx6!#n|ySmw%kK8F0b; zTtZ{p-Y>dsDt25;>?%eLmL!&iivF1ob|ix~{^$8H{W~84Y9TOS9U*6$ z4~&M*CxZZF+ZKCKd_qrrkNAKMM$t#QUwYy+`G~g`ig5lt&ObBp(Fnb{WTBUG)>zy~@g9(7A?o+>%|l z+No|N4|+9sf?E2UK%+!&^T}(TM5J%Xu)EbLseR~6Qktf{uWe0mTv(P_-O7%5vEAFP z9B3?-E`L>gs}b0P3itq1C*r+gZ+=BsCSSBwTS}-QRWviwGd^bCH=p=yz-ab%8S!Vg z!RX-iY-@~t4!(X!0)O}MoUEzbwCN+u{Ky@W>alc6=EX;v6}Qs4T21O5oDL20%o*y4 zU4J~kIicwEf7}5WXT;!XjrP4=q(YQ_OtpZYdBWtPn>oh>`B}Qdsce_Vzg5K?#S@a& zIqmPoOh*^XrTC2XL5Bt}UL!#auApee6&9O;XX`x;CeKWiiDE*~*tpb3LvivjlOj&} zfeG6j|K6~0eSWJ$0i)qpSh#hsWEf_aQkOkphP(t-Q$1@flDeYZSuuDGs@8JC{cV)W znHw`3D9dB2l_5|1^k7>7dpUoIQ=8244ZO+jn3g}Qf^t`{H_WHH5p|q!c;l)PY1JHT z)c}=hYZn2!=@zO%VHlxNIua8xOSvPIeuJ_qSSu@eQG_uV!&*V;t`23IcsWo%XeX97 zj6CYNz`~3|&~gC{c|tPt)zo`R_ApLiLF4vNUfRO9Jk{`-IQScW18(+a#~FDb9CITT z^SzuDR^=QsNSgtX(%xZ_X08_ESfl}UR{ed2l|a>MPwW8FCt*yjd{Tz`i^x>bDD12G zkTNq41*-a?0{!Sc+Qc-Dc$U;n`~DxbL62!LO}OK_#?E}7sxlx{hBQo?>bi|U=AT}8 z+MXNYi!f|@I_fGU<)Thz76=#?ZV3Izz^STHy|-Fdo)AYvBEpQa3A_r{)Nti%@w`P~ zk3<-^?iw-6?k7mLAun(lGG{1Gge9pJVwYU!m;1R9f$@v&`wG`NmAYhBFdc-$2p1>` zkv5V8HbWAO2$7vUZh2kc0DxqY-;xCygb0L#%o|~VIV2=PUfL6J_?#_Obq97+qRHs3 zQll-1e2*X0C6ZR~5~>+|7lank6jPVBVD+UpCISMu~I?S7px1nMve3S-xIh^|Xz1;7oF_qSg`s4dEz+C1!nb`a+T6f5$64ko=@z+kl z2v@7EQ88=#BQ5mr&U*oefg=qJ@S^Ip1XCVuVjn7U8no zfR_?G@B~D{odiY%#a|4$;)x+|coT6b08YH$Tp5rrP1&3_p(4=xAiNI7kkjt2MVHdN z6N(T({7#8Ux?h0Uc!&mCf5;or#mQoCR1tzu&(ca$Ny0b=8A&jqC}8=h2z@v0$LEp> z_1YNU$U8?szX+nh@N*;c{BonwZN!-f1IA$CU`qnV6{C;h%_S==BuSYzc%~NEEUIoa zu%*p55ZdL*XUy90et~xrGjxt`N_S|J=9MILrFIoH!Dgan>LX5E@jOv4(J!^|{jH`;3B^2G1>)>6TJ0x|l77rS;XTkkq-`kt$jn z-M_9*=Ujno$&>UK3H)!B7LaIPPNs~(n;(#E~Kgxb(%8%_GK^gBsb z;>Qw~suvZOBMf6Im8?+&zg&c~p3Dm&!So*;F^pbo%3kNY&#&$gd|W|g*+V!wvEqmw z962|!-~Acs*;j?O3=UNrvtL=8z5S7+MbePD$@+!0LOlPW*%(z%*jei~m+)pHR#S+r zAmPv{5zh3jM02We@wokmUaqD^%OJ*K*F{I-7vVQ00k}haZ&_6Yj}*UIy~qt+1H;$8 zlF2Uqwp)i~Qyvuu^ZY^ks6QU}hrhr&F={?dMJU0D_z~~&nek1o9{YNrT|djHvn@Qy z+U%FFM-F2)HEsIkHt3Ok%Snr`ZV$d^;2)yrZV~;|Sc!l{*^<>KT3HV!iLmC5}I4?YVEpQ>GZ<)Xb zj>{aF_w18`n1{`IkrXbl%fSwM5AtvFmC?K9Qs}~{wURxL@Znr5ulV@op(!Z^Hpm4f zf%Y*gWXE67N6v0?&8<}mnP9U`pZfcv&ZP?A)|F4kJPzYH)YDH>IC}PADN1{Hrms&1 zwMuk-#S`8XW0^YRB_oYDR!Pij$t!~u^OU8e`710WQlTyb;H=7Hq&MxIzj7iWvJz2d+r*nt@#hLe1D!|hKu1s1g4d494IDK(&jy*Pr?>^{0j?hHoc*VYZzR;LMq5C@C+qczJ+BLF4 zjGL9al~zF`x&mbb7x0a6=Ii7~_~dhUCEA;xpv6-fszX%iX1v~-)YB7*D}5@OJVt%4 z=F0 zn%sXp3{2{MjL*|!KZQp*ITUmx&ID;0rEs$~3CimT#;?znEdh#%Xy!%3{kpHSePId- zQykl-o!W3v%nZ-kMLXDTAvSuib}tg-7IKf&{EP+o5LTI9TH`I&P#5mn>a2B67Osc0 zosI*3mQO9XeXR}H>06-CLJdj6`So>bZ>T-@fu>+mw4A${P%hXdSD{qhUH=5=*d zz^A9P&CN$Egsesa#fPo#Ng>56HM8?%&GL?pn+;ZYQPx9ExN0Z2Cy{W%xGSFQN6;T5 z#o03zQ2M;id*S~WDe0EGK(P5PoE2ZAIEO?JPjXGAE{abl0Eq&lJ5C+bY(lPiJ7RPW z+SN9Fup%EmxjRz*7&dfTJXy!HJ55Y`Jn1vzvrK$zLjErAf1Pe4a@ol?cQClvu&MhIyu1OXAE0H~MxBlZUvSsw!gt}_wQcban7XfL zji~(5^cZFJ@;k(@Bo&xbc%I$u{uXOS7akOu$AYyW&qQ;xLFS`Efo%PcT`Yz((<=O(LJ zP1BFO9^}Uu*=pQhUfqr*1h;`(JfAF;PuR!Lb`Cs@-hCv?1xqur>+?KIc2v0?SA`rHtb zk3d1|SDs@19f2V@*P{kjI~;_C~s4uzpam!4p9==<<)2-V!uw`OFTsh+0P>qggy8s zsLM2~e#FS>3$Wu0P1yhr;vUG*e0it|ld+rmh>aS*U|0OYO`jn9-Bwgeqr^$ArWBJ8+M0HvARzgl+1kbsgs>>QFVc-%JY&C?bN@P-T zhu|HG%9lk8s^cz!Z0F|sZa&MLVZz6jK56v62;@+>ynUi$nIXwD@Krs1FJadjKYpGz znfXCR&3l0r&46HLE*Y0}5SPwCVj}33x(T4G%+^KZHplKgxc)}}G+uRfu}2o#JAVM# z^|qj>|9An|%$d0oS@=>uaaeYwv~pkaDolVmSC4_AwE zLM;y|`1bymYj|2u-^73$=6=1PEa zy!qOIOLFrO?UMUdw*Ds<$r0;mdYpJOr&Ea2ALqjxHB}+==N-&|0x`9rsrPhf0fAc+ zywI)kcZ8c+!}3&>4KkZbm4`n}4T0elyk`6kAacfdyUW;a3q3QtthYa7s54WO63onH z>ObUUSOh+WK(83XNREvG9Z+IcNqWb1HTs`RM>6w{-aY+};eENQ9v#uEHccUvgb|f$ zZyOv;a$mAjw!Uq<+B>^0E*=u4-(}JZ=-2jQ_tt9W3FyeXCx$jNSLQm4h6fTK6k=(dKTj7M8IerT5`ZEO71HFx^Xa7muUoSF|3o#ggarZJU7jy$KLvbExZiEZAchCy7 zyLLOL+e>Bb#<7f~3Ix8@m)$>}E?(~R>r#1Bw=GR%u_u;)(n`E(^DbPimE1ZTUt@Jv zj!~Dmj!*GMOz>3QPxStwxow4c`!txWu97k+DV3bWnuN-=dYUm5O-1*HZPW+(sj`mR z3tQRq;%nQ2Ht5Nc1VtN-c^t$_IR4E0D%-RLd z%I=NC;p%ZAxAiWx*$0>9ia5z~0Nqj50Wk~Vf{&I_abu7LxxZ=Sn<6c*d9SNbWCP=c zbPvCNahqtYf7S$Y%TJeA%yPKYnrOKm`=Vf5t{W~mu1~u{2A$e+UzV$5%)N(%q7WA` zxaC%WqGnhZzl`7Iv=V(hhR-NQY3lj$7~&!80HEh{MCPAhP7L%MQaxs&#&VNLG}bCY zO%*Zk*-0)S`W1bwa}1X#2p1tjJl390qEx48ni7rVdXYgUZ`5{iZ0h2n2%A8bu zTgiu?;u0%u$Fj4ULMk+ zvgD$&{2myHI_+qqlsD%c5;M6ukBklCR0#CL=kHfoKQ*cZwxuxS)J}Bk5gazBz+%)| zszu*(@LJ#}_^CYq8#{wb6oKe?Jz(QC9H3iq-(xnhLbCDF6!&5xLq7#@y}$z-bg*wI6*vb@#NCrczlx zlHdHic(rt20b7mpb12<;AvX>0SFg>lbkYt7DSxsge#N&joMVU{MHZ=E8H=8=8zv(_#@E*?3sbDSAX)MxIw`wF@Z&Uu@3zW*tNaA4kHq|s=? z-F1I4I?w*pDL%$zR%#;9UFa{HtB`btQQ*H)LWRn1k(2deUmK~~jFJu8H%JcsLEq}< zpQ!MkXq$lF8$YlS?5Fot&tdknX917(|3!s8Z7jT0U7eiWeusopG;7r`4Kn1$E>2--Nn^bj2u9KIRACNa_iJasxQJes!%y!n+O^ zO*VpSVd|K6W~b52J%tCDM+W#(Dsi*T8EFOgwd0tCWPmPNi6gG!c3t$|eXi(X+Xf=T zJ^P!jG5Lu`O}xrY>W}~!FKLm!JBo;s7)#6{8K6VrzHva5WaI}^c1KL8!wLeXoj*){ zp3pIsxO#0_1oe45ZIb^R%xRRnA`FH7J(1TktOP|nuav-5gFqWCjp>n7`PD_%km^A@ zoG_Ev078q8XI974L|czeE1P-uMoO<}8v zF(j^D=jfw98xs4l^!az zaM*Kk_3>8ujf8}Fxucse*TqYUiMRC!#hEYsoQ?IkR0jLhB)Y$8Gu3C(do3PqXh`^D z${;(SeSuh=j_`o*A_~W~H$p%v9N}jd1*o~{YWo-1p1l~H_8jWE&0rj$QgdMwgC_bb z-ls+woZZN;yD_~Q02*=hvbVjomLHG$o?g7?t-iasUUUmbb4y*MbT9JkrpJ*Hj1|_U z7tRi}b$O&8n9|qL zW7_56+FE^1RLkEa`as_|!zal(F2DAqQts!ggu7MW9?ocjP-=MYY@@aNC>4N=!xzBP@V=(HD|Iut!4;jGG zoL98$QNv5o2@gkn7`!{h{A!+?s?Ue&O|dP($w5Jc112^FvnEP|ai5&d z^6Rs`t?fPMFcUXqRs)lO!kFO^AKq@ z42N5#Cd#FSB$H}2U6&Y;=!m;>_*>c;bfH{@xl?5@4saialL|)kbn@aGjS_0jNUTfc zKS~O?RJwx|D+Tw7y;1@uiBGlCm0eI$U2Pe!x<8Rb?*@Idm-5VRMFNa-a8HIbz+1^j zfZOri?H*YWKeJmgA5OMpp}TZRoFg<%b9g*~78XIjk&yxqOor^PC#%!9!CdpTrI_XQ-jhf}N;tU>GO-^McX z5H4p+9D*l2O!CZ_@73i!$yl}*JMiIa!Pws4^hd8l`Iy)tZFjREtgcMH%z;Js_eDOx z)&iO!zU%vsdBU{!&<_^D-t`JrR{be+CWsElt9)<5Y;Eb_pm@>(tpWN0070al>T>FA-|ska=s@A z5`(^@{^8Yu+#m_`-$h8%6EW$3(|-On6~}7h2vw4-f$%RS4y!wkhXMAvjxcUA*gu*^Q2=POy;DL0|HCP}QNJ`*u z%`3R)n%9DVd(#&~hGxJ4nLwIC5D{@UXgU1fu8BeI!oS<|HaUd zpI`b<$NDdJ=lmBlL