diff --git a/src/share/classes/sun/java2d/marlin/Stroker.java b/src/share/classes/sun/java2d/marlin/Stroker.java index 1aa48411f04ba1cade6d92ca9cc7e250cda2a7fc..2946eda28f064a2cc02241a5baff3efe928d4705 100644 --- a/src/share/classes/sun/java2d/marlin/Stroker.java +++ b/src/share/classes/sun/java2d/marlin/Stroker.java @@ -87,6 +87,7 @@ final class Stroker implements PathConsumer2D, MarlinConst { 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 @@ final class Stroker implements PathConsumer2D, MarlinConst { this.out = pc2d; this.lineWidth2 = lineWidth / 2f; + this.invHalfLineWidth2Sq = 1f / (2f * lineWidth2 * lineWidth2); this.capStyle = capStyle; this.joinStyle = joinStyle; @@ -252,11 +254,11 @@ final class Stroker implements PathConsumer2D, MarlinConst { // 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 class Stroker implements PathConsumer2D, MarlinConst { 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; diff --git a/src/share/classes/sun/java2d/pisces/Stroker.java b/src/share/classes/sun/java2d/pisces/Stroker.java index 6d4a4c75ac54d34a7627b2e9257e6e142edbb45f..f388da4fd0c7c4c52e1660d36f08aed7e9e1ab1b 100644 --- a/src/share/classes/sun/java2d/pisces/Stroker.java +++ b/src/share/classes/sun/java2d/pisces/Stroker.java @@ -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 @@ final class Stroker implements PathConsumer2D { // 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 class Stroker implements PathConsumer2D { 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; diff --git a/test/sun/java2d/marlin/TextClipErrorTest.java b/test/sun/java2d/marlin/TextClipErrorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..efce137256cb3f30be21798b920e897594cd85d5 --- /dev/null +++ b/test/sun/java2d/marlin/TextClipErrorTest.java @@ -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 typeList = new ArrayList(len); + final ArrayList coordsList = new ArrayList(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, + }; + +}