8167310: The graphics clip is incorrectly rounded for some fractional scales
authorserb
Fri, 14 Oct 2016 19:12:37 +0300
changeset 41788 1a90d713a0c5
parent 41787 a7f3130da805
child 41789 a237131eb204
8167310: The graphics clip is incorrectly rounded for some fractional scales Reviewed-by: flar
jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java
jdk/src/java.desktop/share/classes/sun/java2d/pipe/Region.java
jdk/test/java/awt/Graphics/IncorrectFractionalClip/IncorrectFractionalClip.java
--- a/jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java	Fri Oct 14 16:30:28 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/SunGraphics2D.java	Fri Oct 14 19:12:37 2016 +0300
@@ -1902,11 +1902,7 @@
             clipRegion = devClip;
         } else if (usrClip instanceof Rectangle2D) {
             clipState = CLIP_RECTANGULAR;
-            if (usrClip instanceof Rectangle) {
-                clipRegion = devClip.getIntersection((Rectangle)usrClip);
-            } else {
-                clipRegion = devClip.getIntersection(usrClip.getBounds());
-            }
+            clipRegion = devClip.getIntersection((Rectangle2D) usrClip);
         } else {
             PathIterator cpi = usrClip.getPathIterator(null);
             int box[] = new int[4];
--- a/jdk/src/java.desktop/share/classes/sun/java2d/pipe/Region.java	Fri Oct 14 16:30:28 2016 +0300
+++ b/jdk/src/java.desktop/share/classes/sun/java2d/pipe/Region.java	Fri Oct 14 19:12:37 2016 +0300
@@ -28,10 +28,13 @@
 import java.awt.Rectangle;
 import java.awt.Shape;
 import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
 import java.awt.geom.RectangularShape;
 
 import sun.java2d.loops.TransformHelper;
 
+import static java.lang.Double.isNaN;
+
 /**
  * This class encapsulates a definition of a two dimensional region which
  * consists of a number of Y ranges each containing multiple X bands.
@@ -118,6 +121,34 @@
     }
 
     /**
+     * Returns the closest {@code int} to the argument, with ties rounding to
+     * negative infinity.
+     * <p>
+     * Special cases:
+     * <ul><li>If the argument is NaN, the result is 0.
+     * <li>If the argument is negative infinity or any value less than or
+     * equal to the value of {@code Integer.MIN_VALUE}, the result is
+     * equal to the value of {@code Integer.MIN_VALUE}.
+     * <li>If the argument is positive infinity or any value greater than or
+     * equal to the value of {@code Integer.MAX_VALUE}, the result is
+     * equal to the value of {@code Integer.MAX_VALUE}.</ul>
+     *
+     * @param   coordinate a floating-point value to be rounded to an integer
+     * @return  the value of the argument rounded to the nearest
+     *          {@code int} value.
+     */
+    public static int clipRound(final double coordinate) {
+        final double newv = coordinate - 0.5;
+        if (newv < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        if (newv > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        return (int) Math.ceil(newv);
+    }
+
+    /**
      * Multiply the scale factor {@code sv} and the value {@code v} with
      * appropriate clipping to the bounds of Integer resolution. If the answer
      * would be greater than {@code Integer.MAX_VALUE} then {@code
@@ -559,6 +590,33 @@
 
     /**
      * Returns a Region object that represents the intersection of
+     * this object with the specified Rectangle2D. The return value
+     * may be this same object if no clipping occurs.
+     */
+    public Region getIntersection(final Rectangle2D r) {
+        if (r instanceof Rectangle) {
+            return getIntersection((Rectangle) r);
+        }
+        return getIntersectionXYXY(r.getMinX(), r.getMinY(), r.getMaxX(),
+                                   r.getMaxY());
+    }
+
+    /**
+     * Returns a Region object that represents the intersection of
+     * this object with the specified rectangular area. The return
+     * value may be this same object if no clipping occurs.
+     */
+    public Region getIntersectionXYXY(double lox, double loy, double hix,
+                                      double hiy) {
+        if (isNaN(lox) || isNaN(loy) || isNaN(hix) || isNaN(hiy)) {
+            return EMPTY_REGION;
+        }
+        return getIntersectionXYXY(clipRound(lox), clipRound(loy),
+                                   clipRound(hix), clipRound(hiy));
+    }
+
+    /**
+     * Returns a Region object that represents the intersection of
      * this object with the specified rectangular area.  The return
      * value may be this same object if no clipping occurs.
      */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/awt/Graphics/IncorrectFractionalClip/IncorrectFractionalClip.java	Fri Oct 14 19:12:37 2016 +0300
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2016, 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.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.Area;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+import javax.imageio.ImageIO;
+
+import static java.awt.RenderingHints.KEY_STROKE_CONTROL;
+import static java.awt.RenderingHints.VALUE_STROKE_PURE;
+import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
+
+/**
+ * @test
+ * @key headful
+ * @bug 8167310
+ * @summary The clip should be correct if the scale is fractional
+ */
+public final class IncorrectFractionalClip {
+
+    private static final int SIZE = 128;
+
+    public static final Color RED = new Color(255, 0, 0, 100);
+
+    public static final Color GREEN = new Color(0, 255, 0, 100);
+
+    public static final Color WHITE = new Color(0, 0, 0, 0);
+
+    public static final BasicStroke STROKE = new BasicStroke(2.01f);
+
+    private static final double[] SCALES = {
+            0.1, 0.25, 0.4, 0.5, 0.6, 1, 1.4, 1.5, 1.6, 2.0, 2.4, 2.5, 2.6, 4
+    };
+
+    static BufferedImage bi;
+
+    static BufferedImage gold;
+
+    static BufferedImage redI;
+
+    static BufferedImage greenI;
+
+    public static void main(final String[] args) throws Exception {
+        bi = new BufferedImage(SIZE, SIZE, TYPE_INT_ARGB);
+        gold = new BufferedImage(SIZE, SIZE, TYPE_INT_ARGB);
+        redI = createImage(RED);
+        greenI = createImage(GREEN);
+
+        System.out.println("Will test fillRect");
+        test(0, true);
+        test(0, false);
+        System.out.println("Will test DrawImage");
+        test(1, true);
+        test(1, false);
+        System.out.println("Will test drawLine");
+        test(2, true);
+        test(2, false);
+    }
+
+    /**
+     * This method draws/fills a number of rectangle, images and lines. Each
+     * time the clip is set as one vertical/horizontal line. The resulted image
+     * should not have any overlapping of different colors. The clip is set via
+     * rectangle(test) and via shape(gold). Both images should be identical.
+     */
+    private static void test(final int testId, final boolean horiz)
+            throws Exception {
+        for (final double scale : SCALES) {
+            // Initialize the test and gold images
+            drawToImage(testId, horiz, scale, bi, /* Rectangle */ false);
+            drawToImage(testId, horiz, scale, gold, /* Shape */ true);
+            validate(bi, gold, testId);
+        }
+    }
+
+    private static void drawToImage(int testId, boolean horiz, double scale,
+                                    BufferedImage image, boolean shape) {
+        Graphics2D g = image.createGraphics();
+        g.setComposite(AlphaComposite.Src);
+        g.setColor(WHITE);
+        g.fillRect(0, 0, bi.getWidth(), bi.getHeight());
+        g.setComposite(AlphaComposite.SrcOver);
+        g.setRenderingHint(KEY_STROKE_CONTROL, VALUE_STROKE_PURE);
+
+        // set the scale in one direction
+        if (horiz) {
+            g.scale(scale, 1);
+        } else {
+            g.scale(1, scale);
+        }
+        // cover all units in the user space to touch all pixels in the
+        // image after transform
+        final int destSize = (int) Math.ceil(SIZE / scale);
+        final int destW;
+        final int destH;
+        if (horiz) {
+            destW = destSize;
+            destH = SIZE;
+        } else {
+            destW = SIZE;
+            destH = destSize;
+        }
+        for (int step = 0; step < destSize; ++step) {
+            if (horiz) {
+                if (!shape) {
+                    g.setClip(step, 0, 1, SIZE);
+                } else{
+                    g.setClip(new Area(new Rectangle(step, 0, 1, SIZE)));
+                }
+            } else {
+                if (!shape) {
+                    g.setClip(0, step, SIZE, 1);
+                }else{
+                    g.setClip(new Area(new Rectangle(0, step, SIZE, 1)));
+                }
+            }
+            switch (testId) {
+                case 0:
+                    g.setColor(step % 2 == 0 ? RED : GREEN);
+                    g.fillRect(0, 0, destW, destH);
+                    break;
+                case 1:
+                    g.drawImage(step % 2 == 0 ? redI : greenI, 0, 0,
+                                destW, destH, null);
+                    break;
+                case 2:
+                    g.setColor(step % 2 == 0 ? RED : GREEN);
+                    g.setStroke(STROKE);
+                    if (horiz) {
+                        g.drawLine(step, 0, step, SIZE);
+                    } else {
+                        g.drawLine(0, step, SIZE, step);
+                    }
+                    break;
+                default:
+                    throw new RuntimeException();
+            }
+        }
+        g.dispose();
+    }
+
+    private static void validate(final BufferedImage bi, BufferedImage gold,
+                                 final int testID) throws Exception {
+        for (int x = 0; x < SIZE; ++x) {
+            for (int y = 0; y < SIZE; ++y) {
+                int rgb = bi.getRGB(x, y);
+                int goldRGB = gold.getRGB(x, y);
+                if ((rgb != GREEN.getRGB() && rgb != RED.getRGB())
+                        || rgb != goldRGB) {
+                    ImageIO.write(bi, "png", new File("image.png"));
+                    ImageIO.write(gold, "png", new File("gold.png"));
+                    throw new RuntimeException("Test failed.");
+                }
+            }
+        }
+    }
+
+    private static BufferedImage createImage(final Color color) {
+        BufferedImage bi = new BufferedImage(SIZE, SIZE, TYPE_INT_ARGB);
+        Graphics2D g = bi.createGraphics();
+        g.setComposite(AlphaComposite.Src);
+        g.setColor(color);
+        g.fillRect(0, 0, bi.getWidth(), bi.getHeight());
+        g.dispose();
+        return bi;
+    }
+}