diff --git a/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/PolyLineTo.java b/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/PolyLineTo.java index 70d652bda9..495a7d3f39 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/PolyLineTo.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/PolyLineTo.java @@ -27,6 +27,9 @@ import com.microsoft.schemas.office.visio.x2012.main.RowType; public class PolyLineTo implements GeometryRow { + private static final String POLYLINE_FORMULA_PREFIX = "POLYLINE("; + private static final String POLYLINE_FORMULA_SUFFIX = ")"; + PolyLineTo _master; // The x-coordinate of the ending vertex of a polyline. @@ -96,6 +99,39 @@ public class PolyLineTo implements GeometryRow { public void addToPath(java.awt.geom.Path2D.Double path, XDGFShape parent) { if (getDel()) return; - throw new POIXMLException("Polyline support not implemented"); + + // A polyline formula: POLYLINE(xType, yType, x1, y1, x2, y2, ...) + String formula = getA().trim(); + if (!formula.startsWith(POLYLINE_FORMULA_PREFIX) || !formula.endsWith(POLYLINE_FORMULA_SUFFIX)) { + throw new POIXMLException("Invalid POLYLINE formula: " + formula); + } + + String[] components = formula + .substring(POLYLINE_FORMULA_PREFIX.length(), formula.length() - POLYLINE_FORMULA_SUFFIX.length()) + .split(","); + + if (components.length < 2) { + throw new POIXMLException("Invalid POLYLINE formula (not enough arguments): " + formula); + } + + if (components.length % 2 != 0) { + throw new POIXMLException("Invalid POLYLINE formula -- need 2 + n*2 arguments, got " + components.length); + } + + if (components.length > 2) { + // If xType is zero, the X coordinates are interpreted as relative coordinates + double xScale = Integer.parseInt(components[0].trim()) == 0 ? parent.getWidth() : 1.0; + // If yType is zero, the Y coordinates are interpreted as relative coordinates + double yScale = Integer.parseInt(components[1].trim()) == 0 ? parent.getHeight() : 1.0; + + for (int i = 2; i < components.length - 1; i += 2) { + double x = Double.parseDouble(components[i].trim()); + double y = Double.parseDouble(components[i + 1].trim()); + + path.lineTo(x * xScale, y * yScale); + } + } + + path.lineTo(getX(), getY()); } } diff --git a/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/GeometryTestUtils.java b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/GeometryTestUtils.java index 71444f8088..236afe20a8 100644 --- a/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/GeometryTestUtils.java +++ b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/GeometryTestUtils.java @@ -19,7 +19,9 @@ package org.apache.poi.xdgf.usermodel.section.geometry; import com.microsoft.schemas.office.visio.x2012.main.CellType; import com.microsoft.schemas.office.visio.x2012.main.RowType; +import com.microsoft.schemas.office.visio.x2012.main.ShapeSheetType; import org.apache.poi.util.LocaleUtil; +import org.apache.poi.xdgf.usermodel.XDGFShape; import org.junit.jupiter.api.Assertions; import java.awt.geom.Path2D; @@ -99,4 +101,19 @@ public final class GeometryTestUtils { } } + /** + * Mocks a shape for testing geometries with relative coordinates. + */ + public static XDGFShape mockShape(double width, double height) { + ShapeSheetType shapeSheet = ShapeSheetType.Factory.newInstance(); + CellType[] cells = { + createCell("Width", Double.toString(width)), + createCell("Height", Double.toString(height)) + }; + shapeSheet.setCellArray(cells); + + // Parent page and document is not used during parsing. It's safe to leave them as nulls for mocking + return new XDGFShape(shapeSheet, null, null); + } + } diff --git a/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestPolylineTo.java b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestPolylineTo.java new file mode 100644 index 0000000000..57eb00e9cf --- /dev/null +++ b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestPolylineTo.java @@ -0,0 +1,109 @@ +/* ==================================================================== + 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.xdgf.usermodel.section.geometry; + +import com.microsoft.schemas.office.visio.x2012.main.RowType; +import org.apache.poi.ooxml.POIXMLException; +import org.apache.poi.xdgf.usermodel.XDGFShape; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.awt.geom.Path2D; +import java.util.HashMap; + +public class TestPolylineTo { + + private static final double X0 = 0.0; + private static final double Y0 = 0.0; + private static final double X = 100.0; + private static final double Y = 100.0; + + @ParameterizedTest + @ValueSource(strings = { + "POLYLINE(1, 1, 0.0, 50.0, 100.0, 50.0)", + "POLYLINE(1, 0, 0.0, 0.5, 100.0, 0.5)", + "POLYLINE(0, 1, 0.0, 50.0, 1.0, 50.0)", + "POLYLINE(0, 0, 0.0, 0.5, 1.0, 0.5)" + }) + public void shouldAddMultipleLinesToPath(String formula) { + PolyLineTo polyLine = createPolyLine(formula); + + XDGFShape parent = GeometryTestUtils.mockShape(X - X0, Y - Y0); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + polyLine.addToPath(actualPath, parent); + + Path2D expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + expectedPath.lineTo(0.0, 50.0); + expectedPath.lineTo(100.0, 50.0); + expectedPath.lineTo(X, Y); + + GeometryTestUtils.assertPath(expectedPath, actualPath); + } + + @Test + public void shouldAddSingleLineToPath() { + PolyLineTo polyLine = createPolyLine("POLYLINE(1, 1)"); + + XDGFShape parent = GeometryTestUtils.mockShape(X - X0, Y - Y0); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + polyLine.addToPath(actualPath, parent); + + Path2D expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + expectedPath.lineTo(X, Y); + + GeometryTestUtils.assertPath(expectedPath, actualPath); + } + + @ParameterizedTest + @ValueSource(strings = { + "1, 1)", // Does not start with POLYLINE( + "POLYLINE(1, 1", // Does not end with ) + "POLYLINE()", // Empty arguments + "POLYLINE(1)", // Not enough arguments (less than two) + "POLYLINE(1, 1, 100.0)", // Odd number of arguments + }) + public void shouldThrowExceptionWhenPolyLineFormulaIsIncorrect(String formula) { + PolyLineTo polyLine = createPolyLine(formula); + + Path2D.Double path = new Path2D.Double(); + Assertions.assertThrows(POIXMLException.class, () -> polyLine.addToPath(path, null)); + } + + private static PolyLineTo createPolyLine(String formula) { + RowType row = GeometryTestUtils.createRow( + 0L, + new HashMap() {{ + put("X", X); + put("Y", Y); + put("A", formula); + }} + ); + return new PolyLineTo(row); + } + +}