GraphicsUtilities
contains a set of tools to perform
* common graphics operations easily. These operations are divided into
* several themes, listed below.
Compatible images can, and should, be used to increase drawing * performance. This class provides a number of methods to load compatible * images directly from files or to convert existing images to compatibles * images.
*This class provides a number of methods to easily scale down images. * Some of these methods offer a trade-off between speed and result quality and * shouuld be used all the time. They also offer the advantage of producing * compatible images, thus automatically resulting into better runtime * performance.
*All these methodes are both faster than
* {@link java.awt.Image#getScaledInstance(int, int, int)} and produce
* better-looking results than the various drawImage()
methods
* in {@link java.awt.Graphics}, which can be used for image scaling.
This class provides two methods to get and set pixels in a buffered image. * These methods try to avoid unmanaging the image in order to keep good * performance.
* * @author Romain GuyReturns a new BufferedImage
using the same color model
* as the image passed as a parameter. The returned image is only compatible
* with the image passed as a parameter. This does not mean the returned
* image is compatible with the hardware.
BufferedImage
, compatible with the color model
* of image
*/
public static BufferedImage createColorModelCompatibleImage(BufferedImage image) {
ColorModel cm = image.getColorModel();
return new BufferedImage(cm,
cm.createCompatibleWritableRaster(image.getWidth(),
image.getHeight()),
cm.isAlphaPremultiplied(), null);
}
/**
* Returns a new compatible image with the same width, height and * transparency as the image specified as a parameter.
* * @see java.awt.Transparency * @see #createCompatibleImage(int, int) * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) * @see #createCompatibleTranslucentImage(int, int) * @see #loadCompatibleImage(java.net.URL) * @see #toCompatibleImage(java.awt.image.BufferedImage) * @param image the reference image from which the dimension and the * transparency of the new image are obtained * @return a new compatibleBufferedImage
with the same
* dimension and transparency as image
*/
public static BufferedImage createCompatibleImage(BufferedImage image) {
return createCompatibleImage(image, image.getWidth(), image.getHeight());
}
/**
* Returns a new compatible image of the specified width and height, and * the same transparency setting as the image specified as a parameter.
* * @see java.awt.Transparency * @see #createCompatibleImage(java.awt.image.BufferedImage) * @see #createCompatibleImage(int, int) * @see #createCompatibleTranslucentImage(int, int) * @see #loadCompatibleImage(java.net.URL) * @see #toCompatibleImage(java.awt.image.BufferedImage) * @param width the width of the new image * @param height the height of the new image * @param image the reference image from which the transparency of the new * image is obtained * @return a new compatibleBufferedImage
with the same
* transparency as image
and the specified dimension
*/
public static BufferedImage createCompatibleImage(BufferedImage image,
int width, int height) {
return getGraphicsConfiguration().createCompatibleImage(width, height,
image.getTransparency());
}
/**
* Returns a new opaque compatible image of the specified width and * height.
* * @see #createCompatibleImage(java.awt.image.BufferedImage) * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) * @see #createCompatibleTranslucentImage(int, int) * @see #loadCompatibleImage(java.net.URL) * @see #toCompatibleImage(java.awt.image.BufferedImage) * @param width the width of the new image * @param height the height of the new image * @return a new opaque compatibleBufferedImage
of the
* specified width and height
*/
public static BufferedImage createCompatibleImage(int width, int height) {
return getGraphicsConfiguration().createCompatibleImage(width, height);
}
/**
* Returns a new translucent compatible image of the specified width * and height.
* * @see #createCompatibleImage(java.awt.image.BufferedImage) * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) * @see #createCompatibleImage(int, int) * @see #loadCompatibleImage(java.net.URL) * @see #toCompatibleImage(java.awt.image.BufferedImage) * @param width the width of the new image * @param height the height of the new image * @return a new translucent compatibleBufferedImage
of the
* specified width and height
*/
public static BufferedImage createCompatibleTranslucentImage(int width,
int height) {
return getGraphicsConfiguration().createCompatibleImage(width, height,
Transparency.TRANSLUCENT);
}
/**
* Returns a new compatible image from a URL. The image is loaded from the * specified location and then turned, if necessary into a compatible * image.
* * @see #createCompatibleImage(java.awt.image.BufferedImage) * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) * @see #createCompatibleImage(int, int) * @see #createCompatibleTranslucentImage(int, int) * @see #toCompatibleImage(java.awt.image.BufferedImage) * @param resource the URL of the picture to load as a compatible image * @return a new translucent compatibleBufferedImage
of the
* specified width and height
* @throws java.io.IOException if the image cannot be read or loaded
*/
public static BufferedImage loadCompatibleImage(URL resource)
throws IOException {
BufferedImage image = ImageIO.read(resource);
return toCompatibleImage(image);
}
/**
* Return a new compatible image that contains a copy of the specified * image. This method ensures an image is compatible with the hardware, * and therefore optimized for fast blitting operations.
* * @see #createCompatibleImage(java.awt.image.BufferedImage) * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) * @see #createCompatibleImage(int, int) * @see #createCompatibleTranslucentImage(int, int) * @see #loadCompatibleImage(java.net.URL) * @param image the image to copy into a new compatible image * @return a new compatible copy, with the * same width and height and transparency and content, ofimage
*/
public static BufferedImage toCompatibleImage(BufferedImage image) {
if (image.getColorModel().equals(
getGraphicsConfiguration().getColorModel())) {
return image;
}
BufferedImage compatibleImage =
getGraphicsConfiguration().createCompatibleImage(
image.getWidth(), image.getHeight(),
image.getTransparency());
Graphics g = compatibleImage.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return compatibleImage;
}
/**
* Returns a thumbnail of a source image. newSize
defines
* the length of the longest dimension of the thumbnail. The other
* dimension is then computed according to the dimensions ratio of the
* original picture.
This method favors speed over quality. When the new size is less than * half the longest dimension of the source image, * {@link #createThumbnail(BufferedImage, int)} or * {@link #createThumbnail(BufferedImage, int, int)} should be used instead * to ensure the quality of the result without sacrificing too much * performance.
* * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) * @see #createThumbnail(java.awt.image.BufferedImage, int) * @see #createThumbnail(java.awt.image.BufferedImage, int, int) * @param image the source image * @param newSize the length of the largest dimension of the thumbnail * @return a new compatibleBufferedImage
containing a
* thumbnail of image
* @throws IllegalArgumentException if newSize
is larger than
* the largest dimension of image
or <= 0
*/
public static BufferedImage createThumbnailFast(BufferedImage image,
int newSize) {
float ratio;
int width = image.getWidth();
int height = image.getHeight();
if (width > height) {
if (newSize >= width) {
throw new IllegalArgumentException("newSize must be lower than" +
" the image width");
} else if (newSize <= 0) {
throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
ratio = (float) width / (float) height;
width = newSize;
height = (int) (newSize / ratio);
} else {
if (newSize >= height) {
throw new IllegalArgumentException("newSize must be lower than" +
" the image height");
} else if (newSize <= 0) {
throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
ratio = (float) height / (float) width;
height = newSize;
width = (int) (newSize / ratio);
}
BufferedImage temp = createCompatibleImage(image, width, height);
Graphics2D g2 = temp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, 0, 0, temp.getWidth(), temp.getHeight(), null);
g2.dispose();
return temp;
}
/**
* Returns a thumbnail of a source image.
*This method favors speed over quality. When the new size is less than * half the longest dimension of the source image, * {@link #createThumbnail(BufferedImage, int)} or * {@link #createThumbnail(BufferedImage, int, int)} should be used instead * to ensure the quality of the result without sacrificing too much * performance.
* * @see #createThumbnailFast(java.awt.image.BufferedImage, int) * @see #createThumbnail(java.awt.image.BufferedImage, int) * @see #createThumbnail(java.awt.image.BufferedImage, int, int) * @param image the source image * @param newWidth the width of the thumbnail * @param newHeight the height of the thumbnail * @return a new compatibleBufferedImage
containing a
* thumbnail of image
* @throws IllegalArgumentException if newWidth
is larger than
* the width of image
or if code>newHeight is larger
* than the height of image
or if one of the dimensions
* is <= 0
*/
public static BufferedImage createThumbnailFast(BufferedImage image,
int newWidth, int newHeight) {
if (newWidth >= image.getWidth() ||
newHeight >= image.getHeight()) {
throw new IllegalArgumentException("newWidth and newHeight cannot" +
" be greater than the image" +
" dimensions");
} else if (newWidth <= 0 || newHeight <= 0) {
throw new IllegalArgumentException("newWidth and newHeight must" +
" be greater than 0");
}
BufferedImage temp = createCompatibleImage(image, newWidth, newHeight);
Graphics2D g2 = temp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, 0, 0, temp.getWidth(), temp.getHeight(), null);
g2.dispose();
return temp;
}
/**
* Returns a thumbnail of a source image. newSize
defines
* the length of the longest dimension of the thumbnail. The other
* dimension is then computed according to the dimensions ratio of the
* original picture.
This method offers a good trade-off between speed and quality. * The result looks better than * {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when * the new size is less than half the longest dimension of the source * image, yet the rendering speed is almost similar.
* * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) * @see #createThumbnailFast(java.awt.image.BufferedImage, int) * @see #createThumbnail(java.awt.image.BufferedImage, int, int) * @param image the source image * @param newSize the length of the largest dimension of the thumbnail * @return a new compatibleBufferedImage
containing a
* thumbnail of image
* @throws IllegalArgumentException if newSize
is larger than
* the largest dimension of image
or <= 0
*/
public static BufferedImage createThumbnail(BufferedImage image,
int newSize) {
int width = image.getWidth();
int height = image.getHeight();
boolean isWidthGreater = width > height;
if (isWidthGreater) {
if (newSize >= width) {
throw new IllegalArgumentException("newSize must be lower than" +
" the image width");
}
} else if (newSize >= height) {
throw new IllegalArgumentException("newSize must be lower than" +
" the image height");
}
if (newSize <= 0) {
throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
float ratioWH = (float) width / (float) height;
float ratioHW = (float) height / (float) width;
BufferedImage thumb = image;
BufferedImage temp = null;
Graphics2D g2 = null;
int previousWidth = width;
int previousHeight = height;
do {
if (isWidthGreater) {
width /= 2;
if (width < newSize) {
width = newSize;
}
height = (int) (width / ratioWH);
} else {
height /= 2;
if (height < newSize) {
height = newSize;
}
width = (int) (height / ratioHW);
}
if (temp == null) {
temp = createCompatibleImage(image, width, height);
g2 = temp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
}
g2.drawImage(thumb, 0, 0, width, height,
0, 0, previousWidth, previousHeight, null);
previousWidth = width;
previousHeight = height;
thumb = temp;
} while (newSize != (isWidthGreater ? width : height));
g2.dispose();
if (width != thumb.getWidth() || height != thumb.getHeight()) {
temp = createCompatibleImage(image, width, height);
g2 = temp.createGraphics();
g2.drawImage(thumb, 0, 0, null);
g2.dispose();
thumb = temp;
}
return thumb;
}
/**
* Returns a thumbnail of a source image.
*This method offers a good trade-off between speed and quality. * The result looks better than * {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when * the new size is less than half the longest dimension of the source * image, yet the rendering speed is almost similar.
* * @see #createThumbnailFast(java.awt.image.BufferedImage, int) * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) * @see #createThumbnail(java.awt.image.BufferedImage, int) * @param image the source image * @param newWidth the width of the thumbnail * @param newHeight the height of the thumbnail * @return a new compatibleBufferedImage
containing a
* thumbnail of image
* @throws IllegalArgumentException if newWidth
is larger than
* the width of image
or if code>newHeight is larger
* than the height of image or if one the dimensions is not > 0
*/
public static BufferedImage createThumbnail(BufferedImage image,
int newWidth, int newHeight) {
int width = image.getWidth();
int height = image.getHeight();
if (newWidth >= width || newHeight >= height) {
throw new IllegalArgumentException("newWidth and newHeight cannot" +
" be greater than the image" +
" dimensions");
} else if (newWidth <= 0 || newHeight <= 0) {
throw new IllegalArgumentException("newWidth and newHeight must" +
" be greater than 0");
}
BufferedImage thumb = image;
BufferedImage temp = null;
Graphics2D g2 = null;
int previousWidth = width;
int previousHeight = height;
do {
if (width > newWidth) {
width /= 2;
if (width < newWidth) {
width = newWidth;
}
}
if (height > newHeight) {
height /= 2;
if (height < newHeight) {
height = newHeight;
}
}
if (temp == null) {
temp = createCompatibleImage(image, width, height);
g2 = temp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
}
g2.drawImage(thumb, 0, 0, width, height,
0, 0, previousWidth, previousHeight, null);
previousWidth = width;
previousHeight = height;
thumb = temp;
} while (width != newWidth || height != newHeight);
g2.dispose();
if (width != thumb.getWidth() || height != thumb.getHeight()) {
temp = createCompatibleImage(image, width, height);
g2 = temp.createGraphics();
g2.drawImage(thumb, 0, 0, null);
g2.dispose();
thumb = temp;
}
return thumb;
}
/**
* Returns an array of pixels, stored as integers, from a
* BufferedImage
. The pixels are grabbed from a rectangular
* area defined by a location and two dimensions. Calling this method on
* an image of type different from BufferedImage.TYPE_INT_ARGB
* and BufferedImage.TYPE_INT_RGB
will unmanage the image.
pixels
if non-null, a new array of integers
* otherwise
* @throws IllegalArgumentException is pixels
is non-null and
* of length < w*h
*/
public static int[] getPixels(BufferedImage img,
int x, int y, int w, int h, int[] pixels) {
if (w == 0 || h == 0) {
return new int[0];
}
if (pixels == null) {
pixels = new int[w * h];
} else if (pixels.length < w * h) {
throw new IllegalArgumentException("pixels array must have a length" +
" >= w*h");
}
int imageType = img.getType();
if (imageType == BufferedImage.TYPE_INT_ARGB ||
imageType == BufferedImage.TYPE_INT_RGB) {
Raster raster = img.getRaster();
return (int[]) raster.getDataElements(x, y, w, h, pixels);
}
// Unmanages the image
return img.getRGB(x, y, w, h, pixels, 0, w);
}
/**
* Writes a rectangular area of pixels in the destination
* BufferedImage
. Calling this method on
* an image of type different from BufferedImage.TYPE_INT_ARGB
* and BufferedImage.TYPE_INT_RGB
will unmanage the image.
pixels
is non-null and
* of length < w*h
*/
public static void setPixels(BufferedImage img,
int x, int y, int w, int h, int[] pixels) {
if (pixels == null || w == 0 || h == 0) {
return;
} else if (pixels.length < w * h) {
throw new IllegalArgumentException("pixels array must have a length" +
" >= w*h");
}
int imageType = img.getType();
if (imageType == BufferedImage.TYPE_INT_ARGB ||
imageType == BufferedImage.TYPE_INT_RGB) {
WritableRaster raster = img.getRaster();
raster.setDataElements(x, y, w, h, pixels);
} else {
// Unmanages the image
img.setRGB(x, y, w, h, pixels, 0, w);
}
}
}
/*
* Copyright (c) 2007, Romain Guy
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of the TimingFramework project nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* A fast blur filter can be used to blur pictures quickly. This filter is an * implementation of the box blur algorithm. The blurs generated by this * algorithm might show square artifacts, especially on pictures containing * straight lines (rectangles, text, etc.) On most pictures though, the * result will look very good.
*The force of the blur can be controlled with a radius and the * default radius is 3. Since the blur clamps values on the edges of the * source picture, you might need to provide a picture with empty borders * to avoid artifacts at the edges. The performance of this filter are * independant from the radius.
* * @author Romain GuyCreates a new blur filter with a default radius of 3.
*/ public FastBlurFilter() { this(3); } /** *Creates a new blur filter with the specified radius. If the radius * is lower than 1, a radius of 1 will be used automatically.
* * @param radius the radius, in pixels, of the blur */ public FastBlurFilter(int radius) { if (radius < 1) { radius = 1; } this.radius = radius; } /** *Returns the radius used by this filter, in pixels.
* * @return the radius of the blur */ public int getRadius() { return radius; } /** * {@inheritDoc} */ @Override public BufferedImage filter(BufferedImage src, BufferedImage dst) { int width = src.getWidth(); int height = src.getHeight(); if (dst == null) { dst = createCompatibleDestImage(src, null); } int[] srcPixels = new int[width * height]; int[] dstPixels = new int[width * height]; GraphicsUtilities.getPixels(src, 0, 0, width, height, srcPixels); // horizontal pass blur(srcPixels, dstPixels, width, height, radius); // vertical pass //noinspection SuspiciousNameCombination blur(dstPixels, srcPixels, height, width, radius); // the result is now stored in srcPixels due to the 2nd pass GraphicsUtilities.setPixels(dst, 0, 0, width, height, srcPixels); return dst; } /** *Blurs the source pixels into the destination pixels. The force of * the blur is specified by the radius which must be greater than 0.
*The source and destination pixels arrays are expected to be in the * INT_ARGB format.
*After this method is executed, dstPixels contains a transposed and * filtered copy of srcPixels.
* * @param srcPixels the source pixels * @param dstPixels the destination pixels * @param width the width of the source picture * @param height the height of the source picture * @param radius the radius of the blur effect */ static void blur(int[] srcPixels, int[] dstPixels, int width, int height, int radius) { final int windowSize = radius * 2 + 1; final int radiusPlusOne = radius + 1; int sumAlpha; int sumRed; int sumGreen; int sumBlue; int srcIndex = 0; int dstIndex; int pixel; int[] sumLookupTable = new int[256 * windowSize]; for (int i = 0; i < sumLookupTable.length; i++) { sumLookupTable[i] = i / windowSize; } int[] indexLookupTable = new int[radiusPlusOne]; if (radius < width) { for (int i = 0; i < indexLookupTable.length; i++) { indexLookupTable[i] = i; } } else { for (int i = 0; i < width; i++) { indexLookupTable[i] = i; } for (int i = width; i < indexLookupTable.length; i++) { indexLookupTable[i] = width - 1; } } for (int y = 0; y < height; y++) { sumAlpha = sumRed = sumGreen = sumBlue = 0; dstIndex = y; pixel = srcPixels[srcIndex]; sumAlpha += radiusPlusOne * ((pixel >> 24) & 0xFF); sumRed += radiusPlusOne * ((pixel >> 16) & 0xFF); sumGreen += radiusPlusOne * ((pixel >> 8) & 0xFF); sumBlue += radiusPlusOne * ( pixel & 0xFF); for (int i = 1; i <= radius; i++) { pixel = srcPixels[srcIndex + indexLookupTable[i]]; sumAlpha += (pixel >> 24) & 0xFF; sumRed += (pixel >> 16) & 0xFF; sumGreen += (pixel >> 8) & 0xFF; sumBlue += pixel & 0xFF; } for (int x = 0; x < width; x++) { dstPixels[dstIndex] = sumLookupTable[sumAlpha] << 24 | sumLookupTable[sumRed] << 16 | sumLookupTable[sumGreen] << 8 | sumLookupTable[sumBlue]; dstIndex += height; int nextPixelIndex = x + radiusPlusOne; if (nextPixelIndex >= width) { nextPixelIndex = width - 1; } int previousPixelIndex = x - radius; if (previousPixelIndex < 0) { previousPixelIndex = 0; } int nextPixel = srcPixels[srcIndex + nextPixelIndex]; int previousPixel = srcPixels[srcIndex + previousPixelIndex]; sumAlpha += (nextPixel >> 24) & 0xFF; sumAlpha -= (previousPixel >> 24) & 0xFF; sumRed += (nextPixel >> 16) & 0xFF; sumRed -= (previousPixel >> 16) & 0xFF; sumGreen += (nextPixel >> 8) & 0xFF; sumGreen -= (previousPixel >> 8) & 0xFF; sumBlue += nextPixel & 0xFF; sumBlue -= previousPixel & 0xFF; } srcIndex += width; } } } /* * Copyright (c) 2007, Romain Guy * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** *Provides an abstract implementation of the BufferedImageOp
* interface. This class can be used to created new image filters based
* on BufferedImageOp
.
A reflection renderer generates the reflection of a given picture. The * result can be either the reflection itself, or an image containing both the * source image and its reflection.
*A reflection is defined by three properties: *
* ReflectionRenderer renderer = new ReflectionRenderer(0.5f, 0.3f, true); * // .. * renderer = new ReflectionRenderer(); * renderer.setOpacity(0.5f); * renderer.setLength(0.3f); * renderer.setBlurEnabled(true); ** The default constructor provides the following default values: *
A reflection is generated as a BufferedImage
from another
* BufferedImage
. Once the renderer is set up, you must call
* {@link #createReflection(java.awt.image.BufferedImage)} to actually generate
* the reflection:
*
* ReflectionRenderer renderer = new ReflectionRenderer(); * // renderer setup * BufferedImage reflection = renderer.createReflection(bufferedImage); **
The returned image contains only the reflection. You will have to append * it to the source image at painting time to get a realistic results. You can * also asks the rendered to return a picture composed of both the source image * and its reflection: *
* ReflectionRenderer renderer = new ReflectionRenderer(); * // renderer setup * BufferedImage reflection = renderer.appendReflection(bufferedImage); **
This renderer allows to register property change listeners with * {@link #addPropertyChangeListener}. Listening to properties changes is very * useful when you emebed the renderer in a graphical component and give the API * user the ability to access the renderer. By listening to properties changes, * you can easily repaint the component when needed.
*ReflectionRenderer
is not guaranteed to be thread-safe.
Identifies a change to the opacity used to render the reflection.
*/ public static final String OPACITY_CHANGED_PROPERTY = "reflection_opacity"; /** *Identifies a change to the length of the rendered reflection.
*/ public static final String LENGTH_CHANGED_PROPERTY = "reflection_length"; /** *Identifies a change to the blurring of the rendered reflection.
*/ public static final String BLUR_ENABLED_CHANGED_PROPERTY = "reflection_blur"; // opacity of the reflection private float opacity; // length of the reflection private float length; // should the reflection be blurred? private boolean blurEnabled; // notifies listeners of properties changes private PropertyChangeSupport changeSupport; private StackBlurFilter stackBlurFilter; /** *Creates a default good looking reflections generator. * The default reflection renderer provides the following default values: *
These properties provide a regular, good looking reflection.
* * @see #getOpacity() * @see #setOpacity(float) * @see #getLength() * @see #setLength(float) * @see #isBlurEnabled() * @see #setBlurEnabled(boolean) * @see #getBlurRadius() * @see #setBlurRadius(int) */ public ReflectionRenderer() { this(0.35f, 0.4f, false); } /** *Creates a default good looking reflections generator with the * specified opacity. The default reflection renderer provides the following * default values: *
Creates a reflections generator with the specified properties. Both * opacity and length are numbers between 0.0 (0%) and 1.0 (100%). If the * provided numbers are outside this range, they are clamped.
*Enabling the blur generates a different kind of reflections that might * look more natural. The default blur radius is 1 pixel
* * @param opacity the opacity of the reflection * @param length the length of the reflection * @param blurEnabled if true, the reflection is blurred * @see #getOpacity(),#setOpacity(float),#getLength(),#setLength(float) * @see #isBlurEnabled(),#setBlurEnabled(boolean) * @see #getBlurRadius() * @see #setBlurRadius(int) */ public ReflectionRenderer(float opacity, float length, boolean blurEnabled) { //noinspection ThisEscapedInObjectConstruction this.changeSupport = new PropertyChangeSupport(this); this.stackBlurFilter = new StackBlurFilter(1); setOpacity(opacity); setLength(length); setBlurEnabled(blurEnabled); } /** *Add a PropertyChangeListener to the listener list. The listener is
* registered for all properties. The same listener object may be added
* more than once, and will be called as many times as it is added. If
* listener
is null, no exception is thrown and no action
* is taken.
Remove a PropertyChangeListener from the listener list. This removes
* a PropertyChangeListener that was registered for all properties. If
* listener
was added more than once to the same event source,
* it will be notified one less time after being removed. If
* listener
is null, or was never added, no exception is thrown
* and no action is taken.
Gets the opacity used by the factory to generate reflections.
*The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque.
* * @return this factory's shadow opacity * @see #getOpacity() * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public float getOpacity() { return opacity; } /** *Sets the opacity used by the factory to generate reflections.
*Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.
*The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque. If you provide a value out of these * boundaries, it will be restrained to the closest boundary.
* * @param opacity the generated reflection opacity * @see #setOpacity(float) * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public void setOpacity(float opacity) { float oldOpacity = this.opacity; if (opacity < 0.0f) { opacity = 0.0f; } else if (opacity > 1.0f) { opacity = 1.0f; } if (oldOpacity != opacity) { this.opacity = opacity; changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY, oldOpacity, this.opacity); } } /** *Returns the length of the reflection. The result is a number between * 0.0 and 1.0. This number is the fraction of the height of the source * image that is used to compute the size of the reflection.
* * @return the length of the reflection, as a fraction of the source image * height * @see #setLength(float) * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public float getLength() { return length; } /** *Sets the length of the reflection, as a fraction of the height of the * source image.
*Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.
*The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque. If you provide a value out of these * boundaries, it will be restrained to the closest boundary.
* * @param length the length of the reflection, as a fraction of the source * image height * @see #getLength() * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public void setLength(float length) { float oldLength = this.length; if (length < 0.0f) { length = 0.0f; } else if (length > 1.0f) { length = 1.0f; } if (oldLength != length) { this.length = length; changeSupport.firePropertyChange(LENGTH_CHANGED_PROPERTY, oldLength, this.length); } } /** *Returns true if the blurring of the reflection is enabled, false * otherwise. When blurring is enabled, the reflection is blurred to look * more natural.
* * @return true if blur is enabled, false otherwise * @see #setBlurEnabled(boolean) * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public boolean isBlurEnabled() { return blurEnabled; } /** *Setting the blur to true will enable the blurring of the reflection * when {@link #createReflection} is invoked.
*Enabling the blurring of the reflection can yield to more natural * results which may or may not be better looking, depending on the source * picture.
*Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.
* * @param blurEnabled true to enable the blur, false otherwise * @see #isBlurEnabled() * @see #createReflection(java.awt.image.BufferedImage) * @see #appendReflection(java.awt.image.BufferedImage) */ public void setBlurEnabled(boolean blurEnabled) { if (blurEnabled != this.blurEnabled) { boolean oldBlur = this.blurEnabled; this.blurEnabled= blurEnabled; changeSupport.firePropertyChange(BLUR_ENABLED_CHANGED_PROPERTY, oldBlur, this.blurEnabled); } } /** *Returns the effective radius, in pixels, of the blur used by this * renderer when {@link #isBlurEnabled()} is true.
* * @return the effective radius of the blur used when *isBlurEnabled
is true
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #setBlurRadius(int)
* @see #getBlurRadius()
*/
public int getEffectiveBlurRadius() {
return stackBlurFilter.getEffectiveRadius();
}
/**
* Returns the radius, in pixels, of the blur used by this renderer when * {@link #isBlurEnabled()} is true.
* * @return the radius of the blur used whenisBlurEnabled
* is true
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #setBlurRadius(int)
* @see #getEffectiveBlurRadius()
*/
public int getBlurRadius() {
return stackBlurFilter.getRadius();
}
/**
* Sets the radius, in pixels, of the blur used by this renderer when * {@link #isBlurEnabled()} is true. This radius changes the size of the * generated image when blurring is applied.
* * @param radius the radius, in pixels, of the blur * @see #isBlurEnabled() * @see #setBlurEnabled(boolean) * @see #getBlurRadius() */ public void setBlurRadius(int radius) { this.stackBlurFilter = new StackBlurFilter(radius); } /** *Returns the source image and its reflection. The appearance of the * reflection is defined by the opacity, the length and the blur * properties.
* *The width of the generated image will be augmented when * {@link #isBlurEnabled()} is true. The generated image will have the width * of the source image plus twice the effective blur radius (see * {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the * width will be augmented by 6. You might need to take this into account * at drawing time.
*The returned image height depends on the value returned by * {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance, * if the length is 0.5 (or 50%) and the source image is 480 pixels high, * then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.
*You can create only the reflection by calling * {@link #createReflection(java.awt.image.BufferedImage)}.
* * @param image the source image * @return the source image with its reflection below * @see #createReflection(java.awt.image.BufferedImage) */ public BufferedImage appendReflection(BufferedImage image) { BufferedImage reflection = createReflection(image); BufferedImage buffer = GraphicsUtilities.createCompatibleTranslucentImage( reflection.getWidth(), image.getHeight() + reflection.getHeight()); Graphics2D g2 = buffer.createGraphics(); int effectiveRadius = isBlurEnabled() ? stackBlurFilter.getEffectiveRadius() : 0; g2.drawImage(image, effectiveRadius, 0, null); g2.drawImage(reflection, 0, image.getHeight() - effectiveRadius, null); g2.dispose(); reflection.flush(); return buffer; } /** *Returns the reflection of the source image. The appearance of the * reflection is defined by the opacity, the length and the blur * properties.
* *The width of the generated image will be augmented when * {@link #isBlurEnabled()} is true. The generated image will have the width * of the source image plus twice the effective blur radius (see * {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the * width will be augmented by 6. You might need to take this into account * at drawing time.
*The returned image height depends on the value returned by * {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance, * if the length is 0.5 (or 50%) and the source image is 480 pixels high, * then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.
*The returned image contains only * the reflection. You will have to append it to the source image to produce * the illusion of a reflective environement. The method * {@link #appendReflection(java.awt.image.BufferedImage)} provides an easy * way to create an image containing both the source and the reflection.
* * @param image the source image * @return the reflection of the source image * @see #appendReflection(java.awt.image.BufferedImage) */ public BufferedImage createReflection(BufferedImage image) { if (length == 0.0f) { return GraphicsUtilities.createCompatibleTranslucentImage(1, 1); } int blurOffset = isBlurEnabled() ? stackBlurFilter.getEffectiveRadius() : 0; int height = (int) (image.getHeight() * length); BufferedImage buffer = GraphicsUtilities.createCompatibleTranslucentImage( image.getWidth() + blurOffset * 2, height + blurOffset * 2); Graphics2D g2 = buffer.createGraphics(); g2.translate(0, image.getHeight()); g2.scale(1.0, -1.0); g2.drawImage(image, blurOffset, -blurOffset, null); g2.scale(1.0, -1.0); g2.translate(0, -image.getHeight()); g2.setComposite(AlphaComposite.DstIn); g2.setPaint(new GradientPaint(0.0f, 0.0f, new Color(0.0f, 0.0f, 0.0f, getOpacity()), 0.0f, buffer.getHeight(), new Color(0.0f, 0.0f, 0.0f, 0.0f), true)); g2.fillRect(0, 0, buffer.getWidth(), buffer.getHeight()); g2.dispose(); return isBlurEnabled() ? stackBlurFilter.filter(buffer, null) : buffer; } } /* * Copyright (c) 2007, Romain Guy * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** *A stack blur filter can be used to create an approximation of a * gaussian blur. The approximation is controlled by the number of times the * {@link org.jdesktop.swingx.image.FastBlurFilter} is applied onto the source * picture. The default number of iterations, 3, provides a decent compromise * between speed and rendering quality.
*The force of the blur can be controlled with a radius and the * default radius is 3. Since the blur clamps values on the edges of the * source picture, you might need to provide a picture with empty borders * to avoid artifacts at the edges. The performance of this filter are * independant from the radius.
* * @author Romain GuyCreates a new blur filter with a default radius of 3 and 3 iterations.
*/ public StackBlurFilter() { this(3, 3); } /** *Creates a new blur filter with the specified radius and 3 iterations. * If the radius is lower than 1, a radius of 1 will be used automatically.
* * @param radius the radius, in pixels, of the blur */ public StackBlurFilter(int radius) { this(radius, 3); } /** *Creates a new blur filter with the specified radius. If the radius * is lower than 1, a radius of 1 will be used automatically. The number * of iterations controls the approximation to a gaussian blur. If the * number of iterations is lower than 1, one iteration will be used * automatically.
* * @param radius the radius, in pixels, of the blur * @param iterations the number of iterations to approximate a gaussian blur */ public StackBlurFilter(int radius, int iterations) { if (radius < 1) { radius = 1; } if (iterations < 1) { iterations = 1; } this.radius = radius; this.iterations = iterations; } /** *Returns the effective radius of the stack blur. If the radius of the * blur is 1 and the stack iterations count is 3, then the effective blur * radius is 1 * 3 = 3.
* @return the number of iterations times the blur radius */ public int getEffectiveRadius() { return getIterations() * getRadius(); } /** *Returns the radius used by this filter, in pixels.
* * @return the radius of the blur */ public int getRadius() { return radius; } /** *Returns the number of iterations used to approximate a gaussian * blur.
* * @return the number of iterations used by this blur */ public int getIterations() { return iterations; } /** * {@inheritDoc} */ @Override public BufferedImage filter(BufferedImage src, BufferedImage dst) { int width = src.getWidth(); int height = src.getHeight(); if (dst == null) { dst = createCompatibleDestImage(src, null); } int[] srcPixels = new int[width * height]; int[] dstPixels = new int[width * height]; GraphicsUtilities.getPixels(src, 0, 0, width, height, srcPixels); for (int i = 0; i < iterations; i++) { // horizontal pass FastBlurFilter.blur(srcPixels, dstPixels, width, height, radius); // vertical pass FastBlurFilter.blur(dstPixels, srcPixels, height, width, radius); } // the result is now stored in srcPixels due to the 2nd pass GraphicsUtilities.setPixels(dst, 0, 0, width, height, srcPixels); return dst; } }