/* * 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 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 createTests() { List 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(); } }