8264999: GeneralPath.lineTo() to itself produces jagged lines
Reviewed-by: lbourges, prr
This commit is contained in:
parent
43c47b1ad7
commit
4b4d35289a
src/java.desktop/share/classes/sun/java2d/marlin
test/jdk/sun/java2d/marlin
@ -568,6 +568,10 @@ final class Stroker implements DPathConsumer2D, MarlinConst {
|
||||
double dx = x1 - cx0;
|
||||
double dy = y1 - cy0;
|
||||
if (dx == 0.0d && dy == 0.0d) {
|
||||
if (prev == DRAWING_OP_TO) {
|
||||
// skip repeated end-point
|
||||
return;
|
||||
}
|
||||
dx = 1.0d;
|
||||
}
|
||||
computeOffset(dx, dy, lineWidth2, offset0);
|
||||
|
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user