001/* 002Copyright 2006 Jerry Huxtable 003 004Licensed under the Apache License, Version 2.0 (the "License"); 005you may not use this file except in compliance with the License. 006You may obtain a copy of the License at 007 008 http://www.apache.org/licenses/LICENSE-2.0 009 010Unless required by applicable law or agreed to in writing, software 011distributed under the License is distributed on an "AS IS" BASIS, 012WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013See the License for the specific language governing permissions and 014limitations under the License. 015*/ 016 017package com.jhlabs.image; 018 019import java.awt.*; 020import java.awt.geom.*; 021import java.awt.image.*; 022 023/** 024 * A filter which draws a drop shadow based on the alpha channel of the image. 025 */ 026public class ShadowFilter extends AbstractBufferedImageOp { 027 028 private float radius = 5; 029 private float angle = (float)Math.PI*6/4; 030 private float distance = 5; 031 private float opacity = 0.5f; 032 private boolean addMargins = false; 033 private boolean shadowOnly = false; 034 private int shadowColor = 0xff000000; 035 036 /** 037 * Construct a ShadowFilter. 038 */ 039 public ShadowFilter() { 040 } 041 042 /** 043 * Construct a ShadowFilter. 044 * @param radius the radius of the shadow 045 * @param xOffset the X offset of the shadow 046 * @param yOffset the Y offset of the shadow 047 * @param opacity the opacity of the shadow 048 */ 049 public ShadowFilter(float radius, float xOffset, float yOffset, float opacity) { 050 this.radius = radius; 051 this.angle = (float)Math.atan2(yOffset, xOffset); 052 this.distance = (float)Math.sqrt(xOffset*xOffset + yOffset*yOffset); 053 this.opacity = opacity; 054 } 055 056 /** 057 * Specifies the angle of the shadow. 058 * @param angle the angle of the shadow. 059 * @angle 060 * @see #getAngle 061 */ 062 public void setAngle(float angle) { 063 this.angle = angle; 064 } 065 066 /** 067 * Returns the angle of the shadow. 068 * @return the angle of the shadow. 069 * @see #setAngle 070 */ 071 public float getAngle() { 072 return angle; 073 } 074 075 /** 076 * Set the distance of the shadow. 077 * @param distance the distance. 078 * @see #getDistance 079 */ 080 public void setDistance(float distance) { 081 this.distance = distance; 082 } 083 084 /** 085 * Get the distance of the shadow. 086 * @return the distance. 087 * @see #setDistance 088 */ 089 public float getDistance() { 090 return distance; 091 } 092 093 /** 094 * Set the radius of the kernel, and hence the amount of blur. The bigger the radius, the longer this filter will take. 095 * @param radius the radius of the blur in pixels. 096 * @see #getRadius 097 */ 098 public void setRadius(float radius) { 099 this.radius = radius; 100 } 101 102 /** 103 * Get the radius of the kernel. 104 * @return the radius 105 * @see #setRadius 106 */ 107 public float getRadius() { 108 return radius; 109 } 110 111 /** 112 * Set the opacity of the shadow. 113 * @param opacity the opacity. 114 * @see #getOpacity 115 */ 116 public void setOpacity(float opacity) { 117 this.opacity = opacity; 118 } 119 120 /** 121 * Get the opacity of the shadow. 122 * @return the opacity. 123 * @see #setOpacity 124 */ 125 public float getOpacity() { 126 return opacity; 127 } 128 129 /** 130 * Set the color of the shadow. 131 * @param shadowColor the color. 132 * @see #getShadowColor 133 */ 134 public void setShadowColor(int shadowColor) { 135 this.shadowColor = shadowColor; 136 } 137 138 /** 139 * Get the color of the shadow. 140 * @return the color. 141 * @see #setShadowColor 142 */ 143 public int getShadowColor() { 144 return shadowColor; 145 } 146 147 /** 148 * Set whether to increase the size of the output image to accomodate the shadow. 149 * @param addMargins true to add margins. 150 * @see #getAddMargins 151 */ 152 public void setAddMargins(boolean addMargins) { 153 this.addMargins = addMargins; 154 } 155 156 /** 157 * Get whether to increase the size of the output image to accomodate the shadow. 158 * @return true to add margins. 159 * @see #setAddMargins 160 */ 161 public boolean getAddMargins() { 162 return addMargins; 163 } 164 165 /** 166 * Set whether to only draw the shadow without the original image. 167 * @param shadowOnly true to only draw the shadow. 168 * @see #getShadowOnly 169 */ 170 public void setShadowOnly(boolean shadowOnly) { 171 this.shadowOnly = shadowOnly; 172 } 173 174 /** 175 * Get whether to only draw the shadow without the original image. 176 * @return true to only draw the shadow. 177 * @see #setShadowOnly 178 */ 179 public boolean getShadowOnly() { 180 return shadowOnly; 181 } 182 183 public Rectangle2D getBounds2D( BufferedImage src ) { 184 Rectangle r = new Rectangle(0, 0, src.getWidth(), src.getHeight()); 185 if ( addMargins ) { 186 float xOffset = distance*(float)Math.cos(angle); 187 float yOffset = -distance*(float)Math.sin(angle); 188 r.width += (int)(Math.abs(xOffset)+2*radius); 189 r.height += (int)(Math.abs(yOffset)+2*radius); 190 } 191 return r; 192 } 193 194 public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { 195 if ( dstPt == null ) 196 dstPt = new Point2D.Double(); 197 198 if ( addMargins ) { 199 float xOffset = distance*(float)Math.cos(angle); 200 float yOffset = -distance*(float)Math.sin(angle); 201 float topShadow = Math.max( 0, radius-yOffset ); 202 float leftShadow = Math.max( 0, radius-xOffset ); 203 dstPt.setLocation( srcPt.getX()+leftShadow, srcPt.getY()+topShadow ); 204 } else 205 dstPt.setLocation( srcPt.getX(), srcPt.getY() ); 206 207 return dstPt; 208 } 209 210 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 211 int width = src.getWidth(); 212 int height = src.getHeight(); 213 214 float xOffset = distance*(float)Math.cos(angle); 215 float yOffset = -distance*(float)Math.sin(angle); 216 217 if ( dst == null ) { 218 if ( addMargins ) { 219 ColorModel cm = src.getColorModel(); 220 dst = new BufferedImage(cm, cm.createCompatibleWritableRaster(src.getWidth() + (int) (Math.abs(xOffset) + radius), src.getHeight() + (int) (Math.abs(yOffset) + radius)), cm.isAlphaPremultiplied(), null); 221 } else 222 dst = createCompatibleDestImage( src, null ); 223 } 224 225 float shadowR = ((shadowColor >> 16) & 0xff) / 255f; 226 float shadowG = ((shadowColor >> 8) & 0xff) / 255f; 227 float shadowB = (shadowColor & 0xff) / 255f; 228 229 // Make a black mask from the image's alpha channel 230 float[][] extractAlpha = { 231 { 0, 0, 0, shadowR }, 232 { 0, 0, 0, shadowG }, 233 { 0, 0, 0, shadowB }, 234 { 0, 0, 0, opacity } 235 }; 236 BufferedImage shadow = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 237 new BandCombineOp( extractAlpha, null ).filter( src.getRaster(), shadow.getRaster() ); 238 shadow = new GaussianFilter( radius ).filter( shadow, null ); 239 240 Graphics2D g = dst.createGraphics(); 241 g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity ) ); 242 if ( addMargins ) { 243 float radius2 = radius/2; 244 float topShadow = Math.max( 0, radius-yOffset ); 245 float leftShadow = Math.max( 0, radius-xOffset ); 246 g.translate( leftShadow, topShadow ); 247 } 248 g.drawRenderedImage( shadow, AffineTransform.getTranslateInstance( xOffset, yOffset ) ); 249 if ( !shadowOnly ) { 250 g.setComposite( AlphaComposite.SrcOver ); 251 g.drawRenderedImage( src, null ); 252 } 253 g.dispose(); 254 255 return dst; 256 } 257 258 public String toString() { 259 return "Stylize/Drop Shadow..."; 260 } 261}