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}