8144718: Pisces / Marlin Strokers may generate invalid curves with huge coordinates and round joins
authorlbourges
Wed, 16 Dec 2015 13:24:19 -0800
changeset 35645 a96d68e3fda2
parent 35644 3ebb2066bbde
child 35646 845a3ebfeb00
8144718: Pisces / Marlin Strokers may generate invalid curves with huge coordinates and round joins Reviewed-by: flar, prr
jdk/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java
jdk/src/java.desktop/share/classes/sun/java2d/pisces/Stroker.java
jdk/test/sun/java2d/marlin/TextClipErrorTest.java
--- a/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java	Wed Dec 16 17:40:24 2015 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java	Wed Dec 16 13:24:19 2015 -0800
@@ -87,6 +87,7 @@
     private int joinStyle;
 
     private float lineWidth2;
+    private float invHalfLineWidth2Sq;
 
     private final float[] offset0 = new float[2];
     private final float[] offset1 = new float[2];
@@ -158,6 +159,7 @@
         this.out = pc2d;
 
         this.lineWidth2 = lineWidth / 2f;
+        this.invHalfLineWidth2Sq = 1f / (2f * lineWidth2 * lineWidth2);
         this.capStyle = capStyle;
         this.joinStyle = joinStyle;
 
@@ -252,11 +254,11 @@
         // The sign of the dot product of mx,my and omx,omy is equal to the
         // the sign of the cosine of ext
         // (ext is the angle between omx,omy and mx,my).
-        double cosext = omx * mx + omy * my;
+        final float cosext = omx * mx + omy * my;
         // If it is >=0, we know that abs(ext) is <= 90 degrees, so we only
         // need 1 curve to approximate the circle section that joins omx,omy
         // and mx,my.
-        final int numCurves = cosext >= 0 ? 1 : 2;
+        final int numCurves = (cosext >= 0f) ? 1 : 2;
 
         switch (numCurves) {
         case 1:
@@ -302,14 +304,22 @@
                                      final float mx, final float my,
                                      boolean rev)
     {
-        float cosext2 = (omx * mx + omy * my) / (2f * lineWidth2 * lineWidth2);
+        final float cosext2 = (omx * mx + omy * my) * invHalfLineWidth2Sq;
+
+        // check round off errors producing cos(ext) > 1 and a NaN below
+        // cos(ext) == 1 implies colinear segments and an empty join anyway
+        if (cosext2 >= 0.5f) {
+            // just return to avoid generating a flat curve:
+            return;
+        }
+
         // cv is the length of P1-P0 and P2-P3 divided by the radius of the arc
         // (so, cv assumes the arc has radius 1). P0, P1, P2, P3 are the points that
         // define the bezier curve we're computing.
         // It is computed using the constraints that P1-P0 and P3-P2 are parallel
         // to the arc tangents at the endpoints, and that |P1-P0|=|P3-P2|.
-        float cv = (float) ((4.0 / 3.0) * sqrt(0.5-cosext2) /
-                            (1.0 + sqrt(cosext2+0.5)));
+        float cv = (float) ((4.0 / 3.0) * sqrt(0.5 - cosext2) /
+                            (1.0 + sqrt(cosext2 + 0.5)));
         // if clockwise, we need to negate cv.
         if (rev) { // rev is equivalent to isCW(omx, omy, mx, my)
             cv = -cv;
--- a/jdk/src/java.desktop/share/classes/sun/java2d/pisces/Stroker.java	Wed Dec 16 17:40:24 2015 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/pisces/Stroker.java	Wed Dec 16 13:24:19 2015 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2007, 2015, 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
@@ -193,11 +193,11 @@
         // The sign of the dot product of mx,my and omx,omy is equal to the
         // the sign of the cosine of ext
         // (ext is the angle between omx,omy and mx,my).
-        double cosext = omx * mx + omy * my;
+        final float cosext = omx * mx + omy * my;
         // If it is >=0, we know that abs(ext) is <= 90 degrees, so we only
         // need 1 curve to approximate the circle section that joins omx,omy
         // and mx,my.
-        final int numCurves = cosext >= 0 ? 1 : 2;
+        final int numCurves = (cosext >= 0f) ? 1 : 2;
 
         switch (numCurves) {
         case 1:
@@ -242,14 +242,22 @@
                                      final float mx, final float my,
                                      boolean rev)
     {
-        float cosext2 = (omx * mx + omy * my) / (2 * lineWidth2 * lineWidth2);
+        final float cosext2 = (omx * mx + omy * my) / (2f * lineWidth2 * lineWidth2);
+
+        // check round off errors producing cos(ext) > 1 and a NaN below
+        // cos(ext) == 1 implies colinear segments and an empty join anyway
+        if (cosext2 >= 0.5f) {
+            // just return to avoid generating a flat curve:
+            return;
+        }
+
         // cv is the length of P1-P0 and P2-P3 divided by the radius of the arc
         // (so, cv assumes the arc has radius 1). P0, P1, P2, P3 are the points that
         // define the bezier curve we're computing.
         // It is computed using the constraints that P1-P0 and P3-P2 are parallel
         // to the arc tangents at the endpoints, and that |P1-P0|=|P3-P2|.
-        float cv = (float) ((4.0 / 3.0) * sqrt(0.5-cosext2) /
-                            (1.0 + sqrt(cosext2+0.5)));
+        float cv = (float) ((4.0 / 3.0) * sqrt(0.5 - cosext2) /
+                            (1.0 + sqrt(cosext2 + 0.5)));
         // if clockwise, we need to negate cv.
         if (rev) { // rev is equivalent to isCW(omx, omy, mx, my)
             cv = -cv;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/sun/java2d/marlin/TextClipErrorTest.java	Wed Dec 16 13:24:19 2015 -0800
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2015, 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.
+ */
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import static java.awt.geom.PathIterator.SEG_CLOSE;
+import static java.awt.geom.PathIterator.SEG_CUBICTO;
+import static java.awt.geom.PathIterator.SEG_LINETO;
+import static java.awt.geom.PathIterator.SEG_MOVETO;
+import static java.awt.geom.PathIterator.SEG_QUADTO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import javax.imageio.ImageIO;
+
+/**
+ * @test @bug 8144718
+ * @summary Check the Stroker.drawBezApproxForArc() bug (stoke with round
+ * joins): if cosext2 > 0.5, it generates curves with NaN coordinates
+ * @run main TextClipErrorTest
+ */
+public class TextClipErrorTest {
+
+    static final boolean SAVE_IMAGE = false;
+    static final boolean SERIALIZE = false;
+
+    public static void main(String[] args) {
+        Locale.setDefault(Locale.US);
+
+        // initialize j.u.l Looger:
+        final Logger log = Logger.getLogger("sun.java2d.marlin");
+        log.addHandler(new Handler() {
+            @Override
+            public void publish(LogRecord record) {
+                Throwable th = record.getThrown();
+                // detect potential Throwable thrown by XxxArrayCache.check():
+                if (th != null && th.getClass() == Throwable.class) {
+                    StackTraceElement[] stackElements = th.getStackTrace();
+
+                    for (int i = 0; i < stackElements.length; i++) {
+                        StackTraceElement e = stackElements[i];
+
+                        if (e.getClassName().startsWith("sun.java2d.marlin")
+                            && e.getClassName().contains("ArrayCache")
+                            && "check".equals(e.getMethodName()))
+                        {
+                            System.out.println("Test failed:\n"
+                                + record.getMessage());
+                            th.printStackTrace(System.out);
+
+                            throw new RuntimeException("Test failed: ", th);
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public void flush() {
+            }
+
+            @Override
+            public void close() throws SecurityException {
+            }
+        });
+
+        log.info("TextClipErrorTest: start");
+
+        // enable Marlin logging & internal checks:
+        System.setProperty("sun.java2d.renderer.log", "true");
+        System.setProperty("sun.java2d.renderer.useLogger", "true");
+        System.setProperty("sun.java2d.renderer.doChecks", "true");
+
+        BufferedImage image = new BufferedImage(256, 256,
+                                                BufferedImage.TYPE_INT_ARGB);
+
+        Graphics2D g2d = image.createGraphics();
+        g2d.setColor(Color.red);
+        try {
+            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                                 RenderingHints.VALUE_ANTIALIAS_ON);
+
+            Font font = g2d.getFont();
+            FontRenderContext frc = new FontRenderContext(
+                new AffineTransform(), true, true);
+
+            g2d.setStroke(new BasicStroke(4.0f,
+                                          BasicStroke.CAP_ROUND,
+                                          BasicStroke.JOIN_ROUND));
+
+            final Shape badShape;
+            if (SERIALIZE) {
+                final GlyphVector gv1 = font.createGlyphVector(frc, "\u00d6");
+                final Shape textShape = gv1.getOutline();
+
+                final AffineTransform at1 = AffineTransform.getTranslateInstance(
+                    -2091202.554154681, 5548.601436981691);
+                badShape = at1.createTransformedShape(textShape);
+                serializeShape(badShape);
+            } else {
+                badShape = deserializeShape();
+            }
+
+            g2d.draw(badShape);
+
+            // Draw anything within bounds and it fails:
+            g2d.draw(new Line2D.Double(10, 20, 30, 40));
+
+            if (SAVE_IMAGE) {
+                final File file = new File("TextClipErrorTest.png");
+                System.out.println("Writing file: " + file.getAbsolutePath());
+                ImageIO.write(image, "PNG", file);
+            }
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        } finally {
+            g2d.dispose();
+            log.info("TextClipErrorTest: end");
+        }
+    }
+
+    private static void serializeShape(Shape shape) {
+        final double[] coords = new double[6];
+
+        final int len = 32;
+        final ArrayList<Integer> typeList = new ArrayList<Integer>(len);
+        final ArrayList<double[]> coordsList = new ArrayList<double[]>(len);
+
+        for (PathIterator pi = shape.getPathIterator(null);
+                !pi.isDone(); pi.next())
+        {
+            switch (pi.currentSegment(coords)) {
+                case SEG_MOVETO:
+                    typeList.add(SEG_MOVETO);
+                    coordsList.add(Arrays.copyOf(coords, 2));
+                    break;
+                case SEG_LINETO:
+                    typeList.add(SEG_LINETO);
+                    coordsList.add(Arrays.copyOf(coords, 2));
+                    break;
+                case SEG_QUADTO:
+                    typeList.add(SEG_QUADTO);
+                    coordsList.add(Arrays.copyOf(coords, 4));
+                    break;
+                case SEG_CUBICTO:
+                    typeList.add(SEG_CUBICTO);
+                    coordsList.add(Arrays.copyOf(coords, 6));
+                    break;
+                case SEG_CLOSE:
+                    typeList.add(SEG_CLOSE);
+                    coordsList.add(null);
+                    break;
+                default:
+            }
+        }
+
+        final StringBuilder sb = new StringBuilder(1024);
+        // types:
+        sb.append("private static final int[] SHAPE_TYPES = new int[]{\n");
+        for (Integer i : typeList) {
+            sb.append(i).append(",\n");
+        }
+        sb.append("};\n");
+
+        // coords:
+        sb.append("private static final double[][] SHAPE_COORDS = new double[][]{\n");
+        for (double[] c : coordsList) {
+            if (c == null) {
+                sb.append("null,\n");
+            } else {
+                sb.append("new double[]{");
+                for (int i = 0; i < c.length; i++) {
+                    sb.append(c[i]).append(",");
+                }
+                sb.append("},\n");
+            }
+        }
+        sb.append("};\n");
+
+        System.out.println("Shape size: " + typeList.size());
+        System.out.println("Serialized shape:\n" + sb.toString());
+    }
+
+    private static Shape deserializeShape() {
+        final Path2D.Double path = new Path2D.Double();
+
+        for (int i = 0; i < SHAPE_TYPES.length; i++) {
+            double[] coords = SHAPE_COORDS[i];
+
+            switch (SHAPE_TYPES[i]) {
+                case SEG_MOVETO:
+                    path.moveTo(coords[0], coords[1]);
+                    break;
+                case SEG_LINETO:
+                    path.lineTo(coords[0], coords[1]);
+                    break;
+                case SEG_QUADTO:
+                    path.quadTo(coords[0], coords[1],
+                                coords[2], coords[3]);
+                    break;
+                case SEG_CUBICTO:
+                    path.curveTo(coords[0], coords[1],
+                                 coords[2], coords[3],
+                                 coords[4], coords[5]);
+                    break;
+                case SEG_CLOSE:
+                    path.closePath();
+                    break;
+                default:
+            }
+        }
+
+        return path;
+    }
+
+    // generated code:
+    private static final int[] SHAPE_TYPES = new int[]{
+        0,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        4,
+        0,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        4,
+        0,
+        1,
+        1,
+        1,
+        1,
+        4,
+        0,
+        1,
+        1,
+        1,
+        1,
+        4,
+    };
+
+    private static final double[][] SHAPE_COORDS = new double[][]{
+        new double[]{-2091197.819779681, 5540.648311981691,},
+        new double[]{-2091199.116654681, 5540.648311981691, -2091199.874467181, 5541.609249481691,},
+        new double[]{-2091200.632279681, 5542.570186981691, -2091200.632279681, 5544.242061981691,},
+        new double[]{-2091200.632279681, 5545.882686981691, -2091199.874467181, 5546.843624481691,},
+        new double[]{-2091199.116654681, 5547.804561981691, -2091197.819779681, 5547.804561981691,},
+        new double[]{-2091196.538529681, 5547.804561981691, -2091195.780717181, 5546.843624481691,},
+        new double[]{-2091195.022904681, 5545.882686981691, -2091195.022904681, 5544.242061981691,},
+        new double[]{-2091195.022904681, 5542.570186981691, -2091195.780717181, 5541.609249481691,},
+        new double[]{-2091196.538529681, 5540.648311981691, -2091197.819779681, 5540.648311981691,},
+        null,
+        new double[]{-2091197.819779681, 5539.695186981691,},
+        new double[]{-2091195.991654681, 5539.695186981691, -2091194.890092181, 5540.929561981691,},
+        new double[]{-2091193.788529681, 5542.163936981691, -2091193.788529681, 5544.242061981691,},
+        new double[]{-2091193.788529681, 5546.304561981691, -2091194.890092181, 5547.538936981691,},
+        new double[]{-2091195.991654681, 5548.773311981691, -2091197.819779681, 5548.773311981691,},
+        new double[]{-2091199.663529681, 5548.773311981691, -2091200.772904681, 5547.538936981691,},
+        new double[]{-2091201.882279681, 5546.304561981691, -2091201.882279681, 5544.242061981691,},
+        new double[]{-2091201.882279681, 5542.163936981691, -2091200.772904681, 5540.929561981691,},
+        new double[]{-2091199.663529681, 5539.695186981691, -2091197.819779681, 5539.695186981691,},
+        null,
+        new double[]{-2091197.210404681, 5537.835811981691,},
+        new double[]{-2091196.022904681, 5537.835811981691,},
+        new double[]{-2091196.022904681, 5539.023311981691,},
+        new double[]{-2091197.210404681, 5539.023311981691,},
+        new double[]{-2091197.210404681, 5537.835811981691,},
+        null,
+        new double[]{-2091199.632279681, 5537.835811981691,},
+        new double[]{-2091198.444779681, 5537.835811981691,},
+        new double[]{-2091198.444779681, 5539.023311981691,},
+        new double[]{-2091199.632279681, 5539.023311981691,},
+        new double[]{-2091199.632279681, 5537.835811981691,},
+        null,
+    };
+
+}