1 /*
   2  * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.plaf.nimbus;
  26 
  27 import java.awt.AlphaComposite;
  28 import java.awt.Graphics2D;
  29 import java.awt.Transparency;
  30 import java.awt.GraphicsConfiguration;
  31 import java.awt.GraphicsEnvironment;
  32 import java.awt.image.BufferedImage;
  33 import java.awt.image.Raster;
  34 import java.awt.image.WritableRaster;
  35 import java.awt.image.ColorModel;
  36 
  37 /**
  38  * EffectUtils
  39  *
  40  * @author Created by Jasper Potts (Jun 18, 2007)
  41  */
  42 class EffectUtils {
  43 
  44     /**
  45      * Clear a transparent image to 100% transparent
  46      *
  47      * @param img The image to clear
  48      */
  49     static void clearImage(BufferedImage img) {
  50         Graphics2D g2 = img.createGraphics();
  51         g2.setComposite(AlphaComposite.Clear);
  52         g2.fillRect(0, 0, img.getWidth(), img.getHeight());
  53         g2.dispose();
  54     }
  55 
  56     // =================================================================================================================
  57     // Blur
  58 
  59     /**
  60      * Apply Gaussian Blur to Image
  61      *
  62      * @param src    The image tp
  63      * @param dst    The destination image to draw blured src image into, null if you want a new one created
  64      * @param radius The blur kernel radius
  65      * @return The blured image
  66      */
  67     static BufferedImage gaussianBlur(BufferedImage src, BufferedImage dst, int radius) {
  68         int width = src.getWidth();
  69         int height = src.getHeight();
  70         if (dst == null || dst.getWidth() != width || dst.getHeight() != height || src.getType() != dst.getType()) {
  71             dst = createColorModelCompatibleImage(src);
  72         }
  73         float[] kernel = createGaussianKernel(radius);
  74         if (src.getType() == BufferedImage.TYPE_INT_ARGB) {
  75             int[] srcPixels = new int[width * height];
  76             int[] dstPixels = new int[width * height];
  77             getPixels(src, 0, 0, width, height, srcPixels);
  78             // horizontal pass
  79             blur(srcPixels, dstPixels, width, height, kernel, radius);
  80             // vertical pass
  81             //noinspection SuspiciousNameCombination
  82             blur(dstPixels, srcPixels, height, width, kernel, radius);
  83             // the result is now stored in srcPixels due to the 2nd pass
  84             setPixels(dst, 0, 0, width, height, srcPixels);
  85         } else if (src.getType() == BufferedImage.TYPE_BYTE_GRAY) {
  86             byte[] srcPixels = new byte[width * height];
  87             byte[] dstPixels = new byte[width * height];
  88             getPixels(src, 0, 0, width, height, srcPixels);
  89             // horizontal pass
  90             blur(srcPixels, dstPixels, width, height, kernel, radius);
  91             // vertical pass
  92             //noinspection SuspiciousNameCombination
  93             blur(dstPixels, srcPixels, height, width, kernel, radius);
  94             // the result is now stored in srcPixels due to the 2nd pass
  95             setPixels(dst, 0, 0, width, height, srcPixels);
  96         } else {
  97             throw new IllegalArgumentException("EffectUtils.gaussianBlur() src image is not a supported type, type=[" +
  98                     src.getType() + "]");
  99         }
 100         return dst;
 101     }
 102 
 103     /**
 104      * <p>Blurs the source pixels into the destination pixels. The force of the blur is specified by the radius which
 105      * must be greater than 0.</p> <p>The source and destination pixels arrays are expected to be in the INT_ARGB
 106      * format.</p> <p>After this method is executed, dstPixels contains a transposed and filtered copy of
 107      * srcPixels.</p>
 108      *
 109      * @param srcPixels the source pixels
 110      * @param dstPixels the destination pixels
 111      * @param width     the width of the source picture
 112      * @param height    the height of the source picture
 113      * @param kernel    the kernel of the blur effect
 114      * @param radius    the radius of the blur effect
 115      */
 116     private static void blur(int[] srcPixels, int[] dstPixels,
 117                              int width, int height,
 118                              float[] kernel, int radius) {
 119         float a;
 120         float r;
 121         float g;
 122         float b;
 123 
 124         int ca;
 125         int cr;
 126         int cg;
 127         int cb;
 128 
 129         for (int y = 0; y < height; y++) {
 130             int index = y;
 131             int offset = y * width;
 132 
 133             for (int x = 0; x < width; x++) {
 134                 a = r = g = b = 0.0f;
 135 
 136                 for (int i = -radius; i <= radius; i++) {
 137                     int subOffset = x + i;
 138                     if (subOffset < 0 || subOffset >= width) {
 139                         subOffset = (x + width) % width;
 140                     }
 141 
 142                     int pixel = srcPixels[offset + subOffset];
 143                     float blurFactor = kernel[radius + i];
 144 
 145                     a += blurFactor * ((pixel >> 24) & 0xFF);
 146                     r += blurFactor * ((pixel >> 16) & 0xFF);
 147                     g += blurFactor * ((pixel >> 8) & 0xFF);
 148                     b += blurFactor * ((pixel) & 0xFF);
 149                 }
 150 
 151                 ca = (int) (a + 0.5f);
 152                 cr = (int) (r + 0.5f);
 153                 cg = (int) (g + 0.5f);
 154                 cb = (int) (b + 0.5f);
 155 
 156                 dstPixels[index] = ((ca > 255 ? 255 : ca) << 24) |
 157                         ((cr > 255 ? 255 : cr) << 16) |
 158                         ((cg > 255 ? 255 : cg) << 8) |
 159                         (cb > 255 ? 255 : cb);
 160                 index += height;
 161             }
 162         }
 163     }
 164 
 165     /**
 166      * <p>Blurs the source pixels into the destination pixels. The force of the blur is specified by the radius which
 167      * must be greater than 0.</p> <p>The source and destination pixels arrays are expected to be in the BYTE_GREY
 168      * format.</p> <p>After this method is executed, dstPixels contains a transposed and filtered copy of
 169      * srcPixels.</p>
 170      *
 171      * @param srcPixels the source pixels
 172      * @param dstPixels the destination pixels
 173      * @param width     the width of the source picture
 174      * @param height    the height of the source picture
 175      * @param kernel    the kernel of the blur effect
 176      * @param radius    the radius of the blur effect
 177      */
 178     static void blur(byte[] srcPixels, byte[] dstPixels,
 179                             int width, int height,
 180                             float[] kernel, int radius) {
 181         float p;
 182         int cp;
 183         for (int y = 0; y < height; y++) {
 184             int index = y;
 185             int offset = y * width;
 186             for (int x = 0; x < width; x++) {
 187                 p = 0.0f;
 188                 for (int i = -radius; i <= radius; i++) {
 189                     int subOffset = x + i;
 190 //                    if (subOffset < 0) subOffset = 0;
 191 //                    if (subOffset >= width) subOffset = width-1;
 192                     if (subOffset < 0 || subOffset >= width) {
 193                         subOffset = (x + width) % width;
 194                     }
 195                     int pixel = srcPixels[offset + subOffset] & 0xFF;
 196                     float blurFactor = kernel[radius + i];
 197                     p += blurFactor * pixel;
 198                 }
 199                 cp = (int) (p + 0.5f);
 200                 dstPixels[index] = (byte) (cp > 255 ? 255 : cp);
 201                 index += height;
 202             }
 203         }
 204     }
 205 
 206     static float[] createGaussianKernel(int radius) {
 207         if (radius < 1) {
 208             throw new IllegalArgumentException("Radius must be >= 1");
 209         }
 210 
 211         float[] data = new float[radius * 2 + 1];
 212 
 213         float sigma = radius / 3.0f;
 214         float twoSigmaSquare = 2.0f * sigma * sigma;
 215         float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
 216         float total = 0.0f;
 217 
 218         for (int i = -radius; i <= radius; i++) {
 219             float distance = i * i;
 220             int index = i + radius;
 221             data[index] = (float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
 222             total += data[index];
 223         }
 224 
 225         for (int i = 0; i < data.length; i++) {
 226             data[i] /= total;
 227         }
 228 
 229         return data;
 230     }
 231 
 232     // =================================================================================================================
 233     // Get/Set Pixels helper methods
 234 
 235     /**
 236      * <p>Returns an array of pixels, stored as integers, from a {@code BufferedImage}. The pixels are grabbed from
 237      * a rectangular area defined by a location and two dimensions. Calling this method on an image of type different
 238      * from {@code BufferedImage.TYPE_INT_ARGB} and {@code BufferedImage.TYPE_INT_RGB} will unmanage the
 239      * image.</p>
 240      *
 241      * @param img    the source image
 242      * @param x      the x location at which to start grabbing pixels
 243      * @param y      the y location at which to start grabbing pixels
 244      * @param w      the width of the rectangle of pixels to grab
 245      * @param h      the height of the rectangle of pixels to grab
 246      * @param pixels a pre-allocated array of pixels of size w*h; can be null
 247      * @return {@code pixels} if non-null, a new array of integers otherwise
 248      * @throws IllegalArgumentException is {@code pixels} is non-null and of length &lt; w*h
 249      */
 250     static byte[] getPixels(BufferedImage img,
 251                                    int x, int y, int w, int h, byte[] pixels) {
 252         if (w == 0 || h == 0) {
 253             return new byte[0];
 254         }
 255 
 256         if (pixels == null) {
 257             pixels = new byte[w * h];
 258         } else if (pixels.length < w * h) {
 259             throw new IllegalArgumentException("pixels array must have a length >= w*h");
 260         }
 261 
 262         int imageType = img.getType();
 263         if (imageType == BufferedImage.TYPE_BYTE_GRAY) {
 264             Raster raster = img.getRaster();
 265             return (byte[]) raster.getDataElements(x, y, w, h, pixels);
 266         } else {
 267             throw new IllegalArgumentException("Only type BYTE_GRAY is supported");
 268         }
 269     }
 270 
 271     /**
 272      * <p>Writes a rectangular area of pixels in the destination {@code BufferedImage}. Calling this method on an
 273      * image of type different from {@code BufferedImage.TYPE_INT_ARGB} and {@code BufferedImage.TYPE_INT_RGB}
 274      * will unmanage the image.</p>
 275      *
 276      * @param img    the destination image
 277      * @param x      the x location at which to start storing pixels
 278      * @param y      the y location at which to start storing pixels
 279      * @param w      the width of the rectangle of pixels to store
 280      * @param h      the height of the rectangle of pixels to store
 281      * @param pixels an array of pixels, stored as integers
 282      * @throws IllegalArgumentException is {@code pixels} is non-null and of length &lt; w*h
 283      */
 284     static void setPixels(BufferedImage img,
 285                                  int x, int y, int w, int h, byte[] pixels) {
 286         if (pixels == null || w == 0 || h == 0) {
 287             return;
 288         } else if (pixels.length < w * h) {
 289             throw new IllegalArgumentException("pixels array must have a length >= w*h");
 290         }
 291         int imageType = img.getType();
 292         if (imageType == BufferedImage.TYPE_BYTE_GRAY) {
 293             WritableRaster raster = img.getRaster();
 294             raster.setDataElements(x, y, w, h, pixels);
 295         } else {
 296             throw new IllegalArgumentException("Only type BYTE_GRAY is supported");
 297         }
 298     }
 299 
 300     /**
 301      * <p>Returns an array of pixels, stored as integers, from a
 302      * {@code BufferedImage}. The pixels are grabbed from a rectangular
 303      * area defined by a location and two dimensions. Calling this method on
 304      * an image of type different from {@code BufferedImage.TYPE_INT_ARGB}
 305      * and {@code BufferedImage.TYPE_INT_RGB} will unmanage the image.</p>
 306      *
 307      * @param img the source image
 308      * @param x the x location at which to start grabbing pixels
 309      * @param y the y location at which to start grabbing pixels
 310      * @param w the width of the rectangle of pixels to grab
 311      * @param h the height of the rectangle of pixels to grab
 312      * @param pixels a pre-allocated array of pixels of size w*h; can be null
 313      * @return {@code pixels} if non-null, a new array of integers
 314      *   otherwise
 315      * @throws IllegalArgumentException is {@code pixels} is non-null and
 316      *   of length &lt; w*h
 317      */
 318     public static int[] getPixels(BufferedImage img,
 319                                   int x, int y, int w, int h, int[] pixels) {
 320         if (w == 0 || h == 0) {
 321             return new int[0];
 322         }
 323 
 324         if (pixels == null) {
 325             pixels = new int[w * h];
 326         } else if (pixels.length < w * h) {
 327             throw new IllegalArgumentException("pixels array must have a length" +
 328                                                " >= w*h");
 329         }
 330 
 331         int imageType = img.getType();
 332         if (imageType == BufferedImage.TYPE_INT_ARGB ||
 333             imageType == BufferedImage.TYPE_INT_RGB) {
 334             Raster raster = img.getRaster();
 335             return (int[]) raster.getDataElements(x, y, w, h, pixels);
 336         }
 337 
 338         // Unmanages the image
 339         return img.getRGB(x, y, w, h, pixels, 0, w);
 340     }
 341 
 342     /**
 343      * <p>Writes a rectangular area of pixels in the destination
 344      * {@code BufferedImage}. Calling this method on
 345      * an image of type different from {@code BufferedImage.TYPE_INT_ARGB}
 346      * and {@code BufferedImage.TYPE_INT_RGB} will unmanage the image.</p>
 347      *
 348      * @param img the destination image
 349      * @param x the x location at which to start storing pixels
 350      * @param y the y location at which to start storing pixels
 351      * @param w the width of the rectangle of pixels to store
 352      * @param h the height of the rectangle of pixels to store
 353      * @param pixels an array of pixels, stored as integers
 354      * @throws IllegalArgumentException is {@code pixels} is non-null and
 355      *   of length &lt; w*h
 356      */
 357     public static void setPixels(BufferedImage img,
 358                                  int x, int y, int w, int h, int[] pixels) {
 359         if (pixels == null || w == 0 || h == 0) {
 360             return;
 361         } else if (pixels.length < w * h) {
 362             throw new IllegalArgumentException("pixels array must have a length" +
 363                                                " >= w*h");
 364         }
 365 
 366         int imageType = img.getType();
 367         if (imageType == BufferedImage.TYPE_INT_ARGB ||
 368             imageType == BufferedImage.TYPE_INT_RGB) {
 369             WritableRaster raster = img.getRaster();
 370             raster.setDataElements(x, y, w, h, pixels);
 371         } else {
 372             // Unmanages the image
 373             img.setRGB(x, y, w, h, pixels, 0, w);
 374         }
 375     }
 376 
 377     /**
 378      * <p>Returns a new {@code BufferedImage} using the same color model
 379      * as the image passed as a parameter. The returned image is only compatible
 380      * with the image passed as a parameter. This does not mean the returned
 381      * image is compatible with the hardware.</p>
 382      *
 383      * @param image the reference image from which the color model of the new
 384      *   image is obtained
 385      * @return a new {@code BufferedImage}, compatible with the color model
 386      *   of {@code image}
 387      */
 388     public static BufferedImage createColorModelCompatibleImage(BufferedImage image) {
 389         ColorModel cm = image.getColorModel();
 390         return new BufferedImage(cm,
 391             cm.createCompatibleWritableRaster(image.getWidth(),
 392                                               image.getHeight()),
 393             cm.isAlphaPremultiplied(), null);
 394     }
 395 
 396     /**
 397      * <p>Returns a new translucent compatible image of the specified width and
 398      * height. That is, the returned {@code BufferedImage} is compatible with
 399      * the graphics hardware. If the method is called in a headless
 400      * environment, then the returned BufferedImage will be compatible with
 401      * the source image.</p>
 402      *
 403      * @param width the width of the new image
 404      * @param height the height of the new image
 405      * @return a new translucent compatible {@code BufferedImage} of the
 406      *   specified width and height
 407      */
 408     public static BufferedImage createCompatibleTranslucentImage(int width,
 409                                                                  int height) {
 410         return isHeadless() ?
 411                 new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) :
 412                 getGraphicsConfiguration().createCompatibleImage(width, height,
 413                                                    Transparency.TRANSLUCENT);
 414     }
 415 
 416     private static boolean isHeadless() {
 417         return GraphicsEnvironment.isHeadless();
 418     }
 419 
 420     // Returns the graphics configuration for the primary screen
 421     private static GraphicsConfiguration getGraphicsConfiguration() {
 422         return GraphicsEnvironment.getLocalGraphicsEnvironment().
 423                     getDefaultScreenDevice().getDefaultConfiguration();
 424     }
 425 
 426 }