changeset 9669:f3841ee10a08

8041129: [OGL] surface->sw blit is extremely slow 8017626: [OGL] Translucent VolatileImages don't paint correctly Reviewed-by: bae, flar
author serb
date Wed, 04 Jun 2014 16:55:06 +0400
parents f48a12d2d32f
children 3653b5c59f0b
files src/share/classes/sun/java2d/opengl/OGLBlitLoops.java src/share/native/sun/java2d/opengl/OGLBlitLoops.c test/java/awt/image/DrawImage/IncorrectAlphaSurface2SW.java test/java/awt/image/DrawImage/IncorrectDestinationOffset.java test/java/awt/image/DrawImage/IncorrectSourceOffset.java
diffstat 5 files changed, 495 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/sun/java2d/opengl/OGLBlitLoops.java	Fri May 30 14:06:50 2014 +0400
+++ b/src/share/classes/sun/java2d/opengl/OGLBlitLoops.java	Wed Jun 04 16:55:06 2014 +0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2014, 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
@@ -71,6 +71,8 @@
             // surface->sw ops
             new OGLSurfaceToSwBlit(SurfaceType.IntArgb,
                                    OGLSurfaceData.PF_INT_ARGB),
+            new OGLSurfaceToSwBlit(SurfaceType.IntArgbPre,
+                                   OGLSurfaceData.PF_INT_ARGB_PRE),
 
             // sw->surface ops
             blitIntArgbPreToSurface,
@@ -505,12 +507,12 @@
     }
 }
 
-class OGLSurfaceToSwBlit extends Blit {
+final class OGLSurfaceToSwBlit extends Blit {
 
-    private int typeval;
+    private final int typeval;
 
-    // REMIND: destination will actually be opaque/premultiplied...
-    OGLSurfaceToSwBlit(SurfaceType dstType, int typeval) {
+    // destination will actually be ArgbPre or Argb
+    OGLSurfaceToSwBlit(final SurfaceType dstType,final int typeval) {
         super(OGLSurfaceData.OpenGLSurface,
               CompositeType.SrcNoEa,
               dstType);
--- a/src/share/native/sun/java2d/opengl/OGLBlitLoops.c	Fri May 30 14:06:50 2014 +0400
+++ b/src/share/native/sun/java2d/opengl/OGLBlitLoops.c	Wed Jun 04 16:55:06 2014 +0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2014, 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
@@ -34,6 +34,10 @@
 #include "OGLSurfaceData.h"
 #include "GraphicsPrimitiveMgr.h"
 
+#include <stdlib.h> // malloc
+#include <string.h> // memcpy
+#include "IntArgbPre.h"
+
 extern OGLPixelFormat PixelFormats[];
 
 /**
@@ -335,6 +339,9 @@
                                         0, 0, sw, sh,
                                         pf->format, pf->type,
                                         srcInfo->rasBase);
+
+                    j2d_glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
+                    j2d_glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
                 }
 
                 // the texture image is "right side up", so we align the
@@ -697,6 +704,50 @@
 }
 
 /**
+ * This method makes vertical flip of the provided area of Surface and convert
+ * pixel's data from argbPre to argb format if requested.
+ */
+void flip(void *pDst, juint w, juint h, jint scanStride, jboolean convert) {
+    const size_t clippedStride = 4 * w;
+    void *tempRow = (h > 1 && !convert) ? malloc(clippedStride) : NULL;
+    juint i = 0;
+    juint step = 0;
+    // vertical flip and convert argbpre to argb if necessary
+    for (; i < h / 2; ++i) {
+        juint *r1 = PtrAddBytes(pDst, (i * scanStride));
+        juint *r2 = PtrAddBytes(pDst, (h - i - 1) * scanStride);
+        if (tempRow) {
+            // fast path
+            memcpy(tempRow, r1, clippedStride);
+            memcpy(r1, r2, clippedStride);
+            memcpy(r2, tempRow, clippedStride);
+        } else {
+            // slow path
+            for (step = 0; step < w; ++step) {
+                juint tmp = r1[step];
+                if (convert) {
+                    LoadIntArgbPreTo1IntArgb(r2, 0, step, r1[step]);
+                    LoadIntArgbPreTo1IntArgb(&tmp, 0, 0, r2[step]);
+                } else {
+                    r1[step] = r2[step];
+                    r2[step] = tmp;
+                }
+            }
+        }
+    }
+    // convert the middle line if necessary
+    if (convert && h % 2) {
+        juint *r1 = PtrAddBytes(pDst, (i * scanStride));
+        for (step = 0; step < w; ++step) {
+            LoadIntArgbPreTo1IntArgb(r1, 0, step, r1[step]);
+        }
+    }
+    if (tempRow) {
+        free(tempRow);
+    }
+}
+
+/**
  * Specialized blit method for copying a native OpenGL "Surface" (pbuffer,
  * window, etc.) to a system memory ("Sw") surface.
  */
@@ -758,7 +809,9 @@
             width = srcInfo.bounds.x2 - srcInfo.bounds.x1;
             height = srcInfo.bounds.y2 - srcInfo.bounds.y1;
 
-            j2d_glPixelStorei(GL_PACK_SKIP_PIXELS, dstx);
+            pDst = PtrAddBytes(pDst, dstx * dstInfo.pixelStride);
+            pDst = PtrAddBytes(pDst, dsty * dstInfo.scanStride);
+
             j2d_glPixelStorei(GL_PACK_ROW_LENGTH,
                               dstInfo.scanStride / dstInfo.pixelStride);
             j2d_glPixelStorei(GL_PACK_ALIGNMENT, pf.alignment);
@@ -779,27 +832,20 @@
 
             // this accounts for lower-left origin of the source region
             srcx = srcOps->xOffset + srcx;
-            srcy = srcOps->yOffset + srcOps->height - (srcy + 1);
+            srcy = srcOps->yOffset + srcOps->height - srcy - height;
 
-            // we must read one scanline at a time because there is no way
-            // to read starting at the top-left corner of the source region
-            while (height > 0) {
-                j2d_glPixelStorei(GL_PACK_SKIP_ROWS, dsty);
-                j2d_glReadPixels(srcx, srcy, width, 1,
-                                 pf.format, pf.type, pDst);
-                srcy--;
-                dsty++;
-                height--;
-            }
-
+            // Note that glReadPixels() is extremely slow!
+            // So we call it only once and flip the image using memcpy.
+            j2d_glReadPixels(srcx, srcy, width, height,
+                             pf.format, pf.type, pDst);
+            // It was checked above that width and height are positive.
+            flip(pDst, (juint) width, (juint) height, dstInfo.scanStride,
+                 !pf.isPremult && !srcOps->isOpaque);
 #ifdef MACOSX
             if (srcOps->isOpaque) {
                 j2d_glPixelTransferf(GL_ALPHA_BIAS, 0.0);
             }
 #endif
-
-            j2d_glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
-            j2d_glPixelStorei(GL_PACK_SKIP_ROWS, 0);
             j2d_glPixelStorei(GL_PACK_ROW_LENGTH, 0);
             j2d_glPixelStorei(GL_PACK_ALIGNMENT, 4);
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/awt/image/DrawImage/IncorrectAlphaSurface2SW.java	Wed Jun 04 16:55:06 2014 +0400
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2014, 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.Color;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsEnvironment;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.VolatileImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+import static java.awt.Transparency.TRANSLUCENT;
+import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR;
+import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE;
+import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
+import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;
+
+/**
+ * @test
+ * @bug 8017626
+ * @summary Tests drawing transparent volatile image to transparent BI.
+ *          Results of the blit compatibleImage to transparent BI used for
+ *          comparison.
+ * @author Sergey Bylokhov
+ */
+public final class IncorrectAlphaSurface2SW {
+
+    private static final int[] SCALES = {1, 2, 4, 8};
+    private static final int[] SIZES = {1, 2, 3, 127, 128, 254, 255, 256};
+    private static final int[] dstTypes = {TYPE_INT_ARGB, TYPE_INT_ARGB_PRE,
+            TYPE_4BYTE_ABGR, TYPE_4BYTE_ABGR_PRE};
+    private static final int[] srcTypes = {TRANSLUCENT};
+
+
+    public static void main(final String[] args) throws IOException {
+        GraphicsEnvironment ge = GraphicsEnvironment
+                .getLocalGraphicsEnvironment();
+        GraphicsConfiguration gc = ge.getDefaultScreenDevice()
+                                     .getDefaultConfiguration();
+        BufferedImage destVI;
+        BufferedImage destBI;
+        BufferedImage sourceBI;
+        VolatileImage sourceVI;
+
+        for (final int s : SIZES) {
+            for (final int srcType : srcTypes) {
+                for (final int dstType : dstTypes) {
+                    for (final int scale : SCALES) {
+                        int sw = s * scale;
+                        destVI = new BufferedImage(sw, sw, dstType);
+                        destBI = new BufferedImage(sw, sw, dstType);
+                        sourceBI = gc.createCompatibleImage(sw, sw, srcType);
+                        sourceVI = gc.createCompatibleVolatileImage(s, s, srcType);
+
+                        // draw to dest BI using compatible image
+                        fill(sourceBI, s);
+                        Graphics2D big = destBI.createGraphics();
+                        big.setComposite(AlphaComposite.Src);
+                        big.drawImage(sourceBI, 0, 0, sw, sw, null);
+                        big.dispose();
+
+                        // draw to dest BI using compatible image
+                        fill(sourceVI, s);
+                        drawVItoBI(gc, destVI, sourceVI);
+
+                        validate(destVI, destBI);
+                        sourceVI.flush();
+                    }
+                }
+            }
+        }
+        System.out.println("Test PASSED");
+    }
+
+    private static void drawVItoBI(GraphicsConfiguration gc,
+                                   BufferedImage bi, VolatileImage vi) {
+        while (true) {
+            vi.validate(gc);
+            fill(vi, vi.getHeight());
+            if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
+                try {
+                    Thread.sleep(100);
+                } catch (final InterruptedException ignored) {
+                }
+                continue;
+            }
+
+            Graphics2D big = bi.createGraphics();
+            big.setComposite(AlphaComposite.Src);
+            big.drawImage(vi, 0, 0, bi.getWidth(), bi.getHeight(), null);
+            big.dispose();
+
+            if (vi.contentsLost()) {
+                try {
+                    Thread.sleep(100);
+                } catch (final InterruptedException ignored) {
+                }
+                continue;
+            }
+            break;
+        }
+    }
+
+    private static void validate(BufferedImage bi, BufferedImage gold)
+            throws IOException {
+        for (int x = 0; x < bi.getWidth(); ++x) {
+            for (int y = 0; y < bi.getHeight(); ++y) {
+                if (gold.getRGB(x, y) != bi.getRGB(x, y)) {
+                    System.err.println("Expected color = " + gold.getRGB(x, y));
+                    System.err.println("Actual color = " + bi.getRGB(x, y));
+                    ImageIO.write(gold, "png", new File("gold.png"));
+                    ImageIO.write(bi, "png", new File("bi.png"));
+                    throw new RuntimeException("Test failed.");
+                }
+            }
+        }
+    }
+
+    /**
+     * Fills the whole image using different alpha for each row.
+     *
+     * @param image to fill
+     */
+    private static void fill(final Image image, final int size) {
+        Graphics2D graphics = (Graphics2D) image.getGraphics();
+        graphics.setComposite(AlphaComposite.Src);
+        graphics.setColor(Color.GREEN);
+        graphics.fillRect(0, 0, image.getWidth(null), image.getHeight(null));
+        int row = image.getHeight(null) / size;
+        for (int i = 0; i < size; ++i) {
+            graphics.setColor(new Color(23, 127, 189, i));
+            graphics.fillRect(0, i * row, image.getWidth(null), row);
+        }
+        graphics.dispose();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/awt/image/DrawImage/IncorrectDestinationOffset.java	Wed Jun 04 16:55:06 2014 +0400
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2014, 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.Color;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsEnvironment;
+import java.awt.image.BufferedImage;
+import java.awt.image.VolatileImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+/**
+ * @test
+ * @bug 8041129
+ * @summary Destination offset should be correct in case of Surface->SW blit.
+ *          Destination outside of the drawing area should be untouched.
+ * @author Sergey Bylokhov
+ */
+public final class IncorrectDestinationOffset {
+
+    private static final int SIZE = 128;
+    private static final double[] SCALES = {0.25, 0.5, 1, 1.5, 2.0, 4};
+
+    public static void main(final String[] args) throws IOException {
+        GraphicsEnvironment ge = GraphicsEnvironment
+                .getLocalGraphicsEnvironment();
+        GraphicsConfiguration gc = ge.getDefaultScreenDevice()
+                                     .getDefaultConfiguration();
+        VolatileImage vi = gc.createCompatibleVolatileImage(SIZE, SIZE);
+        BufferedImage bi = new BufferedImage(SIZE, SIZE,
+                                             BufferedImage.TYPE_INT_ARGB);
+        for (double scale : SCALES) {
+            while (true) {
+                // initialize Volatile Image
+                vi.validate(gc);
+                Graphics2D g2d = vi.createGraphics();
+                g2d.setColor(Color.green);
+                g2d.fillRect(0, 0, SIZE, SIZE);
+                g2d.dispose();
+
+                if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
+                    try {
+                        Thread.sleep(100);
+                    } catch (InterruptedException ignored) {
+                    }
+                    continue;
+                }
+                // Draw the VolatileImage to BI with scale and offsets
+                Graphics2D g = bi.createGraphics();
+                g.setComposite(AlphaComposite.Src);
+                g.setColor(Color.RED);
+                g.fillRect(0, 0, SIZE / 2, SIZE / 2);
+                g.setColor(Color.BLUE);
+                g.fillRect(SIZE / 2, 0, SIZE / 2, SIZE / 2);
+                g.setColor(Color.ORANGE);
+                g.fillRect(0, SIZE / 2, SIZE / 2, SIZE / 2);
+                g.setColor(Color.MAGENTA);
+                g.fillRect(SIZE / 2, SIZE / 2, SIZE / 2, SIZE / 2);
+
+                int point2draw = (int) (100 * scale);
+                int size2draw = (int) (SIZE * scale);
+                g.drawImage(vi, point2draw, point2draw, size2draw, size2draw,
+                            null);
+                g.dispose();
+
+                if (vi.contentsLost()) {
+                    try {
+                        Thread.sleep(100);
+                    } catch (InterruptedException ignored) {
+                    }
+                    continue;
+                }
+                validate(bi, point2draw, size2draw);
+                break;
+            }
+        }
+    }
+
+    private static void validate(BufferedImage bi, int point2draw,
+                                 int size2draw)
+            throws IOException {
+        for (int x = 0; x < SIZE; ++x) {
+            for (int y = 0; y < SIZE; ++y) {
+                if (isInsideGreenArea(point2draw, size2draw, x, y)) {
+                    if (bi.getRGB(x, y) != Color.green.getRGB()) {
+                        ImageIO.write(bi, "png", new File("image.png"));
+                        throw new RuntimeException("Test failed.");
+                    }
+                } else {
+                    if (isRedArea(x, y)) {
+                        if (bi.getRGB(x, y) != Color.red.getRGB()) {
+                            ImageIO.write(bi, "png", new File("image.png"));
+                            throw new RuntimeException("Test failed.");
+                        }
+                    }
+                    if (isBlueArea(x, y)) {
+                        if (bi.getRGB(x, y) != Color.blue.getRGB()) {
+                            ImageIO.write(bi, "png", new File("image.png"));
+                            throw new RuntimeException("Test failed.");
+                        }
+                    }
+                    if (isOrangeArea(x, y)) {
+                        if (bi.getRGB(x, y) != Color.orange.getRGB()) {
+                            ImageIO.write(bi, "png", new File("image.png"));
+                            throw new RuntimeException("Test failed.");
+                        }
+                    }
+                    if (isMagentaArea(x, y)) {
+                        if (bi.getRGB(x, y) != Color.magenta.getRGB()) {
+                            ImageIO.write(bi, "png", new File("image.png"));
+                            throw new RuntimeException("Test failed.");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private static boolean isRedArea(int x, int y) {
+        return x < SIZE / 2 && y < SIZE / 2;
+    }
+
+    private static boolean isBlueArea(int x, int y) {
+        return x >= SIZE / 2 && y < SIZE / 2;
+    }
+
+    private static boolean isOrangeArea(int x, int y) {
+        return x < SIZE / 2 && y >= SIZE / 2;
+    }
+
+    private static boolean isMagentaArea(int x, int y) {
+        return x >= SIZE / 2 && y >= SIZE / 2;
+    }
+
+    private static boolean isInsideGreenArea(int point2draw, int size2draw,
+                                             int x, int y) {
+        return x >= point2draw && x < point2draw + size2draw && y >=
+                point2draw && y < point2draw + size2draw;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/awt/image/DrawImage/IncorrectSourceOffset.java	Wed Jun 04 16:55:06 2014 +0400
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2014, 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.Color;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsEnvironment;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.VolatileImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+/**
+ * @test
+ * @bug 8041129
+ * @summary Tests asymmetric source offsets.
+ * @author Sergey Bylokhov
+ */
+public final class IncorrectSourceOffset {
+
+    public static void main(final String[] args) throws IOException {
+        GraphicsEnvironment ge = GraphicsEnvironment
+                .getLocalGraphicsEnvironment();
+        GraphicsConfiguration gc = ge.getDefaultScreenDevice()
+                                     .getDefaultConfiguration();
+        VolatileImage vi = gc.createCompatibleVolatileImage(511, 255);
+        BufferedImage bi = new BufferedImage(511, 255,
+                                             BufferedImage.TYPE_INT_ARGB);
+        BufferedImage gold = new BufferedImage(511, 255,
+                                               BufferedImage.TYPE_INT_ARGB);
+        fill(gold);
+        while (true) {
+            vi.validate(gc);
+            fill(vi);
+            if (vi.validate(gc) != VolatileImage.IMAGE_OK) {
+                try {
+                    Thread.sleep(100);
+                } catch (final InterruptedException ignored) {
+                }
+                continue;
+            }
+
+            Graphics2D big = bi.createGraphics();
+            big.drawImage(vi, 7, 11, 127, 111, 7, 11, 127, 111, null);
+            big.dispose();
+            if (vi.contentsLost()) {
+                try {
+                    Thread.sleep(100);
+                } catch (final InterruptedException ignored) {
+                }
+                continue;
+            }
+            break;
+        }
+
+        for (int x = 7; x < 127; ++x) {
+            for (int y = 11; y < 111; ++y) {
+                if (gold.getRGB(x, y) != bi.getRGB(x, y)) {
+                    ImageIO.write(gold, "png", new File("gold.png"));
+                    ImageIO.write(bi, "png", new File("bi.png"));
+                    throw new RuntimeException("Test failed.");
+                }
+            }
+        }
+    }
+
+    private static void fill(Image image) {
+        Graphics2D graphics = (Graphics2D) image.getGraphics();
+        graphics.setComposite(AlphaComposite.Src);
+        for (int i = 0; i < image.getHeight(null); ++i) {
+            graphics.setColor(new Color(i, 0, 0));
+            graphics.fillRect(0, i, image.getWidth(null), 1);
+        }
+        graphics.dispose();
+    }
+}