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