Image reflection
/* * 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. */ import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Paint; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.net.URL; import javax.imageio.ImageIO; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** * @author Romain Guy <romain.guy@mac.com> */ public class ImageReflectionDemoFilthy extends JFrame { private ReflectionPanel reflectionPanel; private JSlider opacitySlider; private JSlider lengthSlider; private JSlider radiusSlider; public ImageReflectionDemoFilthy() { super("Reflections"); setContentPane(new GradientPanel()); reflectionPanel = new ReflectionPanel(); add(reflectionPanel); opacitySlider = new JSlider(0, 100, 35); opacitySlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { reflectionPanel.setOpacity(opacitySlider.getValue() / 100.0f); } }); lengthSlider = new JSlider(0, 100, 40); lengthSlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { reflectionPanel.setLength(lengthSlider.getValue() / 100.0f); } }); radiusSlider = new JSlider(1, 20, 1); radiusSlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { reflectionPanel.setRadius(radiusSlider.getValue()); } }); JPanel controls = new JPanel(new GridBagLayout()); JLabel label; controls.setOpaque(false); controls.add(label = new JLabel("Opacity: 0%"), new GridBagConstraints(0, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 6, 0, 0), 0, 0)); label.setForeground(Color.WHITE); controls.add(opacitySlider, new GridBagConstraints(1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); controls.add(label = new JLabel("100%"), new GridBagConstraints(2, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); label.setForeground(Color.WHITE); controls.add(label = new JLabel("Length: 0%"), new GridBagConstraints(0, 1, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 6, 0, 0), 0, 0)); label.setForeground(Color.WHITE); controls.add(lengthSlider, new GridBagConstraints(1, 1, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); controls.add(label = new JLabel("100%"), new GridBagConstraints(2, 1, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); label.setForeground(Color.WHITE); controls.add(label = new JLabel("Blur Radius: 1px"), new GridBagConstraints(0, 2, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 6, 0, 0), 0, 0)); label.setForeground(Color.WHITE); controls.add(radiusSlider, new GridBagConstraints(1, 2, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); controls.add(label = new JLabel("20px"), new GridBagConstraints(2, 2, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); label.setForeground(Color.WHITE); JCheckBox blurCheckBox = new JCheckBox("Blur Enabled"); blurCheckBox.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent changeEvent) { reflectionPanel.setBlurEnabled( ((JCheckBox) changeEvent.getSource()).isSelected()); } }); blurCheckBox.setOpaque(false); blurCheckBox.setForeground(Color.WHITE); controls.add(blurCheckBox, new GridBagConstraints(0, 3, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 6, 0), 0, 0)); //add(controls, BorderLayout.SOUTH); reflectionPanel.setLayout(new BorderLayout()); reflectionPanel.add(controls, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } private static class ReflectionPanel extends JPanel { private BufferedImage image = null; private BufferedImage imageA; private ReflectionRenderer renderer = new ReflectionRenderer(); public ReflectionPanel() { try { imageA = GraphicsUtilities.loadCompatibleImage(getClass().getResource("A.jpg")); } catch (IOException e) { e.printStackTrace(); } image = renderer.createReflection(imageA); setOpaque(false); } @Override public Dimension getPreferredSize() { return new Dimension(640, 520); } @Override protected void paintComponent(Graphics g) { int x = (getWidth() - imageA.getWidth()) / 2; int y = 24; if (renderer.isBlurEnabled()) { x -= renderer.getEffectiveBlurRadius(); y -= renderer.getEffectiveBlurRadius() + 1; } g.drawImage(image, x, y + imageA.getHeight(), null); if (renderer.isBlurEnabled()) { x += renderer.getEffectiveBlurRadius(); y += renderer.getEffectiveBlurRadius() + 1; } g.drawImage(imageA, x, y, null); } public void setOpacity(float opacity) { renderer.setOpacity(opacity); image = renderer.createReflection(imageA); repaint(); } public void setLength(float length) { renderer.setLength(length); image = renderer.createReflection(imageA); repaint(); } public void setBlurEnabled(boolean selected) { renderer.setBlurEnabled(selected); image = renderer.createReflection(imageA); repaint(); } public void setRadius(int radius) { renderer.setBlurRadius(radius); image = renderer.createReflection(imageA); repaint(); } } private static class GradientPanel extends JPanel { GradientPanel() { super(new BorderLayout()); } @Override protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; Rectangle clip = g2.getClipBounds(); Paint paint = g2.getPaint(); g2.setPaint(new GradientPaint(0.0f, getHeight() * 0.22f, new Color(0x202737), 0.0f, getHeight() * 0.7f, Color.BLACK)); g2.fillRect(clip.x, clip.y, clip.width, clip.height); g2.setPaint(paint); } } public static void main(String... args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new ImageReflectionDemoFilthy().setVisible(true); } }); } } /* * 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. */ /** * <p><code>GraphicsUtilities</code> contains a set of tools to perform * common graphics operations easily. These operations are divided into * several themes, listed below.</p> * <h2>Compatible Images</h2> * <p>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.</p> * <h2>Creating Thumbnails</h2> * <p>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.</p> * <p>All these methodes are both faster than * {@link java.awt.Image#getScaledInstance(int, int, int)} and produce * better-looking results than the various <code>drawImage()</code> methods * in {@link java.awt.Graphics}, which can be used for image scaling.</p> * <h2>Image Manipulation</h2> * <p>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.</p> * * @author Romain Guy <romain.guy@mac.com> */ class GraphicsUtilities { private GraphicsUtilities() { } // Returns the graphics configuration for the primary screen private static GraphicsConfiguration getGraphicsConfiguration() { return GraphicsEnvironment.getLocalGraphicsEnvironment(). getDefaultScreenDevice().getDefaultConfiguration(); } /** * <p>Returns a new <code>BufferedImage</code> 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.</p> * * @param image the reference image from which the color model of the new * image is obtained * @return a new <code>BufferedImage</code>, compatible with the color model * of <code>image</code> */ public static BufferedImage createColorModelCompatibleImage(BufferedImage image) { ColorModel cm = image.getColorModel(); return new BufferedImage(cm, cm.createCompatibleWritableRaster(image.getWidth(), image.getHeight()), cm.isAlphaPremultiplied(), null); } /** * <p>Returns a new compatible image with the same width, height and * transparency as the image specified as a parameter.</p> * * @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 compatible <code>BufferedImage</code> with the same * dimension and transparency as <code>image</code> */ public static BufferedImage createCompatibleImage(BufferedImage image) { return createCompatibleImage(image, image.getWidth(), image.getHeight()); } /** * <p>Returns a new compatible image of the specified width and height, and * the same transparency setting as the image specified as a parameter.</p> * * @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 compatible <code>BufferedImage</code> with the same * transparency as <code>image</code> and the specified dimension */ public static BufferedImage createCompatibleImage(BufferedImage image, int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height, image.getTransparency()); } /** * <p>Returns a new opaque compatible image of the specified width and * height.</p> * * @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 compatible <code>BufferedImage</code> of the * specified width and height */ public static BufferedImage createCompatibleImage(int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height); } /** * <p>Returns a new translucent compatible image of the specified width * and height.</p> * * @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 compatible <code>BufferedImage</code> of the * specified width and height */ public static BufferedImage createCompatibleTranslucentImage(int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT); } /** * <p>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.</p> * * @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 compatible <code>BufferedImage</code> 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); } /** * <p>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.</p> * * @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, of <code>image</code> */ 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; } /** * <p>Returns a thumbnail of a source image. <code>newSize</code> 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.</p> * <p>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.</p> * * @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 compatible <code>BufferedImage</code> containing a * thumbnail of <code>image</code> * @throws IllegalArgumentException if <code>newSize</code> is larger than * the largest dimension of <code>image</code> 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; } /** * <p>Returns a thumbnail of a source image.</p> * <p>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.</p> * * @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 compatible <code>BufferedImage</code> containing a * thumbnail of <code>image</code> * @throws IllegalArgumentException if <code>newWidth</code> is larger than * the width of <code>image</code> or if code>newHeight</code> is larger * than the height of <code>image</code> 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; } /** * <p>Returns a thumbnail of a source image. <code>newSize</code> 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.</p> * <p>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.</p> * * @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 compatible <code>BufferedImage</code> containing a * thumbnail of <code>image</code> * @throws IllegalArgumentException if <code>newSize</code> is larger than * the largest dimension of <code>image</code> 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; } /** * <p>Returns a thumbnail of a source image.</p> * <p>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.</p> * * @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 compatible <code>BufferedImage</code> containing a * thumbnail of <code>image</code> * @throws IllegalArgumentException if <code>newWidth</code> is larger than * the width of <code>image</code> or if code>newHeight</code> is larger * than the height of <code>image or if one the dimensions is not > 0</code> */ 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; } /** * <p>Returns an array of pixels, stored as integers, from a * <code>BufferedImage</code>. 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 <code>BufferedImage.TYPE_INT_ARGB</code> * and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p> * * @param img the source image * @param x the x location at which to start grabbing pixels * @param y the y location at which to start grabbing pixels * @param w the width of the rectangle of pixels to grab * @param h the height of the rectangle of pixels to grab * @param pixels a pre-allocated array of pixels of size w*h; can be null * @return <code>pixels</code> if non-null, a new array of integers * otherwise * @throws IllegalArgumentException is <code>pixels</code> 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); } /** * <p>Writes a rectangular area of pixels in the destination * <code>BufferedImage</code>. Calling this method on * an image of type different from <code>BufferedImage.TYPE_INT_ARGB</code> * and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p> * * @param img the destination image * @param x the x location at which to start storing pixels * @param y the y location at which to start storing pixels * @param w the width of the rectangle of pixels to store * @param h the height of the rectangle of pixels to store * @param pixels an array of pixels, stored as integers * @throws IllegalArgumentException is <code>pixels</code> 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. */ /** * <p>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.</p> * <p>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.</p> * * @author Romain Guy <romain.guy@mac.com> */ class FastBlurFilter extends AbstractFilter { private final int radius; /** * <p>Creates a new blur filter with a default radius of 3.</p> */ public FastBlurFilter() { this(3); } /** * <p>Creates a new blur filter with the specified radius. If the radius * is lower than 1, a radius of 1 will be used automatically.</p> * * @param radius the radius, in pixels, of the blur */ public FastBlurFilter(int radius) { if (radius < 1) { radius = 1; } this.radius = radius; } /** * <p>Returns the radius used by this filter, in pixels.</p> * * @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; } /** * <p>Blurs the source pixels into the destination pixels. The force of * the blur is specified by the radius which must be greater than 0.</p> * <p>The source and destination pixels arrays are expected to be in the * INT_ARGB format.</p> * <p>After this method is executed, dstPixels contains a transposed and * filtered copy of srcPixels.</p> * * @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. */ /** * <p>Provides an abstract implementation of the <code>BufferedImageOp</code> * interface. This class can be used to created new image filters based * on <code>BufferedImageOp</code>.</p> * * @author Romain Guy <romain.guy@mac.com> */ abstract class AbstractFilter implements BufferedImageOp { public abstract BufferedImage filter(BufferedImage src, BufferedImage dest); /** * {@inheritDoc} */ public Rectangle2D getBounds2D(BufferedImage src) { return new Rectangle(0, 0, src.getWidth(), src.getHeight()); } /** * {@inheritDoc} */ public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { if (destCM == null) { destCM = src.getColorModel(); } return new BufferedImage(destCM, destCM.createCompatibleWritableRaster( src.getWidth(), src.getHeight()), destCM.isAlphaPremultiplied(), null); } /** * {@inheritDoc} */ public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { return (Point2D) srcPt.clone(); } /** * {@inheritDoc} */ public RenderingHints getRenderingHints() { return null; } } /* * 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. */ /** * <p>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.</p> * <h2>Reflection Properties</h2> * <p>A reflection is defined by three properties: * <ul> * <li><i>opacity</i>: the opacity of the reflection. You will usually * change this valued according to the background color.</li> * <li><i>length</i>: the length of the reflection. The length is a fraction * of the height of the source image.</li> * <li><i>blur enabled</i>: perfect reflections are hardly natural. You can * blur the reflection to make it look a bit more natural.</li> * </ul> * You can set these properties using the provided mutaters or the appropriate * constructor. Here are two ways of creating a blurred reflection, with an * opacity of 50% and a length of 30% the height of the original image: * <pre> * ReflectionRenderer renderer = new ReflectionRenderer(0.5f, 0.3f, true); * // .. * renderer = new ReflectionRenderer(); * renderer.setOpacity(0.5f); * renderer.setLength(0.3f); * renderer.setBlurEnabled(true); * </pre> * The default constructor provides the following default values: * <ul> * <li><i>opacity</i>: 35%</li> * <li><i>length</i>: 40%</li> * <li><i>blur enabled</i>: false</li> * </ul></p> * <h2>Generating Reflections</h2> * <p>A reflection is generated as a <code>BufferedImage</code> from another * <code>BufferedImage</code>. Once the renderer is set up, you must call * {@link #createReflection(java.awt.image.BufferedImage)} to actually generate * the reflection: * <pre> * ReflectionRenderer renderer = new ReflectionRenderer(); * // renderer setup * BufferedImage reflection = renderer.createReflection(bufferedImage); * </pre></p> * <p>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: * <pre> * ReflectionRenderer renderer = new ReflectionRenderer(); * // renderer setup * BufferedImage reflection = renderer.appendReflection(bufferedImage); * </pre></p> * <h2>Properties Changes</h2> * <p>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.</p> * <h2>Threading Issues</h2> * <p><code>ReflectionRenderer</code> is not guaranteed to be thread-safe.</p> * * @author Romain Guy <romain.guy@mac.com> */ class ReflectionRenderer { /** * <p>Identifies a change to the opacity used to render the reflection.</p> */ public static final String OPACITY_CHANGED_PROPERTY = "reflection_opacity"; /** * <p>Identifies a change to the length of the rendered reflection.</p> */ public static final String LENGTH_CHANGED_PROPERTY = "reflection_length"; /** * <p>Identifies a change to the blurring of the rendered reflection.</p> */ 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; /** * <p>Creates a default good looking reflections generator. * The default reflection renderer provides the following default values: * <ul> * <li><i>opacity</i>: 35%</li> * <li><i>length</i>: 40%</li> * <li><i>blurring</i>: disabled with a radius of 1 pixel</li> * </ul></p> * <p>These properties provide a regular, good looking reflection.</p> * * @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); } /** * <p>Creates a default good looking reflections generator with the * specified opacity. The default reflection renderer provides the following * default values: * <ul> * <li><i>length</i>: 40%</li> * <li><i>blurring</i>: disabled with a radius of 1 pixel</li> * </ul></p> * * @param opacity the opacity of the reflection, between 0.0 and 1.0 * @see #getOpacity() * @see #setOpacity(float) * @see #getLength() * @see #setLength(float) * @see #isBlurEnabled() * @see #setBlurEnabled(boolean) * @see #getBlurRadius() * @see #setBlurRadius(int) */ public ReflectionRenderer(float opacity) { this(opacity, 0.4f, false); } /** * <p>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.</p> * <p>Enabling the blur generates a different kind of reflections that might * look more natural. The default blur radius is 1 pixel</p> * * @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); } /** * <p>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 * <code>listener</code> is null, no exception is thrown and no action * is taken.</p> * * @param listener the PropertyChangeListener to be added */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * <p>Remove a PropertyChangeListener from the listener list. This removes * a PropertyChangeListener that was registered for all properties. If * <code>listener</code> was added more than once to the same event source, * it will be notified one less time after being removed. If * <code>listener</code> is null, or was never added, no exception is thrown * and no action is taken.</p> * * @param listener the PropertyChangeListener to be removed */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } /** * <p>Gets the opacity used by the factory to generate reflections.</p> * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque.</p> * * @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; } /** * <p>Sets the opacity used by the factory to generate reflections.</p> * <p>Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.</p> * <p>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.</p> * * @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); } } /** * <p>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.</p> * * @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; } /** * <p>Sets the length of the reflection, as a fraction of the height of the * source image.</p> * <p>Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.</p> * <p>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.</p> * * @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); } } /** * <p>Returns true if the blurring of the reflection is enabled, false * otherwise. When blurring is enabled, the reflection is blurred to look * more natural.</p> * * @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; } /** * <p>Setting the blur to true will enable the blurring of the reflection * when {@link #createReflection} is invoked.</p> * <p>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.</p> * <p>Consecutive calls to {@link #createReflection} will all use this * opacity until it is set again.</p> * * @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); } } /** * <p>Returns the effective radius, in pixels, of the blur used by this * renderer when {@link #isBlurEnabled()} is true.</p> * * @return the effective radius of the blur used when * <code>isBlurEnabled</code> is true * @see #isBlurEnabled() * @see #setBlurEnabled(boolean) * @see #setBlurRadius(int) * @see #getBlurRadius() */ public int getEffectiveBlurRadius() { return stackBlurFilter.getEffectiveRadius(); } /** * <p>Returns the radius, in pixels, of the blur used by this renderer when * {@link #isBlurEnabled()} is true.</p> * * @return the radius of the blur used when <code>isBlurEnabled</code> * is true * @see #isBlurEnabled() * @see #setBlurEnabled(boolean) * @see #setBlurRadius(int) * @see #getEffectiveBlurRadius() */ public int getBlurRadius() { return stackBlurFilter.getRadius(); } /** * <p>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.</p> * * @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); } /** * <p>Returns the source image and its reflection. The appearance of the * reflection is defined by the opacity, the length and the blur * properties.</p> * * <p>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.</p> * <p>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.</p> * <p>You can create only the reflection by calling * {@link #createReflection(java.awt.image.BufferedImage)}.</p> * * @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; } /** * <p>Returns the reflection of the source image. The appearance of the * reflection is defined by the opacity, the length and the blur * properties.</p> * * <p>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.</p> * <p>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.</p> * <p>The returned image contains <strong>only</strong> * 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.</p> * * @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. */ /** * <p>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.</p> * <p>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.</p> * * @author Romain Guy <romain.guy@mac.com> */ class StackBlurFilter extends AbstractFilter { private final int radius; private final int iterations; /** * <p>Creates a new blur filter with a default radius of 3 and 3 iterations.</p> */ public StackBlurFilter() { this(3, 3); } /** * <p>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.</p> * * @param radius the radius, in pixels, of the blur */ public StackBlurFilter(int radius) { this(radius, 3); } /** * <p>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.</p> * * @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; } /** * <p>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.</p> * @return the number of iterations times the blur radius */ public int getEffectiveRadius() { return getIterations() * getRadius(); } /** * <p>Returns the radius used by this filter, in pixels.</p> * * @return the radius of the blur */ public int getRadius() { return radius; } /** * <p>Returns the number of iterations used to approximate a gaussian * blur.</p> * * @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; } }