diff --git a/poi-ooxml/src/test/java/org/apache/poi/xslf/TestXSLFBugs.java b/poi-ooxml/src/test/java/org/apache/poi/xslf/TestXSLFBugs.java index e41c91a2f1..ef4f487287 100644 --- a/poi-ooxml/src/test/java/org/apache/poi/xslf/TestXSLFBugs.java +++ b/poi-ooxml/src/test/java/org/apache/poi/xslf/TestXSLFBugs.java @@ -25,6 +25,11 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeFalse; import java.awt.Color; +import java.awt.LinearGradientPaint; +import java.awt.MultipleGradientPaint; +import java.awt.Paint; +import java.awt.RadialGradientPaint; +import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.FileInputStream; @@ -247,9 +252,9 @@ class TestXSLFBugs { @Test void bug62587() throws IOException { Object[][] pics = { - {"santa.wmf", PictureType.WMF, XSLFRelation.IMAGE_WMF}, - {"tomcat.png", PictureType.PNG, XSLFRelation.IMAGE_PNG}, - {"clock.jpg", PictureType.JPEG, XSLFRelation.IMAGE_JPEG} + {"santa.wmf", PictureType.WMF, XSLFRelation.IMAGE_WMF}, + {"tomcat.png", PictureType.PNG, XSLFRelation.IMAGE_PNG}, + {"clock.jpg", PictureType.JPEG, XSLFRelation.IMAGE_JPEG} }; try (XMLSlideShow ppt1 = new XMLSlideShow()) { @@ -298,12 +303,12 @@ class TestXSLFBugs { XSLFSlide slide1 = ppt1.getSlides().get(0); Optional shapeToDelete1 = - slide1.getShapes().stream().filter(s -> s instanceof XSLFPictureShape).findFirst(); + slide1.getShapes().stream().filter(s -> s instanceof XSLFPictureShape).findFirst(); assertTrue(shapeToDelete1.isPresent()); slide1.removeShape(shapeToDelete1.get()); assertTrue(slide1.getRelationParts().stream() - .allMatch(rp -> "rId1,rId3".contains(rp.getRelationship().getId()))); + .allMatch(rp -> "rId1,rId3".contains(rp.getRelationship().getId()))); assertNotNull(ppt1.getPackage().getPart(ppn)); } @@ -311,20 +316,20 @@ class TestXSLFBugs { try (XMLSlideShow ppt2 = openSampleDocument("bug60499.pptx")) { XSLFSlide slide2 = ppt2.getSlides().get(0); Optional shapeToDelete2 = - slide2.getShapes().stream().filter(s -> s instanceof XSLFPictureShape).skip(1).findFirst(); + slide2.getShapes().stream().filter(s -> s instanceof XSLFPictureShape).skip(1).findFirst(); assertTrue(shapeToDelete2.isPresent()); slide2.removeShape(shapeToDelete2.get()); assertTrue(slide2.getRelationParts().stream() - .allMatch(rp -> "rId1,rId2".contains(rp.getRelationship().getId()))); + .allMatch(rp -> "rId1,rId2".contains(rp.getRelationship().getId()))); assertNotNull(ppt2.getPackage().getPart(ppn)); } try (XMLSlideShow ppt3 = openSampleDocument("bug60499.pptx")) { XSLFSlide slide3 = ppt3.getSlides().get(0); slide3.getShapes().stream() - .filter(s -> s instanceof XSLFPictureShape) - .collect(Collectors.toList()) - .forEach(slide3::removeShape); + .filter(s -> s instanceof XSLFPictureShape) + .collect(Collectors.toList()) + .forEach(slide3::removeShape); assertNull(ppt3.getPackage().getPart(ppn)); } } @@ -754,8 +759,8 @@ class TestXSLFBugs { void bug59273() throws IOException { try (XMLSlideShow ppt = openSampleDocument("bug59273.potx")) { ppt.getPackage().replaceContentType( - XSLFRelation.PRESENTATIONML_TEMPLATE.getContentType(), - XSLFRelation.MAIN.getContentType() + XSLFRelation.PRESENTATIONML_TEMPLATE.getContentType(), + XSLFRelation.MAIN.getContentType() ); try (XMLSlideShow rwPptx = writeOutAndReadBack(ppt)) { @@ -879,7 +884,7 @@ class TestXSLFBugs { @Test void bug62051() throws IOException { final Function, int[]> ids = (shapes) -> - shapes.stream().mapToInt(Shape::getShapeId).toArray(); + shapes.stream().mapToInt(Shape::getShapeId).toArray(); try (final XMLSlideShow ppt = new XMLSlideShow()) { final XSLFSlide slide = ppt.createSlide(); @@ -997,11 +1002,11 @@ class TestXSLFBugs { assumeFalse(xslfOnly); final double[][] clips = { - { 50.999999999999986, 51.0, 298.0, 98.0 }, - { 51.00000000000003, 51.0, 298.0, 98.0 }, - { 51.0, 51.0, 298.0, 98.0 }, - { 250.02000796164992, 93.10370370370373, 78.61839367617523, 55.89629629629627 }, - { 79.58198774450841, 53.20887318960063, 109.13118501448272, 9.40935058567127 }, + { 50.999999999999986, 51.0, 298.0, 98.0 }, + { 51.00000000000003, 51.0, 298.0, 98.0 }, + { 51.0, 51.0, 298.0, 98.0 }, + { 250.02000796164992, 93.10370370370373, 78.61839367617523, 55.89629629629627 }, + { 79.58198774450841, 53.20887318960063, 109.13118501448272, 9.40935058567127 }, }; DummyGraphics2d dgfx = new DummyGraphics2d(new NullPrintStream()) { @@ -1031,13 +1036,13 @@ class TestXSLFBugs { public void bug65228() throws IOException { try (XMLSlideShow ppt = openSampleDocument("bug65228.pptx")) { TextRun.TextCap act = ppt.getSlides().stream() - .flatMap(s -> s.getShapes().stream()) - .filter(s -> "März 2021\u2026".equals(s.getShapeName())) - .map(XSLFTextShape.class::cast) - .flatMap(s -> s.getTextParagraphs().stream()) - .flatMap(s -> s.getTextRuns().stream()) - .map(XSLFTextRun::getTextCap) - .findFirst().orElse(null); + .flatMap(s -> s.getShapes().stream()) + .filter(s -> "März 2021\u2026".equals(s.getShapeName())) + .map(XSLFTextShape.class::cast) + .flatMap(s -> s.getTextParagraphs().stream()) + .flatMap(s -> s.getTextRuns().stream()) + .map(XSLFTextRun::getTextCap) + .findFirst().orElse(null); assertEquals(TextRun.TextCap.ALL, act); } } @@ -1120,4 +1125,97 @@ class TestXSLFBugs { } } } + + @Test + void identicalGradientStopsBug() throws IOException { + + final ArrayList linearGradients = new ArrayList<>(); + final ArrayList radialGradients = new ArrayList<>(); + final DummyGraphics2d dgfx = new DummyGraphics2d(new NullPrintStream()) + { + public void setPaint(final Paint paint) { + if (paint instanceof LinearGradientPaint) { + linearGradients.add((LinearGradientPaint) paint); + } + if (paint instanceof RadialGradientPaint) { + radialGradients.add((RadialGradientPaint) paint); + } + } + }; + + final List expectedLinearGradients = Arrays.asList( + new LinearGradientPaint(new Point2D.Double(30.731732283464567, 138.7317322834646), + new Point2D.Double(122.91549846753813, 46.54796609939099), + new float[] { 0.0f, 0.99999994f, 1.0f }, + new Color[] { new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255), + new Color(17,21,27, 204) }), + new LinearGradientPaint(new Point2D.Double(174.7317322834646, 138.73173228346457), + new Point2D.Double(266.9154984675381, 46.547966099391004), + new float[] { 0.0f, 0.00000005f, 1.0f }, + new Color[] { new Color(17,21,27, 204), + new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255) }), + new LinearGradientPaint(new Point2D.Double(318.73173228346457, 138.73173228346462), + new Point2D.Double(410.9154984675381, 46.547966099391004), + new float[] { 0.0f, 0.5f, 0.50000006f, 1.0f }, + new Color[] { new Color(17,21,27, 204), + new Color(17,21,27, 204), + new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255) }) + ); + + final List expectedRadialGradients = Arrays.asList( + new RadialGradientPaint(new Point2D.Double(30.731732283464567, 138.7317322834646), + 108.0f, new Point2D.Double(122.91549846753813, 46.54796609939099), + new float[] { 0.0f, 0.5f, 0.50000006f, 1.0f }, + new Color[] { new Color(17,21,27, 204), + new Color(17,21,27, 204), + new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255) }, + MultipleGradientPaint.CycleMethod.NO_CYCLE), + new RadialGradientPaint(new Point2D.Double(228.73173228346457, 226.9755905511811), + 108.0f, new Point2D.Double(282.73173228346457, 280.9755905511811), + new float[] { 0.0f, 0.00000005f, 1.0f }, + new Color[] { new Color(17,21,27, 204), + new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255) }, + MultipleGradientPaint.CycleMethod.NO_CYCLE), + new RadialGradientPaint(new Point2D.Double(84.73173228346457, 226.9755905511811), + 108.0f, new Point2D.Double(138.73173228346457, 280.9755905511811), + new float[] { 0.0f, 0.99999994f, 1.0f }, + new Color[] { new Color(81, 124, 252, 255), + new Color(81, 124, 252, 255), + new Color(17,21,27, 204) }, + MultipleGradientPaint.CycleMethod.NO_CYCLE) + ); + + try (XMLSlideShow slideShowModel = openSampleDocument("minimal-gradient-fill-issue.pptx")) { + // Render the first (and only) slide. + slideShowModel.getSlides().get(0).draw(dgfx); + + // Test that the linear gradients have the expected data (stops modified) + assertEquals(3, linearGradients.size()); + for (int i = 0 ; i < expectedLinearGradients.size() ; i++) { + final LinearGradientPaint expected = expectedLinearGradients.get(i); + final LinearGradientPaint actual = linearGradients.get(i); + assertEquals(expected.getStartPoint(), expected.getStartPoint()); + assertEquals(expected.getEndPoint(), expected.getEndPoint()); + assertArrayEquals(expected.getFractions(), actual.getFractions()); + assertArrayEquals(expected.getColors(), actual.getColors()); + } + + // Test that the radial gradients have the expected data (stops modified) + assertEquals(3, radialGradients.size()); + for (int i = 0 ; i < expectedRadialGradients.size() ; i++) { + final RadialGradientPaint expected = expectedRadialGradients.get(i); + final RadialGradientPaint actual = radialGradients.get(i); + assertEquals(expected.getCenterPoint(), expected.getCenterPoint()); + assertEquals(expected.getFocusPoint(), expected.getFocusPoint()); + assertArrayEquals(expected.getFractions(), actual.getFractions()); + assertArrayEquals(expected.getColors(), actual.getColors()); + } + } + } + } diff --git a/poi/src/main/java/org/apache/poi/sl/draw/DrawPaint.java b/poi/src/main/java/org/apache/poi/sl/draw/DrawPaint.java index 78a24335a3..aa2a27fb0a 100644 --- a/poi/src/main/java/org/apache/poi/sl/draw/DrawPaint.java +++ b/poi/src/main/java/org/apache/poi/sl/draw/DrawPaint.java @@ -613,7 +613,21 @@ public class DrawPaint { // need to remap the fractions, because Java doesn't like repeating fraction values Map m = new TreeMap<>(); for (float fraction : fill.getGradientFractions()) { - m.put(fraction, styles.next()); + float gradientFraction = fraction; + + // Multiple gradient stops at the same location + // can lead to failure when creating AWT gradient, especially + // if there are only two stops and they are both on the exact + // same location. + // (The example of (only) 2 stops at exactly the same location will cause: + // java.lang.IllegalArgumentException: User must specify at least 2 colors). + // + // To fix this we nudge the stop a teeny tiny bit. + if (m.containsKey(gradientFraction)) { + gradientFraction += (gradientFraction == 1.0 ? -1.0 : 1.0) * 0.00000005; + } + + m.put(gradientFraction, styles.next()); } return init.apply(toArray(m.keySet()), m.values().toArray(new Color[0])); diff --git a/test-data/slideshow/minimal-gradient-fill-issue.pptx b/test-data/slideshow/minimal-gradient-fill-issue.pptx new file mode 100644 index 0000000000..9675c621e0 Binary files /dev/null and b/test-data/slideshow/minimal-gradient-fill-issue.pptx differ