| /* | |
| * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* @test | |
| * @summary Pass if app exits without error code | |
| * @bug 8264999 | |
| */ | |
| import java.awt.*; | |
| import java.io.*; | |
| import java.util.List; | |
| import java.util.ArrayList; | |
| import java.awt.geom.*; | |
| import javax.imageio.*; | |
| import java.awt.image.*; | |
| /** | |
| * This tests redundant line segments. That is: if you draw a line from A to B, and then a line from | |
| * B to B, then the expected behavior is for the last redundant segment to NOT affect the miter stroke. | |
| */ | |
| public class JoinMiterRedundantLineSegmentsTest { | |
| public static void main(String[] args) throws Exception { | |
| System.out.println("This test defines a series of shapes with optional shape data. The optional data (which is enclosed in brackets) should not make a difference in how the shape is rendered. This test renders the shape data with and without the bracketed segments and tests to see if those renderings are identical."); | |
| List<Test> tests = createTests(); | |
| int sampleCtr = 1; | |
| boolean[] booleans = new boolean[] {false, true}; | |
| boolean failed = false; | |
| String header = null; | |
| for (Test test : tests) { | |
| header = null; | |
| for (Object strokeHint : new Object[] { RenderingHints.VALUE_STROKE_PURE, RenderingHints.VALUE_STROKE_NORMALIZE } ) { | |
| for (boolean createStrokedShape : booleans) { | |
| for (boolean closePath : booleans) { | |
| try { | |
| test.run(strokeHint, createStrokedShape, closePath); | |
| } catch(TestException e) { | |
| failed = true; | |
| if (header == null) { | |
| System.out.println(); | |
| header = "#############################\n"; | |
| header += "## " + test.name + "\n"; | |
| header += "## " + test.description + "\n"; | |
| header += "## " + test.shapeString + "\n"; | |
| header += "#############################"; | |
| System.out.println(header); | |
| } | |
| System.out.println(); | |
| System.out.println("# sample index = " + (sampleCtr)); | |
| System.out.println("strokeHint = " + strokeHint); | |
| System.out.println("createStrokedShape = " + createStrokedShape); | |
| System.out.println("closePath = " + closePath); | |
| System.out.println("FAILED"); | |
| e.printStackTrace(System.out); | |
| BufferedImage bi = e.getImage(); | |
| File file = new File("failure-"+sampleCtr+".png"); | |
| ImageIO.write(bi, "png", file); | |
| } | |
| sampleCtr++; | |
| } | |
| } | |
| } | |
| } | |
| if (failed) | |
| System.exit(1); | |
| } | |
| private static List<Test> createTests() { | |
| List<Test> tests = new ArrayList<>(); | |
| tests.add(new Test("Redundant diagonal line endpoint", | |
| "m 0 0 l 10 10 [l 10 10]", | |
| "This creates a diagonal line with a redundant endpoint; this is the core problem demonstrated in JDK-8264999.")); | |
| tests.add(new Test("jdk-8264999", | |
| "m 24.954517 159 l 21.097446 157.5 [l 21.097446 157.5] l 17.61364 162 [l 17.61364 162] l 13.756569 163.5 [l 13.756569 163.5] l 11.890244 160.5", | |
| "This is the original shape reported in https://bugs.openjdk.org/browse/JDK-8264999")); | |
| tests.add(new Test("2x and 3x redundant lines", | |
| "m 24.954517 159 l 21.097446 157.5 [l 21.097446 157.5 l 21.097446 157.5] l 17.61364 162 [l 17.61364 162 l 17.61364 162 l 17.61364 162] l 13.756569 163.5 [l 13.756569 163.5 l 13.756569 163.5 l 13.756569 163.5] l 11.890244 160.5", | |
| "This is a derivative of JDK-8264999 that includes two or three redundant lines (instead of just one).")); | |
| tests.add(new Test("cubic curve with redundant line", | |
| "m 17 100 c 7 130 27 130 17 100 [l 17 100]", | |
| "This creates a simple cubic curve (a teardrop shape) with one redundant line at the end.")); | |
| tests.add(new Test("degenerate cubic curve", | |
| "m 19 180 l 20 181 [c 20 181 20 181 20 181]", | |
| "This creates a degenerate cubic curve after the last end point.")); | |
| tests.add(new Test("degenerate quadratic curve", | |
| "m 19 180 l 20 181 [q 20 181 20 181]", | |
| "This creates a degenerate quadratic curve after the last end point.")); | |
| // This test reaches the line in Stroker.java where we detect a change of (+0, +0) | |
| // and manually change the dx: | |
| // dx = 1.0d; | |
| // tests.add(new Test("Redundant lineTo after moveTo", | |
| // "m 0 0 [l 0 0] l 10 10", | |
| // "This creates a diagonal line that may include a redundant lineTo after the moveTo")); | |
| // This test does NOT reach the same "dx = 1.0d" line. I'm not sure what the expected behavior here is. | |
| // tests.add(new Test("lineTo after close", | |
| // "m 0 0 z [l 10 10]", | |
| // "This tests a lineTo after a close (but without a second moveTo)")); | |
| // The following 2 tests fail because the mitered stroke covers different ares. | |
| // They might (?) be working as expected, and I just don't understand the expected behavior? | |
| // tests.add(new Test("Diagonal line, optional lineTo back", | |
| // "m 0 0 l 20 20 [l 0 0]", | |
| // "This creates a diagonal line and optionally returns to the starting point.")); | |
| // | |
| // tests.add(new Test("Diagonal line, optional close", | |
| // "m 0 0 l 20 20 l 0 0 [z]", | |
| // "This creates a diagonal line, returns to the starting point, and optionally closes the path.")); | |
| // We've decided the following commented-out tests are invalid. The current interpretation is: | |
| // "a moveTo statement without any additional information should NOT result in rendering anything" | |
| // tests.add(new Test("empty line", | |
| // "m 19 180 [l 19 180]", | |
| // "This creates an empty shape with a lineTo the starting point.")); | |
| // | |
| // tests.add(new Test("empty degenerate cubic curve", | |
| // "m 19 180 [c 19 180 19 180 19 180]", | |
| // "This creates an empty degenerate cubic curve that is effectively a line to the starting point.")); | |
| // | |
| // tests.add(new Test("empty degenerate quadratic curve", | |
| // "m 19 180 [q 19 180 19 180]", | |
| // "This creates an empty degenerate quadratic curve that is effectively a line to the starting point.")); | |
| // | |
| // tests.add(new Test("moveTo then close", | |
| // "m 19 180 [z]", | |
| // "This moves to a starting position and then optionally closes the path.")); | |
| return tests; | |
| } | |
| } | |
| class TestException extends Exception { | |
| BufferedImage bi; | |
| public TestException(Throwable t, BufferedImage bi) { | |
| super(t); | |
| this.bi = bi; | |
| } | |
| public BufferedImage getImage() { | |
| return bi; | |
| } | |
| } | |
| class Test { | |
| Path2D path_expected, path_actual; | |
| String name, description, shapeString; | |
| /** | |
| * @param name a short name of this test | |
| * @param shape shape data, including optional phrases in brackets. The shape should render the same | |
| * whether the data in brackets is included or not. | |
| * @param description a sentence describing this test | |
| */ | |
| public Test(String name, String shape, String description) { | |
| // make sure the test contains optional path data. Because if it doesn't: this test | |
| // is meaningless because nothing will change. | |
| if (!shape.contains("[")) | |
| throw new IllegalArgumentException("The shape must contain optional path data."); | |
| this.shapeString = shape; | |
| this.name = name; | |
| this.description = description; | |
| path_expected = parse(shape, false); | |
| path_actual = parse(shape, true); | |
| } | |
| @Override | |
| public String toString() { | |
| return name; | |
| } | |
| private String stripBracketPhrases(String str) { | |
| StringBuffer sb = new StringBuffer(); | |
| int ctr = 0; | |
| for (int a = 0; a < str.length(); a++) { | |
| char ch = str.charAt(a); | |
| if (ch == '[') { | |
| ctr++; | |
| } else if (ch == ']') { | |
| ctr--; | |
| } else if (ctr == 0) { | |
| sb.append(ch); | |
| } | |
| } | |
| return sb.toString(); | |
| } | |
| private Path2D.Double parse(String str, boolean includeBrackets) { | |
| if (includeBrackets) { | |
| str = str.replace('[', ' '); | |
| str = str.replace(']', ' '); | |
| } else { | |
| str = stripBracketPhrases(str); | |
| } | |
| Path2D.Double path = new Path2D.Double(); | |
| String[] terms = str.split(" "); | |
| int a = 0; | |
| while (a < terms.length) { | |
| if ("m".equals(terms[a])) { | |
| path.moveTo(Double.parseDouble(terms[a + 1]), Double.parseDouble(terms[a + 2])); | |
| a += 3; | |
| } else if ("l".equals(terms[a])) { | |
| path.lineTo( Double.parseDouble(terms[a+1]), Double.parseDouble(terms[a+2]) ); | |
| a += 3; | |
| } else if ("q".equals(terms[a])) { | |
| path.quadTo( Double.parseDouble(terms[a+1]), Double.parseDouble(terms[a+2]), | |
| Double.parseDouble(terms[a+3]), Double.parseDouble(terms[a+4]) ); | |
| a += 5; | |
| } else if ("c".equals(terms[a])) { | |
| path.curveTo( Double.parseDouble(terms[a+1]), Double.parseDouble(terms[a+2]), | |
| Double.parseDouble(terms[a+3]), Double.parseDouble(terms[a+4]), | |
| Double.parseDouble(terms[a+5]), Double.parseDouble(terms[a+6]) ); | |
| a += 7; | |
| } else if ("z".equals(terms[a])) { | |
| path.closePath(); | |
| a += 1; | |
| } else if(terms[a].trim().isEmpty()) { | |
| a += 1; | |
| } else { | |
| throw new RuntimeException("\""+terms[a]+"\" in \""+str+"\""); | |
| } | |
| } | |
| return path; | |
| } | |
| public void run(Object strokeRenderingHint, boolean createStrokedShape, boolean closePath) throws Exception { | |
| BufferedImage bi_expected = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB); | |
| BufferedImage bi_actual = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB); | |
| paint(path_expected, bi_expected, Color.black, strokeRenderingHint, createStrokedShape, closePath); | |
| paint(path_actual, bi_actual, Color.black, strokeRenderingHint, createStrokedShape, closePath); | |
| try { | |
| assertEquals(bi_expected, bi_actual); | |
| } catch(Exception e) { | |
| BufferedImage composite = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB); | |
| paint(path_expected, composite, Color.blue, strokeRenderingHint, createStrokedShape, closePath); | |
| paint(path_actual, composite, new Color(255,0,0,100), strokeRenderingHint, createStrokedShape, closePath); | |
| throw new TestException(e, composite); | |
| } | |
| } | |
| /** | |
| * Throw an exception if two images are not equal. | |
| */ | |
| private static void assertEquals(BufferedImage bi1, BufferedImage bi2) { | |
| int w = bi1.getWidth(); | |
| int h = bi1.getHeight(); | |
| int[] row1 = new int[w]; | |
| int[] row2 = new int[w]; | |
| for (int y = 0; y < h; y++) { | |
| bi1.getRaster().getDataElements(0,y,w,1,row1); | |
| bi2.getRaster().getDataElements(0,y,w,1,row2); | |
| for (int x = 0; x < w; x++) { | |
| if (row1[x] != row2[x]) | |
| throw new RuntimeException("failure at ("+x+", "+y+"): 0x"+Integer.toHexString(row1[x])+" != 0x"+Integer.toHexString(row2[x])); | |
| } | |
| } | |
| } | |
| /** | |
| * Create a transform that maps from one rectangle to another. | |
| */ | |
| private AffineTransform createTransform(Rectangle2D oldRect,Rectangle2D newRect) { | |
| double scaleX = newRect.getWidth() / oldRect.getWidth(); | |
| double scaleY = newRect.getHeight() / oldRect.getHeight(); | |
| double translateX = -oldRect.getX() * scaleX + newRect.getX(); | |
| double translateY = -oldRect.getY() * scaleY + newRect.getY(); | |
| return new AffineTransform(scaleX, 0, 0, scaleY, translateX, translateY); | |
| } | |
| /** | |
| * Paint a path to an image. | |
| */ | |
| private void paint(Path2D path, BufferedImage dst, Color color, Object strokeRenderingHint, | |
| boolean createStrokedShape, boolean closePath) { | |
| Rectangle2D pathBounds = path.getBounds2D(); | |
| pathBounds.setFrame(pathBounds.getX() - 10, | |
| pathBounds.getY() - 10, | |
| pathBounds.getWidth() + 20, | |
| pathBounds.getHeight() + 20); | |
| Rectangle imageBounds = new Rectangle(0, 0, dst.getWidth(), dst.getHeight()); | |
| Path2D p = new Path2D.Double(); | |
| p.append(path, false); | |
| if (closePath) | |
| p.closePath(); | |
| Graphics2D g = dst.createGraphics(); | |
| g.transform(createTransform(pathBounds, imageBounds)); | |
| g.setColor(color); | |
| g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |
| g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, strokeRenderingHint); | |
| Stroke stroke = new BasicStroke(3); | |
| if (createStrokedShape) { | |
| g.fill( stroke.createStrokedShape(p) ); | |
| } else { | |
| g.setStroke(stroke); | |
| g.draw(p); | |
| } | |
| g.dispose(); | |
| } | |
| } |