8144938: Handle properly coordinate overflow in Marlin Renderer
Summary: skip point coordinates with NaN/Infinity values
Reviewed-by: flar, prr
--- a/jdk/src/java.desktop/share/classes/sun/java2d/marlin/MarlinRenderingEngine.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/marlin/MarlinRenderingEngine.java Wed Mar 23 21:20:25 2016 +0100
@@ -51,6 +51,9 @@
private static final float MIN_PEN_SIZE = 1f / NORM_SUBPIXELS;
+ static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
+ static final float LOWER_BND = -UPPER_BND;
+
/**
* Public constructor
*/
@@ -279,7 +282,7 @@
float dashphase,
PathConsumer2D pc2d)
{
- // We use strokerat and outat so that in Stroker and Dasher we can work only
+ // We use strokerat so that in Stroker and Dasher we can work only
// with the pre-transformation coordinates. This will repeat a lot of
// computations done in the path iterator, but the alternative is to
// work with transformed paths and compute untransformed coordinates
@@ -289,15 +292,11 @@
// However, if a path's width is constant after a transformation,
// we can skip all this untransforming.
- // If normalization is off we save some transformations by not
- // transforming the input to pisces. Instead, we apply the
- // transformation after the path processing has been done.
- // We can't do this if normalization is on, because it isn't a good
- // idea to normalize before the transformation is applied.
+ // As pathTo() will check transformed coordinates for invalid values
+ // (NaN / Infinity) to ignore such points, it is necessary to apply the
+ // transformation before the path processing.
AffineTransform strokerat = null;
- AffineTransform outat = null;
- PathIterator pi;
int dashLen = -1;
boolean recycleDashes = false;
@@ -333,6 +332,7 @@
// leave a bit of room for error.
if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) {
final float scale = (float) Math.sqrt(a*a + c*c);
+
if (dashes != null) {
recycleDashes = true;
dashLen = dashes.length;
@@ -349,48 +349,35 @@
System.arraycopy(dashes, 0, newDashes, 0, dashLen);
dashes = newDashes;
for (int i = 0; i < dashLen; i++) {
- dashes[i] = scale * dashes[i];
+ dashes[i] *= scale;
}
- dashphase = scale * dashphase;
+ dashphase *= scale;
}
- width = scale * width;
- pi = getNormalizingPathIterator(rdrCtx, normalize,
- src.getPathIterator(at));
+ width *= scale;
- // by now strokerat == null && outat == null. Input paths to
+ // by now strokerat == null. Input paths to
// stroker (and maybe dasher) will have the full transform at
// applied to them and nothing will happen to the output paths.
} else {
- if (normalize != NormMode.OFF) {
- strokerat = at;
- pi = getNormalizingPathIterator(rdrCtx, normalize,
- src.getPathIterator(at));
+ strokerat = at;
- // by now strokerat == at && outat == null. Input paths to
- // stroker (and maybe dasher) will have the full transform at
- // applied to them, then they will be normalized, and then
- // the inverse of *only the non translation part of at* will
- // be applied to the normalized paths. This won't cause problems
- // in stroker, because, suppose at = T*A, where T is just the
- // translation part of at, and A is the rest. T*A has already
- // been applied to Stroker/Dasher's input. Then Ainv will be
- // applied. Ainv*T*A is not equal to T, but it is a translation,
- // which means that none of stroker's assumptions about its
- // input will be violated. After all this, A will be applied
- // to stroker's output.
- } else {
- outat = at;
- pi = src.getPathIterator(null);
- // outat == at && strokerat == null. This is because if no
- // normalization is done, we can just apply all our
- // transformations to stroker's output.
- }
+ // by now strokerat == at. Input paths to
+ // stroker (and maybe dasher) will have the full transform at
+ // applied to them, then they will be normalized, and then
+ // the inverse of *only the non translation part of at* will
+ // be applied to the normalized paths. This won't cause problems
+ // in stroker, because, suppose at = T*A, where T is just the
+ // translation part of at, and A is the rest. T*A has already
+ // been applied to Stroker/Dasher's input. Then Ainv will be
+ // applied. Ainv*T*A is not equal to T, but it is a translation,
+ // which means that none of stroker's assumptions about its
+ // input will be violated. After all this, A will be applied
+ // to stroker's output.
}
} else {
// either at is null or it's the identity. In either case
// we don't transform the path.
- pi = getNormalizingPathIterator(rdrCtx, normalize,
- src.getPathIterator(null));
+ at = null;
}
if (useSimplifier) {
@@ -399,12 +386,7 @@
pc2d = rdrCtx.simplifier.init(pc2d);
}
- // by now, at least one of outat and strokerat will be null. Unless at is not
- // a constant multiple of an orthogonal transformation, they will both be
- // null. In other cases, outat == at if normalization is off, and if
- // normalization is on, strokerat == at.
final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
- pc2d = transformerPC2D.transformConsumer(pc2d, outat);
pc2d = transformerPC2D.deltaTransformConsumer(pc2d, strokerat);
pc2d = rdrCtx.stroker.init(pc2d, width, caps, join, miterlimit);
@@ -417,18 +399,22 @@
recycleDashes);
}
pc2d = transformerPC2D.inverseDeltaTransformConsumer(pc2d, strokerat);
+
+ final PathIterator pi = getNormalizingPathIterator(rdrCtx, normalize,
+ src.getPathIterator(at));
+
pathTo(rdrCtx, pi, pc2d);
/*
* Pipeline seems to be:
- * shape.getPathIterator
- * -> NormalizingPathIterator
- * -> inverseDeltaTransformConsumer
- * -> Dasher
+ * shape.getPathIterator(at)
+ * -> (NormalizingPathIterator)
+ * -> (inverseDeltaTransformConsumer)
+ * -> (Dasher)
* -> Stroker
- * -> deltaTransformConsumer OR transformConsumer
+ * -> (deltaTransformConsumer)
*
- * -> CollinearSimplifier to remove redundant segments
+ * -> (CollinearSimplifier) to remove redundant segments
*
* -> pc2d = Renderer (bounding box)
*/
@@ -642,27 +628,109 @@
private static void pathToLoop(final float[] coords, final PathIterator pi,
final PathConsumer2D pc2d)
{
+ // ported from DuctusRenderingEngine.feedConsumer() but simplified:
+ // - removed skip flag = !subpathStarted
+ // - removed pathClosed (ie subpathStarted not set to false)
+ boolean subpathStarted = false;
+
for (; !pi.isDone(); pi.next()) {
switch (pi.currentSegment(coords)) {
- case PathIterator.SEG_MOVETO:
+ case PathIterator.SEG_MOVETO:
+ /* Checking SEG_MOVETO coordinates if they are out of the
+ * [LOWER_BND, UPPER_BND] range. This check also handles NaN
+ * and Infinity values. Skipping next path segment in case of
+ * invalid data.
+ */
+ if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
+ coords[1] < UPPER_BND && coords[1] > LOWER_BND)
+ {
pc2d.moveTo(coords[0], coords[1]);
- continue;
- case PathIterator.SEG_LINETO:
- pc2d.lineTo(coords[0], coords[1]);
- continue;
- case PathIterator.SEG_QUADTO:
- pc2d.quadTo(coords[0], coords[1],
- coords[2], coords[3]);
- continue;
- case PathIterator.SEG_CUBICTO:
- pc2d.curveTo(coords[0], coords[1],
- coords[2], coords[3],
- coords[4], coords[5]);
- continue;
- case PathIterator.SEG_CLOSE:
+ subpathStarted = true;
+ }
+ break;
+ case PathIterator.SEG_LINETO:
+ /* Checking SEG_LINETO coordinates if they are out of the
+ * [LOWER_BND, UPPER_BND] range. This check also handles NaN
+ * and Infinity values. Ignoring current path segment in case
+ * of invalid data. If segment is skipped its endpoint
+ * (if valid) is used to begin new subpath.
+ */
+ if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
+ coords[1] < UPPER_BND && coords[1] > LOWER_BND)
+ {
+ if (subpathStarted) {
+ pc2d.lineTo(coords[0], coords[1]);
+ } else {
+ pc2d.moveTo(coords[0], coords[1]);
+ subpathStarted = true;
+ }
+ }
+ break;
+ case PathIterator.SEG_QUADTO:
+ // Quadratic curves take two points
+ /* Checking SEG_QUADTO coordinates if they are out of the
+ * [LOWER_BND, UPPER_BND] range. This check also handles NaN
+ * and Infinity values. Ignoring current path segment in case
+ * of invalid endpoints's data. Equivalent to the SEG_LINETO
+ * if endpoint coordinates are valid but there are invalid data
+ * among other coordinates
+ */
+ if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
+ coords[3] < UPPER_BND && coords[3] > LOWER_BND)
+ {
+ if (subpathStarted) {
+ if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
+ coords[1] < UPPER_BND && coords[1] > LOWER_BND)
+ {
+ pc2d.quadTo(coords[0], coords[1],
+ coords[2], coords[3]);
+ } else {
+ pc2d.lineTo(coords[2], coords[3]);
+ }
+ } else {
+ pc2d.moveTo(coords[2], coords[3]);
+ subpathStarted = true;
+ }
+ }
+ break;
+ case PathIterator.SEG_CUBICTO:
+ // Cubic curves take three points
+ /* Checking SEG_CUBICTO coordinates if they are out of the
+ * [LOWER_BND, UPPER_BND] range. This check also handles NaN
+ * and Infinity values. Ignoring current path segment in case
+ * of invalid endpoints's data. Equivalent to the SEG_LINETO
+ * if endpoint coordinates are valid but there are invalid data
+ * among other coordinates
+ */
+ if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
+ coords[5] < UPPER_BND && coords[5] > LOWER_BND)
+ {
+ if (subpathStarted) {
+ if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
+ coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
+ coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
+ coords[3] < UPPER_BND && coords[3] > LOWER_BND)
+ {
+ pc2d.curveTo(coords[0], coords[1],
+ coords[2], coords[3],
+ coords[4], coords[5]);
+ } else {
+ pc2d.lineTo(coords[4], coords[5]);
+ }
+ } else {
+ pc2d.moveTo(coords[4], coords[5]);
+ subpathStarted = true;
+ }
+ }
+ break;
+ case PathIterator.SEG_CLOSE:
+ if (subpathStarted) {
pc2d.closePath();
- continue;
- default:
+ // do not set subpathStarted to false
+ // in case of missing moveTo() after close()
+ }
+ break;
+ default:
}
}
pc2d.pathDone();
--- a/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java Wed Mar 23 21:20:25 2016 +0100
@@ -338,11 +338,6 @@
}
private void drawRoundCap(float cx, float cy, float mx, float my) {
- // the first and second arguments of the following two calls
- // are really will be ignored by emitCurveTo (because of the false),
- // but we put them in anyway, as opposed to just giving it 4 zeroes,
- // because it's just 4 additions and it's not good to rely on this
- // sort of assumption (right now it's true, but that may change).
emitCurveTo(cx+mx-C*my, cy+my+C*mx,
cx-my+C*mx, cy+mx+C*my,
cx-my, cy+mx);
--- a/jdk/src/java.desktop/share/classes/sun/java2d/marlin/TransformingPathConsumer2D.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/marlin/TransformingPathConsumer2D.java Wed Mar 23 21:20:25 2016 +0100
@@ -35,7 +35,7 @@
// used by RendererContext
}
- // recycled PathConsumer2D instance from transformConsumer()
+ // recycled PathConsumer2D instance from wrapPath2d()
private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper();
PathConsumer2D wrapPath2d(Path2D.Float p2d)
@@ -43,46 +43,6 @@
return wp_Path2DWrapper.init(p2d);
}
- // recycled PathConsumer2D instances from transformConsumer()
- private final TranslateFilter tx_TranslateFilter = new TranslateFilter();
- private final DeltaScaleFilter tx_DeltaScaleFilter = new DeltaScaleFilter();
- private final ScaleFilter tx_ScaleFilter = new ScaleFilter();
- private final DeltaTransformFilter tx_DeltaTransformFilter = new DeltaTransformFilter();
- private final TransformFilter tx_TransformFilter = new TransformFilter();
-
- PathConsumer2D transformConsumer(PathConsumer2D out,
- AffineTransform at)
- {
- if (at == null) {
- return out;
- }
- float mxx = (float) at.getScaleX();
- float mxy = (float) at.getShearX();
- float mxt = (float) at.getTranslateX();
- float myx = (float) at.getShearY();
- float myy = (float) at.getScaleY();
- float myt = (float) at.getTranslateY();
- if (mxy == 0f && myx == 0f) {
- if (mxx == 1f && myy == 1f) {
- if (mxt == 0f && myt == 0f) {
- return out;
- } else {
- return tx_TranslateFilter.init(out, mxt, myt);
- }
- } else {
- if (mxt == 0f && myt == 0f) {
- return tx_DeltaScaleFilter.init(out, mxx, myy);
- } else {
- return tx_ScaleFilter.init(out, mxx, myy, mxt, myt);
- }
- }
- } else if (mxt == 0f && myt == 0f) {
- return tx_DeltaTransformFilter.init(out, mxx, mxy, myx, myy);
- } else {
- return tx_TransformFilter.init(out, mxx, mxy, mxt, myx, myy, myt);
- }
- }
-
// recycled PathConsumer2D instances from deltaTransformConsumer()
private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter();
private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter();
@@ -97,6 +57,7 @@
float mxy = (float) at.getShearX();
float myx = (float) at.getShearY();
float myy = (float) at.getScaleY();
+
if (mxy == 0f && myx == 0f) {
if (mxx == 1f && myy == 1f) {
return out;
@@ -122,6 +83,7 @@
float mxy = (float) at.getShearX();
float myx = (float) at.getShearY();
float myy = (float) at.getScaleY();
+
if (mxy == 0f && myx == 0f) {
if (mxx == 1f && myy == 1f) {
return out;
@@ -138,197 +100,6 @@
}
}
- static final class TranslateFilter implements PathConsumer2D {
- private PathConsumer2D out;
- private float tx, ty;
-
- TranslateFilter() {}
-
- TranslateFilter init(PathConsumer2D out,
- float tx, float ty)
- {
- this.out = out;
- this.tx = tx;
- this.ty = ty;
- return this; // fluent API
- }
-
- @Override
- public void moveTo(float x0, float y0) {
- out.moveTo(x0 + tx, y0 + ty);
- }
-
- @Override
- public void lineTo(float x1, float y1) {
- out.lineTo(x1 + tx, y1 + ty);
- }
-
- @Override
- public void quadTo(float x1, float y1,
- float x2, float y2)
- {
- out.quadTo(x1 + tx, y1 + ty,
- x2 + tx, y2 + ty);
- }
-
- @Override
- public void curveTo(float x1, float y1,
- float x2, float y2,
- float x3, float y3)
- {
- out.curveTo(x1 + tx, y1 + ty,
- x2 + tx, y2 + ty,
- x3 + tx, y3 + ty);
- }
-
- @Override
- public void closePath() {
- out.closePath();
- }
-
- @Override
- public void pathDone() {
- out.pathDone();
- }
-
- @Override
- public long getNativeConsumer() {
- return 0;
- }
- }
-
- static final class ScaleFilter implements PathConsumer2D {
- private PathConsumer2D out;
- private float sx, sy, tx, ty;
-
- ScaleFilter() {}
-
- ScaleFilter init(PathConsumer2D out,
- float sx, float sy,
- float tx, float ty)
- {
- this.out = out;
- this.sx = sx;
- this.sy = sy;
- this.tx = tx;
- this.ty = ty;
- return this; // fluent API
- }
-
- @Override
- public void moveTo(float x0, float y0) {
- out.moveTo(x0 * sx + tx, y0 * sy + ty);
- }
-
- @Override
- public void lineTo(float x1, float y1) {
- out.lineTo(x1 * sx + tx, y1 * sy + ty);
- }
-
- @Override
- public void quadTo(float x1, float y1,
- float x2, float y2)
- {
- out.quadTo(x1 * sx + tx, y1 * sy + ty,
- x2 * sx + tx, y2 * sy + ty);
- }
-
- @Override
- public void curveTo(float x1, float y1,
- float x2, float y2,
- float x3, float y3)
- {
- out.curveTo(x1 * sx + tx, y1 * sy + ty,
- x2 * sx + tx, y2 * sy + ty,
- x3 * sx + tx, y3 * sy + ty);
- }
-
- @Override
- public void closePath() {
- out.closePath();
- }
-
- @Override
- public void pathDone() {
- out.pathDone();
- }
-
- @Override
- public long getNativeConsumer() {
- return 0;
- }
- }
-
- static final class TransformFilter implements PathConsumer2D {
- private PathConsumer2D out;
- private float mxx, mxy, mxt, myx, myy, myt;
-
- TransformFilter() {}
-
- TransformFilter init(PathConsumer2D out,
- float mxx, float mxy, float mxt,
- float myx, float myy, float myt)
- {
- this.out = out;
- this.mxx = mxx;
- this.mxy = mxy;
- this.mxt = mxt;
- this.myx = myx;
- this.myy = myy;
- this.myt = myt;
- return this; // fluent API
- }
-
- @Override
- public void moveTo(float x0, float y0) {
- out.moveTo(x0 * mxx + y0 * mxy + mxt,
- x0 * myx + y0 * myy + myt);
- }
-
- @Override
- public void lineTo(float x1, float y1) {
- out.lineTo(x1 * mxx + y1 * mxy + mxt,
- x1 * myx + y1 * myy + myt);
- }
-
- @Override
- public void quadTo(float x1, float y1,
- float x2, float y2)
- {
- out.quadTo(x1 * mxx + y1 * mxy + mxt,
- x1 * myx + y1 * myy + myt,
- x2 * mxx + y2 * mxy + mxt,
- x2 * myx + y2 * myy + myt);
- }
-
- @Override
- public void curveTo(float x1, float y1,
- float x2, float y2,
- float x3, float y3)
- {
- out.curveTo(x1 * mxx + y1 * mxy + mxt,
- x1 * myx + y1 * myy + myt,
- x2 * mxx + y2 * mxy + mxt,
- x2 * myx + y2 * myy + myt,
- x3 * mxx + y3 * mxy + mxt,
- x3 * myx + y3 * myy + myt);
- }
-
- @Override
- public void closePath() {
- out.closePath();
- }
-
- @Override
- public void pathDone() {
- out.pathDone();
- }
-
- @Override
- public long getNativeConsumer() {
- return 0;
- }
- }
static final class DeltaScaleFilter implements PathConsumer2D {
private PathConsumer2D out;
--- a/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Version.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Version.java Wed Mar 23 21:20:25 2016 +0100
@@ -27,7 +27,7 @@
public final class Version {
- private static final String version = "marlin-0.7.3.2-Unsafe-OpenJDK";
+ private static final String version = "marlin-0.7.3.3-Unsafe-OpenJDK";
public static String getVersion() {
return version;
--- a/jdk/src/java.desktop/share/classes/sun/java2d/pipe/AAShapePipe.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/pipe/AAShapePipe.java Wed Mar 23 21:20:25 2016 +0100
@@ -28,7 +28,6 @@
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
-import java.util.concurrent.ConcurrentLinkedQueue;
import sun.awt.SunHints;
import sun.java2d.ReentrantContext;
import sun.java2d.ReentrantContextProvider;
--- a/jdk/test/sun/java2d/marlin/CrashNaNTest.java Wed Mar 23 17:25:27 2016 +0530
+++ b/jdk/test/sun/java2d/marlin/CrashNaNTest.java Wed Mar 23 21:20:25 2016 +0100
@@ -21,14 +21,22 @@
* questions.
*/
+import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.Stroke;
import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
+import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
+import static java.lang.Double.NEGATIVE_INFINITY;
+import static java.lang.Double.POSITIVE_INFINITY;
import static java.lang.Double.NaN;
+import java.util.Arrays;
import java.util.Locale;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
@@ -37,8 +45,9 @@
/**
* @test
- * @bug 8149338
- * @summary Verifies that Marlin supports NaN coordinates and no JVM crash happens !
+ * @bug 8149338 8144938
+ * @summary Verifies that Marlin supports NaN coordinates (no JVM crash)
+ * but also it skips properly point coordinates with NaN / Infinity values
* @run main CrashNaNTest
*/
public class CrashNaNTest {
@@ -77,23 +86,108 @@
System.setProperty("sun.java2d.renderer.useLogger", "true");
System.setProperty("sun.java2d.renderer.doChecks", "true");
+ testFillDefaultAt();
+ testDrawComplexAt();
+
+ testPathClosed();
+
+ testStrokedShapes();
+ }
+
+ private static void testFillDefaultAt() {
final int width = 400;
final int height = 400;
final BufferedImage image = new BufferedImage(width, height,
- BufferedImage.TYPE_INT_ARGB);
+ BufferedImage.TYPE_INT_ARGB);
final Graphics2D g2d = (Graphics2D) image.getGraphics();
try {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
- RenderingHints.VALUE_ANTIALIAS_ON);
+ RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setBackground(Color.WHITE);
g2d.clearRect(0, 0, width, height);
final Path2D.Double path = new Path2D.Double();
- path.moveTo(30, 30);
- path.lineTo(100, 100);
+ path.moveTo(100, 100);
+
+ for (int i = 0; i < 20000; i++) {
+ path.lineTo(110 + 0.01 * i, 110);
+ path.lineTo(111 + 0.01 * i, 100);
+ }
+
+ path.lineTo(NaN, 200);
+ path.lineTo(200, 200);
+ path.lineTo(200, NaN);
+ path.lineTo(300, 300);
+ path.lineTo(NaN, NaN);
+ path.lineTo(100, 200);
+ path.closePath();
+
+ final Path2D.Double path2 = new Path2D.Double();
+ path2.moveTo(0, 0);
+ path2.lineTo(100, height);
+ path2.lineTo(0, 200);
+ path2.closePath();
+
+ g2d.setColor(Color.BLUE);
+ g2d.fill(path);
+ g2d.setColor(Color.GREEN);
+ g2d.fill(path2);
+
+ g2d.setColor(Color.BLACK);
+ g2d.draw(path);
+ g2d.draw(path2);
+
+ if (SAVE_IMAGE) {
+ try {
+ final File file = new File("CrashNaNTest-fill.png");
+ System.out.println("Writing file: "
+ + file.getAbsolutePath());
+ ImageIO.write(image, "PNG", file);
+ } catch (IOException ex) {
+ System.out.println("Writing file failure:");
+ ex.printStackTrace();
+ }
+ }
+
+ // Check image on few pixels:
+ final Raster raster = image.getData();
+
+ checkPixel(raster, 200, 195, Color.BLUE.getRGB());
+ checkPixel(raster, 105, 195, Color.BLUE.getRGB());
+ checkPixel(raster, 286, 290, Color.BLUE.getRGB());
+
+ checkPixel(raster, 108, 105, Color.WHITE.getRGB());
+ checkPixel(raster, 205, 200, Color.WHITE.getRGB());
+
+ checkPixel(raster, 5, 200, Color.GREEN.getRGB());
+
+ } finally {
+ g2d.dispose();
+ }
+ }
+
+ private static void testDrawComplexAt() {
+ final int width = 400;
+ final int height = 400;
+
+ final BufferedImage image = new BufferedImage(width, height,
+ BufferedImage.TYPE_INT_ARGB);
+
+ final Graphics2D g2d = (Graphics2D) image.getGraphics();
+ try {
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_PURE);
+
+ g2d.setBackground(Color.WHITE);
+ g2d.clearRect(0, 0, width, height);
+
+ final Path2D.Double path = new Path2D.Double();
+ path.moveTo(100, 100);
for (int i = 0; i < 20000; i++) {
path.lineTo(110 + 0.01 * i, 110);
@@ -105,39 +199,224 @@
path.lineTo(200, NaN);
path.lineTo(300, 300);
path.lineTo(NaN, NaN);
- path.lineTo(100, 100);
+ path.lineTo(100, 200);
path.closePath();
final Path2D.Double path2 = new Path2D.Double();
- path2.moveTo(0,0);
- path2.lineTo(width,height);
- path2.lineTo(10, 10);
+ path2.moveTo(0, 0);
+ path2.lineTo(100, height);
+ path2.lineTo(0, 200);
path2.closePath();
- for (int i = 0; i < 1; i++) {
- final long start = System.nanoTime();
- g2d.setColor(Color.BLUE);
- g2d.fill(path);
+ // Define an non-uniform transform:
+ g2d.scale(0.5, 1.0);
+ g2d.rotate(Math.PI / 31);
- g2d.fill(path2);
-
- final long time = System.nanoTime() - start;
- System.out.println("paint: duration= " + (1e-6 * time) + " ms.");
- }
+ g2d.setColor(Color.BLACK);
+ g2d.draw(path);
+ g2d.draw(path2);
if (SAVE_IMAGE) {
try {
- final File file = new File("CrashNaNTest.png");
+ final File file = new File("CrashNaNTest-draw.png");
System.out.println("Writing file: "
- + file.getAbsolutePath());
+ + file.getAbsolutePath());
+ ImageIO.write(image, "PNG", file);
+ } catch (IOException ex) {
+ System.out.println("Writing file failure:");
+ ex.printStackTrace();
+ }
+ }
+
+ // Check image on few pixels:
+ final Raster raster = image.getData();
+
+ checkPixelNotWhite(raster, 40, 210);
+ checkPixelNotWhite(raster, 44, 110);
+ checkPixelNotWhite(raster, 60, 120);
+ checkPixelNotWhite(raster, 89, 219);
+ checkPixelNotWhite(raster, 28, 399);
+ checkPixelNotWhite(raster, 134, 329);
+
+ } finally {
+ g2d.dispose();
+ }
+ }
+ private static void testPathClosed() {
+ final int width = 100;
+ final int height = 100;
+
+ final BufferedImage image = new BufferedImage(width, height,
+ BufferedImage.TYPE_INT_ARGB);
+
+ final Graphics2D g2d = (Graphics2D) image.getGraphics();
+ try {
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+
+ g2d.setBackground(Color.WHITE);
+ g2d.clearRect(0, 0, width, height);
+
+ final Path2D.Double path = new Path2D.Double();
+ path.moveTo(40, 40);
+ path.lineTo(0, 0);
+ path.lineTo(80, 0);
+ path.closePath();
+ path.lineTo(80, 80);
+ path.lineTo(0, 80);
+ path.closePath();
+
+ g2d.setColor(Color.BLUE);
+ g2d.fill(path);
+
+ g2d.setColor(Color.BLACK);
+ g2d.draw(path);
+
+ if (SAVE_IMAGE) {
+ try {
+ final File file = new File("CrashNaNTest-path-closed.png");
+ System.out.println("Writing file: "
+ + file.getAbsolutePath());
ImageIO.write(image, "PNG", file);
} catch (IOException ex) {
System.out.println("Writing file failure:");
ex.printStackTrace();
}
}
+
+ // Check image on few pixels:
+ final Raster raster = image.getData();
+
+ checkPixel(raster, 10, 05, Color.BLUE.getRGB());
+ checkPixel(raster, 70, 05, Color.BLUE.getRGB());
+ checkPixel(raster, 40, 35, Color.BLUE.getRGB());
+
+ checkPixel(raster, 10, 75, Color.BLUE.getRGB());
+ checkPixel(raster, 70, 75, Color.BLUE.getRGB());
+ checkPixel(raster, 40, 45, Color.BLUE.getRGB());
+
} finally {
g2d.dispose();
}
}
+
+ private static void testStrokedShapes() {
+ final Stroke stroke = new BasicStroke();
+
+ final Path2D.Double path = new Path2D.Double();
+ Shape s;
+
+ // Check filtering NaN values:
+ path.reset();
+ path.moveTo(100, NaN);
+ path.lineTo(NaN, 100);
+ path.lineTo(NaN, NaN);
+
+ path.quadTo(NaN, 100, NaN, 100);
+ path.quadTo(100, NaN, 100, NaN);
+ path.quadTo(NaN, NaN, NaN, NaN);
+
+ path.curveTo(NaN, 100, NaN, 100, NaN, 100);
+ path.curveTo(100, NaN, 100, NaN, 100, NaN);
+ path.curveTo(NaN, NaN, NaN, NaN, NaN, NaN);
+ path.closePath();
+
+ s = stroke.createStrokedShape(path);
+ checkEmptyPath(s);
+
+ // Check filtering +Infinity values:
+ path.reset();
+ path.moveTo(100, POSITIVE_INFINITY);
+ path.lineTo(POSITIVE_INFINITY, 100);
+ path.lineTo(POSITIVE_INFINITY, POSITIVE_INFINITY);
+
+ path.quadTo(POSITIVE_INFINITY, 100,
+ POSITIVE_INFINITY, 100);
+ path.quadTo(100, POSITIVE_INFINITY,
+ 100, POSITIVE_INFINITY);
+ path.quadTo(POSITIVE_INFINITY, POSITIVE_INFINITY,
+ POSITIVE_INFINITY, POSITIVE_INFINITY);
+
+ path.curveTo(POSITIVE_INFINITY, 100,
+ POSITIVE_INFINITY, 100,
+ POSITIVE_INFINITY, 100);
+ path.curveTo(100, POSITIVE_INFINITY,
+ 100, POSITIVE_INFINITY,
+ 100, POSITIVE_INFINITY);
+ path.curveTo(POSITIVE_INFINITY, POSITIVE_INFINITY,
+ POSITIVE_INFINITY, POSITIVE_INFINITY,
+ POSITIVE_INFINITY, POSITIVE_INFINITY);
+ path.closePath();
+
+ s = stroke.createStrokedShape(path);
+ checkEmptyPath(s);
+
+ // Check filtering -Infinity values:
+ path.reset();
+ path.moveTo(100, NEGATIVE_INFINITY);
+ path.lineTo(NEGATIVE_INFINITY, 100);
+ path.lineTo(NEGATIVE_INFINITY, NEGATIVE_INFINITY);
+
+ path.quadTo(NEGATIVE_INFINITY, 100,
+ NEGATIVE_INFINITY, 100);
+ path.quadTo(100, NEGATIVE_INFINITY,
+ 100, NEGATIVE_INFINITY);
+ path.quadTo(NEGATIVE_INFINITY, NEGATIVE_INFINITY,
+ NEGATIVE_INFINITY, NEGATIVE_INFINITY);
+
+ path.curveTo(NEGATIVE_INFINITY, 100,
+ NEGATIVE_INFINITY, 100,
+ NEGATIVE_INFINITY, 100);
+ path.curveTo(100, NEGATIVE_INFINITY,
+ 100, NEGATIVE_INFINITY,
+ 100, NEGATIVE_INFINITY);
+ path.curveTo(NEGATIVE_INFINITY, NEGATIVE_INFINITY,
+ NEGATIVE_INFINITY, NEGATIVE_INFINITY,
+ NEGATIVE_INFINITY, NEGATIVE_INFINITY);
+ path.closePath();
+
+ s = stroke.createStrokedShape(path);
+ checkEmptyPath(s);
+ }
+
+ private static void checkEmptyPath(final Shape s) {
+ final float[] coords = new float[6];
+ final PathIterator it = s.getPathIterator(null);
+
+ int n = 0;
+ for (; !it.isDone(); it.next()) {
+ int type = it.currentSegment(coords);
+ System.out.println("unexpected segment type= " + type
+ + " with coords: " + Arrays.toString(coords));
+ n++;
+ }
+ if (n != 0) {
+ System.out.println("path elements = " + n);
+ throw new IllegalStateException("Not empty path: "
+ + n + " path elements !");
+ }
+ }
+
+ private static void checkPixel(final Raster raster,
+ final int x, final int y,
+ final int expected) {
+
+ final int[] rgb = (int[]) raster.getDataElements(x, y, null);
+
+ if (rgb[0] != expected) {
+ throw new IllegalStateException("bad pixel at (" + x + ", " + y
+ + ") = " + rgb[0] + " expected: " + expected);
+ }
+ }
+
+ private static void checkPixelNotWhite(final Raster raster,
+ final int x, final int y) {
+
+ final int[] rgb = (int[]) raster.getDataElements(x, y, null);
+
+ if (rgb[0] == Color.WHITE.getRGB()) {
+ throw new IllegalStateException("bad pixel at (" + x + ", " + y
+ + ") is white (empty)");
+ }
+ }
}